diff --git a/systems/jeeves/kanidm.md b/systems/jeeves/kanidm.md new file mode 100644 index 0000000..1dc3741 --- /dev/null +++ b/systems/jeeves/kanidm.md @@ -0,0 +1,96 @@ +# Kanidm on Jeeves + +Kanidm is served at **https://auth.tmmworkshop.com**. Portal terminates public +HTTPS and forwards requests over Tailscale to Jeeves on port 8443, verifying +Jeeves's TLS certificate. Only the Tailscale interface exposes that backend port. + +The service module is automatically imported by `services/default.nix`. +`services/kanidm.nix` pins Kanidm to the 1.11 release series; review the +[upgrade guide](https://kanidm.github.io/kanidm/stable/server_updates.html) +before changing that series. The flake lock determines the patch release. + +## Before deployment + +1. Point `auth.tmmworkshop.com` at Portal's public address, using the same DNS + arrangement as `gitea.tmmworkshop.com`. Port 80 must reach Portal for ACME. +2. Enable [HTTPS certificates in Tailscale](https://tailscale.com/kb/1153/enabling-https). + Jeeves must be able to obtain a certificate for `jeeves.taile39c31.ts.net`. +3. Allow Portal to reach Jeeves on TCP 8443 in your Tailscale policy. + +The trusted proxy address in `services/kanidm.nix` is Portal's current Tailscale +IPv4 address, `100.80.102.33`. Update it if Portal is re-enrolled with a new address. +HAProxy connects over IPv4 and replaces incoming `X-Forwarded-For` headers. + +## Deploy + +Ensure the new Nix file is tracked by Git before using this Git-backed flake. +From this repository, using the existing SSH aliases: + +```sh +nixos-rebuild switch --flake .#jeeves --target-host jeeves --sudo +nixos-rebuild switch --flake .#portal-1 --target-host portal-1 --sudo +``` + +Deploy Jeeves first, then Portal. Portal manages the public Let's Encrypt +certificate. `kanidm-tls.service` on Jeeves obtains the backend certificate and +checks for renewal daily, restarting Kanidm only when the certificate changes. +Private keys are generated at runtime and never stored in this repository or +the Nix store. + +On Jeeves, check startup: + +```sh +sudo systemctl status kanidm-tls.service kanidm.service +sudo journalctl -u kanidm-tls.service -u kanidm.service -n 100 +``` + +If the initial certificate request fails, fix the Tailscale HTTPS configuration +and run `sudo systemctl restart kanidm.service`; its dependency retries the +certificate request. A successful oneshot certificate service normally appears +as inactive after finishing. + +After both hosts are deployed: + +```sh +curl --fail https://auth.tmmworkshop.com/status +``` + +## First login + +On Jeeves, recover the initial administrator passwords interactively: + +```sh +sudo kanidmd recover-account admin +sudo kanidmd recover-account idm_admin +kanidm login --name idm_admin +kanidm person create richie "Richie" +kanidm person credential create-reset-token richie +``` + +Store the recovered passwords securely. Follow the reset URL to enroll your +personal account's passkey. `admin` manages the Kanidm server; `idm_admin` +manages people, groups, and application integrations. Creating `richie` does +not automatically grant administrator rights. The client is installed and +configured to use `https://auth.tmmworkshop.com`. + +Register each website as its own OAuth2/OIDC client and each backend identity +as a service account, then grant application scopes through groups. This +deployment does not create application clients or change existing sites' login +settings. See the [OAuth2 guide](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html) +and [service-account guide](https://kanidm.github.io/kanidm/stable/accounts/service_accounts.html). + +## Storage and recovery + +- Database: `/var/lib/kanidm/kanidm.db` on Jeeves's existing ZFS `/var` filesystem. +- Online backups: `/var/lib/kanidm/backups`, daily at 02:30, retaining 14. +- Backend certificate and key: `/var/lib/kanidm-tls`, readable by Kanidm. +- Server configuration: `/etc/kanidm/server.toml`. + +The existing `root_pool/var` snapshot policy covers the database and backups. +Copy backups off Jeeves as part of disaster recovery; local snapshots do not +protect against loss of the host. Follow Kanidm's +[backup and restore instructions](https://kanidm.github.io/kanidm/stable/backup_and_restore.html). + +The configured identity domain and origin are both `auth.tmmworkshop.com`. +Changing them after enrollment can affect passkeys and application integrations; +follow the [domain rename guide](https://kanidm.github.io/kanidm/stable/domain_rename.html). diff --git a/systems/jeeves/networking/firewall.nix b/systems/jeeves/networking/firewall.nix index ae2ead0..4cebd7a 100644 --- a/systems/jeeves/networking/firewall.nix +++ b/systems/jeeves/networking/firewall.nix @@ -8,6 +8,7 @@ let grafana = 3000; jellyfin_http = 8096; jellyswarrm = 3100; + kanidm = 8443; nix_binary_cache = 5000; ollama = 11434; open_webui = 8080; @@ -28,6 +29,7 @@ in ports.gems ports.jellyfin_http ports.jellyswarrm + ports.kanidm ports.nix_binary_cache ports.share ]; diff --git a/systems/jeeves/services/kanidm.nix b/systems/jeeves/services/kanidm.nix new file mode 100644 index 0000000..1857d22 --- /dev/null +++ b/systems/jeeves/services/kanidm.nix @@ -0,0 +1,101 @@ +{ config, pkgs, ... }: +let + certificateDir = "/var/lib/kanidm-tls"; +in +{ + services.kanidm = { + # Pin the minor release: Kanidm upgrades require following its upgrade guide. + package = pkgs.kanidm_1_11; + server = { + enable = true; + settings = { + domain = "auth.tmmworkshop.com"; + origin = "https://auth.tmmworkshop.com"; + bindaddress = "0.0.0.0:8443"; + db_fs_type = "zfs"; + tls_chain = "${certificateDir}/certificate.pem"; + tls_key = "${certificateDir}/private-key.pem"; + + # Only Portal may supply the original client address. HAProxy replaces + # X-Forwarded-For, so clients cannot supply a forged address themselves. + http_client_address_info.x-forward-for = [ "100.80.102.33" ]; + + # /var/lib/kanidm is on root_pool/var, which is already snapshotted. + online_backup = { + schedule = "30 2 * * *"; + versions = 14; + }; + }; + }; + client = { + enable = true; + settings.uri = "https://auth.tmmworkshop.com"; + }; + }; + + systemd = { + services = { + kanidm = { + requires = [ "kanidm-tls.service" ]; + after = [ "kanidm-tls.service" ]; + }; + + # Kanidm requires TLS even behind HAProxy. Use Jeeves's Tailscale + # certificate so Portal can verify both the CA and the backend hostname. + # HTTPS certificates must be enabled in the Tailscale admin console. + kanidm-tls = { + description = "Obtain and renew Kanidm's backend TLS certificate"; + requires = [ "tailscaled.service" ]; + wants = [ "tailscaled-autoconnect.service" ]; + after = [ + "tailscaled.service" + "tailscaled-autoconnect.service" + ]; + path = [ + config.services.tailscale.package + pkgs.coreutils + pkgs.systemd + ]; + serviceConfig = { + Type = "oneshot"; + User = "root"; + Group = "kanidm"; + StateDirectory = "kanidm-tls"; + StateDirectoryMode = "0750"; + RuntimeDirectory = "kanidm-tls"; + RuntimeDirectoryMode = "0700"; + UMask = "0077"; + TimeoutStartSec = "2min"; + }; + script = '' + set -euo pipefail + tailscale cert --min-validity 720h \ + --cert-file /run/kanidm-tls/certificate.pem \ + --key-file /run/kanidm-tls/private-key.pem \ + jeeves.taile39c31.ts.net + + if ! cmp -s /run/kanidm-tls/certificate.pem ${certificateDir}/certificate.pem \ + || ! cmp -s /run/kanidm-tls/private-key.pem ${certificateDir}/private-key.pem; then + install -m 0640 -o root -g kanidm /run/kanidm-tls/certificate.pem ${certificateDir}/certificate.pem + install -m 0640 -o root -g kanidm /run/kanidm-tls/private-key.pem ${certificateDir}/private-key.pem + + # The NixOS service bind-mounts the certificate files, so restart to + # pick up renewals. Do not wait on a service that depends on this one. + if systemctl is-active --quiet kanidm.service; then + systemctl --no-block try-restart kanidm.service + fi + fi + ''; + }; + }; + + timers.kanidm-tls = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + RandomizedDelaySec = "1h"; + }; + }; + }; +} diff --git a/systems/portal-1/haproxy/haproxy.cfg b/systems/portal-1/haproxy/haproxy.cfg index 6f88d18..5a9587f 100644 --- a/systems/portal-1/haproxy/haproxy.cfg +++ b/systems/portal-1/haproxy/haproxy.cfg @@ -32,7 +32,7 @@ defaults #Application Setup frontend ContentSwitching bind [::]:80 v4v6 - bind [::]:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/gems.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem crt + bind [::]:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/auth.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/gems.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem mode http # ACME challenge routing (must be first) @@ -40,6 +40,7 @@ frontend ContentSwitching # Host ACLs (defined early so rate-limiting can scope to a single vhost) acl host_audiobookshelf hdr(host) -i audiobookshelf.tmmworkshop.com + acl host_auth hdr(host) -i auth.tmmworkshop.com acl host_cache hdr(host) -i cache.tmmworkshop.com acl host_jellyfin hdr(host) -i jellyfin.tmmworkshop.com acl host_share hdr(host) -i share.tmmworkshop.com @@ -111,6 +112,7 @@ frontend ContentSwitching use_backend acme_challenge if is_acme use_backend audiobookshelf_nodes if host_audiobookshelf + use_backend kanidm if host_auth use_backend cache_nodes if host_cache use_backend jellyfin if host_jellyfin use_backend share_nodes if host_share @@ -141,6 +143,15 @@ backend audiobookshelf_nodes mode http server server jeeves:8000 +backend kanidm + mode http + http-request set-header X-Forwarded-For %[src] + http-request set-header X-Forwarded-Proto https + option httpchk + http-check send meth GET uri /status ver HTTP/1.1 hdr Host auth.tmmworkshop.com + http-check expect status 200 + server kanidm jeeves.taile39c31.ts.net:8443 ssl verify required ca-file /etc/ssl/certs/ca-certificates.crt verifyhost jeeves.taile39c31.ts.net sni str(jeeves.taile39c31.ts.net) check check-sni jeeves.taile39c31.ts.net resolve-prefer ipv4 + backend cache_nodes mode http server server jeeves:5000