From 8d96ef7684bac912cd5508e1e73288279b33d0ea Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:01:45 -0600 Subject: [PATCH 1/3] fix (podman-secrets): Use user systemd unit User systemd units cannot wait for system units. Fix race condition bugs in user Quadlet services by using a user service that the Quadlet services can use as a `After=` or `Requires=` directive. --- modules/podman-secrets.nix | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/modules/podman-secrets.nix b/modules/podman-secrets.nix index 83073b3..fb99285 100644 --- a/modules/podman-secrets.nix +++ b/modules/podman-secrets.nix @@ -77,7 +77,7 @@ let (builtins.readFile ./../bin/secret-translator.jl); # Submodule type for each service's secrets configuration - serviceSecretsType = types.submodule { + serviceSecretsType = types.submodule ({ config, name, ... }: { options = { user = mkOption { type = types.str; @@ -97,18 +97,28 @@ let ''; default = [ ]; }; - }; - }; - # Generate a systemd service for each configured service + ref = mkOption { + type = types.str; + description = + "Reference to the systemd service name for declaring dependencies"; + readOnly = true; + default = "podman-secrets-${name}.service"; + example = "podman-secrets-caddy.service"; + }; + }; + }); + + # Generate a systemd user service for each configured service mkSecretsService = name: serviceCfg: nameValuePair "podman-secrets-${name}" { description = "Podman secrets converter service for ${name}"; - wantedBy = [ "multi-user.target" ]; + wantedBy = [ "default.target" ]; + + unitConfig.ConditionUser = "${serviceCfg.user}"; serviceConfig = { Type = "oneshot"; - User = serviceCfg.user; ProtectProc = "invisible"; ExecStart = "${secret-translator}/bin/secret-translator ${ lib.concatStringsSep " " serviceCfg.secrets-files @@ -128,8 +138,8 @@ in { type = types.attrsOf serviceSecretsType; description = '' Per-service Podman secrets configuration. - Each attribute creates a separate systemd service that translates TOML secrets - files into Podman secrets for the specified user. + Each attribute creates a separate systemd user service that translates TOML secrets + files into Podman secrets. ''; example = literalExpression '' { @@ -152,6 +162,6 @@ in { }; config = mkIf (enabledServices != { }) { - systemd.services = lib.mapAttrs' mkSecretsService enabledServices; + systemd.user.services = lib.mapAttrs' mkSecretsService enabledServices; }; } From b2477b9f24d650cc13cda7abd0523234e4d54c9f Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:03:20 -0600 Subject: [PATCH 2/3] fix (authentik): Too many fixes to list Bad on me, but I have spent way too long making edits that are all required on mcentire to get Authentik semi-working. There are lots of notes in here on reasoning of why stuff is the way it is. Backup still needs to be configured, and potentially Crowdsec. --- services/authentik.nix | 225 ++++++++++++++++++++++++++--------------- 1 file changed, 141 insertions(+), 84 deletions(-) diff --git a/services/authentik.nix b/services/authentik.nix index ce924b6..ee57695 100644 --- a/services/authentik.nix +++ b/services/authentik.nix @@ -5,8 +5,8 @@ let port = "9000"; in { - # Secrets are translated in the system scope, but performed by the user, - # so are available in the user's Podman secrets + # Secrets are translated in the system NixOS scope, but performed by the user, + # via a user systemd unit, so are available in the user's Podman secrets age.secrets = { "authentik.toml" = { file = ./../secrets/authentik.toml.age; @@ -19,7 +19,15 @@ in { secrets-files = [ config.age.secrets."authentik.toml".path ]; }; - systemd.tmpfiles.rules = [ "d ${state-directory} 1775 ${user} ${user} -" ]; + # Podman, unlike Docker apparently, does not automatically create mount points + # within folders, so every mounted folder needs to be specified here. + systemd.tmpfiles.rules = [ + "d ${state-directory} 1775 ${user} ${user} -" + "d ${state-directory}/database 1775 ${user} ${user} -" + "d ${state-directory}/media 1775 ${user} ${user} -" + "d ${state-directory}/certs 1775 ${user} ${user} -" + "d ${state-directory}/custom-templates 1775 ${user} ${user} -" + ]; services.caddy.virtualHosts."auth.millironx.com".extraConfig = '' reverse_proxy http://127.0.0.1:${port} @@ -27,104 +35,153 @@ in { # Create a dedicated user for this service users.users."${user}" = { + # Group is technically not mandatory, but NixOS complains that an unset + # group is "unsafe." The group has to be declared below group = "${user}"; - isSystemUser = true; + + # System users don't have a shell. For security purposes, that *would* be + # superior, but that means that, e.g. borgmatic can't `sudo` into the + # account to access Podman commands + isNormalUser = true; + + # A home directory is mandatory in order to allow systemd user units to be + # created and run home = "${state-directory}"; createHome = true; + + # Settings for running containers while a login shell is not active linger = true; autoSubUidGidRange = true; }; users.groups."${user}" = { }; - home-manager.users."${user}" = { config, ... }: { + home-manager.users."${user}" = { config, osConfig, ... }: { imports = [ home-manager-quadlet-nix ]; home.stateVersion = "25.05"; - virtualisation.quadlet.containers = { - authentik-db = { - autoStart = true; - containerConfig = { - image = "docker.io/library/postgres:16"; - environments = { - POSTGRES_DB = "${user}"; - POSTGRES_USER = "${user}"; + virtualisation.quadlet = { + containers = { + authentik-db = { + autoStart = true; + containerConfig = { + image = "docker.io/library/postgres:16"; + environments = { + POSTGRES_DB = "${user}"; + POSTGRES_USER = "${user}"; + }; + secrets = [ + # POSTGRES_PASSWORD is used by the container to setup the + # database, PGPASSWORD is used by pg_dump to authenticate for + # backup purposes + "AUTHENTIK_POSTGRESQL__PASSWORD,type=env,target=POSTGRES_PASSWORD" + "AUTHENTIK_POSTGRESQL__PASSWORD,type=env,target=PGPASSWORD" + ]; + + # Double dollarsigns are required for use of *container* environment + # variables, Nix escaping creates the weird $\${} syntax + healthCmd = "pg_isready -d $\${POSTGRES_DB} -U $\${POSTGRES_USER}"; + healthInterval = "30s"; + healthRetries = 5; + healthStartPeriod = "20s"; + + # Volumes have to be bound with the :U label to allow for username + # remapping in rootless containers. :Z/:z is not needed b/c NixOS + # does not support SELinux + volumes = + [ "${state-directory}/database:/var/lib/postgresql/data:U" ]; + + # A network is actually required for these containers to talk to one + # another. I suppose the more idiomatic way would be for these + # related containers to be in a "pod," but I'm still struggling + # learning the idiosyncrasies of quadlet/rootless/podman, so we'll + # stick with this for now + networks = + [ config.virtualisation.quadlet.networks.authentik-net.ref ]; + }; - secrets = [ - "AUTHENTIK_POSTGRESQL__PASSWORD,type=env,target=POSTGRES_PASSWORD" - ]; - healthCmd = "pg_isready -d \${POSTGRES_DB} -U \${POSTGRES_USER}"; - healthInterval = "30s"; - healthRetries = 5; - healthStartPeriod = "20s"; - volumes = [ "${state-directory}/database:/var/lib/postgresql/data" ]; + + # Allowing this container to start before the secrets are translated + # will lead to errors. Use osConfig b/c secrets are declared in the + # system NixOS scope, even though it is a user process. + unitConfig.Requires = + [ osConfig.millironx.podman-secrets.authentik.ref ]; + unitConfig.After = + [ osConfig.millironx.podman-secrets.authentik.ref ]; + }; + + authentik-worker = { + autoStart = true; + containerConfig = { + image = "ghcr.io/goauthentik/server:2025.10.2"; + environments = { + AUTHENTIK_POSTGRESQL__HOST = "authentik-db"; + AUTHENTIK_POSTGRESQL__NAME = "${user}"; + AUTHENTIK_POSTGRESQL__USER = "${user}"; + }; + exec = "worker"; + secrets = [ + "AUTHENTIK_POSTGRESQL__PASSWORD,type=env" + "AUTHENTIK_SECRET_KEY,type=env" + ]; + volumes = [ + "${state-directory}/media:/media:U" + "${state-directory}/custom-templates:/templates:U" + "${state-directory}/certs:/certs:U" + ]; + networks = + [ config.virtualisation.quadlet.networks.authentik-net.ref ]; + }; + unitConfig.Requires = + [ config.virtualisation.quadlet.containers.authentik-db.ref ]; + unitConfig.After = + [ config.virtualisation.quadlet.containers.authentik-db.ref ]; + }; + + authentik = { + autoStart = true; + containerConfig = { + image = "ghcr.io/goauthentik/server:2025.10.2"; + environments = { + AUTHENTIK_POSTGRESQL__HOST = "authentik-db"; + AUTHENTIK_POSTGRESQL__NAME = "${user}"; + AUTHENTIK_POSTGRESQL__USER = "${user}"; + }; + exec = "server"; + secrets = [ + "AUTHENTIK_POSTGRESQL__PASSWORD,type=env" + "AUTHENTIK_SECRET_KEY,type=env" + "AUTHENTIK_EMAIL__HOST,type=env" + "AUTHENTIK_EMAIL__PORT,type=env" + "AUTHENTIK_EMAIL__USERNAME,type=env" + "AUTHENTIK_EMAIL__PASSWORD,type=env" + "AUTHENTIK_EMAIL__USE_SSL,type=env" + "AUTHENTIK_EMAIL__FROM,type=env" + ]; + + # Change from Traefik: publish ports to localhost only via 127.0.0.1 + # and then reverse proxy to that port. Authentik does not appear to + # have a way to configure the port, so we will use the default of + # 9000 for non-secured traffic. + publishPorts = [ "127.0.0.1:${port}:${port}" ]; + volumes = [ + "${state-directory}/media:/media:U" + "${state-directory}/custom-templates:/templates:U" + ]; + networks = + [ config.virtualisation.quadlet.networks.authentik-net.ref ]; + }; + unitConfig.Requires = + [ config.virtualisation.quadlet.containers.authentik-db.ref ]; + unitConfig.After = + [ config.virtualisation.quadlet.containers.authentik-db.ref ]; }; }; - authentik-worker = { - autoStart = true; - containerConfig = { - image = "ghcr.io/goauthentik/server:2025.10.2"; - environments = { - AUTHENTIK_POSTGRESQL__HOST = "authentik-db"; - AUTHENTIK_POSTGRESQL__NAME = "${user}"; - AUTHENTIK_POSTGRESQL__USER = "${user}"; - }; - exec = "worker"; - secrets = [ - "AUTHENTIK_POSTGRESQL__PASSWORD,type=env" - "AUTHENTIK_SECRET_KEY,type=env" - ]; - volumes = [ - "${state-directory}/media:/media" - "${state-directory}/custom-templates:/templates" - "${state-directory}/certs:/certs" - ]; - }; - unitConfig.Requires = [ - config.virtualisation.quadlet.containers.authentik-db.ref - "network-online.target" - ]; - unitConfig.After = [ - config.virtualisation.quadlet.containers.authentik-db.ref - "network-online.target" - ]; - }; + networks.authentik-net = { }; - authentik = { - autoStart = true; - containerConfig = { - image = "ghcr.io/goauthentik/server:2025.10.2"; - environments = { - AUTHENTIK_POSTGRESQL__HOST = "authentik-db"; - AUTHENTIK_POSTGRESQL__NAME = "${user}"; - AUTHENTIK_POSTGRESQL__USER = "${user}"; - }; - secrets = [ - "AUTHENTIK_POSTGRESQL__PASSWORD,type=env" - "AUTHENTIK_SECRET_KEY,type=env" - "AUTHENTIK_EMAIL__HOST,type=env" - "AUTHENTIK_EMAIL__PORT,type=env" - "AUTHENTIK_EMAIL__USERNAME,type=env" - "AUTHENTIK_EMAIL__PASSWORD,type=env" - "AUTHENTIK_EMAIL__USE_SSL,type=env" - "AUTHENTIK_EMAIL__FROM,type=env" - ]; - publishPorts = [ "127.0.0.1:${port}:${port}" ]; - volumes = [ - "${state-directory}/media:/media" - "${state-directory}/custom-templates:/templates" - ]; - }; - unitConfig.Requires = [ - config.virtualisation.quadlet.containers.authentik-db.ref - "network-online.target" - ]; - unitConfig.After = [ - config.virtualisation.quadlet.containers.authentik-db.ref - "network-online.target" - ]; - }; + # One of the main advantages of using Quadlet + autoUpdate.enable = true; }; }; } From 6a1cc7877ce1e59dd199776e893c8f91d338225e Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:52:06 -0600 Subject: [PATCH 3/3] users (mcentire): Add millironx to adm group --- systems/linux/mcentire.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systems/linux/mcentire.nix b/systems/linux/mcentire.nix index 4a1b09a..9171e5a 100644 --- a/systems/linux/mcentire.nix +++ b/systems/linux/mcentire.nix @@ -44,7 +44,7 @@ millironx = { isNormalUser = true; description = "Thomas A. Christensen II"; - extraGroups = [ "wheel" ]; + extraGroups = [ "adm" "wheel" ]; }; };