{ 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) 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)); # 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 a wrapper script (same pattern as openldap). 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: - domain: - "auth.${domain}" policy: "bypass" - domain: - "ldapadmin.${domain}" subject: - "group:admins" policy: "two_factor" - domain: - "ldapadmin.${domain}" policy: "deny" - domain: - "torrent.${domain}" subject: - "group:admins" policy: "two_factor" - domain: - "torrent.${domain}" policy: "deny" - domain: - "git.${domain}" policy: "one_factor" - domain: - "nextcloud.${domain}" policy: "one_factor" - domain: - "jellyfin.${domain}" policy: "one_factor" - domain: - "uptime.${domain}" subject: - "group:admins" policy: "two_factor" - domain: - "uptime.${domain}" policy: "deny" - domain: - "grafana.${domain}" subject: - "group:admins" policy: "two_factor" - domain: - "grafana.${domain}" policy: "deny" # ntfy: bypass — ntfy enforces its own token/password auth; # the mobile app must be able to connect without Authelia SSO. - domain: - "ntfy.${domain}" policy: "bypass" 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 = { enable = lib.mkEnableOption "Authelia SSO gateway"; 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 { # ----------------------------------------------------------------------- # 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" ]; }; # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- homey.monitoring.monitors = [{ name = "Authelia"; url = "https://auth.${domain}/api/health"; interval = 60; }]; }; }