{ config, lib, pkgs, homeyConfig, ... }: # Authelia — SSO gateway. # # Connects to OpenLDAP on 127.0.0.1:389. # Exposes port 9091 on localhost; Caddy reverse-proxies it and provides # the forward_auth endpoint for protected vhosts. # # Volume layout: # /authelia/config/ → /config (sqlite db, notification log, etc.) # # The configuration file is rendered by Nix (no Go templates) and written # to a NixOS-managed path, then bind-mounted read-only into the container. # # Secrets consumed from sops: # authelia/jwt_secret # authelia/session_secret # authelia/storage_encryption_key # openldap/ro_password (shared with openldap module) # # Access control rules are NOT declared here. Each service module contributes # its own rules via homey.authelia.accessControlRules, which are sorted by # priority and merged into the final config at build time. let cfg = config.homey.authelia; dataDir = config.homey.storage.mountPoint; domain = homeyConfig.domain; # LDAP base DN derived from domain: zakobar.com → dc=zakobar,dc=com ldapBaseDN = lib.concatStringsSep "," (map (p: "dc=${p}") (lib.splitString "." domain)); # Render a single access_control rule attrset to a YAML list item. # Indented for insertion into the access_control.rules block (4 spaces # before "- domain:", matching the 2-space indent of "rules:"). renderRule = rule: let domainLines = lib.concatMapStringsSep "\n" (d: " - \"${d}\"") rule.domain; subjectBlock = lib.optionalString (rule.subject != []) ( "\n subject:\n" + lib.concatMapStringsSep "\n" (s: " - \"${s}\"") rule.subject ); resourcesBlock = lib.optionalString (rule.resources != []) ( "\n resources:\n" + lib.concatMapStringsSep "\n" (r: " - \"${r}\"") rule.resources ); in " - domain:\n${domainLines}${subjectBlock}${resourcesBlock}\n policy: \"${rule.policy}\"\n"; sortedRules = lib.sort (a: b: a.priority < b.priority) cfg.accessControlRules; rulesYaml = lib.concatStrings (map renderRule sortedRules); # The authelia config is written as a Nix string so all values are # resolved at build time except for secrets, which are injected at # runtime via environment variables. autheliaConfig = '' ############################################################### # Authelia configuration # # Generated by NixOS — do not edit by hand # ############################################################### theme: "light" log: level: "info" # jwt_secret injected at runtime via env var AUTHELIA_JWT_SECRET_FILE authentication_backend: ldap: implementation: "custom" url: "ldap://openldap:389" timeout: "5s" start_tls: false base_dn: "${ldapBaseDN}" users_filter: "({username_attribute}={input})" username_attribute: "uid" additional_users_dn: "ou=users" groups_filter: "(&(uniquemember=uid={input},ou=users,${ldapBaseDN})(objectclass=groupOfUniqueNames))" group_name_attribute: "cn" additional_groups_dn: "ou=groups" mail_attribute: "mail" display_name_attribute: "uid" permit_referrals: false permit_unauthenticated_bind: false user: "cn=readonly,${ldapBaseDN}" # password injected at runtime via AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE totp: issuer: "${domain}" disable: false session: name: authelia_session # secret injected at runtime via AUTHELIA_SESSION_SECRET_FILE expiration: 3600 inactivity: 7200 domain: "${domain}" storage: local: path: "/config/db.sqlite3" # encryption_key injected at runtime via AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE access_control: default_policy: "deny" rules: ${rulesYaml} notifier: filesystem: filename: "/config/emails.txt" ntp: address: "udp://time.cloudflare.com:123" version: 3 max_desync: "3s" disable_startup_check: false disable_failure: true ''; in { options.homey.authelia = { # Declared unconditionally so any service module can contribute rules # even when Authelia itself is disabled. accessControlRules = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { priority = lib.mkOption { type = lib.types.int; default = 100; description = "Order within access_control.rules — lower values appear first. Authelia evaluates rules top-to-bottom and stops at the first match."; }; domain = lib.mkOption { type = lib.types.listOf lib.types.str; description = "Domain glob(s) this rule matches."; }; policy = lib.mkOption { type = lib.types.enum [ "bypass" "one_factor" "two_factor" "deny" ]; description = "Authelia policy applied when the rule matches."; }; subject = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; description = "Optional subject constraints (e.g. \"group:admins\")."; }; resources = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; description = "Optional URL path regex constraints."; }; }; }); default = []; description = "Access control rules contributed by service modules. Merged and sorted by priority at build time."; }; enable = lib.mkEnableOption "Authelia SSO gateway" // { default = true; }; image = lib.mkOption { type = lib.types.str; default = "docker.io/authelia/authelia:latest"; }; port = lib.mkOption { type = lib.types.port; default = 9091; description = "Host port Authelia listens on (bound to 127.0.0.1)."; }; }; config = lib.mkIf cfg.enable { # ----------------------------------------------------------------------- # Authelia's own bypass rule — must be first so the login UI is reachable. # ----------------------------------------------------------------------- homey.authelia.accessControlRules = [{ priority = 0; domain = [ "auth.${domain}" ]; policy = "bypass"; }]; # ----------------------------------------------------------------------- # Secrets # ----------------------------------------------------------------------- sops.secrets."authelia/jwt_secret" = { owner = "root"; }; sops.secrets."authelia/session_secret" = { owner = "root"; }; sops.secrets."authelia/storage_encryption_key" = { owner = "root"; }; # openldap/ro_password is declared in openldap.nix; reference it here too # (sops-nix deduplicates identical declarations) sops.secrets."openldap/ro_password" = { owner = "root"; }; # ----------------------------------------------------------------------- # Write the config file into /etc (read-only in the container) # ----------------------------------------------------------------------- environment.etc."authelia/configuration.yml" = { text = autheliaConfig; mode = "0444"; }; # ----------------------------------------------------------------------- # Container # ----------------------------------------------------------------------- virtualisation.oci-containers.containers.authelia = { image = cfg.image; ports = [ "127.0.0.1:${toString cfg.port}:9091" ]; environment = { TZ = homeyConfig.timezone; # Tell authelia to read secrets from files (its native mechanism) AUTHELIA_JWT_SECRET_FILE = "/run/secrets/jwt_secret"; AUTHELIA_SESSION_SECRET_FILE = "/run/secrets/session_secret"; AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = "/run/secrets/storage_encryption_key"; AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = "/run/secrets/ldap_ro_password"; # Changing this forces a container restart when the config changes. # NixOS bind-mounts resolve symlinks at container start, so the running # container would otherwise keep the old nix-store config until restarted. NIXOS_CONFIG_HASH = builtins.hashString "sha256" autheliaConfig; }; volumes = [ "/etc/authelia/configuration.yml:/config/configuration.yml:ro" "${dataDir}/authelia/config:/config" # Mount sops secret files into the container under /run/secrets/ "${config.sops.secrets."authelia/jwt_secret".path}:/run/secrets/jwt_secret:ro" "${config.sops.secrets."authelia/session_secret".path}:/run/secrets/session_secret:ro" "${config.sops.secrets."authelia/storage_encryption_key".path}:/run/secrets/storage_encryption_key:ro" "${config.sops.secrets."openldap/ro_password".path}:/run/secrets/ldap_ro_password:ro" ]; extraOptions = [ "--network=homey" "--hostname=authelia" ]; }; # ----------------------------------------------------------------------- # Systemd — wait for openldap and external HD # ----------------------------------------------------------------------- systemd.services."podman-authelia" = { after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" "podman-homey-network.service" ]; }; # ----------------------------------------------------------------------- # Caddy virtual host — no forward_auth (Authelia IS the auth gateway) # ----------------------------------------------------------------------- homey.caddy.virtualHosts = [{ subdomain = "auth"; port = cfg.port; auth = false; }]; # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- homey.storage.extraDirs = [ { path = "authelia"; } { path = "authelia/config"; } ]; # ----------------------------------------------------------------------- # Backup # ----------------------------------------------------------------------- homey.backup.extraPaths = [ "${dataDir}/authelia" ]; # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- homey.monitoring.monitors = [{ name = "Authelia"; url = "https://auth.${domain}/api/health"; interval = 60; }]; }; }