diff --git a/lib/xlib/dirs.nix b/lib/xlib/dirs.nix index a3537a6..ecf8368 100644 --- a/lib/xlib/dirs.nix +++ b/lib/xlib/dirs.nix @@ -24,6 +24,7 @@ in services-folder = "${server-home}/Services"; services-nodes-folder = "${services-mnt-folder}/nodes"; postgresql-folder = "${services-mnt-folder}/postgresql"; + authelia-folder = "${services-mnt-folder}/authelia"; music-library = "${user-home}/Music"; archive-drive = "/mnt/archive"; diff --git a/modules/essentials/shell.nix b/modules/essentials/shell.nix index b2ed6ab..c22b090 100644 --- a/modules/essentials/shell.nix +++ b/modules/essentials/shell.nix @@ -63,6 +63,29 @@ z-p-1 = "ssh pubray-1"; z-map-local-proxy = "ssh -R 10808:localhost:10808"; + # authelia — Argon2id hash with the exact params configured for the + # authelia daemon (memory=65536, iterations=3, parallelism=4, + # salt-length=16). Defaults already match, but explicit so an upstream + # default change can't silently produce a hash the daemon rejects. + # + # Note: the old `authelia hash-password --argon2id` form is from + # Authelia ≤4.35. Current CLI is `crypto hash generate argon2 + # -v argon2id`. The subcommand takes no positional password — it + # prompts with confirmation (omit confirmation with `--no-confirm`) + # or accepts `--password `. + # + # Example usage: + # authelia-hash + # # interactive prompt (twice — confirmation), then hash on stdout + # authelia-hash --no-confirm + # # single prompt, hash on stdout + # authelia-hash --no-confirm --password 'mypassword' + # # non-interactive (for scripts) + # + # Output goes to stdout — append it to users_database.yml by hand: + # authelia-hash --no-confirm >> /mnt/services/authelia/users_database.yml + authelia-hash = "authelia crypto hash generate argon2 -v argon2id -i 3 -m 65536 -p 4 -s 16"; + # Somethings reboot-bios = "sudo systemctl reboot --firmware-setup"; diff --git a/modules/server/authelia.nix b/modules/server/authelia.nix index 8a6e0e8..5f46b62 100644 --- a/modules/server/authelia.nix +++ b/modules/server/authelia.nix @@ -2,6 +2,7 @@ config, lib, pkgs, + xlib, ... }: # Authelia — SSO reverse-proxy (single-factor password login) for protected @@ -12,26 +13,30 @@ # Wiring: # - The internal API listens on 127.0.0.1:9091 only (overrides the nixpkgs # default `tcp://:9091/` which would bind all interfaces). -# - Three secrets (jwt, storage encryption key, users_database) come from +# - Two secrets (jwt, storage encryption key) come from # modules/server/secrets/authelia.yaml via sops-nix, materialised at # /run/secrets/ by the time authelia.service starts. # - nginx is the only ingress: authelia.zeroq.su vhosts the login UI and # every protected vhost (currently vtimeline.zeroq.su) does # `auth_request /authelia` against 127.0.0.1:9091 (see nginx.nix). -# - users_database.yml is symlinked into /var/lib/authelia/ so the path -# configured in `settings.authentication_backend.file.path` resolves -# to the sops materialised file. The symlink target is created by -# sops-nix before this unit starts, so no race. +# - users_database lives at ${xlib.dirs.authelia-folder}/users_database.yml +# under /mnt/services/authelia/ — outside of sops, intentionally, so it +# can be edited at runtime without `nixos-rebuild` (authelia lazy-reads +# the file on each authentication attempt). The directory + file are +# pre-created by systemd.tmpfiles below with `authelia:authelia` 0750/0400. # # Version: pinned transitively via flake inputs.nixpkgs → pkgs.authelia. -# `nix flake update` will roll it forward; no overlay needed. +# `nix flake update` will roll it forward; no overlay needed. Package is +# also exposed via `environment.systemPackages` so the `authelia` CLI is on +# PATH for the `authelia-hash` shell alias (Argon2id password hashing). # # Secrets layout in modules/server/secrets/authelia.yaml (sops-encrypted): # jwt_secret -> /run/secrets/authelia-jwt-secret # storage_encryption_key -> /run/secrets/authelia-storage-encryption-key -# users_database -> /run/secrets/authelia-users-database -# (multiline YAML string, written verbatim by -# sops-nix and consumed as a settingsFile) +# +# The users database is NOT in this file — it lives at +# ${xlib.dirs.authelia-folder}/users_database.yml (see the wiring block +# above), edited at runtime without `nixos-rebuild`. # # Guarded by `builtins.pathExists` so a missing sops file does NOT break # `nixos-rebuild switch` — the flake evaluates, Authelia stays disabled @@ -76,6 +81,22 @@ in }; config = lib.mkIf cfg.enable { + # Authelia CLI binary on PATH so the `authelia-hash` shell alias works + # without `nix-shell`. Same `pkgs.authelia` as the daemon's `package` + # below — nothing is rebuilt, just exposed. + environment.systemPackages = [ pkgs.authelia ]; + + # Pre-create the runtime users_database location at /mnt/services/authelia/. + # Authelia is enabled only on `server`-typed hosts (see server/default.nix) + # where /mnt/services is the durable mountpoint from `mkServiceStorage`. + # The "d" rule creates the directory; the "f" rule seeds an empty file + # if absent so authelia.service starts cleanly (and rejects every login + # until the file is populated — desired safe default). + systemd.tmpfiles.rules = [ + "d ${xlib.dirs.authelia-folder} 0750 authelia authelia - -" + "f ${xlib.dirs.authelia-folder}/users_database.yml 0400 authelia authelia - -" + ]; + services.authelia.instances."" = { enable = true; # Default is already pkgs.authelia; pinned here for clarity and to @@ -98,13 +119,14 @@ in format = "text"; }; authentication_backend.file = { - # Read directly from the sops materialised file at /run/secrets/. + # Live users database under /mnt/services/authelia/users_database.yml. # Authelia does NOT validate-config this path — it only opens it - # when verifying a user password (lazy read). Putting the same - # file into settingsFiles would force viper to parse it as - # configuration, and the `users:` top-level key would fail the - # schema check (users.* is schema-foreign). - path = sopsPath "authelia-users-database"; + # when verifying a user password (lazy read on every login attempt), + # so the file can be edited at runtime without restarting the + # service. Permissions/owner are set by the systemd.tmpfiles rule + # above; sops materialisation was removed intentionally so editing + # works without `nixos-rebuild`. + path = "${xlib.dirs.authelia-folder}/users_database.yml"; password = { algorithm = "argon2id"; iterations = 3; @@ -178,7 +200,10 @@ in # evaluates when ./secrets/authelia.yaml hasn't been created yet — # a clean checkout would otherwise fail every nixos-rebuild switch. # Once the file exists and is encrypted, this condition becomes true - # and the three secrets are wired in. + # and the two secrets (jwt + storage encryption key) are wired in. + # `users_database` is not sops-managed — see the comment on + # `settings.authentication_backend.file.path` above for its runtime + # location under ${xlib.dirs.authelia-folder}/. sops.secrets = lib.optionalAttrs sopsReady { "authelia-jwt-secret" = { format = "yaml"; @@ -196,19 +221,6 @@ in group = "authelia"; mode = "0400"; }; - # users_database is a multiline YAML string in the sops file - # (top-level `users_database: |` block with `users: : ...` - # beneath). sops-nix writes the decoded block verbatim to - # /run/secrets/authelia-users-database, where the symlink rule - # above makes it appear at /var/lib/authelia/users_database.yml. - "authelia-users-database" = { - format = "yaml"; - key = "users_database"; - sopsFile = ./secrets/authelia.yaml; - owner = "authelia"; - group = "authelia"; - mode = "0400"; - }; }; }; } \ No newline at end of file diff --git a/modules/server/secrets/authelia.yaml b/modules/server/secrets/authelia.yaml index 27967c6..86533c2 100644 --- a/modules/server/secrets/authelia.yaml +++ b/modules/server/secrets/authelia.yaml @@ -1,7 +1,5 @@ jwt_secret: ENC[AES256_GCM,data:Sq3SR3GF2PKU/EmtosEIgkVBGx0ycQfYBy3SmNB46bflTX3HvjY7gXSXt13khLRNjYwqnLQ/VWnhSF7QmjO+QA==,iv:L1c/Tb1nb1JNY9FdU7SoZih9CdRO0sDErA+OBmCe1nY=,tag:aYS8NoMW1OqVT/q60nVU6A==,type:str] -session_secret: ENC[AES256_GCM,data:gJnvSc8IvfJEemptMvq72Tiv6O19E63fSpzPtzKy4u1a9HdwUGJADz9nzQbLTqk5lP4OSQnKEgZyiDvCpFNE6A==,iv:oWG/ht5McEGqYXdls4BsDSLMF4G8lX957urZL3vlyxY=,tag:SoY4DvzdPrVftKOQQcgmfQ==,type:str] storage_encryption_key: ENC[AES256_GCM,data:1uoto0pqv1ahIxc00XzY+X0I0Eb3po/FPWOsmyFlcOhtpzK3od0+4a2jL8PicqbIf6F0hNp1gYUc11ofO7Mj1g==,iv:IFw4Y/cL3Hqa0fIPeKhN3SdrlOLFFJiJLe1MTNue+gk=,tag:eSAFNxpkaYWkyFVoWzSuMA==,type:str] -users_database: ENC[AES256_GCM,data:KCbWYaXNk33ybfYrE7oJPREBLSQDlQfdzxzbCqYTX4ZTjb3R3yRPzRCqLzjy3UP165q5MjOcsXmluJ4U0Apd8+sKJj5+XkEL3GMIhxExB2JpVjriyObX4iayuoUlYXD7JVTBVVIZIfXWH5ySTbLw35sN0LmbBaRPSE5YNliOtUe91W82nbifP3qYvOaYFYFe5MaBLc2XpaWJcv+Gio3iQ5X5mYhTgwNMlCZK5/KqlS3oNQswkuvSuzWNG3+/ev+Byt8KY6ec/bnM74ebTs7obJK3,iv:mEFmiDdzw+s7jusN4GggZ7FO5C2IUUKGAJ8PIBMC/KY=,tag:MwlfPGs3egq0B4RxfAQ+Jw==,type:str] sops: age: - enc: | @@ -13,7 +11,7 @@ sops: ra8EJtInXy4HeNWye9sdR3BHD7QWYT46RAHBSdn+4SxtV6LOHiXBgg== -----END AGE ENCRYPTED FILE----- recipient: age13l2gtk0nzr484zprp7e0pkrt0ne0j4asyn2pjmlaw73nte7t7d8q4sqtxm - lastmodified: "2026-10-08T00:38:38Z" - mac: ENC[AES256_GCM,data:EERhYt8a9ElV9RvcvqcM7lxLE9lwx0juW9RQgZD1rNaLs8wx+/1hTt7HmVRyiuFwMOmmL3lrwxC7cBV7GUkF6hbnWz+tuDO1EXBq3ooN/KIikFnjia6A95TinZzRKYmv3K/CdISGu5yGpZ9bVSqaq0YdB0MB32e6Q0iEXVxda4o=,iv:UZxgFjGs6FqavbTk4XYAs1MmCLgV3JktOrmVcy/nM3Q=,tag:EP7z+S1vyRSn8vchc0tgRw==,type:str] + lastmodified: "2026-10-09T13:32:43Z" + mac: ENC[AES256_GCM,data:Sde8LhaRmbGu43Aa+jvV3ZjR6e1wkAXVOtoQ4fcmi4UJEWPGJNUu6fGHRCWEvlNFo1XCN0WEQuC9yycMaz8nIGizCAT8P31qgamV+f++Anxct1TwcF65H4ZN7cfAyqJaODGBsue7GjvrSJMLstOEB37sQKrK/e3ktG8lTtUzKsA=,iv:5eQmj/xy51TKwPF/0joezYga0LYjeik7FK1PamPbP6M=,tag:bHw04M8wpRDTX6uM8vz2kQ==,type:str] unencrypted_suffix: _unencrypted version: 3.13.3