Closes #12 The worker gets access to the ACME-managed certs via `LoadCredential`, however that doesn't refresh files when the files in the credential source change. Explicitly restart the worker to make sure these changes are reflected in what the worker sees.
624 lines
21 KiB
Nix
624 lines
21 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
inherit (lib)
|
|
types
|
|
;
|
|
|
|
inherit (lib.attrsets)
|
|
attrNames
|
|
getAttrs
|
|
mapAttrsToList
|
|
;
|
|
|
|
inherit (lib.lists)
|
|
flatten
|
|
toList
|
|
;
|
|
|
|
inherit (lib.modules)
|
|
mkDefault
|
|
mkIf
|
|
mkMerge
|
|
mkOverride
|
|
;
|
|
|
|
inherit (lib.options)
|
|
mkEnableOption
|
|
mkOption
|
|
;
|
|
|
|
inherit (lib.strings)
|
|
concatStringsSep
|
|
optionalString
|
|
versionOlder
|
|
;
|
|
|
|
inherit (lib.trivial)
|
|
boolToString
|
|
isBool
|
|
;
|
|
|
|
settingsFormat = pkgs.formats.yaml { };
|
|
|
|
pathToSecret = types.pathWith {
|
|
inStore = false;
|
|
absolute = true;
|
|
};
|
|
in
|
|
{
|
|
options.services = {
|
|
# authentik server
|
|
authentik = {
|
|
enable = mkEnableOption "authentik";
|
|
|
|
authentikComponents = mkOption {
|
|
type = types.attrsOf types.package;
|
|
};
|
|
|
|
settings = mkOption {
|
|
type = types.submodule {
|
|
freeformType = settingsFormat.type;
|
|
options = { };
|
|
};
|
|
};
|
|
|
|
createDatabase = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
|
|
nginx = {
|
|
enable = mkEnableOption "basic nginx configuration";
|
|
enableACME = mkEnableOption "Let's Encrypt and certificate discovery";
|
|
host = mkOption {
|
|
type = types.str;
|
|
example = "auth.example.com";
|
|
description = ''
|
|
Specify the name for the server in {option}`services.nginx.virtualHosts` and
|
|
for the associated Let's Encrypt certificate.
|
|
'';
|
|
};
|
|
};
|
|
|
|
environmentFile = mkOption {
|
|
type = types.nullOr pathToSecret;
|
|
default = null;
|
|
example = "/run/secrets/authentik/authentik-env";
|
|
description = ''
|
|
Environment file as defined in {manpage}`systemd.exec(5)`.
|
|
|
|
Secrets may be passed to the service without adding them to the world-readable
|
|
/nix/store, by specifying the desied secrets as environment variables according
|
|
to the authentic documentation.
|
|
|
|
```
|
|
# example content
|
|
AUTHENTIK_SECRET_KEY=<secret key>
|
|
AUTHENTIK_EMAIL__PASSWORD=<smtp password>
|
|
```
|
|
'';
|
|
};
|
|
|
|
worker = {
|
|
listenHTTP = mkOption {
|
|
type = types.str;
|
|
default = "[::1]:9001";
|
|
description = ''
|
|
Listen address for the HTTP server of the worker.
|
|
Overrides the default listen setting that's also used by the server.
|
|
'';
|
|
};
|
|
listenMetrics = mkOption {
|
|
type = types.str;
|
|
default = "[::1]:9301";
|
|
description = ''
|
|
Listen address for the metrics server of the worker.
|
|
Overrides the default listen setting that's also used by the server.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
# LDAP oupost
|
|
authentik-ldap = {
|
|
enable = mkEnableOption "authentik LDAP outpost";
|
|
|
|
listenMetrics = mkOption {
|
|
type = types.str;
|
|
default = "[::1]:9302";
|
|
description = ''
|
|
Listen address for the metrics server of the LDAP outpost.
|
|
Overrides the default listen setting that's also used by the server.
|
|
'';
|
|
};
|
|
|
|
environmentFile = mkOption {
|
|
type = types.nullOr pathToSecret;
|
|
default = null;
|
|
example = "/run/secrets/authentik-ldap/authentik-ldap-env";
|
|
description = ''
|
|
Environment file as defined in {manpage}`systemd.exec(5)`.
|
|
|
|
Secrets may be passed to the service without adding them to the world-readable
|
|
/nix/store, by specifying the desied secrets as environment variables according
|
|
to the authentic documentation.
|
|
|
|
```
|
|
# example content
|
|
AUTHENTIK_TOKEN=<token from authentik for this outpost>
|
|
```
|
|
'';
|
|
};
|
|
};
|
|
|
|
# Proxy oupost
|
|
authentik-proxy = {
|
|
enable = mkEnableOption "authentik Proxy outpost";
|
|
|
|
listenMetrics = mkOption {
|
|
type = types.str;
|
|
default = "[::1]:9303";
|
|
description = ''
|
|
Listen address for the metrics server of the proxy outpost.
|
|
Overrides the default listen setting that's also used by the server.
|
|
'';
|
|
};
|
|
listenHTTPS = mkOption {
|
|
type = types.str;
|
|
default = "[::1]:9004";
|
|
description = ''
|
|
Listen address for the HTTPS server of the proxy outpost.
|
|
Overrides the default listen setting that's also used by the server.
|
|
'';
|
|
};
|
|
listenHTTP = mkOption {
|
|
type = types.str;
|
|
default = "[::1]:9005";
|
|
description = ''
|
|
Listen address for the HTTP server of the proxy outpost.
|
|
Overrides the default listen setting that's also used by the server.
|
|
'';
|
|
};
|
|
|
|
environmentFile = mkOption {
|
|
type = types.nullOr pathToSecret;
|
|
default = null;
|
|
example = "/run/secrets/authentik-proxy/authentik-proxy-env";
|
|
description = ''
|
|
Environment file as defined in {manpage}`systemd.exec(5)`.
|
|
|
|
Secrets may be passed to the service without adding them to the world-readable
|
|
/nix/store, by specifying the desied secrets as environment variables according
|
|
to the authentic documentation.
|
|
|
|
```
|
|
# example content
|
|
AUTHENTIK_TOKEN=<token from authentik for this outpost>
|
|
```
|
|
'';
|
|
};
|
|
};
|
|
|
|
# RAC oupost
|
|
authentik-rac = {
|
|
enable = mkEnableOption "authentik RAC outpost";
|
|
|
|
environmentFile = mkOption {
|
|
type = types.nullOr pathToSecret;
|
|
default = null;
|
|
example = "/run/secrets/authentik-rac/authentik-rac-env";
|
|
description = ''
|
|
Environment file as defined in {manpage}`systemd.exec(5)`.
|
|
|
|
Secrets may be passed to the service without adding them to the world-readable
|
|
/nix/store, by specifying the desied secrets as environment variables according
|
|
to the authentic documentation.
|
|
|
|
```
|
|
# example content
|
|
AUTHENTIK_TOKEN=<token from authentik for this outpost>
|
|
```
|
|
'';
|
|
};
|
|
};
|
|
|
|
# RADIUS oupost
|
|
authentik-radius = {
|
|
enable = mkEnableOption "authentik RADIUS outpost";
|
|
|
|
listenMetrics = mkOption {
|
|
type = types.str;
|
|
default = "[::1]:9306";
|
|
description = ''
|
|
Listen address for the metrics server of the RADIUS outpost.
|
|
Overrides the default listen setting that's also used by the server.
|
|
'';
|
|
};
|
|
|
|
environmentFile = mkOption {
|
|
type = types.nullOr pathToSecret;
|
|
default = null;
|
|
example = "/run/secrets/authentik-radius/authentik-radius-env";
|
|
description = ''
|
|
Environment file as defined in {manpage}`systemd.exec(5)`.
|
|
|
|
Secrets may be passed to the service without adding them to the world-readable
|
|
/nix/store, by specifying the desied secrets as environment variables according
|
|
to the authentic documentation.
|
|
|
|
```
|
|
# example content
|
|
AUTHENTIK_TOKEN=<token from authentik for this outpost>
|
|
```
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkMerge [
|
|
# authentik server
|
|
(mkIf config.services.authentik.enable (
|
|
let
|
|
cfg = config.services.authentik;
|
|
|
|
# https://goauthentik.io/docs/installation/docker-compose#startup
|
|
tz = "UTC";
|
|
|
|
# Passed to each service and to the `ak` wrapper using `systemd-run(1)`
|
|
environment.PROMETHEUS_MULTIPROC_DIR = "%S/authentik/prometheus";
|
|
serviceDefaults = {
|
|
DynamicUser = true;
|
|
User = "authentik";
|
|
EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
|
|
ExecStartPre = [
|
|
"${pkgs.coreutils}/bin/mkdir -p \${PROMETHEUS_MULTIPROC_DIR}"
|
|
];
|
|
};
|
|
akOptions = flatten (
|
|
mapAttrsToList
|
|
# Map defaults for each authentik service (listed above) to command line parameters for
|
|
# `systemd-run(1)` in order to spin up an environment with correct (dynamic) user,
|
|
# state directory and environment to run `ak` inside.
|
|
(k: vs: map (v: "--property ${k}=${if isBool v then boolToString v else toString v}") (toList vs))
|
|
# Read properties from `authentik.service`. That way, users can customize the properties using
|
|
# module system primitives and the like.
|
|
(
|
|
removeAttrs config.systemd.services.authentik.serviceConfig [
|
|
"ExecStart"
|
|
"ExecStartPre"
|
|
"Restart"
|
|
"RestartSec"
|
|
# systemd-run doesn't expand the %S specifier, so this is passed separately below.
|
|
"WorkingDirectory"
|
|
]
|
|
)
|
|
);
|
|
in
|
|
{
|
|
assertions = [
|
|
{
|
|
assertion = cfg.nginx.enableACME -> cfg.nginx.enable;
|
|
message = ''
|
|
Cannot enable `services.authentik.nginx.enableACME` when
|
|
`services.authentik.nginx.enable` is `false`.
|
|
'';
|
|
}
|
|
];
|
|
|
|
services = {
|
|
authentik.settings = {
|
|
blueprints_dir = mkDefault "${cfg.authentikComponents.staticWorkdirDeps}/blueprints";
|
|
template_dir = mkDefault "${cfg.authentikComponents.staticWorkdirDeps}/templates";
|
|
postgresql = mkIf cfg.createDatabase {
|
|
user = mkDefault "authentik";
|
|
name = mkDefault "authentik";
|
|
host = mkDefault "/run/postgresql";
|
|
};
|
|
cert_discovery_dir = mkIf (cfg.nginx.enable && cfg.nginx.enableACME) "env://CREDENTIALS_DIRECTORY";
|
|
storage.media = {
|
|
backend = mkDefault "file";
|
|
file = mkDefault {
|
|
path = "/var/lib/authentik/media";
|
|
};
|
|
};
|
|
media.enable_upload = mkDefault true;
|
|
};
|
|
postgresql = mkIf cfg.createDatabase {
|
|
enable = true;
|
|
ensureDatabases = [ "authentik" ];
|
|
ensureUsers = [
|
|
{
|
|
name = "authentik";
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
environment.systemPackages = [
|
|
(pkgs.writeShellScriptBin "ak" ''
|
|
exec ${config.systemd.package}/bin/systemd-run --pty --collect \
|
|
${concatStringsSep " \\\n" akOptions} \
|
|
--working-directory /var/lib/authentik \
|
|
-- ${cfg.authentikComponents.manage}/bin/manage.py "$@"
|
|
'')
|
|
];
|
|
|
|
environment.etc."authentik/config.yml".source =
|
|
settingsFormat.generate "authentik.yml" cfg.settings;
|
|
|
|
systemd.services = {
|
|
authentik-migrate = {
|
|
requires = lib.optionals cfg.createDatabase [ "postgresql.service" ];
|
|
wants = [ "network-online.target" ];
|
|
after = [ "network-online.target" ] ++ lib.optionals cfg.createDatabase [ "postgresql.service" ];
|
|
before = [ "authentik.service" "authentik-migrate.service" ];
|
|
restartTriggers = [ config.environment.etc."authentik/config.yml".source ];
|
|
environment = mkMerge [
|
|
environment
|
|
{ TZ = tz; }
|
|
];
|
|
serviceConfig = mkMerge [
|
|
serviceDefaults
|
|
{
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
RuntimeDirectory = "authentik-migrate";
|
|
WorkingDirectory = "%t/authentik-migrate";
|
|
ExecStartPre = [
|
|
# needs access to "authentik/sources/schemas"
|
|
"${pkgs.coreutils}/bin/ln -svf ${cfg.authentikComponents.staticWorkdirDeps}/authentik"
|
|
];
|
|
ExecStart = "${cfg.authentikComponents.migrate}/bin/migrate.py";
|
|
Restart = "on-failure";
|
|
RestartSec = "1s";
|
|
inherit (config.systemd.services.authentik.serviceConfig) StateDirectory;
|
|
}
|
|
];
|
|
};
|
|
authentik-worker = {
|
|
wants = [ "network-online.target" ];
|
|
after = [ "network-online.target" ];
|
|
before = [ "authentik.service" ];
|
|
restartTriggers = [ config.environment.etc."authentik/config.yml".source ];
|
|
preStart = ''
|
|
ln -svf ${config.services.authentik.authentikComponents.staticWorkdirDeps}/* /run/authentik/
|
|
'';
|
|
environment = mkMerge [
|
|
environment
|
|
{
|
|
TZ = tz;
|
|
AUTHENTIK_LISTEN__HTTP = cfg.worker.listenHTTP;
|
|
AUTHENTIK_LISTEN__METRICS = cfg.worker.listenMetrics;
|
|
}
|
|
];
|
|
serviceConfig = mkMerge [
|
|
serviceDefaults
|
|
{
|
|
RuntimeDirectory = "authentik";
|
|
WorkingDirectory = "%t/authentik";
|
|
ExecStart = "${cfg.authentikComponents.manage}/bin/manage.py worker --pid-file %t/authentik/worker.pid";
|
|
Restart = "on-failure";
|
|
RestartSec = "1s";
|
|
LoadCredential = mkIf (cfg.nginx.enable && cfg.nginx.enableACME) [
|
|
"${cfg.nginx.host}.pem:${config.security.acme.certs.${cfg.nginx.host}.directory}/fullchain.pem"
|
|
"${cfg.nginx.host}.key:${config.security.acme.certs.${cfg.nginx.host}.directory}/key.pem"
|
|
];
|
|
# needs access to $StateDirectory/media/public
|
|
inherit (config.systemd.services.authentik.serviceConfig) StateDirectory;
|
|
}
|
|
];
|
|
};
|
|
authentik = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ "network-online.target" ];
|
|
requires = [
|
|
"authentik-migrate.service"
|
|
"authentik-worker.service"
|
|
];
|
|
after = [
|
|
"network-online.target"
|
|
]
|
|
++ (lib.optionals cfg.createDatabase [ "postgresql.service" ]);
|
|
restartTriggers = [ config.environment.etc."authentik/config.yml".source ];
|
|
preStart = ''
|
|
ln -svf ${cfg.authentikComponents.staticWorkdirDeps}/* /var/lib/authentik/
|
|
${optionalString (cfg.settings.storage.media.backend == "file") ''
|
|
mkdir -p ${cfg.settings.storage.media.file.path}
|
|
''}
|
|
'';
|
|
environment = mkMerge [
|
|
environment
|
|
{ TZ = tz; }
|
|
];
|
|
serviceConfig = mkMerge [
|
|
serviceDefaults
|
|
{
|
|
StateDirectory = "authentik";
|
|
UMask = "0027";
|
|
# TODO /run might be sufficient
|
|
WorkingDirectory = "%S/authentik";
|
|
ExecStart = "${cfg.authentikComponents.gopkgs}/bin/server";
|
|
Restart = "on-failure";
|
|
RestartSec = "1s";
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
security.acme.certs = mkIf cfg.nginx.enableACME {
|
|
${cfg.nginx.host}.postRun = ''
|
|
systemctl restart authentik-worker.service
|
|
'';
|
|
};
|
|
|
|
services.nginx = mkIf cfg.nginx.enable {
|
|
enable = true;
|
|
recommendedTlsSettings = true;
|
|
recommendedProxySettings = true;
|
|
virtualHosts.${cfg.nginx.host} = {
|
|
inherit (cfg.nginx) enableACME;
|
|
forceSSL = cfg.nginx.enableACME;
|
|
locations."/" = {
|
|
proxyWebsockets = true;
|
|
proxyPass = "https://localhost:9443";
|
|
};
|
|
};
|
|
};
|
|
}
|
|
))
|
|
|
|
# LDAP outpost
|
|
(mkIf config.services.authentik-ldap.enable (
|
|
let
|
|
cfg = config.services.authentik-ldap;
|
|
in
|
|
{
|
|
systemd.services.authentik-ldap = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ "network-online.target" ];
|
|
after = [
|
|
"network-online.target"
|
|
"authentik.service"
|
|
];
|
|
environment.AUTHENTIK_LISTEN__METRICS = cfg.listenMetrics;
|
|
serviceConfig = {
|
|
RuntimeDirectory = "authentik-ldap";
|
|
UMask = "0027";
|
|
WorkingDirectory = "%t/authentik-ldap";
|
|
DynamicUser = true;
|
|
ExecStart = "${config.services.authentik.authentikComponents.gopkgs.ldap}/bin/ldap";
|
|
EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
|
|
Restart = "on-failure";
|
|
};
|
|
};
|
|
}
|
|
))
|
|
|
|
# Proxy outpost
|
|
(mkIf config.services.authentik-proxy.enable (
|
|
let
|
|
cfg = config.services.authentik-proxy;
|
|
in
|
|
{
|
|
systemd.services.authentik-proxy = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ "network-online.target" ];
|
|
after = [
|
|
"network-online.target"
|
|
"authentik.service"
|
|
];
|
|
environment = {
|
|
AUTHENTIK_LISTEN__METRICS = cfg.listenMetrics;
|
|
AUTHENTIK_LISTEN__HTTP = cfg.listenHTTP;
|
|
AUTHENTIK_LISTEN__HTTPS = cfg.listenHTTPS;
|
|
};
|
|
serviceConfig = {
|
|
RuntimeDirectory = "authentik-proxy";
|
|
UMask = "0027";
|
|
WorkingDirectory = "%t/authentik-proxy";
|
|
DynamicUser = true;
|
|
ExecStart = "${config.services.authentik.authentikComponents.gopkgs.proxy}/bin/proxy";
|
|
EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
|
|
Restart = "on-failure";
|
|
};
|
|
};
|
|
}
|
|
))
|
|
|
|
# RAC outpost
|
|
(mkIf config.services.authentik-rac.enable (
|
|
let
|
|
cfg = config.services.authentik-rac;
|
|
in
|
|
{
|
|
assertions = [
|
|
{
|
|
assertion = config.services.authentik.authentikComponents.gopkgs?rac;
|
|
message = ''
|
|
guacamole-server is not available on the host's platform!
|
|
'';
|
|
}
|
|
];
|
|
systemd.services.authentik-rac = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ "network-online.target" ];
|
|
after = [
|
|
"network-online.target"
|
|
"authentik.service"
|
|
];
|
|
serviceConfig = {
|
|
RuntimeDirectory = "authentik-rac";
|
|
UMask = "0027";
|
|
WorkingDirectory = "%t/authentik-rac";
|
|
DynamicUser = true;
|
|
ExecStart = "${config.services.authentik.authentikComponents.gopkgs.rac}/bin/rac";
|
|
EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
|
|
Restart = "on-failure";
|
|
};
|
|
};
|
|
}
|
|
))
|
|
|
|
# RADIUS outpost
|
|
(mkIf config.services.authentik-radius.enable (
|
|
let
|
|
cfg = config.services.authentik-radius;
|
|
in
|
|
{
|
|
systemd.services.authentik-radius = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ "network-online.target" ];
|
|
after = [
|
|
"network-online.target"
|
|
"authentik.service"
|
|
];
|
|
environment.AUTHENTIK_LISTEN__METRICS = cfg.listenMetrics;
|
|
serviceConfig = {
|
|
RuntimeDirectory = "authentik-radius";
|
|
UMask = "0027";
|
|
WorkingDirectory = "%t/authentik-radius";
|
|
DynamicUser = true;
|
|
ExecStart = "${config.services.authentik.authentikComponents.gopkgs.radius}/bin/radius";
|
|
EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
|
|
Restart = "on-failure";
|
|
};
|
|
};
|
|
}
|
|
))
|
|
|
|
# This is an attempt to solve a rather ugly problem that was
|
|
# caused by previously setting a default for the option
|
|
# `services.postgresql.package` in this module.
|
|
#
|
|
# The problem is that some installations with a state version other than
|
|
# 22.05, 22.11 or 23.05 may have used this module, meaning their postgresql
|
|
# version was overridden by this module. Merely removing the setting here,
|
|
# would cause their config to fall back to their respective default release,
|
|
# resulting in a (temporarily) broken installation.
|
|
#
|
|
# While recovering from this is relatively easy, i.e. they would need to
|
|
# override the posgresql package in their own config, it is not desirable
|
|
# to break those installations.
|
|
#
|
|
# The idea is to no longer set a default value for the package for new
|
|
# installations. Instead new installations use the sensible default provided
|
|
# by nixpkgs. At the same time this should keep the previous default
|
|
# for old installations.
|
|
#
|
|
# After postgresql_14 has been removed from nixpkgs, this workaround can be dropped.
|
|
(mkIf (versionOlder config.system.stateVersion "24.05") {
|
|
# The upstream postgresl module is using mkDefault
|
|
# to specify the default value for the package option.
|
|
# Unfortunately this forces us to specify this default with
|
|
# a higher priority, i.e. lower number, than mkDefault which
|
|
# has priority 1000
|
|
services.postgresql.package = mkOverride 999 pkgs.postgresql_14;
|
|
})
|
|
];
|
|
}
|