diff --git a/flake.lock b/flake.lock index 57f82db..1e5fab1 100644 --- a/flake.lock +++ b/flake.lock @@ -52,6 +52,28 @@ "type": "github" } }, + "devshell": { + "inputs": { + "nixpkgs": [ + "keycloak-theme-pub-solar", + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1688380630, + "narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=", + "owner": "numtide", + "repo": "devshell", + "rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -87,6 +109,24 @@ } }, "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1689068808, + "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { "locked": { "lastModified": 1634851050, "narHash": "sha256-N83GlSGPJJdcqhUxSCS/WwW5pksYf3VP1M13cDRTSVA=", @@ -121,6 +161,29 @@ "type": "github" } }, + "keycloak-theme-pub-solar": { + "inputs": { + "devshell": "devshell", + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1689875310, + "narHash": "sha256-gJxh8fVX24nZXBxstZcrzZhMRFG9jyOnQEfkgoRr39I=", + "ref": "main", + "rev": "c2c86bbf9855f16a231a596b75b443232a7b9395", + "revCount": 24, + "type": "git", + "url": "https://git.pub.solar/pub-solar/keycloak-theme" + }, + "original": { + "ref": "main", + "type": "git", + "url": "https://git.pub.solar/pub-solar/keycloak-theme" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -227,17 +290,48 @@ "deploy-rs": "deploy-rs", "flake-parts": "flake-parts", "home-manager": "home-manager", + "keycloak-theme-pub-solar": "keycloak-theme-pub-solar", "nix-darwin": "nix-darwin", "nixos-flake": "nixos-flake", "nixpkgs": "nixpkgs_2", "terranix": "terranix" } }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "terranix": { "inputs": { "bats-assert": "bats-assert", "bats-support": "bats-support", - "flake-utils": "flake-utils", + "flake-utils": "flake-utils_2", "nixpkgs": "nixpkgs_3", "terranix-examples": "terranix-examples" }, diff --git a/flake.nix b/flake.nix index ac242f8..64a0b45 100644 --- a/flake.nix +++ b/flake.nix @@ -13,6 +13,9 @@ terranix.url = "github:terranix/terranix"; deploy-rs.url = "github:serokell/deploy-rs"; + + keycloak-theme-pub-solar.url = "git+https://git.pub.solar/pub-solar/keycloak-theme?ref=main"; + keycloak-theme-pub-solar.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = inputs@{ self, terranix, ... }: diff --git a/hosts/nachtigall/apps/caddy.nix b/hosts/nachtigall/apps/caddy.nix new file mode 100644 index 0000000..c424393 --- /dev/null +++ b/hosts/nachtigall/apps/caddy.nix @@ -0,0 +1,39 @@ +{ + config, + lib, + pkgs, + self, + ... +}: let + maintenanceMode = { + logFormat = lib.mkForce '' + output discard + ''; + extraConfig = '' + root * /srv/www/pub.solar + + error * "Scheduled Maintenance" 503 + + handle_errors { + root * /srv/www/pub.solar + rewrite * /maintenance/index.html + file_server + } + ''; + }; +in { + systemd.tmpfiles.rules = [ + "d '/data/srv/www/os/download/' 0750 hakkonaut hakkonaut - -" + ]; + + services.caddy = { + enable = lib.mkForce true; + group = "hakkonaut"; + email = "admins@pub.solar"; + enableReload = true; + globalConfig = lib.mkForce '' + grace_period 60s + ''; + }; + networking.firewall.allowedTCPPorts = [80 443]; +} diff --git a/hosts/nachtigall/apps/forgejo.nix b/hosts/nachtigall/apps/forgejo.nix new file mode 100644 index 0000000..cb24f1c --- /dev/null +++ b/hosts/nachtigall/apps/forgejo.nix @@ -0,0 +1,91 @@ +{ + config, + lib, + pkgs, + self, + ... +}: { + age.secrets.gitea-database-password = { + file = "${self}/secrets/gitea-database-password.age"; + mode = "600"; + owner = "gitea"; + }; + age.secrets.gitea-mailer-password = { + file = "${self}/secrets/gitea-mailer-password.age"; + mode = "600"; + owner = "gitea"; + }; + + services.caddy.virtualHosts."git.pub.solar" = { + logFormat = lib.mkForce '' + output discard + ''; + extraConfig = '' + redir /user/login /user/oauth2/keycloak temporary + reverse_proxy :3000 + ''; + }; + + services.gitea = { + enable = true; + package = pkgs.forgejo; + appName = "pub.solar git server"; + database = { + type = "postgres"; + passwordFile = config.age.secrets.gitea-database-password.path; + }; + lfs.enable = true; + mailerPasswordFile = config.age.secrets.gitea-mailer-password.path; + settings = { + server = { + ROOT_URL = "https://git.pub.solar"; + DOMAIN = "git.pub.solar"; + HTTP_ADDR = "127.0.0.1"; + HTTP_PORT = 3000; + }; + mailer = { + ENABLED = true; + PROTOCOL = "smtps"; + SMTP_ADDR = "mx2.greenbaum.cloud"; + SMTP_PORT = 465; + FROM = ''"pub.solar git server" ''; + USER = "admins@pub.solar"; + }; + "repository.signing" = { + SIGNING_KEY = "default"; + MERGES = "always"; + }; + openid = { + ENABLE_OPENID_SIGNIN = true; + ENABLE_OPENID_SIGNUP = true; + }; + # uncomment after initial deployment, first user is admin user + # required to setup SSO (oauth openid-connect, keycloak auth provider) + service.ALLOW_ONLY_EXTERNAL_REGISTRATION = true; + service.ENABLE_NOTIFY_MAIL = true; + session.COOKIE_SECURE = lib.mkForce true; + }; + }; + + # See: https://docs.gitea.io/en-us/signing/#installing-and-generating-a-gpg-key-for-gitea + # Required for gitea server side gpg signatures + # configured/setup manually in: + # /var/lib/gitea/data/home/.gitconfig + # /var/lib/gitea/data/home/.gnupg/ + # sudo su gitea + # export GNUPGHOME=/var/lib/gitea/data/home/.gnupg + # gpg --quick-gen-key 'pub.solar gitea ' ed25519 + # TODO: implement declarative GPG key generation and + # gitea gitconfig + programs.gnupg.agent = { + enable = true; + pinentryFlavor = "curses"; + }; + # Required to make gpg work without a graphical environment? + # otherwise generating a new gpg key fails with this error: + # gpg: agent_genkey failed: No pinentry + # see: https://github.com/NixOS/nixpkgs/issues/97861#issuecomment-827951675 + environment.variables = { + GPG_TTY = "$(tty)"; + }; +} diff --git a/hosts/nachtigall/apps/homepage.nix b/hosts/nachtigall/apps/homepage.nix new file mode 100644 index 0000000..37e9db0 --- /dev/null +++ b/hosts/nachtigall/apps/homepage.nix @@ -0,0 +1,72 @@ +{ + config, + lib, + pkgs, + self, + ... +}: let +in { + services.caddy.virtualHosts = { + "pub.solar" = { + logFormat = lib.mkForce '' + output discard + ''; + extraConfig = '' + # Named matcher, used below for Mastodon webfinger + @query query resource=* + + # PubSolarOS images + handle /os/download/* { + root * /data/srv/www + file_server /os/download/* browse + } + # serve base domain pub.solar for mastodon.pub.solar + # https://masto.host/mastodon-usernames-different-from-the-domain-used-for-installation/ + handle /.well-known/host-meta { + redir https://mastodon.pub.solar{uri} + } + + # Tailscale OIDC webfinger requirement plus Mastodon webfinger redirect + handle /.well-known/webfinger { + # Redirect requests that match /.well-known/webfinger?resource=* to Mastodon + handle @query { + redir https://mastodon.pub.solar{uri} + } + respond 200 { + body `{ + "subject": "acct:admins@pub.solar", + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://auth.pub.solar/realms/pub.solar" + } + ] + }` + } + } + + # redirect to statutes + redir /satzung https://cloud.pub.solar/s/2tRCP9aZFCiWxQy temporary + + # pub.solar website + handle { + root * /srv/www/pub.solar + try_files {path}.html {path} + file_server + } + # minimal error handling, respond with status code and text + handle_errors { + respond "{http.error.status_code} {http.error.status_text}" + } + ''; + }; + "www.pub.solar" = { + logFormat = lib.mkForce '' + output discard + ''; + extraConfig = '' + redir https://pub.solar{uri} + ''; + }; + }; +} diff --git a/hosts/nachtigall/apps/keycloak.nix b/hosts/nachtigall/apps/keycloak.nix new file mode 100644 index 0000000..376fcda --- /dev/null +++ b/hosts/nachtigall/apps/keycloak.nix @@ -0,0 +1,41 @@ +{ + flake, + config, + lib, + inputs, + pkgs, + self, + ... +}: { + age.secrets.keycloak-database-password = { + file = "${self}/secrets/keycloak-database-password.age"; + mode = "700"; + #owner = "keycloak"; + }; + + services.caddy.virtualHosts."auth.pub.solar" = { + # logFormat = lib.mkForce '' + # output discard + # ''; + extraConfig = '' + redir / /realms/pub.solar/account temporary + reverse_proxy :8080 + ''; + }; + + # keycloak + services.keycloak = { + enable = true; + database.passwordFile = config.age.secrets.keycloak-database-password.path; + settings = { + hostname = "auth.pub.solar"; + http-host = "127.0.0.1"; + http-port = 8080; + proxy = "edge"; + features = "declarative-user-profile"; + }; + themes = { + "pub.solar" = flake.inputs.keycloak-theme-pub-solar.legacyPackages.${pkgs.system}.keycloak-theme-pub-solar; + }; + }; +} diff --git a/hosts/nachtigall/apps/mailman.nix b/hosts/nachtigall/apps/mailman.nix new file mode 100644 index 0000000..bfe2587 --- /dev/null +++ b/hosts/nachtigall/apps/mailman.nix @@ -0,0 +1,116 @@ +{ + config, + lib, + pkgs, + self, + ... +}: let + # Source: https://github.com/NixOS/nixpkgs/blob/nixos-22.11/nixos/modules/services/mail/mailman.nix#L9C10-L10 + # webEnv is required by the mailman-uwsgi systemd service + inherit (pkgs.mailmanPackages.buildEnvs {}) webEnv; +in { + networking.firewall.allowedTCPPorts = [25]; + + services.caddy.virtualHosts."list.pub.solar" = { + # logFormat = lib.mkForce '' + # output discard + # ''; + extraConfig = '' + handle_path /static/* { + root * /var/lib/mailman-web-static + file_server + } + + reverse_proxy :18507 + ''; + }; + + services.postfix = { + enable = true; + relayDomains = ["hash:/var/lib/mailman/data/postfix_domains"]; + # get TLS certs for list.pub.solar from caddy + # TODO: when caddy renews certs, postfix doesn't know about it + # implement custom built caddy with events exec handler or systemd-reload + # hook so postfix reloads, too + sslCert = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/list.pub.solar/list.pub.solar.crt"; + sslKey = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/list.pub.solar/list.pub.solar.key"; + config = { + transport_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"]; + local_recipient_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"]; + }; + rootAlias = "admins@pub.solar"; + postmasterAlias = "admins@pub.solar"; + hostname = "list.pub.solar"; + }; + + systemd.paths.watcher-caddy-ssl-file = { + description = "Watches for changes in caddy's TLS cert file (after renewals) to reload postfix"; + documentation = ["systemd.path(5)"]; + partOf = ["postfix-reload.service"]; + pathConfig = { + PathChanged = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/list.pub.solar/list.pub.solar.crt"; + Unit = "postfix-reload.service"; + }; + wantedBy = ["multi-user.target"]; + }; + + systemd.services."postfix-reload" = { + description = "Reloads postfix config, e.g. after TLS certs change, notified by watcher-caddy-ssl-file.path"; + documentation = ["systemd.path(5)"]; + requires = ["postfix.service"]; + after = ["postfix.service"]; + startLimitIntervalSec = 10; + startLimitBurst = 5; + serviceConfig.Type = "oneshot"; + script = '' + ${pkgs.systemd}/bin/systemctl reload postfix + ''; + wantedBy = ["multi-user.target"]; + }; + + services.mailman = { + enable = true; + # We use caddy instead of nginx + #serve.enable = true; + hyperkitty.enable = true; + webHosts = ["list.pub.solar"]; + siteOwner = "admins@pub.solar"; + }; + + # TODO add django-keycloak as auth provider + # https://django-keycloak.readthedocs.io/en/latest/ + ## Extend settings.py directly since this can't be done via JSON + ## settings (services.mailman.webSettings) + #environment.etc."mailman3/settings.py".text = '' + # INSTALLED_APPS.extend([ + # "allauth.socialaccount.providers.github", + # "allauth.socialaccount.providers.gitlab" + # ]) + #''; + + systemd.services.mailman-uwsgi = let + uwsgiConfig.uwsgi = { + type = "normal"; + plugins = ["python3"]; + home = webEnv; + manage-script-name = true; + mount = "/=mailman_web.wsgi:application"; + http = "127.0.0.1:18507"; + }; + uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig); + in { + wantedBy = ["multi-user.target"]; + after = ["postgresql.service"]; + requires = ["mailman-web-setup.service" "postgresql.service"]; + restartTriggers = [config.environment.etc."mailman3/settings.py".source]; + serviceConfig = { + # Since the mailman-web settings.py obstinately creates a logs + # dir in the cwd, change to the (writable) runtime directory before + # starting uwsgi. + ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override {plugins = ["python3"];}}/bin/uwsgi --json ${uwsgiConfigFile}"; + User = "mailman-web"; + Group = "mailman"; + RuntimeDirectory = "mailman-uwsgi"; + }; + }; +}