1
0
Fork 0
This commit is contained in:
Pierre Penninckx 2024-10-13 21:30:30 +00:00 committed by GitHub
commit bdfb4d7371
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 249 additions and 74 deletions

View file

@ -34,6 +34,7 @@
allModules = [ allModules = [
modules/blocks/authelia.nix modules/blocks/authelia.nix
modules/blocks/davfs.nix modules/blocks/davfs.nix
modules/blocks/hardcodedsecret.nix
modules/blocks/ldap.nix modules/blocks/ldap.nix
modules/blocks/monitoring.nix modules/blocks/monitoring.nix
modules/blocks/nginx.nix modules/blocks/nginx.nix

View file

@ -1,7 +1,7 @@
{ pkgs, lib }: { pkgs, lib }:
let let
inherit (builtins) isAttrs hasAttr; inherit (builtins) isAttrs hasAttr;
inherit (lib) concatStringsSep; inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList;
in in
rec { rec {
# Replace secrets in a file. # Replace secrets in a file.
@ -34,14 +34,30 @@ rec {
replaceSecretsScript = { file, resultPath, replacements, user ? null, permissions ? "u=r,g=r,o=" }: replaceSecretsScript = { file, resultPath, replacements, user ? null, permissions ? "u=r,g=r,o=" }:
let let
templatePath = resultPath + ".template"; templatePath = resultPath + ".template";
sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements);
sedCmd = if replacements == {} t = { transform ? null, ... }: if isNull transform then x: x else transform;
genReplacement = secret:
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
# We check that the files containing the secrets have the
# correct permissions for us to read them in this separate
# step. Otherwise, the $(cat ...) commands inside the sed
# replacements could fail but not fail individually but
# not fail the whole script.
checkPermissions = concatMapStringsSep "\n" (pattern: "cat ${pattern.source} > /dev/null") replacements;
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (map genReplacement replacements);
sedCmd = if replacements == []
then "cat" then "cat"
else "${pkgs.gnused}/bin/sed ${sedPatterns}"; else "${pkgs.gnused}/bin/sed ${sedPatterns}";
in in
'' ''
set -euo pipefail set -euo pipefail
${checkPermissions}
mkdir -p $(dirname ${templatePath}) mkdir -p $(dirname ${templatePath})
ln -fs ${file} ${templatePath} ln -fs ${file} ${templatePath}
rm -f ${resultPath} rm -f ${resultPath}
@ -71,8 +87,8 @@ rec {
}; };
}; };
secretName = name: secretName = names:
"%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) name)}%"; "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
withReplacements = attrs: withReplacements = attrs:
let let
@ -91,15 +107,8 @@ rec {
else value // { name = name; }; else value // { name = name; };
secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs; secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
allSecrets = collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
t = { transform ? null, ... }: if isNull transform then x: x else transform;
genReplacement = secret:
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
in in
lib.attrsets.listToAttrs (map genReplacement allSecrets); collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
mapAttrsRecursiveCond = mapAttrsRecursiveCond =
@ -238,7 +247,7 @@ rec {
results = pkgs.lib.runTests tests; results = pkgs.lib.runTests tests;
in in
if results != [ ] then if results != [ ] then
builtins.throw (builtins.concatStringsSep "\n" (map resultToString (lib.traceValSeqN 3 results))) builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeqN 3 results)))
else else
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out"; pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";

View file

