Compare commits

...

3 commits

Author SHA1 Message Date
6a1cc7877c
users (mcentire): Add millironx to adm group 2025-12-01 13:52:06 -06:00
b2477b9f24
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.
2025-12-01 12:03:20 -06:00
8d96ef7684
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.
2025-12-01 12:01:45 -06:00
3 changed files with 161 additions and 94 deletions

View file

@ -77,7 +77,7 @@ let
(builtins.readFile ./../bin/secret-translator.jl); (builtins.readFile ./../bin/secret-translator.jl);
# Submodule type for each service's secrets configuration # Submodule type for each service's secrets configuration
serviceSecretsType = types.submodule { serviceSecretsType = types.submodule ({ config, name, ... }: {
options = { options = {
user = mkOption { user = mkOption {
type = types.str; type = types.str;
@ -97,18 +97,28 @@ let
''; '';
default = [ ]; 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: mkSecretsService = name: serviceCfg:
nameValuePair "podman-secrets-${name}" { nameValuePair "podman-secrets-${name}" {
description = "Podman secrets converter service for ${name}"; description = "Podman secrets converter service for ${name}";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "default.target" ];
unitConfig.ConditionUser = "${serviceCfg.user}";
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
User = serviceCfg.user;
ProtectProc = "invisible"; ProtectProc = "invisible";
ExecStart = "${secret-translator}/bin/secret-translator ${ ExecStart = "${secret-translator}/bin/secret-translator ${
lib.concatStringsSep " " serviceCfg.secrets-files lib.concatStringsSep " " serviceCfg.secrets-files
@ -128,8 +138,8 @@ in {
type = types.attrsOf serviceSecretsType; type = types.attrsOf serviceSecretsType;
description = '' description = ''
Per-service Podman secrets configuration. Per-service Podman secrets configuration.
Each attribute creates a separate systemd service that translates TOML secrets Each attribute creates a separate systemd user service that translates TOML secrets
files into Podman secrets for the specified user. files into Podman secrets.
''; '';
example = literalExpression '' example = literalExpression ''
{ {
@ -152,6 +162,6 @@ in {
}; };
config = mkIf (enabledServices != { }) { config = mkIf (enabledServices != { }) {
systemd.services = lib.mapAttrs' mkSecretsService enabledServices; systemd.user.services = lib.mapAttrs' mkSecretsService enabledServices;
}; };
} }

View file

@ -5,8 +5,8 @@ let
port = "9000"; port = "9000";
in { in {
# Secrets are translated in the system scope, but performed by the user, # Secrets are translated in the system NixOS scope, but performed by the user,
# so are available in the user's Podman secrets # via a user systemd unit, so are available in the user's Podman secrets
age.secrets = { age.secrets = {
"authentik.toml" = { "authentik.toml" = {
file = ./../secrets/authentik.toml.age; file = ./../secrets/authentik.toml.age;
@ -19,7 +19,15 @@ in {
secrets-files = [ config.age.secrets."authentik.toml".path ]; 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 = '' services.caddy.virtualHosts."auth.millironx.com".extraConfig = ''
reverse_proxy http://127.0.0.1:${port} reverse_proxy http://127.0.0.1:${port}
@ -27,104 +35,153 @@ in {
# Create a dedicated user for this service # Create a dedicated user for this service
users.users."${user}" = { 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}"; 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}"; home = "${state-directory}";
createHome = true; createHome = true;
# Settings for running containers while a login shell is not active
linger = true; linger = true;
autoSubUidGidRange = true; autoSubUidGidRange = true;
}; };
users.groups."${user}" = { }; users.groups."${user}" = { };
home-manager.users."${user}" = { config, ... }: { home-manager.users."${user}" = { config, osConfig, ... }: {
imports = [ home-manager-quadlet-nix ]; imports = [ home-manager-quadlet-nix ];
home.stateVersion = "25.05"; home.stateVersion = "25.05";
virtualisation.quadlet.containers = { virtualisation.quadlet = {
authentik-db = { containers = {
autoStart = true; authentik-db = {
containerConfig = { autoStart = true;
image = "docker.io/library/postgres:16"; containerConfig = {
environments = { image = "docker.io/library/postgres:16";
POSTGRES_DB = "${user}"; environments = {
POSTGRES_USER = "${user}"; 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" # Allowing this container to start before the secrets are translated
]; # will lead to errors. Use osConfig b/c secrets are declared in the
healthCmd = "pg_isready -d \${POSTGRES_DB} -U \${POSTGRES_USER}"; # system NixOS scope, even though it is a user process.
healthInterval = "30s"; unitConfig.Requires =
healthRetries = 5; [ osConfig.millironx.podman-secrets.authentik.ref ];
healthStartPeriod = "20s"; unitConfig.After =
volumes = [ "${state-directory}/database:/var/lib/postgresql/data" ]; [ 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 = { networks.authentik-net = { };
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 = { # One of the main advantages of using Quadlet
autoStart = true; autoUpdate.enable = 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"
];
};
}; };
}; };
} }

View file

@ -44,7 +44,7 @@
millironx = { millironx = {
isNormalUser = true; isNormalUser = true;
description = "Thomas A. Christensen II"; description = "Thomas A. Christensen II";
extraGroups = [ "wheel" ]; extraGroups = [ "adm" "wheel" ];
}; };
}; };