1
0
Fork 0
selfhostblocks/modules/blocks/authelia.nix

506 lines
17 KiB
Nix
Raw Permalink Normal View History

{ config, options, pkgs, lib, ... }:
2023-08-10 05:39:32 +02:00
let
cfg = config.shb.authelia;
opt = options.shb.authelia;
2023-08-10 05:39:32 +02:00
2024-01-12 08:22:46 +01:00
contracts = pkgs.callPackage ../contracts {};
2024-02-10 05:56:26 +01:00
shblib = pkgs.callPackage ../../lib {};
2024-01-12 08:22:46 +01:00
2023-08-10 05:39:32 +02:00
fqdn = "${cfg.subdomain}.${cfg.domain}";
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
2023-08-10 05:39:32 +02:00
autheliaCfg = config.services.authelia.instances.${fqdn};
in
{
options.shb.authelia = {
enable = lib.mkEnableOption "selfhostblocks.authelia";
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under which Authelia will be served.";
2023-11-30 19:38:35 +01:00
example = "auth";
2023-08-10 05:39:32 +02:00
};
domain = lib.mkOption {
type = lib.types.str;
description = "domain under which Authelia will be served.";
example = "mydomain.com";
};
port = lib.mkOption {
description = "If given, adds a port to the `<subdomain>.<domain>` endpoint.";
type = lib.types.nullOr lib.types.port;
default = null;
};
2024-01-12 08:22:46 +01:00
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
ldapHostname = lib.mkOption {
2023-08-10 05:39:32 +02:00
type = lib.types.str;
description = "Hostname of the LDAP authentication backend.";
example = "ldap.example.com";
};
ldapPort = lib.mkOption {
type = lib.types.port;
description = "Port of the LDAP authentication backend.";
example = "389";
2023-08-10 05:39:32 +02:00
};
dcdomain = lib.mkOption {
type = lib.types.str;
description = "dc domain for ldap.";
example = "dc=mydomain,dc=com";
};
autheliaUser = lib.mkOption {
type = lib.types.str;
description = "System user for this Authelia instance.";
default = "authelia";
};
secrets = lib.mkOption {
description = "Secrets needed by Authelia";
type = lib.types.submodule {
options = {
jwtSecret = contracts.secret.mkOption {
description = "JWT secret.";
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
ldapAdminPassword = contracts.secret.mkOption {
description = "LDAP admin user password.";
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
sessionSecret = contracts.secret.mkOption {
description = "Session secret.";
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
storageEncryptionKey = contracts.secret.mkOption {
description = "Storage encryption key.";
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
identityProvidersOIDCHMACSecret = contracts.secret.mkOption {
description = "Identity provider OIDC HMAC secret.";
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
identityProvidersOIDCIssuerPrivateKey = contracts.secret.mkOption {
description = ''
Identity provider OIDC issuer private key.
Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048`
'';
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
};
};
2023-08-10 05:39:32 +02:00
};
oidcClients = lib.mkOption {
description = "OIDC clients";
default = [
{
client_id = "dummy_client";
client_name = "Dummy Client so Authelia can start";
client_secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret";
public = false;
authorization_policy = "one_factor";
redirect_uris = [];
}
];
2024-03-01 00:34:53 +01:00
type = lib.types.listOf (lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.anything;
options = {
client_id = lib.mkOption {
2024-03-01 00:34:53 +01:00
type = lib.types.str;
description = "Unique identifier of the OIDC client.";
};
client_name = lib.mkOption {
2024-03-01 00:34:53 +01:00
type = lib.types.nullOr lib.types.str;
description = "Human readable description of the OIDC client.";
default = null;
};
client_secret = lib.mkOption {
2024-03-01 00:34:53 +01:00
type = shblib.secretFileType;
description = ''
File containing the shared secret with the OIDC client.
Generate with:
```
nix run nixpkgs#authelia -- \
crypto hash generate pbkdf2 \
--variant sha512 \
--random \
--random.length 72 \
--random.charset rfc3986
```
'';
2024-03-01 00:34:53 +01:00
};
public = lib.mkOption {
type = lib.types.bool;
description = "If the OIDC client is public or not.";
default = false;
apply = v: if v then "true" else "false";
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
redirect_uris = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of uris that are allowed to be redirected to.";
};
scopes = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Scopes to ask for";
example = [ "openid" "profile" "email" "groups" ];
default = [];
};
};
});
2023-08-10 05:39:32 +02:00
};
2023-12-11 06:56:31 +01:00
smtp = lib.mkOption {
description = ''
If a string is given, writes notifications to the given path.Otherwise, send notifications
by smtp.
https://www.authelia.com/configuration/notifications/introduction/
'';
default = "/tmp/authelia-notifications";
type = lib.types.oneOf [
lib.types.str
(lib.types.nullOr (lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
description = "SMTP address from which the emails originate.";
example = "authelia@mydomain.com";
};
from_name = lib.mkOption {
type = lib.types.str;
description = "SMTP name from which the emails originate.";
default = "Authelia";
};
host = lib.mkOption {
type = lib.types.str;
description = "SMTP host to send the emails to.";
};
port = lib.mkOption {
type = lib.types.port;
description = "SMTP port to send the emails to.";
default = 25;
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the SMTP host.";
};
password = contracts.secret.mkOption {
description = "File containing the password to connect to the SMTP host.";
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
};
2023-12-11 06:56:31 +01:00
};
}))
];
2023-08-10 05:39:32 +02:00
};
2023-08-07 07:36:31 +02:00
rules = lib.mkOption {
type = lib.types.listOf lib.types.anything;
description = "Rule based clients";
default = [];
};
2024-08-12 02:33:33 +02:00
mount = lib.mkOption {
type = contracts.mount;
description = ''
Mount configuration. This is an output option.
Use it to initialize a block implementing the "mount" contract.
For example, with a zfs dataset:
```
shb.zfs.datasets."authelia" = {
poolName = "root";
} // config.shb.authelia.mount;
```
'';
readOnly = true;
default = { path = "/var/lib/authelia-authelia.${cfg.domain}"; };
defaultText = { path = "/var/lib/authelia-authelia.example.com"; };
};
mountRedis = lib.mkOption {
type = contracts.mount;
description = ''
Mount configuration for Redis. This is an output option.
Use it to initialize a block implementing the "mount" contract.
For example, with a zfs dataset:
```
shb.zfs.datasets."redis-authelia" = {
poolName = "root";
} // config.shb.authelia.mountRedis;
```
'';
readOnly = true;
default = { path = "/var/lib/redis-authelia"; };
};
2023-08-10 05:39:32 +02:00
};
config = lib.mkIf cfg.enable {
2023-12-11 06:56:31 +01:00
assertions = [
{
assertion = builtins.length cfg.oidcClients > 0;
message = "Must have at least one oidc client otherwise Authelia refuses to start.";
}
];
2023-08-10 05:39:32 +02:00
# Overriding the user name so we don't allow any weird characters anywhere. For example, postgres users do not accept the '.'.
users = {
groups.${autheliaCfg.user} = {};
users.${autheliaCfg.user} = {
isSystemUser = true;
group = autheliaCfg.user;
};
};
services.authelia.instances.${fqdn} = {
enable = true;
user = cfg.autheliaUser;
2023-08-10 05:39:32 +02:00
secrets = {
jwtSecretFile = cfg.secrets.jwtSecret.result.path;
storageEncryptionKeyFile = cfg.secrets.storageEncryptionKey.result.path;
2023-08-10 05:39:32 +02:00
};
# See https://www.authelia.com/configuration/methods/secrets/
environmentVariables = {
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPassword.result.path;
AUTHELIA_SESSION_SECRET_FILE = toString cfg.secrets.sessionSecret.result.path;
2023-08-10 05:39:32 +02:00
# Not needed since we use peer auth.
# AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password";
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = toString cfg.secrets.storageEncryptionKey.result.path;
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
2023-12-11 06:56:31 +01:00
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.password.result.path);
2023-08-10 05:39:32 +02:00
};
settings = {
server.address = "tcp://127.0.0.1:9091";
2023-08-10 05:39:32 +02:00
# Inspired from https://github.com/lldap/lldap/blob/7d1f5abc137821c500de99c94f7579761fc949d8/example_configs/authelia_config.yml
authentication_backend = {
refresh_interval = "5m";
password_reset = {
disable = "false";
};
ldap = {
implementation = "custom";
address = "ldap://${cfg.ldapHostname}:${toString cfg.ldapPort}";
2023-08-10 05:39:32 +02:00
timeout = "5s";
start_tls = "false";
base_dn = cfg.dcdomain;
additional_users_dn = "ou=people";
# Sign in with username or email.
users_filter = "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))";
additional_groups_dn = "ou=groups";
groups_filter = "(member={dn})";
user = "uid=admin,ou=people,${cfg.dcdomain}";
attributes = {
username = "uid";
group_name = "cn";
mail = "mail";
display_name = "displayName";
};
2023-08-10 05:39:32 +02:00
};
};
totp = {
disable = "false";
issuer = fqdnWithPort;
2023-08-10 05:39:32 +02:00
algorithm = "sha1";
digits = "6";
period = "30";
skew = "1";
secret_size = "32";
};
# Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis
session = {
name = "authelia_session";
cookies = [{
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
}];
2023-08-10 05:39:32 +02:00
same_site = "lax";
expiration = "1h";
inactivity = "5m";
remember_me = "1M";
2023-08-10 05:39:32 +02:00
redis = {
host = config.services.redis.servers.authelia.unixSocket;
port = 0;
};
};
storage = {
postgres = {
address = "unix:///run/postgresql";
2023-08-10 05:39:32 +02:00
username = autheliaCfg.user;
database = autheliaCfg.user;
# Uses peer auth for local users, so we don't need a password.
password = "test";
};
};
notifier = {
filesystem = lib.mkIf (builtins.isString cfg.smtp) {
filename = cfg.smtp;
2023-12-11 06:56:31 +01:00
};
smtp = lib.mkIf (!(builtins.isString cfg.smtp)) {
2023-12-11 06:56:31 +01:00
host = cfg.smtp.host;
port = cfg.smtp.port;
username = cfg.smtp.username;
sender = "${cfg.smtp.from_name} <${cfg.smtp.from_address}>";
2023-08-10 05:39:32 +02:00
subject = "[Authelia] {title}";
startup_check_address = "test@authelia.com";
};
};
access_control = {
default_policy = "deny";
networks = [
{
name = "internal";
networks = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/18" ];
}
];
rules = [
{
domain = fqdnWithPort;
2023-08-09 09:09:56 +02:00
policy = "bypass";
resources = [
"^/api/.*"
];
2023-08-10 05:39:32 +02:00
}
2023-08-09 09:09:56 +02:00
] ++ cfg.rules;
2023-08-10 05:39:32 +02:00
};
telemetry = {
metrics = {
enabled = true;
address = "tcp://127.0.0.1:9959";
};
};
};
2023-12-11 06:56:31 +01:00
settingsFiles = [ "/var/lib/authelia-${fqdn}/oidc_clients.yaml" ];
2023-08-10 05:39:32 +02:00
};
2023-12-11 06:56:31 +01:00
systemd.services."authelia-${fqdn}".preStart =
let
mkCfg = clients:
2024-03-01 00:34:53 +01:00
shblib.replaceSecrets {
userConfig = {
identity_providers.oidc.clients = clients;
};
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
generator = shblib.replaceSecretsGeneratorAdapter (lib.generators.toYAML {});
2024-03-01 00:34:53 +01:00
};
2023-12-11 06:56:31 +01:00
in
lib.mkBefore (mkCfg cfg.oidcClients + ''
${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -v -w1 ${cfg.ldapHostname} ${toString cfg.ldapPort}; do echo "Waiting for port ${cfg.ldapHostname}:${toString cfg.ldapPort} to open..."; sleep 2; done); sleep 2'
'');
2023-12-11 06:56:31 +01:00
2023-08-10 05:39:32 +02:00
services.nginx.virtualHosts.${fqdn} = {
2024-01-12 08:22:46 +01:00
forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
2023-09-03 00:00:41 +02:00
# Taken from https://github.com/authelia/authelia/issues/178
# TODO: merge with config from https://matwick.ca/authelia-nginx-sso/
locations."/".extraConfig = ''
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
2023-08-10 05:39:32 +02:00
2023-09-03 00:00:41 +02:00
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
proxy_pass http://127.0.0.1:9091;
2023-09-03 00:00:41 +02:00
proxy_intercept_errors on;
if ($request_method !~ ^(POST)$){
error_page 401 = /error/401;
error_page 403 = /error/403;
error_page 404 = /error/404;
}
'';
locations."/api/verify".extraConfig = ''
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
proxy_set_header Host $http_x_forwarded_host;
proxy_pass http://127.0.0.1:9091;
2023-09-03 00:00:41 +02:00
'';
2023-08-10 05:39:32 +02:00
};
services.redis.servers.authelia = {
enable = true;
user = autheliaCfg.user;
};
shb.postgresql.ensures = [
2023-11-06 01:37:50 +01:00
{
username = autheliaCfg.user;
database = autheliaCfg.user;
}
];
2023-08-10 05:39:32 +02:00
services.prometheus.scrapeConfigs = [
{
job_name = "authelia";
static_configs = [
{
targets = ["127.0.0.1:9959"];
}
];
}
];
};
}