diff --git a/hosts/sobotka/server.nix b/hosts/sobotka/server.nix index c848458b..4b67c4af 100644 --- a/hosts/sobotka/server.nix +++ b/hosts/sobotka/server.nix @@ -1,5 +1,4 @@ -{ config, ... }: -{ +{config, ...}: { server = { enable = true; email = "adam@cnst.dev"; @@ -45,6 +44,15 @@ uptime-kuma = { enable = true; }; + keycloak = { + enable = true; + url = "login.cnst.dev"; + dbPasswordFile = config.age.secrets.keycloakDbPasswordFile.path; + cloudflared = { + tunnelId = "590f60f8-baaa-4106-b2d1-43740c79531e"; + credentialsFile = config.age.secrets.keycloakCloudflared.path; + }; + }; vaultwarden = { enable = true; url = "vault.cnst.dev"; @@ -53,12 +61,12 @@ credentialsFile = config.age.secrets.vaultwardenCloudflared.path; }; }; - nextcloud = { + ocis = { enable = true; url = "cloud.cnst.dev"; cloudflared = { - tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5"; - credentialsFile = config.age.secrets.nextcloudCloudflared.path; + tunnelId = "8871dad0-e6ff-424c-9a6b-222ef0f492df"; + credentialsFile = config.age.secrets.ocisCloudflared.path; }; }; fail2ban = { diff --git a/modules/default.nix b/modules/default.nix index 5c217a00..5c7c8f4d 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -126,6 +126,8 @@ ./server/caddy ./server/fail2ban ./server/homepage-dashboard + ./server/ocis + ./server/keycloak ./server/vaultwarden ./server/bazarr ./server/prowlarr diff --git a/modules/nixos/services/agenix/default.nix b/modules/nixos/services/agenix/default.nix index 29651444..81d34eb1 100644 --- a/modules/nixos/services/agenix/default.nix +++ b/modules/nixos/services/agenix/default.nix @@ -5,17 +5,16 @@ pkgs, self, ... -}: -let - inherit (lib) +}: let + inherit + (lib) mkIf mkEnableOption mkOption mkMerge ; cfg = config.nixos.services.agenix; -in -{ +in { options = { nixos.services.agenix = { enable = mkEnableOption "Enables agenix system environment"; @@ -75,6 +74,10 @@ in wgCredentials.file = "${self}/secrets/wgCredentials.age"; wgSobotkaPrivateKey.file = "${self}/secrets/wgSobotkaPrivateKey.age"; gluetunEnvironment.file = "${self}/secrets/gluetunEnvironment.age"; + keycloakCloudflared.file = "${self}/secrets/keycloakCloudflared.age"; + keycloakDbPasswordFile.file = "${self}/secrets/keycloakDbPasswordFile.age"; + nextcloudAdminPass.file = "${self}/secrets/nextcloudAdminPass.age"; + ocisCloudflared.file = "${self}/secrets/ocisCloudflared.age"; vaultwardenCloudflared.file = "${self}/secrets/vaultwardenCloudflared.age"; vaultwardenEnvironment.file = "${self}/secrets/vaultwardenEnvironment.age"; homepageEnvironment.file = "${self}/secrets/homepageEnvironment.age"; diff --git a/modules/server/README.md b/modules/server/README.md new file mode 100644 index 00000000..f6b50b75 --- /dev/null +++ b/modules/server/README.md @@ -0,0 +1,2 @@ +This server/homelab configuration is largely a copy (with some tweaks) of +@notthebee's homelab setup. diff --git a/modules/server/keycloak/default.nix b/modules/server/keycloak/default.nix new file mode 100644 index 00000000..0ff84a23 --- /dev/null +++ b/modules/server/keycloak/default.nix @@ -0,0 +1,100 @@ +{ + config, + lib, + pkgs, + ... +}: let + unit = "keycloak"; + cfg = config.server.${unit}; + srv = config.server; +in { + options.server.${unit} = { + enable = lib.mkEnableOption { + description = "Enable ${unit}"; + }; + url = lib.mkOption { + type = lib.types.str; + default = "login.${srv.domain}"; + }; + homepage.name = lib.mkOption { + type = lib.types.str; + default = "Keycloak"; + }; + homepage.description = lib.mkOption { + type = lib.types.str; + default = "Open Source Identity and Access Management"; + }; + homepage.icon = lib.mkOption { + type = lib.types.str; + default = "keycloak.svg"; + }; + homepage.category = lib.mkOption { + type = lib.types.str; + default = "Services"; + }; + dbPasswordFile = lib.mkOption { + type = lib.types.path; + }; + cloudflared = { + credentialsFile = lib.mkOption { + type = lib.types.str; + example = lib.literalExpression '' + pkgs.writeText "cloudflare-credentials.json" ''' + {"AccountTag":"secret"."TunnelSecret":"secret","TunnelID":"secret"} + ''' + ''; + }; + tunnelId = lib.mkOption { + type = lib.types.str; + example = "00000000-0000-0000-0000-000000000000"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + server.postgresql.databases = [ + { + database = "keycloak"; + } + ]; + services.cloudflared = { + enable = true; + tunnels.${cfg.cloudflared.tunnelId} = { + credentialsFile = cfg.cloudflared.credentialsFile; + default = "http_status:404"; + ingress."${cfg.url}".service = "http://127.0.0.1:${ + toString config.services.${unit}.settings.http-port + }"; + }; + }; + + environment.systemPackages = [ + pkgs.keycloak + ]; + + services.${unit} = { + enable = true; + initialAdminPassword = "pwpwpw"; + database = { + type = "postgresql"; + host = "127.0.0.1"; + port = 5432; + name = "keycloak"; + username = "keycloak"; + passwordFile = cfg.dbPasswordFile; + useSSL = false; + }; + settings = { + spi-theme-static-max-age = "-1"; + spi-theme-cache-themes = false; + spi-theme-cache-templates = false; + http-port = 8821; + hostname = cfg.url; + hostname-strict = false; + hostname-strict-https = false; + proxy-headers = "xforwarded"; + http-enabled = true; + }; + }; + }; +} diff --git a/modules/server/nextcloud/default.nix b/modules/server/nextcloud/default.nix deleted file mode 100644 index 4aa83ada..00000000 --- a/modules/server/nextcloud/default.nix +++ /dev/null @@ -1,138 +0,0 @@ -{ - config, - pkgs, - lib, - ... -}: -let - unit = "nextcloud"; - cfg = config.server.${unit}; - srv = config.server; -in -{ - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - adminUser = lib.mkOption { - type = lib.types.str; - default = "cnst"; - }; - adminPass = lib.mkOption { - type = lib.types.path; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "cloud.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Nextcloud"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "A safe home for all your data"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "nextcloud.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Services"; - }; - cloudflared = { - credentialsFile = lib.mkOption { - type = lib.types.str; - example = lib.literalExpression '' - pkgs.writeText "cloudflare-credentials.json" ''' - {"AccountTag":"secret"."TunnelSecret":"secret","TunnelID":"secret"} - ''' - ''; - }; - tunnelId = lib.mkOption { - type = lib.types.str; - example = "00000000-0000-0000-0000-000000000000"; - }; - }; - - }; - config = lib.mkIf cfg.enable { - server = { - postgresql.databases = [ - { - database = "nextcloud"; - } - ]; - fail2ban = lib.mkIf config.server.fail2ban.enable { - jails = { - nextcloud = { - serviceName = "phpfm-nextcloud"; - failRegex = "^.*Login failed:.*(Remote IP: ).*$"; - }; - }; - }; - }; - - services = { - cloudflared = { - enable = true; - tunnels.${cfg.cloudflared.tunnelId} = { - credentialsFile = cfg.cloudflared.credentialsFile; - default = "http_status:404"; - ingress."${cfg.url}".service = "http://127.0.0.1:8083"; - }; - }; - - ${unit} = { - enable = true; - package = pkgs.nextcloud31; - hostName = "nextcloud"; - configureRedis = true; - caching = { - redis = true; - }; - maxUploadSize = "50G"; - settings = { - trusted_proxies = [ "127.0.0.1" ]; - overwriteprotocol = "https"; - overwritehost = "cloud.${srv.domain}"; - overwrite.cli.url = "https://cloud.${srv.domain}"; - mail_smtpmode = "sendmail"; - mail_sendmailmode = "pipe"; - user_oidc = { - allow_multiple_user_backends = 0; - }; - forwarded_for_headers = [ - "HTTP_CF_CONNECTING_IP" - ]; - enabledPreviewProviders = [ - "OC\\Preview\\BMP" - "OC\\Preview\\GIF" - "OC\\Preview\\JPEG" - "OC\\Preview\\Krita" - "OC\\Preview\\MarkDown" - "OC\\Preview\\MP3" - "OC\\Preview\\OpenDocument" - "OC\\Preview\\PNG" - "OC\\Preview\\TXT" - "OC\\Preview\\XBitmap" - "OC\\Preview\\HEIC" - ]; - }; - config = { - dbtype = "pgsql"; - dbuser = "nextcloud"; - dbhost = "/run/postgresql"; - dbname = "nextcloud"; - adminuser = cfg.adminUser; - adminpassFile = cfg.adminPass; - }; - }; - }; - }; -} diff --git a/modules/server/ocis/default.nix b/modules/server/ocis/default.nix new file mode 100644 index 00000000..28abc1d1 --- /dev/null +++ b/modules/server/ocis/default.nix @@ -0,0 +1,155 @@ +{ + config, + pkgs, + lib, + ... +}: let + unit = "ocis"; + cfg = config.server.${unit}; + srv = config.server; +in { + options.server.${unit} = { + enable = lib.mkEnableOption { + description = "Enable ${unit}"; + }; + adminUser = lib.mkOption { + type = lib.types.str; + default = "cnst"; + }; + adminPass = lib.mkOption { + type = lib.types.path; + }; + configDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/${unit}"; + }; + url = lib.mkOption { + type = lib.types.str; + default = "cloud.${srv.domain}"; + }; + homepage.name = lib.mkOption { + type = lib.types.str; + default = "OCIS"; + }; + homepage.description = lib.mkOption { + type = lib.types.str; + default = "A safe home for all your data"; + }; + homepage.icon = lib.mkOption { + type = lib.types.str; + default = "owncloud.svg"; + }; + homepage.category = lib.mkOption { + type = lib.types.str; + default = "Services"; + }; + cloudflared = { + credentialsFile = lib.mkOption { + type = lib.types.str; + example = lib.literalExpression '' + pkgs.writeText "cloudflare-credentials.json" ''' + {"AccountTag":"secret"."TunnelSecret":"secret","TunnelID":"secret"} + ''' + ''; + }; + tunnelId = lib.mkOption { + type = lib.types.str; + example = "00000000-0000-0000-0000-000000000000"; + }; + }; + }; + config = lib.mkIf cfg.enable { + server = { + postgresql.databases = [ + { + database = "ocis"; + } + ]; + fail2ban = lib.mkIf config.server.fail2ban.enable { + jails = { + nextcloud = { + serviceName = "phpfm-nextcloud"; + failRegex = "^.*Login failed:.*(Remote IP: ).*$"; + }; + }; + }; + }; + systemd.services.ocis.preStart = '' + ${lib.getExe pkgs.ocis} init || true + ''; + services = { + cloudflared = { + enable = true; + tunnels.${cfg.cloudflared.tunnelId} = { + credentialsFile = cfg.cloudflared.credentialsFile; + default = "http_status:404"; + ingress."${cfg.url}".service = "http://${config.services.ocis.address}:${toString config.services.ocis.port}"; + }; + }; + ${unit} = { + enable = true; + url = "https://${cfg.url}"; + environment = let + cspFormat = pkgs.formats.yaml {}; + cspConfig = { + directives = { + child-src = ["'self'"]; + connect-src = [ + "'self'" + "blob:" + "https://${srv.keycloak.url}" + ]; + default-src = ["'none'"]; + font-src = ["'self'"]; + frame-ancestors = ["'none'"]; + frame-src = [ + "'self'" + "blob:" + "https://embed.diagrams.net" + ]; + img-src = [ + "'self'" + "data:" + "blob:" + ]; + manifest-src = ["'self'"]; + media-src = ["'self'"]; + object-src = [ + "'self'" + "blob:" + ]; + script-src = [ + "'self'" + "'unsafe-inline'" + ]; + style-src = [ + "'self'" + "'unsafe-inline'" + ]; + }; + }; + in { + PROXY_AUTOPROVISION_ACCOUNTS = "true"; + PROXY_ROLE_ASSIGNMENT_DRIVER = "oidc"; + OCIS_OIDC_ISSUER = "https://${srv.keycloak.url}/realms/master"; + PROXY_OIDC_REWRITE_WELLKNOWN = "true"; + WEB_OIDC_CLIENT_ID = "ocis"; + OCIS_LOG_LEVEL = "error"; + PROXY_TLS = "false"; + PROXY_USER_OIDC_CLAIM = "preferred_username"; + PROXY_USER_CS3_CLAIM = "username"; + OCIS_ADMIN_USER_ID = ""; + OCIS_INSECURE = "false"; + OCIS_EXCLUDE_RUN_SERVICES = "idp"; + GRAPH_ASSIGN_DEFAULT_USER_ROLE = "false"; + PROXY_CSP_CONFIG_FILE_LOCATION = toString (cspFormat.generate "csp.yaml" cspConfig); + GRAPH_USERNAME_MATCH = "none"; + # PROXY_OIDC_ACCESS_TOKEN_VERIFY_METHOD = "none"; + PROXY_ROLE_ASSIGNMENT_ENABLED = "true"; + PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM = "realm_access.roles"; + PROXY_ROLE_ASSIGNMENT_MAPPING = "ocisAdmin:admin,cnst:user"; + }; + }; + }; + }; +} diff --git a/modules/server/postgres/postgres.nix b/modules/server/postgres/postgres.nix index ed6f5023..595aa297 100644 --- a/modules/server/postgres/postgres.nix +++ b/modules/server/postgres/postgres.nix @@ -54,14 +54,28 @@ in { local all postgres peer local sameuser all peer - # extra users + # local peer access for extra users ${lib.concatMapStringsSep "\n" ( { database, extraUsers, ... }: - lib.concatMapStringsSep "\n" (user: "local ${database} ${user} peer") extraUsers + lib.concatMapStringsSep "\n" (user: "local ${database} ${user} peer") ([database] ++ extraUsers) + ) + cfg.databases} + + # host access (TCP) for databases and their users + ${lib.concatMapStringsSep "\n" ( + { + database, + extraUsers, + ... + }: + lib.concatMapStringsSep "\n" (user: '' + host ${database} ${user} 127.0.0.1/32 trust + host ${database} ${user} ::1/128 trust + '') ([database] ++ extraUsers) ) cfg.databases} ''; diff --git a/secrets/keycloakCloudflared.age b/secrets/keycloakCloudflared.age new file mode 100644 index 00000000..3dc86358 Binary files /dev/null and b/secrets/keycloakCloudflared.age differ diff --git a/secrets/keycloakDbPasswordFile.age b/secrets/keycloakDbPasswordFile.age new file mode 100644 index 00000000..84a6443c --- /dev/null +++ b/secrets/keycloakDbPasswordFile.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 t9iOEg JGPsdwpo+QYwoEtQp5O7esvdpxDmBtSaAFa42xUrziA +O/fLTeyC+01JM0Iqc3PfW0LTEW1CTSCXvnzcTpGhiZs +-> ssh-ed25519 KUYMFA ELcXHREdinnJGyLmGP52mCe6wT6FPBAh6STAmphI1gM +UEAyd9Pl4GqxfgRvX4TkNjX+iCoPbIaBe4jZRgfxtKM +-> ssh-ed25519 76RhUQ iLb/XlmQRU7NBJ0qACMWzdcekWLJdewuY8XK88QMgho +cwydjm9RJ3Oj36OhBCCpdN1nIZICqpVcm/Icl+1buC8 +-> ssh-ed25519 Jf8sqw /VcINacatv58hd3KxVjVWm6Jg6ZZszbrA7xMuScfMwE +e5GHPaF3oXfmRKrcII3w48XH+nvvEJGdORUnfJR/jE0 +--- HxSA44rqWWFjoHeJaGi7VCsPdmksyMCytkv75TaTGfU +Muމ<(~82apfII^Lw˧ഐ%2㛔bq/As:B]]Ż \ No newline at end of file diff --git a/secrets/nextcloudAdminPass.age b/secrets/nextcloudAdminPass.age new file mode 100644 index 00000000..841d7f0f --- /dev/null +++ b/secrets/nextcloudAdminPass.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 t9iOEg PQ9P0XIzZ/u/gSgdOlfQXFVW1GemJxDaVCtqJkBYsBY +KOw543xyHFcTzkCyOAB40bbArhQW+cGPySfFB4ceMn4 +-> ssh-ed25519 KUYMFA GdUikncmoMYip/ASuwLKXwv+Wa3qyfT/CBjQxaptIRY +vnHv5sJVDcce02IaKwPFefNzIzvlwgIKi1ZVo+2tq+0 +-> ssh-ed25519 76RhUQ xLBZfVIBGSuEqGlV8ny+uDhDnHZrHv0b7PVOCyiFdRs +gK3tkiaZmHBTpwYImlftYgcyNc7k6kRl1dyOaEN/zls +-> ssh-ed25519 Jf8sqw YFwHA8v2BZvppparLQ1ts75lBCuS1exwNbLt4vRLuQY +qV/PXIouLPAx0amjeeS8aQx3tqgG7VqHhSjqIu+kOF4 +--- SAbSjt7w+XOwUQI+1saRnttNRuC9NOUvQXWu/+MdLn0 +3U\'UUKUQgk"PRL'r*rAcOy{%AbLKM9' \ No newline at end of file diff --git a/secrets/nextcloudCloudflared.age b/secrets/nextcloudCloudflared.age deleted file mode 100644 index 6c56e017..00000000 Binary files a/secrets/nextcloudCloudflared.age and /dev/null differ diff --git a/secrets/ocisCloudflared.age b/secrets/ocisCloudflared.age new file mode 100644 index 00000000..d4142a47 --- /dev/null +++ b/secrets/ocisCloudflared.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 t9iOEg v2avJ6U6RgP+ZxIa1QzzqxB48cslbdB0TlDlcz4F0RE +JTyOfbyFWOh9Br07l2SZpDH/2xk5+Cnz3BEl+1DdQdA +-> ssh-ed25519 KUYMFA pGewUt3eOluT+v/+Yuf8zsuQtZVRSbYQPVw6CkbrOUk +H3G8Gxug7dww1fkfpuErLCndsD0HHcEQkndIkkZaW4I +-> ssh-ed25519 76RhUQ o0kqSfuXPVvPGk3snfUGdAZqJG1I5KOhbEK21XUcCXs +ZKFxlayTAQptEgfNdPawCB1EYSphO6CzgVOxP56n71U +-> ssh-ed25519 Jf8sqw uor01IOxnc3p9iRi2019HjkZzs1ph9G+oiYIPKrXb3A +Q2uxuYsSmMc3N2g7IQ/87YHXNdcgF2MpIz8P53kPBSs +--- 3JvyOSSYzDA6OPnYq45RUKdgGE6EgzkP1kDyu1nRbww +`l0n˵ 8yiĕKnP&G2O `YwH+:(qpoA|͸dzg \Kh/+2hq1x6gǒ56Fz$Z28ߏUfOQ[8uaАuo-:&P  49Q?blif-OxxؼsjUSf[ \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 2d2be4b5..67418f79 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -40,7 +40,10 @@ in { "homepageEnvironment.age".publicKeys = core ++ sobotka; "cloudflareFirewallApiKey.age".publicKeys = core ++ sobotka; "vaultwardenCloudflared.age".publicKeys = core ++ sobotka; - "nextcloudCloudflared.age".publicKeys = core ++ sobotka; + "keycloakDbPasswordFile.age".publicKeys = core ++ sobotka; + "keycloakCloudflared.age".publicKeys = core ++ sobotka; + "ocisCloudflared.age".publicKeys = core ++ sobotka; + "ocisAdminPass.age".publicKeys = core ++ sobotka; "cloudflareDnsApiToken.age".publicKeys = core ++ sobotka; "cloudflareDnsCredentials.age".publicKeys = core ++ sobotka; "wgCredentials.age".publicKeys = core ++ sobotka;