From 3fd32ffa45c984a6eb09c6dfcf44d2fe0cd58406 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:19:28 -0600 Subject: [PATCH 1/2] feat: Add podman-secrets module --- bin/secret-translator.jl | 30 ++++++++ modules/podman-secrets.nix | 154 +++++++++++++++++++++++++++++++++++++ systems/linux/mcentire.nix | 1 + 3 files changed, 185 insertions(+) create mode 100644 bin/secret-translator.jl create mode 100644 modules/podman-secrets.nix diff --git a/bin/secret-translator.jl b/bin/secret-translator.jl new file mode 100644 index 0000000..ce50af8 --- /dev/null +++ b/bin/secret-translator.jl @@ -0,0 +1,30 @@ +#!/usr/bin/env julia +using TOML: TOML + +function remove_podman_secrets() + run(`podman secret rm --all`) + return nothing +end #function + +function create_podman_secret(name, secret) + run( + Cmd( + `podman secret create --env=true --replace $name SECRET`; + env = Dict("SECRET" => secret), + ), + ) + return nothing +end #function + +function parse_toml_secrets(file) + foreach(f -> create_podman_secret(f[1], f[2]), TOML.parsefile(file)) + return nothing +end #function + +function main() + remove_podman_secrets() + foreach(parse_toml_secrets, ARGS) + return 0 +end #function + +main() diff --git a/modules/podman-secrets.nix b/modules/podman-secrets.nix new file mode 100644 index 0000000..41a01c5 --- /dev/null +++ b/modules/podman-secrets.nix @@ -0,0 +1,154 @@ +# Warning to my future self: This module was "vibe coded" by Claude Sonnet 4.5. +# I originally had a hand-written module in here that did the secrets +# translation as the root user. I knew that I would want to support multiple +# secrets files and multiple users, and thought I should be able to create an +# arbitrary number of them, similar to how you can have an arbitrary number +# of `programs.firefox.profiles`. Unfortunately, even after looking at lots of +# example modules, I could not figure out the syntax (Nix has a serious lack +# of minimum working examples), so I broke down and asked Claude to rewrite it +# for me. Based on everything that I read, this seems to be exactly what I asked +# for. +# +# Here is the prompt I used to get here: +# [@Per service isolation on NixOS with Traefik](zed:///agent/thread/94cb8a22-ff0c-4772-a1a1-018b0107f334?name=Per+service+isolation+on+NixOS+with+Traefik) +# +# [@podman-secrets.nix](file:///home/millironx/.config/home-manager/modules/podman-secrets.nix) +# +# I originally wrote this module assuming that I would use Podman as root for +# all containers. I would like to fix the module to have as many secrets files +# processed as needed with services setup for each secret that is run as the +# appropriate user. Ideally, the syntax for working with secrets to be +# translated would be +# +# ```nix +# { config, ... }: { +# age.secrets = { +# "caddy.toml" = { +# file = ./../secrets/caddy.toml.age; +# owner = "caddy"; +# group = "caddy"; +# }; +# +# "authentik.toml" = { +# file = ./../secrets/authentik.toml.age; +# owner = "authentik"; +# group = "authentik"; +# }; +# +# "freshrss.toml" = { +# file = ./../secrets/freshrss.toml.age; +# owner = "freshrss"; +# group = "freshrss"; +# }; +# }; +# +# millironx.podman-secrets = with config; { +# caddy = { +# user = "caddy"; +# secrets-files = [ +# ./../not-really-secret.toml +# age.secrets."caddy.toml".path +# ]; +# }; +# authentik = { +# user = "authentik"; +# secrets-files = [ +# age.secrets."authentik.toml".path +# ]; +# }; +# freshrss = { +# user = "freshrss"; +# secrets-files = [ +# age.secrets."freshrss.toml".path +# ]; +# }; +# }; +# } +# ``` +# +# Can you help me rewrite the module to accomplish this? +# +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.millironx.podman-secrets; + + secret-translator = pkgs.writeScriptBin "secret-translator" + (builtins.readFile ./../bin/secret-translator.jl); + + # Submodule type for each service's secrets configuration + serviceSecretsType = types.submodule { + options = { + user = mkOption { + type = types.str; + description = "User account to run the secrets translation service as"; + example = "caddy"; + }; + + secrets-files = mkOption { + type = types.listOf (types.either types.path types.string); + description = + "List of TOML files containing secrets to translate to Podman secrets"; + example = literalExpression '' + [ + "/run/agenix/caddy.toml" + ./../not-really-secret.toml + ] + ''; + default = [ ]; + }; + }; + }; + + # Generate a systemd service for each configured service + mkSecretsService = name: serviceCfg: + nameValuePair "podman-secrets-${name}" { + description = "Podman secrets converter service for ${name}"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "oneshot"; + User = serviceCfg.user; + ExecStart = "${secret-translator}/bin/secret-translator ${ + lib.concatStringsSep " " serviceCfg.secrets-files + }"; + Path = [ "${pkgs.podman}/bin" "${pkgs.julia-lts-bin}/bin" ]; + }; + }; + + # Filter out services with no secrets-files + enabledServices = + lib.filterAttrs (name: serviceCfg: serviceCfg.secrets-files != [ ]) cfg; + +in { + options.millironx.podman-secrets = mkOption { + 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. + ''; + example = literalExpression '' + { + caddy = { + user = "caddy"; + secrets-files = [ + ./../not-really-secret.toml + config.age.secrets."caddy.toml".path + ]; + }; + authentik = { + user = "authentik"; + secrets-files = [ + config.age.secrets."authentik.toml".path + ]; + }; + } + ''; + default = { }; + }; + + config = mkIf (enabledServices != { }) { + systemd.services = lib.mapAttrs' mkSecretsService enabledServices; + }; +} diff --git a/systems/linux/mcentire.nix b/systems/linux/mcentire.nix index 60fd158..7c0634a 100644 --- a/systems/linux/mcentire.nix +++ b/systems/linux/mcentire.nix @@ -3,6 +3,7 @@ { imports = [ # Include the results of the hardware scan. ./hardware-configuration/mcentire.nix + ./../../modules/podman-secrets.nix ./../../services/nixos-update.nix ./../../services/borgmatic.nix ./../../services/crowdsec.nix From 2b0684863245e19fff61f3f9790ff69ec2852e56 Mon Sep 17 00:00:00 2001 From: "Thomas A. Christensen II" <25492070+MillironX@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:12:50 -0600 Subject: [PATCH 2/2] service (mcentire): Add authentik service --- flake.nix | 4 ++ secrets.nix | 2 + secrets/authentik.toml.age | Bin 0 -> 1222 bytes services/authentik.nix | 128 +++++++++++++++++++++++++++++++++++++ systems/linux/mcentire.nix | 5 ++ 5 files changed, 139 insertions(+) create mode 100644 secrets/authentik.toml.age create mode 100644 services/authentik.nix diff --git a/flake.nix b/flake.nix index a703651..204941e 100644 --- a/flake.nix +++ b/flake.nix @@ -138,9 +138,13 @@ "mcentire" = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; + specialArgs = { + home-manager-quadlet-nix = quadlet-nix.homeManagerModules.quadlet; + }; modules = [ ./systems/linux/mcentire.nix agenix.nixosModules.default + home-manager.nixosModules.home-manager quadlet-nix.nixosModules.quadlet crowdsec.nixosModules.crowdsec crowdsec.nixosModules.crowdsec-firewall-bouncer diff --git a/secrets.nix b/secrets.nix index 1de9bc8..8042bf6 100644 --- a/secrets.nix +++ b/secrets.nix @@ -26,6 +26,8 @@ let in { "secrets/ansible-vault-password.age".publicKeys = system-administrators; + "secrets/authentik.toml.age".publicKeys = system-administrators + ++ [ mcentire-host ]; "secrets/borgmatic-passphrase.age".publicKeys = system-administrators ++ [ mcentire-host ]; "secrets/borgmatic-ssh-config.age".publicKeys = system-administrators diff --git a/secrets/authentik.toml.age b/secrets/authentik.toml.age new file mode 100644 index 0000000000000000000000000000000000000000..1ef8f6e277acf7a7b989bfa87f553f5f1d1d0ac8 GIT binary patch literal 1222 zcmZY4>u(bU003~zNPtQJGXe}9)UhD$GJ0+AXm33Gyz66qTzh>DNUpu>_1f!o?e*?@ zhs%KBruZPTY>o(sKsF;j!ec-NI6t5P1p=6W43LPB03w6ML;_|Qzs|qlmmkT{jWnMs zDXnF;z%MP77@efoG_KMLLZ*?X3^1%mv@B=j)S%X+Qyo~v>MND-v>Hu#Q;5L?O5w7> z73Z2=B3@*&32z`mazs!=mLzXBg^CK1T}Gt6Qa<7aqXB`;M4eE~nr|g5E{POv8AqXc zqNZnb6IRS6bL!?<_j?{&PLLmh3V$^LSEl{#Z|95L_)KcC8=6A}x!71b6m_KZhz`T=l z`Qf0sxsXqZVW&C7ILa)Xb!a;7-?tA{bz_~-o`vtdFb~hEIkJEITMc`r9~@+xc0L+= zspZeZdBeyi&6$JacV0e(u0Co~77wm&Upr;20a;zc&YC=@2637veS$6+?5Yba;x9Zs zwe8n|@Y3q4f}FWd^%It?XYTFKuJ`L3>VeJ=HtxE5ZPn&(_(jck;PLt}d2Y0K^SuxE z%(~IHb?>4lwGSV>{xs43)2j1b>XOj|-We^`$IgB<^6bWgTlzkcJU!(?!u8G%l|QI? zEv}~9na9%1sprZYzQ1zMzVnN5vcB`EZsE`ncXeIgordSzB0G1lNRiIRTX#P^eAx7U zsrOx>|D=UFxsSWKa`+H@v8C>ERMiTaSC0SU9-qAUY3Yy|J$w2MY1{g{Z|>@rR=jp9 zf4cY9`oYi7cMeRFo}C>2XZx33;fwye$CmUVJ=-^=91bXXbabG4?nAk1?QhDJX&p!O zYij*Vt<$?+eRk>je0oP{9`n_SGvM3KsR!qsBS(g>W#sgwj>#K79$aqtbj|E7?FZi3 zdpYJ?vGMWp9^?Ftwfl}WAk{NRcGUfR;?Xzq!j{^WzXu+)&7L7{s&Bf#u3c%X3r_R& zT->rAnxoJ4OdT2@oqID$2ET1<;<0{pezZ4S9vcaUYq=`Jg0_|I9bY@IpZR@klKsh# J`)~X;@-Hw|+A;tD literal 0 HcmV?d00001 diff --git a/services/authentik.nix b/services/authentik.nix new file mode 100644 index 0000000..30ee781 --- /dev/null +++ b/services/authentik.nix @@ -0,0 +1,128 @@ +{ config, home-manager-quadlet-nix, ... }: +let + user = "authentik"; + state-directory = "/var/lib/authentik"; + port = "9000"; + +in { + # Secrets are translated in the system scope, but performed by the user, + # so are available in the user's Podman secrets + age.secrets = { + "authentik.toml" = { + file = ./../secrets/authentik.toml.age; + owner = "${user}"; + }; + }; + + millironx.podman-secrets.authentik = { + user = "${user}"; + secrets-files = [ config.age.secrets."authentik.toml".path ]; + }; + + systemd.tmpfiles.rules = [ "d ${state-directory} 1775 ${user} ${user} -" ]; + + services.caddy.virtualHosts."auth.millironx.com".extraConfig = '' + reverse_proxy http://127.0.0.1:${port} + ''; + + # Create a dedicated user for this service + users.users."${user}" = { + group = "${user}"; + isSystemUser = true; + linger = true; + autoSubUidGidRange = true; + }; + users.groups."${user}" = {}; + + home-manager.users."${user}" = { config, ... }: { + 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}"; + }; + 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" ]; + }; + }; + + 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" + ]; + }; + + 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" + ]; + }; + }; + }; +} diff --git a/systems/linux/mcentire.nix b/systems/linux/mcentire.nix index 7c0634a..4a1b09a 100644 --- a/systems/linux/mcentire.nix +++ b/systems/linux/mcentire.nix @@ -7,6 +7,7 @@ ./../../services/nixos-update.nix ./../../services/borgmatic.nix ./../../services/crowdsec.nix + ./../../services/authentik.nix ]; # Use the GRUB 2 boot loader. @@ -17,6 +18,7 @@ useDHCP = false; interfaces.eth0.useDHCP = true; hostName = "mcentire"; # Define your hostname. + firewall.allowedTCPPorts = [ 80 443 ]; }; # Set your time zone. @@ -56,8 +58,11 @@ services = { openssh.enable = true; tailscale.enable = true; + caddy.enable = true; }; + virtualisation.quadlet.enable = true; + system.stateVersion = "25.05"; # Did you read the comment? nix = { extraOptions = "experimental-features = nix-command flakes"; }; }