@ -67,33 +67,45 @@ in
description = "Secrets needed by Authelia"; description = "Secrets needed by Authelia";
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
jwtSecretFile = lib.mkOption { jwtSecret = contracts.secret.mkOption {
type = lib.types.path; description = "JWT secret.";
description = "File containing the JWT secret."; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
}; };
ldapAdminPasswordFile = lib.mkOption { ldapAdminPassword = contracts.secret.mkOption {
type = lib.types.path; description = "LDAP admin user password.";
description = "File containing the LDAP admin user password."; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
}; };
sessionSecretFile = lib.mkOption { sessionSecret = contracts.secret.mkOption {
type = lib.types.path; description = "Session secret.";
description = "File containing the session secret."; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
}; };
storageEncryptionKeyFile = lib.mkOption { storageEncryptionKey = contracts.secret.mkOption {
type = lib.types.path; description = "Storage encryption key.";
description = "File containing the storage encryption key."; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
}; };
identityProvidersOIDCHMACSecretFile = lib.mkOption { identityProvidersOIDCHMACSecret = contracts.secret.mkOption {
type = lib.types.path; description = "Identity provider OIDC HMAC secret.";
description = "File containing the identity provider OIDC HMAC secret."; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
}; };
identityProvidersOIDCIssuerPrivateKeyFile = lib.mkOption { identityProvidersOIDCIssuerPrivateKey = contracts.secret.mkOption {
type = lib.types.path;
description = '' description = ''
File containing the identity provider OIDC issuer private key. Identity provider OIDC issuer private key.
Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048` Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048`
''; '';
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
}; };
}; };
}; };
@ -207,9 +219,11 @@ in
type = lib.types.str; type = lib.types.str;
description = "Username to connect to the SMTP host."; description = "Username to connect to the SMTP host.";
}; };
passwordFile = lib.mkOption { password = contracts.secret.mkOption {
type = lib.types.str;
description = "File containing the password to connect to the SMTP host."; description = "File containing the password to connect to the SMTP host.";
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
}; };
}; };
})) }))
@ -282,19 +296,20 @@ in
user = cfg.autheliaUser; user = cfg.autheliaUser;
secrets = { secrets = {
inherit (cfg.secrets) jwtSecretFile storageEncryptionKeyFile; jwtSecretFile = cfg.secrets.jwtSecret.result.path;
storageEncryptionKeyFile = cfg.secrets.storageEncryptionKey.result.path;
}; };
# See https://www.authelia.com/configuration/methods/secrets/ # See https://www.authelia.com/configuration/methods/secrets/
environmentVariables = { environmentVariables = {
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPasswordFile; AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPassword.result.path;
AUTHELIA_SESSION_SECRET_FILE = toString cfg.secrets.sessionSecretFile; AUTHELIA_SESSION_SECRET_FILE = toString cfg.secrets.sessionSecret.result.path;
# Not needed since we use peer auth. # Not needed since we use peer auth.
# AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password"; # AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password";
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = toString cfg.secrets.storageEncryptionKeyFile; AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = toString cfg.secrets.storageEncryptionKey.result.path;
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecretFile; AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKeyFile; AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.passwordFile); AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.password.result.path);
}; };
settings = { settings = {
server.address = "tcp://127.0.0.1:9091"; server.address = "tcp://127.0.0.1:9091";

View file

@ -0,0 +1,95 @@
{ config, options, lib, pkgs, ... }:
let
cfg = config.shb.hardcodedsecret;
opt = options.shb.hardcodedsecret;
inherit (lib) mapAttrs' mkOption nameValuePair;
inherit (lib.types) attrsOf listOf path nullOr str submodule;
inherit (pkgs) writeText;
in
{
options.shb.hardcodedsecret = mkOption {
default = {};
type = attrsOf (submodule ({ name, ... }: {
options = {
mode = mkOption {
description = ''
Mode of the secret file.
'';
type = str;
default = "0400";
};
owner = mkOption {
description = ''
Linux user owning the secret file.
'';
type = str;
default = "root";
};
group = mkOption {
description = ''
Linux group owning the secret file.
'';
type = str;
default = "root";
};
restartUnits = mkOption {
description = ''
Systemd units to restart after the secret is updated.
'';
type = listOf str;
default = [];
};
path = mkOption {
type = path;
description = ''
Path to the file containing the secret generated out of band.
This path will exist after deploying to a target host,
it is not available through the nix store.
'';
default = "/run/hardcodedsecrets/hardcodedsecret_${name}";
};
content = mkOption {
type = nullOr str;
description = ''
Content of the secret.
This will be stored in the nix store and should only be used for testing or maybe in dev.
'';
default = null;
};
source = mkOption {
type = nullOr str;
description = ''
Source of the content of the secret.
'';
default = null;
};
};
}));
};
config = {
system.activationScripts = mapAttrs' (n: cfg':
let
source = if cfg'.source != null
then cfg'.source
else writeText "hardcodedsecret_${n}_content" cfg'.content;
in
nameValuePair "hardcodedsecret_${n}" ''
mkdir -p "$(dirname "${cfg'.path}")"
touch "${cfg'.path}"
chmod ${cfg'.mode} "${cfg'.path}"
chown ${cfg'.owner}:${cfg'.group} "${cfg'.path}"
cp ${source} "${cfg'.path}"
''
) cfg;
};
}

View file

@ -67,9 +67,12 @@ in
default = "jellyfin_admin"; default = "jellyfin_admin";
}; };
passwordFile = lib.mkOption { passwordFile = contracts.secret.mkOption {
type = lib.types.path; description = "LDAP admin password.";
description = "File containing the LDAP admin password."; mode = "0440";
owner = "jellyfin";
group = "jellyfin";
restartUnits = [ "jellyfin.service" ];
}; };
}; };
}; };
@ -118,9 +121,18 @@ in
default = "one_factor"; default = "one_factor";
}; };
secretFile = lib.mkOption { secretFile = contracts.secret.mkOption {
type = lib.types.path; description = "OIDC shared secret for Jellyfin.";
description = "File containing the OIDC shared secret."; mode = "0440";
owner = "jellyfin";
group = "jellyfin";
restartUnits = [ "jellyfin.service" ];
};
secretFileAuthelia = contracts.secret.mkOption {
description = "OIDC shared secret for Authelia.";
mode = "0400";
owner = config.shb.authelia.autheliaUser;
}; };
}; };
}; };
@ -400,30 +412,35 @@ in
lib.strings.optionalString cfg.ldap.enable (shblib.replaceSecretsScript { lib.strings.optionalString cfg.ldap.enable (shblib.replaceSecretsScript {
file = ldapConfig; file = ldapConfig;
resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml"; resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml";
replacements = { replacements = [
"%LDAP_PASSWORD%" = "$(cat ${cfg.ldap.passwordFile})"; {
}; name = [ "%LDAP_PASSWORD%" ];
source = cfg.ldap.passwordFile.result.path;
}
];
}) })
+ lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript { + lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
file = ssoConfig; file = ssoConfig;
resultPath = "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml"; resultPath = "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml";
replacements = { replacements = [
"%SSO_SECRET%" = "$(cat ${cfg.sso.secretFile})"; {
}; name = [ "%SSO_SECRET%" ];
source = cfg.sso.secretFile.result.path;
}
];
}) })
+ lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript { + lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
file = brandingConfig; file = brandingConfig;
resultPath = "/var/lib/jellyfin/config/branding.xml"; resultPath = "/var/lib/jellyfin/config/branding.xml";
replacements = { replacements = [
"%a%" = "%a%"; ];
};
}); });
shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [ shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [
{ {
client_id = cfg.sso.clientID; client_id = cfg.sso.clientID;
client_name = "Jellyfin"; client_name = "Jellyfin";
client_secret.source = cfg.sso.secretFile; client_secret.source = cfg.sso.secretFileAuthelia.result.path;
public = false; public = false;
authorization_policy = cfg.sso.authorization_policy; authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ]; redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ];

View file

@ -1,6 +1,4 @@
{ { lib }:
lib,
}:
let let
baseImports = pkgs: [ baseImports = pkgs: [
(pkgs.path + "/nixos/modules/profiles/headless.nix") (pkgs.path + "/nixos/modules/profiles/headless.nix")
@ -109,6 +107,7 @@ in
../modules/blocks/postgresql.nix ../modules/blocks/postgresql.nix
../modules/blocks/authelia.nix ../modules/blocks/authelia.nix
../modules/blocks/nginx.nix ../modules/blocks/nginx.nix
../modules/blocks/hardcodedsecret.nix
] ]
++ additionalModules; ++ additionalModules;
@ -138,7 +137,7 @@ in
systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ]; systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
}; };
ldap = domain: pkgs: { ldap = domain: pkgs: { config, ... }: {
imports = [ imports = [
../modules/blocks/ldap.nix ../modules/blocks/ldap.nix
]; ];
@ -147,6 +146,13 @@ in
"127.0.0.1" = [ "ldap.${domain}" ]; "127.0.0.1" = [ "ldap.${domain}" ];
}; };
shb.hardcodedsecret.ldapUserPassword = config.shb.ldap.ldapUserPassword.request // {
content = "ldapUserPassword";
};
shb.hardcodedsecret.jwtSecret = config.shb.ldap.ldapUserPassword.request // {
content = "jwtSecrets";
};
shb.ldap = { shb.ldap = {
enable = true; enable = true;
inherit domain; inherit domain;
@ -154,8 +160,8 @@ in
ldapPort = 3890; ldapPort = 3890;
webUIListenPort = 17170; webUIListenPort = 17170;
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
ldapUserPassword.result.path = pkgs.writeText "ldapUserPassword" "ldapUserPassword"; ldapUserPassword.result.path = config.shb.hardcodedsecret.ldapUserPassword.path;
jwtSecret.result.path = pkgs.writeText "jwtSecret" "jwtSecret"; jwtSecret.result.path = config.shb.hardcodedsecret.jwtSecret.path;
}; };
}; };
@ -179,17 +185,36 @@ in
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
secrets = { secrets = {
jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret"; jwtSecret.result.path = config.shb.hardcodedsecret.autheliaJwtSecret.path;
ldapAdminPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword"; ldapAdminPassword.result.path = config.shb.hardcodedsecret.ldapAdminPassword.path;
sessionSecretFile = pkgs.writeText "sessionSecret" "sessionSecret"; sessionSecret.result.path = config.shb.hardcodedsecret.sessionSecret.path;
storageEncryptionKeyFile = pkgs.writeText "storageEncryptionKey" "storageEncryptionKey"; storageEncryptionKey.result.path = config.shb.hardcodedsecret.storageEncryptionKey.path;
identityProvidersOIDCHMACSecretFile = pkgs.writeText "identityProvidersOIDCHMACSecret" "identityProvidersOIDCHMACSecret"; identityProvidersOIDCHMACSecret.result.path = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.path;
identityProvidersOIDCIssuerPrivateKeyFile = (pkgs.runCommand "gen-private-key" {} '' identityProvidersOIDCIssuerPrivateKey.result.path = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.path;
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem";
}; };
}; };
shb.hardcodedsecret.autheliaJwtSecret = config.shb.authelia.secrets.jwtSecret.request // {
content = "jwtSecret";
};
shb.hardcodedsecret.ldapAdminPassword = config.shb.authelia.secrets.ldapAdminPassword.request // {
content = "ldapUserPassword";
};
shb.hardcodedsecret.sessionSecret = config.shb.authelia.secrets.sessionSecret.request // {
content = "sessionSecret";
};
shb.hardcodedsecret.storageEncryptionKey = config.shb.authelia.secrets.storageEncryptionKey.request // {
content = "storageEncryptionKey";
};
shb.hardcodedsecret.identityProvidersOIDCHMACSecret = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request // {
content = "identityProvidersOIDCHMACSecret";
};
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request // {
source = (pkgs.runCommand "gen-private-key" {} ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem";
};
}; };
} }

View file

@ -43,9 +43,13 @@ let
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
passwordFile = config.shb.ldap.ldapUserPassword.result.path; passwordFile.result.path = config.shb.hardcodedsecret.jellyfinLdapUserPassword.path;
}; };
}; };
shb.hardcodedsecret.jellyfinLdapUserPassword = config.shb.jellyfin.ldap.passwordFile.request // {
content = "ldapUserPassword";
};
}; };
sso = { config, ... }: { sso = { config, ... }: {
@ -53,9 +57,18 @@ let
sso = { sso = {
enable = true; enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secretFile = pkgs.writeText "ssoSecretFile" "ssoSecretFile"; secretFile.result.path = config.shb.hardcodedsecret.jellyfinSSOPassword.path;
secretFileAuthelia.result.path = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.path;
}; };
}; };
shb.hardcodedsecret.jellyfinSSOPassword = config.shb.jellyfin.sso.secretFile.request // {
content = "ssoPassword";
};
shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = config.shb.jellyfin.sso.secretFileAuthelia.request // {
content = "ssoPassword";
};
}; };
in in
{ {