From 5c25d746671f387dc3cd675442920b67919cdaf3 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Wed, 20 Nov 2024 18:26:34 +0100 Subject: [PATCH] update secret contract and add sops module --- CHANGELOG.md | 1 + README.md | 14 +- docs/blocks.md | 4 + docs/contracts.md | 9 +- docs/default.nix | 5 + docs/preface.md | 12 +- docs/usage.md | 8 +- modules/blocks/authelia.nix | 84 ++++--- modules/blocks/hardcodedsecret.nix | 118 ++++----- modules/blocks/ldap.nix | 28 ++- modules/blocks/monitoring.nix | 30 ++- modules/blocks/monitoring/docs/default.md | 4 +- modules/blocks/restic.nix | 47 ++-- modules/blocks/restic/docs/default.md | 19 +- modules/blocks/sops.nix | 36 +++ modules/blocks/sops/docs/default.md | 47 ++++ modules/contracts/backup.nix | 4 +- modules/contracts/backup/test.nix | 4 +- modules/contracts/databasebackup/test.nix | 21 +- modules/contracts/default.nix | 41 +++- modules/contracts/secret.nix | 191 +++++++-------- modules/contracts/secret/docs/default.md | 135 ++++++----- modules/contracts/secret/dummyModule.nix | 14 +- modules/contracts/secret/test.nix | 19 +- modules/services/forgejo.nix | 227 ++++++++++-------- modules/services/forgejo/docs/default.md | 87 ++++--- modules/services/jellyfin.nix | 38 ++- modules/services/nextcloud-server.nix | 50 ++-- .../services/nextcloud-server/docs/default.md | 69 +++--- modules/services/vaultwarden.nix | 29 ++- test/blocks/authelia.nix | 55 +++-- test/blocks/ldap.nix | 18 +- test/blocks/restic.nix | 31 ++- test/common.nix | 56 +++-- test/contracts/backup.nix | 22 +- test/contracts/databasebackup.nix | 14 +- test/contracts/secret.nix | 8 +- test/services/forgejo.nix | 35 +-- test/services/jellyfin.nix | 21 +- test/services/monitoring.nix | 13 +- test/services/nextcloud.nix | 23 +- test/services/vaultwarden.nix | 14 +- 42 files changed, 1023 insertions(+), 682 deletions(-) create mode 100644 modules/blocks/sops.nix create mode 100644 modules/blocks/sops/docs/default.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ac0224..8b42c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - `shb.forgejo.databasePasswordFile` -> `shb.forgejo.databasePassword.result.path`. - Backup: - `shb.restic.instances` options has been split between `shb.restic.instances.request` and `shb.restic.instances.settings`, matching better with contracts. +- Use of secret contract everywhere. ## User Facing Backwards Compatible Changes diff --git a/README.md b/README.md index dbc72b1..856beb5 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ SHB's first goal is to provide unified [building blocks](#available-blocks) and by extension configuration interface, for self-hosting. Compare the configuration for Nextcloud and Forgejo in Self Host Blocks. -The following snippets focus on similitudes and assume the relevant blocks are configured off-screen. +The following snippets focus on similitudes and assume the relevant blocks - like secrets - are configured off-screen. It also does not show specific options for each service. These are still complete snippets that configure HTTPS, subdomain serving the service, LDAP and SSO integration. @@ -87,14 +87,14 @@ shb.nextcloud = { host = "127.0.0.1"; port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; - adminPasswordFile = config.sops.secrets."nextcloud/ldap_admin_password".path; + adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result; }; apps.sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - secretFile = config.sops.secrets."nextcloud/sso/secret".path; - secretFileForAuthelia = config.sops.secrets."authelia/nextcloud_sso_secret".path; + secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result; + secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result; }; }; ``` @@ -112,15 +112,15 @@ shb.forgejo = { host = "127.0.0.1"; port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; - adminPasswordFile = config.sops.secrets."forgejo/ldap_admin_password".path; + adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result; }; sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - secretFile = config.sops.secrets."forgejo/ssoSecret".path; - secretFileForAuthelia = config.sops.secrets."forgejo/authelia/ssoSecret".path; + secret.result = config.shb.sops.secrets."forgejo/sso/secret".result; + secretForAuthelia.result = config.shb.sops.secrets."forgejo/sso/secretForAuthelia".result; }; }; ``` diff --git a/docs/blocks.md b/docs/blocks.md index e978128..b89bb30 100644 --- a/docs/blocks.md +++ b/docs/blocks.md @@ -30,6 +30,10 @@ services you already have deployed. Not all blocks are yet documented. You can find all available blocks [in the repository](@REPO@/modules/blocks). ::: +```{=include=} chapters html:into-file=//blocks-sops.html +modules/blocks/sops/docs/default.md +``` + ```{=include=} chapters html:into-file=//blocks-ssl.html modules/blocks/ssl/docs/default.md ``` diff --git a/docs/contracts.md b/docs/contracts.md index 8ddb07a..16d24d8 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -148,11 +148,14 @@ Provided contracts are: - [SSL generator contract](contracts-ssl.html) to generate SSL certificates. Two providers are implemented: self-signed and Let's Encrypt. - [Backup contract](contracts-backup.html) to backup directories. - One provider is implemented: Restic. + One provider is implemented: [Restic][]. - [Database Backup contract](contracts-databasebackup.html) to backup database dumps. - One provider is implemented: Restic. + One provider is implemented: [Restic][]. - [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store. - One provider is implemented: SOPS. + One provider is implemented: [SOPS][]. + +[restic]: blocks-restic.html +[sops]: blocks-sops.html ```{=include=} chapters html:into-file=//contracts-ssl.html modules/contracts/ssl/docs/default.md diff --git a/docs/default.nix b/docs/default.nix index 410d5e4..c30758a 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -142,6 +142,11 @@ in stdenv.mkDerivation { '@OPTIONS_JSON@' \ ${individualModuleOptionsDocs [ ../modules/blocks/restic.nix ]}/share/doc/nixos/options.json + substituteInPlace ./modules/blocks/sops/docs/default.md \ + --replace \ + '@OPTIONS_JSON@' \ + ${individualModuleOptionsDocs [ ../modules/blocks/sops.nix ]}/share/doc/nixos/options.json + substituteInPlace ./modules/services/nextcloud-server/docs/default.md \ --replace \ '@OPTIONS_JSON@' \ diff --git a/docs/preface.md b/docs/preface.md index 219fec5..19460fb 100644 --- a/docs/preface.md +++ b/docs/preface.md @@ -39,14 +39,14 @@ shb.nextcloud = { host = "127.0.0.1"; port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; - adminPasswordFile = config.sops.secrets."nextcloud/ldap_admin_password".path; + adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result; }; apps.sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - secretFile = config.sops.secrets."nextcloud/sso/secret".path; - secretFileForAuthelia = config.sops.secrets."authelia/nextcloud_sso_secret".path; + secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result; + secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result; }; }; ``` @@ -64,15 +64,15 @@ shb.forgejo = { host = "127.0.0.1"; port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; - adminPasswordFile = config.sops.secrets."forgejo/ldap_admin_password".path; + adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result; }; sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - secretFile = config.sops.secrets."forgejo/ssoSecret".path; - secretFileForAuthelia = config.sops.secrets."forgejo/authelia/ssoSecret".path; + secret.result = config.shb.sops.secrets."forgejo/sso/secret".result; + secretForAuthelia.result = config.shb.sops.secrets."forgejo/sso/secretForAuthelia".result; }; }; ``` diff --git a/docs/usage.md b/docs/usage.md index 03e513c..9751188 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -286,8 +286,8 @@ One way to setup secrets management using `sops-nix`: Setting the default this way makes all sops instances use that same file. 7. Reference the secrets in nix: ```nix - shb.nextcloud.adminPass.result.path = config.sops.secrets."nextcloud/adminpass".path; - - sops.secrets."nextcloud/adminpass" = config.shb.nextcloud.adminPass.request; + shb.sops.secrets."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request; + shb.nextcloud.adminPass.result = config.shb.sops.secrets."nextcloud/adminpass".result; ``` - The above snippet uses the [secrets contract](./contracts-secret.html) to ease configuration. + The above snippet uses the [secrets contract](./contracts-secret.html) + and [sops block](./blocks-sops.html) to ease the configuration. diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix index 1c27dd3..06eaa51 100644 --- a/modules/blocks/authelia.nix +++ b/modules/blocks/authelia.nix @@ -68,45 +68,69 @@ in description = "Secrets needed by Authelia"; type = lib.types.submodule { options = { - jwtSecret = contracts.secret.mkOption { + jwtSecret = lib.mkOption { description = "JWT secret."; - mode = "0400"; - owner = cfg.autheliaUser; - restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + }; + }; }; - ldapAdminPassword = contracts.secret.mkOption { + ldapAdminPassword = lib.mkOption { description = "LDAP admin user password."; - mode = "0400"; - owner = cfg.autheliaUser; - restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + }; + }; }; - sessionSecret = contracts.secret.mkOption { + sessionSecret = lib.mkOption { description = "Session secret."; - mode = "0400"; - owner = cfg.autheliaUser; - restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + }; + }; }; - storageEncryptionKey = contracts.secret.mkOption { + storageEncryptionKey = lib.mkOption { description = "Storage encryption key."; - mode = "0400"; - owner = cfg.autheliaUser; - restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + }; + }; }; - identityProvidersOIDCHMACSecret = contracts.secret.mkOption { + identityProvidersOIDCHMACSecret = lib.mkOption { description = "Identity provider OIDC HMAC secret."; - mode = "0400"; - owner = cfg.autheliaUser; - restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + }; + }; }; - identityProvidersOIDCIssuerPrivateKey = contracts.secret.mkOption { + identityProvidersOIDCIssuerPrivateKey = lib.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}" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; + }; + }; }; }; }; @@ -220,11 +244,15 @@ in type = lib.types.str; description = "Username to connect to the SMTP host."; }; - password = contracts.secret.mkOption { + password = lib.mkOption { description = "File containing the password to connect to the SMTP host."; - mode = "0400"; - owner = cfg.autheliaUser; - restartUnits = [ "authelia-${fqdn}" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${fqdn}" ]; + }; + }; }; }; })) diff --git a/modules/blocks/hardcodedsecret.nix b/modules/blocks/hardcodedsecret.nix index 4a174d0..2c0ee84 100644 --- a/modules/blocks/hardcodedsecret.nix +++ b/modules/blocks/hardcodedsecret.nix @@ -1,10 +1,11 @@ -{ config, options, lib, pkgs, ... }: +{ config, lib, pkgs, ... }: let cfg = config.shb.hardcodedsecret; - opt = options.shb.hardcodedsecret; + + contracts = pkgs.callPackage ../contracts {}; inherit (lib) mapAttrs' mkOption nameValuePair; - inherit (lib.types) attrsOf listOf path nullOr str submodule; + inherit (lib.types) attrsOf nullOr str submodule; inherit (pkgs) writeText; in { @@ -16,74 +17,49 @@ in example = lib.literalExpression '' { mySecret = { - user = "me"; - mode = "0400"; - restartUnits = [ "myservice.service" ]; - content = "My Secrets"; + request = { + user = "me"; + mode = "0400"; + restartUnits = [ "myservice.service" ]; + }; + settings.content = "My Secret"; }; } ''; type = attrsOf (submodule ({ name, ... }: { - options = { - mode = mkOption { + options = contracts.secret.mkProvider { + settings = mkOption { description = '' - Mode of the secret file. + Settings specific to the hardcoded secret module. + + Give either `content` or `source`. ''; - type = str; - default = "0400"; + + type = submodule { + options = { + content = mkOption { + type = nullOr str; + description = '' + Content of the secret as a string. + + 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 as a path in the nix store. + ''; + default = null; + }; + }; + }; }; - 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; + resultCfg = { + path = "/run/hardcodedsecrets/hardcodedsecret_${name}"; }; }; })); @@ -92,16 +68,16 @@ in config = { system.activationScripts = mapAttrs' (n: cfg': let - source = if cfg'.source != null - then cfg'.source - else writeText "hardcodedsecret_${n}_content" cfg'.content; + source = if cfg'.settings.source != null + then cfg'.settings.source + else writeText "hardcodedsecret_${n}_content" cfg'.settings.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}" + mkdir -p "$(dirname "${cfg'.result.path}")" + touch "${cfg'.result.path}" + chmod ${cfg'.request.mode} "${cfg'.result.path}" + chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}" + cp ${source} "${cfg'.result.path}" '' ) cfg; }; diff --git a/modules/blocks/ldap.nix b/modules/blocks/ldap.nix index 6bee35b..ef2ed61 100644 --- a/modules/blocks/ldap.nix +++ b/modules/blocks/ldap.nix @@ -47,20 +47,28 @@ in default = 17170; }; - ldapUserPassword = contracts.secret.mkOption { + ldapUserPassword = lib.mkOption { description = "LDAP admin user secret."; - mode = "0440"; - owner = "lldap"; - group = "lldap"; - restartUnits = [ "lldap.service" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "lldap"; + group = "lldap"; + restartUnits = [ "lldap.service" ]; + }; + }; }; - jwtSecret = contracts.secret.mkOption { + jwtSecret = lib.mkOption { description = "JWT secret."; - mode = "0440"; - owner = "lldap"; - group = "lldap"; - restartUnits = [ "lldap.service" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "lldap"; + group = "lldap"; + restartUnits = [ "lldap.service" ]; + }; + }; }; restrictAccessIPRange = lib.mkOption { diff --git a/modules/blocks/monitoring.nix b/modules/blocks/monitoring.nix index f59c4a6..ed34e7d 100644 --- a/modules/blocks/monitoring.nix +++ b/modules/blocks/monitoring.nix @@ -82,14 +82,28 @@ in default = []; }; - adminPasswordFile = lib.mkOption { - type = lib.types.path; - description = "File containing the initial admin password."; + adminPassword = lib.mkOption { + description = "Initial admin password."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "grafana"; + group = "grafana"; + restartUnits = [ "grafana.service" ]; + }; + }; }; - secretKeyFile = lib.mkOption { - type = lib.types.path; - description = "File containing the secret key used for signing."; + secretKey = lib.mkOption { + description = "Secret key used for signing."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "grafana"; + group = "grafana"; + restartUnits = [ "grafana.service" ]; + }; + }; }; smtp = lib.mkOption { @@ -159,9 +173,9 @@ in }; security = { - secret_key = "$__file{${cfg.secretKeyFile}}"; + secret_key = "$__file{${cfg.secretKey.result.path}}"; disable_initial_admin_creation = false; # Enable when LDAP support is configured. - admin_password = "$__file{${cfg.adminPasswordFile}}"; # Remove when LDAP support is configured. + admin_password = "$__file{${cfg.adminPassword.result.path}}"; # Remove when LDAP support is configured. }; server = { diff --git a/modules/blocks/monitoring/docs/default.md b/modules/blocks/monitoring/docs/default.md index 17240e7..a0774ef 100644 --- a/modules/blocks/monitoring/docs/default.md +++ b/modules/blocks/monitoring/docs/default.md @@ -16,8 +16,8 @@ shb.monitoring = { subdomain = "grafana"; inherit domain; contactPoints = [ "me@example.com" ]; - adminPasswordFile = config.sops.secrets."monitoring/admin_password".path; - secretKeyFile = config.sops.secrets."monitoring/secret_key".path; + adminPassword.result = config.sops.secrets."monitoring/admin_password".reuslt; + secretKey.result = config.sops.secrets."monitoring/secret_key".result; }; sops.secrets."monitoring/admin_password" = { diff --git a/modules/blocks/restic.nix b/modules/blocks/restic.nix index 2a9b370..0f51fb5 100644 --- a/modules/blocks/restic.nix +++ b/modules/blocks/restic.nix @@ -6,11 +6,11 @@ let shblib = pkgs.callPackage ../../lib {}; contracts = pkgs.callPackage ../contracts {}; - inherit (lib) concatStringsSep filterAttrs flatten literalExpression optionals optionalString listToAttrs mapAttrsToList mkEnableOption mkOption mkMerge; + inherit (lib) concatStringsSep filterAttrs flatten literalExpression optionals listToAttrs mapAttrsToList mkEnableOption mkOption mkMerge; inherit (lib) generators hasPrefix mkIf nameValuePair optionalAttrs removePrefix; - inherit (lib.types) attrsOf enum int ints listOf oneOf nonEmptyListOf nonEmptyStr nullOr path str submodule; + inherit (lib.types) attrsOf enum int ints oneOf nonEmptyStr nullOr str submodule; - commonOptions = { name, options, prefix, ... }: { + commonOptions = { name, prefix, config, ... }: { enable = mkEnableOption '' this backup intance. @@ -18,13 +18,17 @@ let but still provides the helper tool to restore snapshots ''; - passphrase = contracts.secret.mkOption { + passphrase = lib.mkOption { description = "Encryption key for the backup repository."; - mode = "0400"; - owner = options.request.value.user; - ownerText = "[shb.restic.${prefix}..request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)"; - restartUnits = [ (fullName name options.settings.value.repository) ]; - restartUnitsText = "[ [shb.restic.${prefix}..settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]"; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = config.request.user; + ownerText = "[shb.restic.${prefix}..request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)"; + restartUnits = [ (fullName name config.settings.repository) ]; + restartUnitsText = "[ [shb.restic.${prefix}..settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]"; + }; + }; }; repository = mkOption { @@ -99,7 +103,6 @@ let }; repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (removePrefix "/" name); - backupName = name: repository: "${name}_${repoSlugName repository.path}"; fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}"; in { @@ -107,7 +110,7 @@ in instances = mkOption { description = "Files to backup following the [backup contract](./contracts-backup.html)."; default = {}; - type = attrsOf (submodule ({ name, options, ... }: { + type = attrsOf (submodule ({ name, config, ... }: { options = { request = mkOption { description = '' @@ -125,7 +128,7 @@ in ''; type = submodule { - options = commonOptions { inherit name options; prefix = "instances"; }; + options = commonOptions { inherit name config; prefix = "instances"; }; }; }; @@ -136,16 +139,16 @@ in Contains the output of the Restic provider. ''; default = { - restoreScript = fullName name options.settings.value.repository; - backupService = "${fullName name options.settings.value.repository}.service"; + restoreScript = fullName name config.settings.repository; + backupService = "${fullName name config.settings.repository}.service"; }; defaultText = { restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; }; type = contracts.backup.result { - restoreScript = fullName name options.settings.value.repository; - backupService = "${fullName name options.settings.value.repository}.service"; + restoreScript = fullName name config.settings.repository; + backupService = "${fullName name config.settings.repository}.service"; restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; }; @@ -157,7 +160,7 @@ in databases = mkOption { description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html)."; default = {}; - type = attrsOf (submodule ({ name, options, ... }: { + type = attrsOf (submodule ({ name, config, ... }: { options = { request = mkOption { description = '' @@ -175,7 +178,7 @@ in ''; type = submodule { - options = commonOptions { inherit name options; prefix = "databases"; }; + options = commonOptions { inherit name config; prefix = "databases"; }; }; }; @@ -186,16 +189,16 @@ in Contains the output of the Restic provider. ''; default = { - restoreScript = fullName name options.settings.value.repository; - backupService = "${fullName name options.settings.value.repository}.service"; + restoreScript = fullName name config.settings.repository; + backupService = "${fullName name config.settings.repository}.service"; }; defaultText = { restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; }; type = contracts.databasebackup.result { - restoreScript = fullName name options.settings.value.repository; - backupService = "${fullName name options.settings.value.repository}.service"; + restoreScript = fullName name config.settings.repository; + backupService = "${fullName name config.settings.repository}.service"; restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; }; diff --git a/modules/blocks/restic/docs/default.md b/modules/blocks/restic/docs/default.md index 24fce18..5209a81 100644 --- a/modules/blocks/restic/docs/default.md +++ b/modules/blocks/restic/docs/default.md @@ -12,7 +12,7 @@ Specific integration tests are defined in [`/test/blocks/restic.nix`](@REPO@/tes ## Provider Contracts {#blocks-restic-contract-provider} -This block provides: +This block provides the following contracts: - [backup contract](contracts-backup.html) under the [`shb.restic.instances`][instances] option. It is tested with [contract tests][backup contract tests]. @@ -31,10 +31,11 @@ the only requirement to run it is to be able to `sudo` in the expected user. ## Usage {#blocks-restic-usage} -The following examples assume usage of SOPS to provide secrets +The following examples assume usage of the [sops block][] to provide secrets although any blocks providing the [secrets contract][] works too. -The [secrets setup section](usage.html#usage-secrets) explains -how to setup SOPS. + +[sops block]: ./blocks-sops.html +[secrets contract]: ./contracts-secrets.html ### One folder backed up manually {#blocks-restic-usage-provider-manual} @@ -55,7 +56,7 @@ shb.restic.instances."myservice" = { settings = { enable = true; - passphraseFile = ""; + passphrase.result = shb.sops.secret."passphrase".result; repository = { path = "/srv/backups/myservice"; @@ -74,6 +75,9 @@ shb.restic.instances."myservice" = { }; }; }; + +shb.sops.secret."passphrase".request = + shb.restic.instances."myservice".settings.passphrase.request; ``` ### One folder backed up with contract {#blocks-restic-usage-provider-contract} @@ -89,7 +93,7 @@ shb.restic.instances."myservice" = { settings = { enable = true; - passphraseFile = ""; + passphrase.result = shb.sops.secret."passphrase".result; repository = { path = "/srv/backups/myservice"; @@ -108,6 +112,9 @@ shb.restic.instances."myservice" = { }; }; }; + +shb.sops.secret."passphrase".request = + shb.restic.instances."myservice".settings.passphrase.request; ``` ### One folder backed up to S3 {#blocks-restic-usage-provider-remote} diff --git a/modules/blocks/sops.nix b/modules/blocks/sops.nix new file mode 100644 index 0000000..4f9f26a --- /dev/null +++ b/modules/blocks/sops.nix @@ -0,0 +1,36 @@ +{ lib, pkgs, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) attrsOf anything submodule; + + contracts = pkgs.callPackage ../contracts {}; +in +{ + options.shb.sops = { + secret = mkOption { + description = "Secret following the [secret contract](./contracts-secret.html)."; + default = {}; + type = attrsOf (submodule ({ name, options, ... }: { + options = contracts.secret.mkProvider { + settings = mkOption { + description = '' + Settings specific to the Sops provider. + + This is a passthrough option to set [sops-nix options](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix). + + Note though that the `mode`, `owner`, `group`, and `restartUnits` + are managed by the [shb.sops.secret..request](#blocks-sops-options-shb.sops.secret._name_.request) option. + ''; + + type = anything; + }; + + resultCfg = { + path = "/run/secrets/${name}"; + pathText = "/run/secrets/"; + }; + }; + })); + }; + }; +} diff --git a/modules/blocks/sops/docs/default.md b/modules/blocks/sops/docs/default.md new file mode 100644 index 0000000..f833c48 --- /dev/null +++ b/modules/blocks/sops/docs/default.md @@ -0,0 +1,47 @@ +# SOPS Block {#blocks-sops} + +Defined in [`/modules/blocks/sops.nix`](@REPO@/modules/blocks/sops.nix). + +This block sets up a [sops-nix][] secret. + +It is only a small layer on top of `sops-nix` options +to adapt it to the [secret contract](./contract-secret.html). + +[sops-nix]: https://github.com/Mic92/sops-nix + +## Provider Contracts {#blocks-sops-contract-provider} + +This block provides the following contracts: + +- [secret contract][] under the [`shb.sops.secrets`][secret] option. + It is not yet tested with [contract tests][secret contract tests] but it is used extensively on several machines. + +[secret]: #blocks-sops-options-shb.sops.secret +[secret contract]: contracts-secret.html +[secret contract tests]: @REPO@/test/contracts/secret.nix + +As requested by the contract, when asking for a secret with the `shb.sops` module, +the path where the secret will be located can be found under the [`shb.sops.secrets..result`][result] option. + +[result]: #blocks-sops-options-shb.sops.secret._name_.result + +## Usage {#blocks-sops-usage} + +First, a file with encrypted secrets must be created by following the [secrets setup section](usage.html#usage-secrets). + +### With Requester Module {#blocks-sops-usage-requester} + +This example shows how to use this sops block +to fulfill the request of a module using the [secret contract][] under the option `services.mymodule.mysecret`. + +```nix + +``` + +## Options Reference {#blocks-sops-options} + +```{=include=} options +id-prefix: blocks-sops-options- +list-id: selfhostblocks-block-sops-options +source: @OPTIONS_JSON@ +``` diff --git a/modules/contracts/backup.nix b/modules/contracts/backup.nix index 9b0b258..1422dbb 100644 --- a/modules/contracts/backup.nix +++ b/modules/contracts/backup.nix @@ -1,7 +1,7 @@ { lib, ... }: let inherit (lib) mkOption; - inherit (lib.types) anything listOf nonEmptyListOf submodule str; + inherit (lib.types) listOf nonEmptyListOf submodule str; in { request = submodule { @@ -9,8 +9,6 @@ in user = mkOption { description = '' Unix user doing the backups. - - Most of the time, this should be the user owning the files. ''; type = str; }; diff --git a/modules/contracts/backup/test.nix b/modules/contracts/backup/test.nix index b6b1832..52d8bf0 100644 --- a/modules/contracts/backup/test.nix +++ b/modules/contracts/backup/test.nix @@ -16,7 +16,7 @@ in "/opt/files/B" ], settings, # { repository, config } -> attrset - extraConfig ? null, # { username } -> attrset + extraConfig ? null, # { username, config } -> attrset }: pkgs.testers.runNixOSTest { inherit name; @@ -41,7 +41,7 @@ in group = "root"; }; }) - (optionalAttrs (extraConfig != null) (extraConfig { inherit username; })) + (optionalAttrs (extraConfig != null) (extraConfig { inherit username config; })) ]; }; diff --git a/modules/contracts/databasebackup/test.nix b/modules/contracts/databasebackup/test.nix index e9d374a..bd09fa5 100644 --- a/modules/contracts/databasebackup/test.nix +++ b/modules/contracts/databasebackup/test.nix @@ -9,11 +9,10 @@ in { name, requesterRoot, providerRoot, - extraConfig ? null, # { username, database } -> attrset + extraConfig ? null, # { config, database } -> attrset modules ? [], - username ? "me", database ? "me", - settings, # repository -> attrset + settings, # { repository, config } -> attrset }: pkgs.testers.runNixOSTest { inherit name; @@ -22,16 +21,19 @@ in config = lib.mkMerge [ (setAttrByPath providerRoot { request = (getAttrFromPath requesterRoot config).databasebackup; - settings = settings "/opt/repos/database"; + settings = settings { + inherit config; + repository = "/opt/repos/database"; + }; }) - (mkIf (username != "root") { - users.users.${username} = { + (mkIf (database != "root") { + users.users.${database} = { isSystemUser = true; extraGroups = [ "sudoers" ]; group = "root"; }; }) - (optionalAttrs (extraConfig != null) (extraConfig { inherit username database; })) + (optionalAttrs (extraConfig != null) (extraConfig { inherit config database; })) ]; }; @@ -45,7 +47,7 @@ in machine.wait_for_open_port(5432) def peer_cmd(cmd, db="me"): - return "sudo -u me psql -U me {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db) + return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db) def query(query): res = machine.succeed(peer_cmd(query)) @@ -68,10 +70,11 @@ in with subtest("backup"): print(machine.succeed("systemctl cat ${provider.backupService}")) + print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase")) machine.succeed("systemctl start ${provider.backupService}") with subtest("drop database"): - machine.succeed(peer_cmd("DROP DATABASE me", db="postgres")) + machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres")) machine.fail(peer_cmd("SELECT * FROM test")) with subtest("restore"): diff --git a/modules/contracts/default.nix b/modules/contracts/default.nix index d5a2873..911279a 100644 --- a/modules/contracts/default.nix +++ b/modules/contracts/default.nix @@ -1,9 +1,48 @@ { pkgs, lib }: +let + inherit (lib) mkOption optionalAttrs; + inherit (lib.types) anything; + + mkContractFunctions = + { mkRequest, + mkResult, + }: { + mkRequester = requestCfg: { + request = mkRequest requestCfg; + + result = mkResult {}; + }; + + mkProvider = + { resultCfg, + settings ? {}, + }: { + request = mkRequest {}; + + result = mkResult resultCfg; + } // optionalAttrs (settings != {}) { inherit settings; }; + + contract = { + request = mkRequest {}; + + result = mkResult {}; + + settings = mkOption { + description = '' + Optional attribute set with options specific to the provider. + ''; + type = anything; + }; + }; + }; +in { + inherit mkContractFunctions; + databasebackup = import ./databasebackup.nix { inherit lib; }; backup = import ./backup.nix { inherit lib; }; mount = import ./mount.nix { inherit lib; }; - secret = import ./secret.nix { inherit lib; }; + secret = import ./secret.nix { inherit pkgs lib; }; ssl = import ./ssl.nix { inherit lib; }; test = { secret = import ./secret/test.nix { inherit pkgs lib; }; diff --git a/modules/contracts/secret.nix b/modules/contracts/secret.nix index b679bab..49ab678 100644 --- a/modules/contracts/secret.nix +++ b/modules/contracts/secret.nix @@ -1,114 +1,115 @@ -{ lib, ... }: -{ - mkOption = - { description, - mode ? "0400", +{ pkgs, lib, ... }: +let + inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString; + inherit (lib.types) anything listOf submodule str; + + contractsLib = import ./default.nix { inherit pkgs lib; }; + + mkRequest = + { mode ? "0400", owner ? "root", ownerText ? null, group ? "root", restartUnits ? [], restartUnitsText ? null, - }: lib.mkOption { - inherit description; + }: mkOption { + description = '' + Request part of the secret contract. - type = lib.types.submodule { + Options set by the requester module + enforcing some properties the secret should have. + ''; + + default = { + inherit mode owner group restartUnits; + }; + + defaultText = optionalString (ownerText != null || restartUnitsText != null) (literalMD '' + { + mode = ${mode}; + owner = ${if ownerText != null then ownerText else owner}; + group = ${group}; + restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + concatStringsSep " " restartUnits + " ]"}; + } + ''); + + type = submodule { options = { - request = lib.mkOption { - default = { - inherit mode owner group restartUnits; - }; - - defaultText = lib.optionalString (ownerText != null || restartUnitsText != null) (lib.literalMD '' - { - mode = ${mode}; - owner = ${if ownerText != null then ownerText else owner}; - group = ${group}; - restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + lib.concatStringsSep " " restartUnits + " ]"}; - } - ''); - - readOnly = true; - + mode = mkOption { description = '' - Options set by the requester module - enforcing some properties the secret should have. - - Use the `contracts.secret.mkOption` function to - create a secret option for a requester module. - See the [requester usage section](contracts-secret.html#secret-contract-usage-requester) for an example. - - Some providers will need more options to be defined and this is allowed. - These extra options will be set by the user. - For example, the `sops` implementation requires to be given - the sops key in which the secret is encrypted. - - `request` options are set read-only - because they must be set through option defaults, - they shouldn't be changed in the `config` section. - This would otherwise lead to infinite recursion - during evaluation. - This is handled automatically when using the `contracts.secret.mkOption` function. + Mode of the secret file. ''; - type = lib.types.submodule { - freeformType = lib.types.anything; - - options = { - mode = lib.mkOption { - description = '' - Mode of the secret file. - ''; - type = lib.types.str; - default = mode; - }; - - owner = lib.mkOption { - description = '' - Linux user owning the secret file. - ''; - type = lib.types.str; - default = owner; - defaultText = if ownerText != null then lib.literalMD ownerText else null; - }; - - group = lib.mkOption { - description = '' - Linux group owning the secret file. - ''; - type = lib.types.str; - default = group; - }; - - restartUnits = lib.mkOption { - description = '' - Systemd units to restart after the secret is updated. - ''; - type = lib.types.listOf lib.types.str; - default = restartUnits; - defaultText = if restartUnitsText != null then lib.literalMD restartUnitsText else null; - }; - }; - }; + type = str; + default = mode; }; - result = lib.mkOption { + owner = mkOption ({ description = '' - Options set by the provider module that indicates where the secret can be found. + Linux user owning the secret file. ''; - type = lib.types.submodule { - options = { - path = lib.mkOption { - type = lib.types.path; - description = '' - Path to the file containing the secret generated out of band. + type = str; + default = owner; + } // optionalAttrs (ownerText != null) { + defaultText = literalMD ownerText; + }); - This path will exist after deploying to a target host, - it is not available through the nix store. - ''; - }; - }; - }; + group = mkOption { + description = '' + Linux group owning the secret file. + ''; + type = str; + default = group; }; + + restartUnits = mkOption ({ + description = '' + Systemd units to restart after the secret is updated. + ''; + type = listOf str; + default = restartUnits; + } // optionalAttrs (restartUnitsText != null) { + defaultText = literalMD restartUnitsText; + }); }; }; }; + + mkResult = + { + path ? "/run/secrets/secret", + pathText ? null, + }: + mkOption ({ + description = '' + Result part of the secret contract. + + Options set by the provider module that indicates where the secret can be found. + ''; + default = { + inherit path; + }; + type = submodule { + options = { + path = mkOption { + type = lib.types.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 = path; + } // optionalAttrs (pathText != null) { + defaultText = pathText; + }; + }; + }; + } // optionalAttrs (pathText != null) { + defaultText = { + path = pathText; + }; + }); +in +contractsLib.mkContractFunctions { + inherit mkRequest mkResult; } diff --git a/modules/contracts/secret/docs/default.md b/modules/contracts/secret/docs/default.md index 3fd1e8c..ca2a18f 100644 --- a/modules/contracts/secret/docs/default.md +++ b/modules/contracts/secret/docs/default.md @@ -7,7 +7,7 @@ and that must be placed in an expected location with expected permission. More formally, this contract is made between a requester module - the one needing a secret - and a provider module - the one creating the secret and making it available. -## Problem Statement {#secret-contract-problem} +## Motivation {#secret-contract-motivation} Let's provide the [ldap SHB module][ldap-module] option `ldapUserPasswordFile` with a secret managed by [sops-nix][]. @@ -19,14 +19,14 @@ Without the secret contract, configuring the option would look like so: ```nix sops.secrets."ldap/user_password" = { - sopsFile = ./secrets.yaml; mode = "0440"; owner = "lldap"; group = "lldap"; restartUnits = [ "lldap.service" ]; + sopsFile = ./secrets.yaml; }; -shb.ldap.ldapUserPasswordFile = config.sops.secrets."ldap/user_password".path; +shb.ldap.userPassword.result = config.sops.secrets."ldap/user_password".result; ``` The problem this contract intends to fix is how to ensure @@ -38,19 +38,21 @@ or more likely, they will need to figure it out by looking at the module source code. Not a great user experience. -Now, with this contract, the configuration becomes: +Now, with this contract, a layer on top of `sops` is added which is found under `shb.sops`. +The configuration then becomes: ```nix -sops.secrets."ldap/user_password" = config.shb.ldap.secret.ldapUserPassword.request // { - sopsFile = ./secrets.yaml; +shb.sops.secrets."ldap/user_password" = { + request = config.shb.ldap.userPassword.request; + settings.sopsFile = ./secrets.yaml; }; -shb.ldap.ldapUserPassword.result.path = config.sops.secrets."ldap/user_password".path; +shb.ldap.userPassword.result = config.shb.sops.secrets."ldap/user_password".result; ``` -The issue is now gone at the expense of some plumbing. -The module maintainer is now in charge of describing -how the module expects the secret to be provided. +The issue is now gone as the responsibility falls +on the module maintainer +for describing how the secret should be provided. If taking advantage of the `sops.defaultSopsFile` option like so: @@ -61,9 +63,9 @@ sops.defaultSopsFile = ./secrets.yaml; Then the snippet above is even more simplified: ```nix -sops.secrets."ldap/user_password" = config.shb.ldap.secret.ldapUserPassword.request; +shb.sops.secrets."ldap/user_password".request = config.shb.ldap.userPassword.request; -shb.ldap.ldapUserPassword.result.path = config.sops.secrets."ldap/user_password".path; +shb.ldap.userPassword.result = config.shb.sops.secrets."ldap/user_password".result; ``` ## Contract Reference {#secret-contract-options} @@ -92,18 +94,22 @@ Here is an example module requesting two secrets through the `secret` contract. ```nix { config, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) submodule; +in { options = { - myservice = lib.mkOption { - type = lib.types.submodule { + myservice = mkOption { + type = submodule { options = { - adminPassword = contracts.secret.mkOption { + adminPassword = contracts.secret.mkRequester { owner = "myservice"; group = "myservice"; mode = "0440"; restartUnits = [ "myservice.service" ]; }; - databasePassword = contracts.secret.mkOption { + databasePassword = contracts.secret.mkRequester { owner = "myservice"; # group defaults to "root" # mode defaults to "0400" @@ -130,44 +136,43 @@ and that one can create multiple instances. ```nix { config, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) attrsOf submodule; + + contracts = pkgs.callPackage ./contracts {}; +in { - options = { - secretservice = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule { - options = { - mode = lib.mkOption { - description = "Mode of the secret file."; - type = lib.types.str; - }; + options.secretservice.secret = mkOption { + description = "Secret following the secret contract."; + default = {}; + type = attrsOf (submodule ({ name, options, ... }: { + options = contracts.secret.mkProvider { + settings = mkOption { + description = '' + Settings specific to the secrets provider. + ''; - owner = lib.mkOption { - description = "Linux user owning the secret file."; - type = lib.types.str; - }; - - group = lib.mkOption { - description = "Linux group owning the secret file."; - type = lib.types.str; - }; - - restartUnits = lib.mkOption { - description = "Systemd units to restart after the secret is updated."; - type = lib.types.listOf lib.types.str; - }; - - path = lib.mkOption { - description = "Path where the secret file will be located."; - type = lib.types.str; - }; - - // The contract allows more options to be defined to accomodate specific implementations. - secretFile = lib.mkOption { - description = "File containing the encrypted secret."; - type = lib.types.path; + type = submodule { + options = { + secretFile = lib.mkOption { + description = "File containing the encrypted secret."; + type = lib.types.path; + }; + }; }; }; - }); - }; + + resultCfg = { + path = "/run/secrets/${name}"; + pathText = "/run/secrets/"; + }; + }; + })); + }; + + config = { + // ... }; } ``` @@ -178,15 +183,20 @@ The end user's responsibility is now to do some plumbing. They will setup the provider module - here `secretservice` - with the options set by the requester module, while also setting other necessary options to satisfy the provider service. +And then they will give back the result to the requester module `myservice`. ```nix -secretservice.adminPassword = myservice.secret.adminPassword.request // { - secretFile = ./secret.yaml; +secretservice.secret."adminPassword" = { + request = myservice.adminPasswor".request; + settings.secretFile = ./secret.yaml; }; +myservice.adminPassword.result = secretservice.secret."adminPassword".result; -secretservice.databasePassword = myservice.secret.databasePassword.request // { - secretFile = ./secret.yaml; +secretservice.secret."databasePassword" = { + request = myservice.databasePassword.request; + settings.secretFile = ./secret.yaml; }; +myservice.databasePassword.result = secretservice.service."databasePassword".result; ``` Assuming the `secretservice` module accepts default options, @@ -195,14 +205,13 @@ the above snippet could be reduced to: ```nix secretservice.default.secretFile = ./secret.yaml; -secretservice.adminPassword = myservice.secret.adminPassword.request; -secretservice.databasePassword = myservice.secret.databasePassword.request; +secretservice.secret."adminPassword".request = myservice.adminPasswor".request; +myservice.adminPassword.result = secretservice.secret."adminPassword".result; + +secretservice.secret."databasePassword".request = myservice.databasePassword.request; +myservice.databasePassword.result = secretservice.service."databasePassword".result; ``` -Then they will setup the requester module - here `myservice` - with the result of the provider module. - -```nix -myservice.secret.adminPassword.result.path = secretservice.adminPassword.result.path; - -myservice.secret.databasePassword.result.path = secretservice.adminPassword.result.path; -``` +The plumbing of request from the requester to the provider +and then the result from the provider back to the requester +is quite explicit in this snippet. diff --git a/modules/contracts/secret/dummyModule.nix b/modules/contracts/secret/dummyModule.nix index 7a998be..ab41e0b 100644 --- a/modules/contracts/secret/dummyModule.nix +++ b/modules/contracts/secret/dummyModule.nix @@ -1,21 +1,27 @@ { pkgs, lib, ... }: let contracts = pkgs.callPackage ../. {}; + + inherit (lib) mkOption; + inherit (lib.types) submodule; in { - options.shb.contracts.secret = contracts.secret.mkOption { + options.shb.contracts.secret = mkOption { description = '' Contract for secrets between a requester module and a provider module. The requester communicates to the provider some properties the secret should have - through the `request` options. + through the `request.*` options. - The provider reads from the `request` options + The provider reads from the `request.*` options and creates the secret as requested. It then communicates to the requester where the secret can be found - through the `result` options. + through the `result.*` options. ''; + type = submodule { + options = contracts.secret.contract; + }; }; } diff --git a/modules/contracts/secret/test.nix b/modules/contracts/secret/test.nix index 046b8d7..517b56a 100644 --- a/modules/contracts/secret/test.nix +++ b/modules/contracts/secret/test.nix @@ -9,7 +9,7 @@ let in { name, configRoot, - createContent, # config to create a secret with value "secretA". + settingsCfg, # str -> attrset modules ? [], owner ? "root", group ? "root", @@ -23,8 +23,11 @@ in config = lib.mkMerge [ (setAttrByPath configRoot { A = { - inherit owner group mode restartUnits; - } // createContent; + request = { + inherit owner group mode restartUnits; + }; + settings = settingsCfg "secretA"; + }; }) (mkIf (owner != "root") { users.users.${owner}.isNormalUser = true; @@ -37,26 +40,26 @@ in testScript = { nodes, ... }: let - cfg = (getAttrFromPath configRoot nodes.machine)."A"; + result = (getAttrFromPath configRoot nodes.machine)."A".result; in '' - owner = machine.succeed("stat -c '%U' ${cfg.path}").strip() + owner = machine.succeed("stat -c '%U' ${result.path}").strip() print(f"Got owner {owner}") if owner != "${owner}": raise Exception(f"Owner should be '${owner}' but got '{owner}'") - group = machine.succeed("stat -c '%G' ${cfg.path}").strip() + group = machine.succeed("stat -c '%G' ${result.path}").strip() print(f"Got group {group}") if group != "${group}": raise Exception(f"Group should be '${group}' but got '{group}'") - mode = str(int(machine.succeed("stat -c '%a' ${cfg.path}").strip())) + mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip())) print(f"Got mode {mode}") wantedMode = str(int("${mode}")) if mode != wantedMode: raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'") - content = machine.succeed("cat ${cfg.path}").strip() + content = machine.succeed("cat ${result.path}").strip() print(f"Got content {content}") if content != "secretA": raise Exception(f"Content should be 'secretA' but got '{content}'") diff --git a/modules/services/forgejo.nix b/modules/services/forgejo.nix index 50fafb6..894373e 100644 --- a/modules/services/forgejo.nix +++ b/modules/services/forgejo.nix @@ -4,13 +4,16 @@ let cfg = config.shb.forgejo; contracts = pkgs.callPackage ../contracts {}; + + inherit (lib) getExe lists literalExpression mkBefore mkEnableOption mkForce mkIf mkMerge mkOption mkOverride optionals; + inherit (lib.types) bool enum listOf nullOr package port submodule str; in { options.shb.forgejo = { - enable = lib.mkEnableOption "selfhostblocks.forgejo"; + enable = mkEnableOption "selfhostblocks.forgejo"; - subdomain = lib.mkOption { - type = lib.types.str; + subdomain = mkOption { + type = str; description = '' Subdomain under which Forgejo will be served. @@ -21,7 +24,7 @@ in example = "forgejo"; }; - domain = lib.mkOption { + domain = mkOption { description = '' Domain under which Forgejo is served. @@ -29,75 +32,79 @@ in .[:] ``` ''; - type = lib.types.str; + type = str; example = "domain.com"; }; - ssl = lib.mkOption { + ssl = mkOption { description = "Path to SSL files"; - type = lib.types.nullOr contracts.ssl.certs; + type = nullOr contracts.ssl.certs; default = null; }; - ldap = lib.mkOption { + ldap = mkOption { description = '' LDAP Integration. ''; default = {}; - type = lib.types.nullOr (lib.types.submodule { + type = nullOr (submodule { options = { - enable = lib.mkEnableOption "LDAP integration."; + enable = mkEnableOption "LDAP integration."; - provider = lib.mkOption { - type = lib.types.enum [ "LLDAP" ]; + provider = mkOption { + type = enum [ "LLDAP" ]; description = "LDAP provider name, used for display."; default = "LLDAP"; }; - host = lib.mkOption { - type = lib.types.str; + host = mkOption { + type = str; description = '' Host serving the LDAP server. ''; default = "127.0.0.1"; }; - port = lib.mkOption { - type = lib.types.port; + port = mkOption { + type = port; description = '' Port of the service serving the LDAP server. ''; default = 389; }; - dcdomain = lib.mkOption { - type = lib.types.str; + dcdomain = mkOption { + type = str; description = "dc domain for ldap."; example = "dc=mydomain,dc=com"; }; - adminName = lib.mkOption { - type = lib.types.str; + adminName = mkOption { + type = str; description = "Admin user of the LDAP server."; default = "admin"; }; - adminPassword = contracts.secret.mkOption { + adminPassword = mkOption { description = "LDAP admin password."; - mode = "0440"; - owner = "forgejo"; - group = "forgejo"; - restartUnits = [ "forgejo.service" ]; + type = submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "forgejo"; + group = "forgejo"; + restartUnits = [ "forgejo.service" ]; + }; + }; }; - userGroup = lib.mkOption { - type = lib.types.str; + userGroup = mkOption { + type = str; description = "Group users must belong to be able to login."; default = "forgejo_user"; }; - adminGroup = lib.mkOption { - type = lib.types.str; + adminGroup = mkOption { + type = str; description = "Group users must belong to be admins."; default = "forgejo_admin"; }; @@ -105,81 +112,97 @@ in }); }; - sso = lib.mkOption { + sso = mkOption { description = '' Setup SSO integration. ''; default = {}; - type = lib.types.submodule { + type = submodule { options = { - enable = lib.mkEnableOption "SSO integration."; + enable = mkEnableOption "SSO integration."; - provider = lib.mkOption { - type = lib.types.enum [ "Authelia" ]; + provider = mkOption { + type = enum [ "Authelia" ]; description = "OIDC provider name, used for display."; default = "Authelia"; }; - endpoint = lib.mkOption { - type = lib.types.str; + endpoint = mkOption { + type = str; description = "OIDC endpoint for SSO."; example = "https://authelia.example.com"; }; - clientID = lib.mkOption { - type = lib.types.str; + clientID = mkOption { + type = str; description = "Client ID for the OIDC endpoint."; default = "forgejo"; }; - authorization_policy = lib.mkOption { - type = lib.types.enum [ "one_factor" "two_factor" ]; + authorization_policy = mkOption { + type = enum [ "one_factor" "two_factor" ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; - sharedSecret = contracts.secret.mkOption { + sharedSecret = mkOption { description = "OIDC shared secret for Forgejo."; - mode = "0440"; - owner = "forgejo"; - group = "forgejo"; - restartUnits = [ "forgejo.service" ]; + type = submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "forgejo"; + group = "forgejo"; + restartUnits = [ "forgejo.service" ]; + }; + }; }; - sharedSecretForAuthelia = contracts.secret.mkOption { + sharedSecretForAuthelia = mkOption { description = "OIDC shared secret for Authelia."; - mode = "0400"; - owner = "authelia"; + type = submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "authelia"; + }; + }; }; }; }; }; - adminPassword = contracts.secret.mkOption { + adminPassword = mkOption { description = "File containing the Forgejo admin user password."; - mode = "0440"; - owner = "forgejo"; - group = "forgejo"; - restartUnits = [ "forgejo.service" ]; + type = submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "forgejo"; + group = "forgejo"; + restartUnits = [ "forgejo.service" ]; + }; + }; }; - databasePassword = contracts.secret.mkOption { + databasePassword = mkOption { description = "File containing the Forgejo database password."; - mode = "0440"; - owner = "forgejo"; - group = "forgejo"; - restartUnits = [ "forgejo.service" ]; + type = submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "forgejo"; + group = "forgejo"; + restartUnits = [ "forgejo.service" ]; + }; + }; }; - repositoryRoot = lib.mkOption { - type = lib.types.nullOr lib.types.str; + repositoryRoot = mkOption { + type = nullOr str; description = "Path where to store the repositories. If null, uses the default under the Forgejo StateDir."; default = null; example = "/srv/forgejo"; }; - localActionRunner = lib.mkOption { - type = lib.types.bool; + localActionRunner = mkOption { + type = bool; default = true; description = '' Enable local action runner that runs for all labels. @@ -187,8 +210,8 @@ in }; - hostPackages = lib.mkOption { - type = lib.types.listOf lib.types.package; + hostPackages = mkOption { + type = listOf package; default = with pkgs; [ bash coreutils @@ -199,7 +222,7 @@ in nodejs wget ]; - defaultText = lib.literalExpression '' + defaultText = literalExpression '' with pkgs; [ bash coreutils @@ -217,7 +240,7 @@ in ''; }; - backup = lib.mkOption { + backup = mkOption { type = contracts.backup.request; description = '' Backup configuration. This is an output option. @@ -239,13 +262,13 @@ in user = options.services.forgejo.user.value; sourceDirectories = [ options.services.forgejo.dump.backupDir.value - ] ++ lib.optionals (cfg.repositoryRoot != null) [ + ] ++ optionals (cfg.repositoryRoot != null) [ cfg.repositoryRoot ]; }; }; - mount = lib.mkOption { + mount = mkOption { type = contracts.mount; description = '' Mount configuration. This is an output option. @@ -263,51 +286,51 @@ in default = { path = config.services.forgejo.stateDir; }; }; - smtp = lib.mkOption { + smtp = mkOption { description = '' Send notifications by smtp. ''; default = null; - type = lib.types.nullOr (lib.types.submodule { + type = nullOr (submodule { options = { - from_address = lib.mkOption { - type = lib.types.str; + from_address = mkOption { + type = str; description = "SMTP address from which the emails originate."; example = "authelia@mydomain.com"; }; - host = lib.mkOption { - type = lib.types.str; + host = mkOption { + type = str; description = "SMTP host to send the emails to."; }; - port = lib.mkOption { - type = lib.types.port; + port = mkOption { + type = port; description = "SMTP port to send the emails to."; default = 25; }; - username = lib.mkOption { - type = lib.types.str; + username = mkOption { + type = str; description = "Username to connect to the SMTP host."; }; - passwordFile = lib.mkOption { - type = lib.types.str; + passwordFile = mkOption { + type = str; description = "File containing the password to connect to the SMTP host."; }; }; }); }; - debug = lib.mkOption { + debug = mkOption { description = "Enable debug logging."; - type = lib.types.bool; + type = bool; default = false; }; }; - config = lib.mkMerge [ - (lib.mkIf cfg.enable { + config = mkMerge [ + (mkIf cfg.enable { services.forgejo = { enable = true; - repositoryRoot = lib.mkIf (cfg.repositoryRoot != null) cfg.repositoryRoot; + repositoryRoot = mkIf (cfg.repositoryRoot != null) cfg.repositoryRoot; settings = { server = { DOMAIN = cfg.domain; @@ -328,10 +351,10 @@ in }; # 1 lower than default, to solve conflict between shb.postgresql and nixpkgs' forgejo module. - services.postgresql.enable = lib.mkOverride 999 true; + services.postgresql.enable = mkOverride 999 true; # https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113 - systemd.services.forgejo.serviceConfig.Type = lib.mkForce "exec"; + systemd.services.forgejo.serviceConfig.Type = mkForce "exec"; shb.nginx.vhosts = [{ inherit (cfg) domain subdomain ssl; @@ -339,7 +362,7 @@ in }]; }) - (lib.mkIf cfg.enable { + (mkIf cfg.enable { services.forgejo.database = { type = "postgres"; @@ -347,7 +370,7 @@ in }; }) - (lib.mkIf cfg.enable { + (mkIf cfg.enable { services.forgejo.dump = { enable = true; type = "tar.gz"; @@ -358,12 +381,12 @@ in # For Forgejo setup: https://github.com/lldap/lldap/blob/main/example_configs/gitea.md # For cli info: https://docs.gitea.com/usage/command-line # Security protocols in: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/services/auth/source/ldap/security_protocol.go#L27-L31 - (lib.mkIf (cfg.enable && cfg.ldap.enable != false) { + (mkIf (cfg.enable && cfg.ldap.enable != false) { # The delimiter in the `cut` command is a TAB! systemd.services.forgejo.preStart = let provider = "SHB-${cfg.ldap.provider}"; in '' - auth="${lib.getExe config.services.forgejo.package} admin auth" + auth="${getExe config.services.forgejo.package} admin auth" echo "Trying to find existing ldap configuration for ${provider}"... set +e -o pipefail @@ -417,7 +440,7 @@ in # For Authelia to Forgejo integration: https://www.authelia.com/integration/openid-connect/gitea/ # For Forgejo config: https://forgejo.org/docs/latest/admin/config-cheat-sheet # For cli info: https://docs.gitea.com/usage/command-line - (lib.mkIf (cfg.enable && cfg.sso.enable != false) { + (mkIf (cfg.enable && cfg.sso.enable != false) { services.forgejo.settings = { oauth2 = { ENABLED = true; @@ -430,7 +453,7 @@ in }; service = { - # DISABLE_REGISTRATION = lib.mkForce false; + # DISABLE_REGISTRATION = mkForce false; # ALLOW_ONLY_EXTERNAL_REGISTRATION = false; SHOW_REGISTRATION_BUTTON = false; }; @@ -440,7 +463,7 @@ in systemd.services.forgejo.preStart = let provider = "SHB-${cfg.sso.provider}"; in '' - auth="${lib.getExe config.services.forgejo.package} admin auth" + auth="${getExe config.services.forgejo.package} admin auth" echo "Trying to find existing sso configuration for ${provider}"... set +e -o pipefail @@ -468,7 +491,7 @@ in fi ''; - shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [ + shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [ (let provider = "SHB-${cfg.sso.provider}"; in { @@ -482,15 +505,15 @@ in ]; }) - (lib.mkIf cfg.enable { + (mkIf cfg.enable { systemd.services.forgejo.preStart = '' - admin="${lib.getExe config.services.forgejo.package} admin user" + admin="${getExe config.services.forgejo.package} admin user" $admin create --admin --email "root@localhost" --username meadmin --password "$(tr -d '\n' < ${cfg.adminPassword.result.path})" || true $admin change-password --username meadmin --password "$(tr -d '\n' < ${cfg.adminPassword.result.path})" || true ''; }) - (lib.mkIf (cfg.enable && cfg.smtp != null) { + (mkIf (cfg.enable && cfg.smtp != null) { services.forgejo.settings.mailer = { ENABLED = true; SMTP_ADDR = "${cfg.smtp.host}:${toString cfg.smtp.port}"; @@ -502,13 +525,13 @@ in }) # https://wiki.nixos.org/wiki/Forgejo#Runner - (lib.mkIf cfg.enable { + (mkIf cfg.enable { services.forgejo.settings.actions = { ENABLED = true; DEFAULT_ACTIONS_URL = "github"; }; - services.gitea-actions-runner = lib.mkIf cfg.localActionRunner { + services.gitea-actions-runner = mkIf cfg.localActionRunner { package = pkgs.forgejo-actions-runner; instances.local = { enable = true; @@ -530,9 +553,9 @@ in # This combined with the next statement takes care of # automatically registering a forgejo runner. - systemd.services.forgejo.postStart = lib.mkIf cfg.localActionRunner (lib.mkBefore '' + systemd.services.forgejo.postStart = mkIf cfg.localActionRunner (mkBefore '' ${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -U ${config.services.forgejo.settings.server.HTTP_ADDR}; do echo "Waiting for unix ${config.services.forgejo.settings.server.HTTP_ADDR} to open..."; sleep 2; done); sleep 2' - actions="${lib.getExe config.services.forgejo.package} actions" + actions="${getExe config.services.forgejo.package} actions" echo -n TOKEN= > /run/forgejo/forgejo-runner-token $actions generate-runner-token >> /run/forgejo/forgejo-runner-token ''); diff --git a/modules/services/forgejo/docs/default.md b/modules/services/forgejo/docs/default.md index 8ad3ffc..5b0b84e 100644 --- a/modules/services/forgejo/docs/default.md +++ b/modules/services/forgejo/docs/default.md @@ -15,7 +15,7 @@ LDAP and SSO integration as well as one local runner. - Declarative [local runner](#services-forgejo-options-shb.forgejo.localActionRunner) Configuration. - Access through [subdomain](#services-forgejo-options-shb.forgejo.subdomain) using reverse proxy. [Manual](#services-forgejo-usage-basic). - Access through [HTTPS](#services-forgejo-options-shb.forgejo.ssl) using reverse proxy. [Manual](#services-forgejo-usage-basic). -- [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html) with the . [Manual](#services-forgejo-usage-backup). +- [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html). [Manual](#services-forgejo-usage-backup). ## Usage {#services-forgejo-usage} @@ -44,7 +44,7 @@ Then you can use that secret: shb.forgejo.adminPasswordFile = config.sops.secrets."forgejo/adminPasswordFile".path; ``` -### Forgejo through HTTP(S) {#services-forgejo-usage-basic} +### Forgejo through HTTPS {#services-forgejo-usage-basic} This will set up a Forgejo service that runs on the NixOS target machine, reachable at `http://forgejo.example.com`. @@ -60,25 +60,22 @@ shb.forgejo = { If the `shb.ssl` block is used (see [manual](blocks-ssl.html#usage) on how to set it up), the instance will be reachable at `https://fogejo.example.com`. -Here is an example with self-signed certificates: +Here is an example with Let's Encrypt certificates, validated using the HTTP method: ```nix -shb.certs = { - cas.selfsigned.myca = { - name = "My CA"; - }; - certs.selfsigned = { - foregejo = { - ca = config.shb.certs.cas.selfsigned.myca; - domain = "forgejo.example.com"; - }; - }; +shb.certs.certs.letsencrypt."example.com" = { + domain = "example.com"; + group = "nginx"; + reloadServices = [ "nginx.service" ]; + adminEmail = "myemail@mydomain.com"; }; ``` Then you can tell Forgejo to use those certificates. ```nix +shb.certs.certs.letsencrypt."example.com".extraDomains = [ "forgejo.example.com" ]; + shb.forgejo = { ssl = config.shb.certs.certs.selfsigned.forgejo; }; @@ -87,7 +84,7 @@ shb.forgejo = { ### With LDAP Support {#services-forgejo-usage-ldap} :::: {.note} -We will build upon the [Forgejo through HTTP(S)](#services-forgejo-usage-basic) section, +We will build upon the [HTTPS](#services-forgejo-usage-basic) section, so please follow that first. :::: @@ -99,12 +96,18 @@ shb.ldap = { enable = true; domain = "example.com"; subdomain = "ldap"; + ssl = config.shb.certs.certs.letsencrypt."example.com"; ldapPort = 3890; webUIListenPort = 17170; dcdomain = "dc=example,dc=com"; - ldapUserPasswordFile = ; - jwtSecretFile = ; + ldapUserPassword.result = config.shb.sops.secrets."ldap/userPassword".result; + jwtSecret.result = config.shb.sops.secrets."ldap/jwtSecret".result; }; + +shb.certs.certs.letsencrypt."example.com".extraDomains = [ "ldap.example.com" ]; + +shb.sops.secrets."ldap/userPassword".request = config.shb.ldap.userPassword.request; +shb.sops.secrets."ldap/jwtSecret".request = config.shb.ldap.jwtSecret.request; ``` We also need to configure the `forgejo` service @@ -116,7 +119,12 @@ shb.forgejo.ldap host = "127.0.0.1"; port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; - adminPasswordFile = ; + adminPassword.result = config.shb.sops.secrets."forgejo/ldap/adminPassword".result +}; + +shb.sops.secrets."forgejo/ldap/adminPassword" = { + request = config.shb.forgejo.ldap.adminPassword.request; + settings.key = "ldap/userPassword"; }; ``` @@ -135,29 +143,10 @@ When that's done, go back to the Forgejo server at ### With SSO Support {#services-forgejo-usage-sso} :::: {.note} -We will build upon the [With LDAP Support](#services-forgejo-usage-ldap) section, +We will build upon the [LDAP](#services-forgejo-usage-ldap) section, so please follow that first. :::: -Here though, we must setup SSL certificates -because the SSO provider only works with the https protocol. -Let's add self-signed certificates for Authelia and LLDAP: - -```nix -shb.certs = { - certs.selfsigned = { - auth = { - ca = config.shb.certs.cas.selfsigned.myca; - domain = "auth.example.com"; - }; - ldap = { - ca = config.shb.certs.cas.selfsigned.myca; - domain = "ldap.example.com"; - }; - }; -}; -``` - We then need to setup the SSO provider, here Authelia thanks to the corresponding SHB block: @@ -166,21 +155,31 @@ shb.authelia = { enable = true; domain = "example.com"; subdomain = "auth"; - ssl = config.shb.certs.certs.selfsigned.auth; + ssl = config.shb.certs.certs.letsencrypt."example.com"; ldapHostname = "127.0.0.1"; ldapPort = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; secrets = { - jwtSecretFile = ; - ldapAdminPasswordFile = ; - sessionSecretFile = ; - storageEncryptionKeyFile = ; - identityProvidersOIDCHMACSecretFile = ; - identityProvidersOIDCIssuerPrivateKeyFile = ; + jwtSecret.result = config.shb.sops.secrets."authelia/jwt_secret".result; + ldapAdminPassword.result = config.shb.sops.secrets."authelia/ldap_admin_password".result; + sessionSecret.result = config.shb.sops.secrets."authelia/session_secret".result; + storageEncryptionKey.result = config.shb.sops.secrets."authelia/storage_encryption_key".result; + identityProvidersOIDCHMACSecret.result = config.shb.sops.secrets."authelia/hmac_secret".result; + identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secrets."authelia/private_key".result; }; }; + +shb.certs.certs.letsencrypt."example.com".extraDomains = [ "auth.example.com" ]; + +shb.sops.secrets."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request; +shb.sops.secrets."authelia/ldap_admin_password".request = config.shb.authelia.secrets.ldapAdminPassword.request; +shb.sops.secrets."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request; +shb.sops.secrets."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request; +shb.sops.secrets."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; +shb.sops.secrets."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; +shb.sops.secrets."authelia/smtp_password".request = config.shb.authelia.smtp.password.request; ``` The `shb.authelia.secrets.ldapAdminPasswordFile` must be the same diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index 506340c..40cd697 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -67,12 +67,16 @@ in default = "jellyfin_admin"; }; - adminPassword = contracts.secret.mkOption { + adminPassword = lib.mkOption { description = "LDAP admin password."; - mode = "0440"; - owner = "jellyfin"; - group = "jellyfin"; - restartUnits = [ "jellyfin.service" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "jellyfin"; + group = "jellyfin"; + restartUnits = [ "jellyfin.service" ]; + }; + }; }; }; }; @@ -121,18 +125,26 @@ in default = "one_factor"; }; - sharedSecret = contracts.secret.mkOption { + sharedSecret = lib.mkOption { description = "OIDC shared secret for Jellyfin."; - mode = "0440"; - owner = "jellyfin"; - group = "jellyfin"; - restartUnits = [ "jellyfin.service" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "jellyfin"; + group = "jellyfin"; + restartUnits = [ "jellyfin.service" ]; + }; + }; }; - sharedSecretForAuthelia = contracts.secret.mkOption { + sharedSecretForAuthelia = lib.mkOption { description = "OIDC shared secret for Authelia."; - mode = "0400"; - owner = config.shb.authelia.autheliaUser; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = config.shb.authelia.autheliaUser; + }; + }; }; }; }; diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix index f675326..426fddc 100644 --- a/modules/services/nextcloud-server.nix +++ b/modules/services/nextcloud-server.nix @@ -95,13 +95,18 @@ in default = "root"; }; - adminPass = contracts.secret.mkOption { + adminPass = lib.mkOption { description = "Nextcloud admin password."; - mode = "0400"; - owner = "nextcloud"; - restartUnits = [ "phpfpm-nextcloud.service" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "nextcloud"; + restartUnits = [ "phpfpm-nextcloud.service" ]; + }; + }; }; + maxUploadSize = lib.mkOption { default = "4G"; type = lib.types.str; @@ -374,13 +379,18 @@ in default = "admin"; }; - adminPassword = contracts.secret.mkOption { + adminPassword = lib.mkOption { description = "LDAP server admin password."; - mode = "0400"; - owner = "nextcloud"; - restartUnits = [ "phpfpm-nextcloud.service" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "nextcloud"; + restartUnits = [ "phpfpm-nextcloud.service" ]; + }; + }; }; + userGroup = lib.mkOption { type = lib.types.str; description = "Group users must belong to to be able to login to Nextcloud."; @@ -441,19 +451,29 @@ in default = "one_factor"; }; - secret = contracts.secret.mkOption { + secret = lib.mkOption { description = "OIDC shared secret."; - mode = "0400"; - owner = "nextcloud"; - restartUnits = [ "phpfpm-nextcloud.service" ]; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "nextcloud"; + restartUnits = [ "phpfpm-nextcloud.service" ]; + }; + }; }; - secretForAuthelia = contracts.secret.mkOption { + + secretForAuthelia = lib.mkOption { description = "OIDC shared secret. Content must be the same as `secretFile` option."; - mode = "0400"; - owner = "authelia"; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "authelia"; + }; + }; }; + fallbackDefaultAuth = lib.mkOption { type = lib.types.bool; description = '' diff --git a/modules/services/nextcloud-server/docs/default.md b/modules/services/nextcloud-server/docs/default.md index 7eac7bc..fb67dae 100644 --- a/modules/services/nextcloud-server/docs/default.md +++ b/modules/services/nextcloud-server/docs/default.md @@ -11,8 +11,8 @@ It is based on the nixpkgs Nextcloud server and provides opinionated defaults. to configure those with the UI. - [LDAP](#services-nextcloud-server-usage-ldap) app: enables app and sets up integration with an existing LDAP server, in this case LLDAP. - - [OIDC](#services-nextcloud-server-usage-oidc) app: - enables app and sets up integration with an existing OIDC server, in this case Authelia. + - [SSO](#services-nextcloud-server-usage-oidc) app: + enables app and sets up integration with an existing SSO server, in this case Authelia. - [Preview Generator](#services-nextcloud-server-usage-previewgenerator) app: enables app and sets up required cron job. - [External Storage](#services-nextcloud-server-usage-externalstorage) app: @@ -66,10 +66,10 @@ shb.nextcloud = { domain = "example.com"; subdomain = "n"; defaultPhoneRegion = "US"; - adminPass.result.path = config.sops.secrets."nextcloud/adminpass".path; + adminPass.result = config.shb.sops.secrets."nextcloud/adminpass".result; }; -sops.secrets."nextcloud/adminpass" = config.shb.nextcloud.adminPass.request; +shb.sops.secrets."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request; ``` This assumes secrets are setup with SOPS as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual. @@ -154,15 +154,18 @@ shb.ldap = { enable = true; domain = "example.com"; subdomain = "ldap"; + ssl = config.shb.certs.certs.letsencrypt."example.com"; ldapPort = 3890; webUIListenPort = 17170; dcdomain = "dc=example,dc=com"; - ldapUserPassword.result.path = config.sops.secrets."ldap/userPassword".path; - jwtSecret.result.path = config.sops.secrets."ldap/jwtSecret".path; + ldapUserPassword.result = config.shb.sops.secrets."ldap/userPassword".result; + jwtSecret.result = config.shb.sops.secrets."ldap/jwtSecret".result; }; -sops.secrets."ldap/userPassword" = config.shb.ldap.userPassword.request; -sops.secrets."ldap/jwtSecret" = config.shb.ldap.jwtSecret.request; +shb.certs.certs.letsencrypt."example.com".extraDomains = [ "ldap.example.com" ]; + +shb.sops.secrets."ldap/userPassword".request = config.shb.ldap.userPassword.request; +shb.sops.secrets."ldap/jwtSecret".request = config.shb.ldap.jwtSecret.request; ``` On the `nextcloud` module side, we need to configure it to talk to the LDAP server we @@ -175,12 +178,13 @@ shb.nextcloud.apps.ldap = { port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; adminName = "admin"; - adminPassword.result.path = config.sops.secrets."nextcloud/ldapUserPassword".path + adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/adminPassword".result userGroup = "nextcloud_user"; }; -sops.secrets."nextcloud/ldapUserPassword" = config.shb.nextcloud.adminPasswordFile.request // { - key = "ldap/userPassword"; +shb.sops.secrets."nextcloud/ldap/adminPassword" = { + request = config.shb.nextcloud.apps.ldap.adminPassword.request; + settings.key = "ldap/userPassword"; }; ``` @@ -202,7 +206,7 @@ so you need to create a normal user like above, login with it once so it is known to Nextcloud, then logout, login with the admin Nextcloud user and promote that new user to admin level. -### With OIDC Support {#services-nextcloud-server-usage-oidc} +### With SSO Support {#services-nextcloud-server-usage-oidc} :::: {.note} This section corresponds to the `sso` section of the [Nextcloud @@ -230,26 +234,28 @@ shb.authelia = { port = 587; username = "postmaster@mg.example.com"; from_address = "authelia@example.com"; - password.result.path = config.sops.secrets."authelia/smtp_password".path; + password.result = config.shb.sops.secrets."authelia/smtp_password".result; }; secrets = { - jwtSecret.result.path = config.sops.secrets."authelia/jwt_secret".path; - ldapAdminPassword.result.path = config.sops.secrets."authelia/ldap_admin_password".path; - sessionSecret.result.path = config.sops.secrets."authelia/session_secret".path; - storageEncryptionKey.result.path = config.sops.secrets."authelia/storage_encryption_key".path; - identityProvidersOIDCHMACSecret.result.path = config.sops.secrets."authelia/hmac_secret".path; - identityProvidersOIDCIssuerPrivateKey.result.path = config.sops.secrets."authelia/private_key".path; + jwtSecret.result = config.shb.sops.secrets."authelia/jwt_secret".result; + ldapAdminPassword.result = config.shb.sops.secrets."authelia/ldap_admin_password".result; + sessionSecret.result = config.shb.sops.secrets."authelia/session_secret".result; + storageEncryptionKey.result = config.shb.sops.secrets."authelia/storage_encryption_key".result; + identityProvidersOIDCHMACSecret.result = config.shb.sops.secrets."authelia/hmac_secret".result; + identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secrets."authelia/private_key".result; }; }; -sops.secrets."authelia/jwt_secret" = config.shb.authelia.secrets.jwtSecret.request; -sops.secrets."authelia/ldap_admin_password" = config.shb.authelia.secrets.ldapAdminPassword.request; -sops.secrets."authelia/session_secret" = config.shb.authelia.secrets.sessionSecret.request; -sops.secrets."authelia/storage_encryption_key" = config.shb.authelia.secrets.storageEncryptionKey.request; -sops.secrets."authelia/hmac_secret" = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; -sops.secrets."authelia/private_key" = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; -sops.secrets."authelia/smtp_password" = config.shb.authelia.smtp.password.request; +shb.certs.certs.letsencrypt."example.com".extraDomains = [ "auth.example.com" ]; + +shb.sops.secrets."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request; +shb.sops.secrets."authelia/ldap_admin_password".request = config.shb.authelia.secrets.ldapAdminPassword.request; +shb.sops.secrets."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request; +shb.sops.secrets."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request; +shb.sops.secrets."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; +shb.sops.secrets."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; +shb.sops.secrets."authelia/smtp_password".request = config.shb.authelia.smtp.password.request; ``` The secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`. @@ -263,13 +269,14 @@ shb.nextcloud.apps.sso = { clientID = "nextcloud"; fallbackDefaultAuth = false; - secret.result.path = config.sops.secrets."nextcloud/sso/secret".path; - secretForAuthelia.result.path = config.sops.secrets."nextcloud/sso/secretForAuthelia".path; + secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result; + secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result; }; -sops.secret."nextcloud/sso/secret" = config.shb.nextcloud.apps.sso.secret.request; -sops.secret."nextcloud/sso/secretForAuthelia" = config.shb.nextcloud.apps.sso.secretForAuthelia.request // { - key = "nextcloud/sso/secret"; +shb.sops.secret."nextcloud/sso/secret".request = config.shb.nextcloud.apps.sso.secret.request; +shb.sops.secret."nextcloud/sso/secretForAuthelia" = { + request = config.shb.nextcloud.apps.sso.secretForAuthelia.request; + settings.key = "nextcloud/sso/secret"; }; ``` diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix index 710a9e6..680fbeb 100644 --- a/modules/services/vaultwarden.nix +++ b/modules/services/vaultwarden.nix @@ -45,9 +45,16 @@ in example = "https://authelia.example.com"; }; - databasePasswordFile = lib.mkOption { - type = lib.types.path; - description = "File containing the password to connect to the postgresql database."; + databasePassword = lib.mkOption { + description = "File containing the Vaultwarden database password."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "vaultwarden"; + group = "postgres"; + restartUnits = [ "vaultwarden.service" "postgresql.service" ]; + }; + }; }; smtp = lib.mkOption { @@ -88,9 +95,15 @@ in description = "Auth mechanism."; default = "Login"; }; - passwordFile = lib.mkOption { - type = lib.types.str; + password = lib.mkOption { description = "File containing the password to connect to the SMTP host."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "vaultwarden"; + restartUnits = [ "vaultwarden.service" ]; + }; + }; }; }; }); @@ -187,10 +200,10 @@ in systemd.services.vaultwarden.preStart = shblib.replaceSecrets { userConfig = { - DATABASE_URL.source = cfg.databasePasswordFile; + DATABASE_URL.source = cfg.databasePassword.result.path; DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden"; } // lib.optionalAttrs (cfg.smtp != null) { - SMTP_PASSWORD.source = cfg.smtp.passwordFile; + SMTP_PASSWORD.source = cfg.smtp.password.result.path; }; resultPath = "${dataFolder}/vaultwarden.env"; generator = name: v: pkgs.writeText "template" (lib.generators.toINIWithGlobalSection {} { globalSection = v; }); @@ -224,7 +237,7 @@ in { username = "vaultwarden"; database = "vaultwarden"; - passwordFile = builtins.toString cfg.databasePasswordFile; + passwordFile = cfg.databasePassword.result.path; } ]; # TODO: make this work. diff --git a/test/blocks/authelia.nix b/test/blocks/authelia.nix index 5c523c5..fdbe939 100644 --- a/test/blocks/authelia.nix +++ b/test/blocks/authelia.nix @@ -33,8 +33,17 @@ in dcdomain = "dc=example,dc=com"; subdomain = "ldap"; domain = "machine.com"; - ldapUserPassword.result.path = pkgs.writeText "user_password" ldapAdminPassword; - jwtSecret.result.path = pkgs.writeText "jwt_secret" "securejwtsecret"; + ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; + jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; + }; + + shb.hardcodedsecret.ldapUserPassword = { + request = config.shb.ldap.ldapUserPassword.request; + settings.content = ldapAdminPassword; + }; + shb.hardcodedsecret.jwtSecret = { + request = config.shb.ldap.jwtSecret.request; + settings.content = "jwtsecret"; }; shb.authelia = { @@ -45,12 +54,12 @@ in ldapPort = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; secrets = { - jwtSecret.result.path = config.shb.hardcodedsecret.autheliaJwtSecret.path; - ldapAdminPassword.result.path = config.shb.hardcodedsecret.ldapAdminPassword.path; - sessionSecret.result.path = config.shb.hardcodedsecret.sessionSecret.path; - storageEncryptionKey.result.path = config.shb.hardcodedsecret.storageEncryptionKey.path; - identityProvidersOIDCHMACSecret.result.path = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.path; - identityProvidersOIDCIssuerPrivateKey.result.path = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.path; + jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result; + ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result; + sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result; + storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result; + identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result; + identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result; }; oidcClients = [ @@ -73,23 +82,29 @@ in ]; }; - shb.hardcodedsecret.autheliaJwtSecret = config.shb.authelia.secrets.jwtSecret.request // { - content = "jwtSecret"; + shb.hardcodedsecret.autheliaJwtSecret = { + request = config.shb.authelia.secrets.jwtSecret.request; + settings.content = "jwtSecret"; }; - shb.hardcodedsecret.ldapAdminPassword = config.shb.authelia.secrets.ldapAdminPassword.request // { - content = ldapAdminPassword; + shb.hardcodedsecret.ldapAdminPassword = { + request = config.shb.authelia.secrets.ldapAdminPassword.request; + settings.content = ldapAdminPassword; }; - shb.hardcodedsecret.sessionSecret = config.shb.authelia.secrets.sessionSecret.request // { - content = "sessionSecret"; + shb.hardcodedsecret.sessionSecret = { + request = config.shb.authelia.secrets.sessionSecret.request; + settings.content = "sessionSecret"; }; - shb.hardcodedsecret.storageEncryptionKey = config.shb.authelia.secrets.storageEncryptionKey.request // { - content = "storageEncryptionKey"; + shb.hardcodedsecret.storageEncryptionKey = { + request = config.shb.authelia.secrets.storageEncryptionKey.request; + settings.content = "storageEncryptionKey"; }; - shb.hardcodedsecret.identityProvidersOIDCHMACSecret = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request // { - content = "identityProvidersOIDCHMACSecret"; + shb.hardcodedsecret.identityProvidersOIDCHMACSecret = { + request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; + settings.content = "identityProvidersOIDCHMACSecret"; }; - shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request // { - source = (pkgs.runCommand "gen-private-key" {} '' + shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = { + request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; + settings.source = (pkgs.runCommand "gen-private-key" {} '' mkdir $out ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 '') + "/private.pem"; diff --git a/test/blocks/ldap.nix b/test/blocks/ldap.nix index b158939..eeb23cc 100644 --- a/test/blocks/ldap.nix +++ b/test/blocks/ldap.nix @@ -1,6 +1,8 @@ { pkgs, lib, ... }: let pkgs' = pkgs; + + password = "securepassword"; in { auth = pkgs.testers.runNixOSTest { @@ -15,6 +17,7 @@ in shb.ssl.enable = lib.mkEnableOption "ssl"; }; } + ../../modules/blocks/hardcodedsecret.nix ../../modules/blocks/ldap.nix ]; @@ -23,10 +26,19 @@ in dcdomain = "dc=example,dc=com"; subdomain = "ldap"; domain = "example.com"; - ldapUserPassword.result.path = pkgs.writeText "user_password" "securepw"; - jwtSecret.result.path = pkgs.writeText "jwt_secret" "securejwtsecret"; + ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; + jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; debug = true; }; + shb.hardcodedsecret.ldapUserPassword = { + request = config.shb.ldap.ldapUserPassword.request; + settings.content = password; + }; + shb.hardcodedsecret.jwtSecret = { + request = config.shb.ldap.jwtSecret.request; + settings.content = "jwtSecret"; + }; + networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port }; @@ -63,7 +75,7 @@ in + """ -H "Content-type: application/json" """ + """ -H "Host: ldap.example.com" """ + " http://server/auth/simple/login " - + """ -d '{"username": "admin", "password": "securepw"}' """ + + """ -d '{"username": "admin", "password": "${password}"}' """ ))['token'] data = json.loads(client.succeed( diff --git a/test/blocks/restic.nix b/test/blocks/restic.nix index 5bafc33..a5406bd 100644 --- a/test/blocks/restic.nix +++ b/test/blocks/restic.nix @@ -19,23 +19,32 @@ let ]; shb.hardcodedsecret.A = { - owner = "root"; - group = "keys"; - mode = "0440"; - content = "secretA"; + request = { + owner = "root"; + group = "keys"; + mode = "0440"; + }; + settings.content = "secretA"; }; shb.hardcodedsecret.B = { - owner = "root"; - group = "keys"; - mode = "0440"; - content = "secretB"; + request = { + owner = "root"; + group = "keys"; + mode = "0440"; + }; + settings.content = "secretB"; + }; + + shb.hardcodedsecret.passphrase = { + request = config.shb.restic.instances."testinstance".settings.passphrase.request; + settings.content = "secretB"; }; shb.restic.instances."testinstance" = { settings = { enable = true; - passphrase.result.path = pkgs.writeText "passphrase" "PassPhrase"; + passphrase.result = config.shb.hardcodedsecret.passphrase.result; repository = { path = "/opt/repos/A"; @@ -46,8 +55,8 @@ let # Those are not needed by the repository but are still included # so we can test them in the hooks section. secrets = { - A.source = config.shb.hardcodedsecret.A.path; - B.source = config.shb.hardcodedsecret.B.path; + A.source = config.shb.hardcodedsecret.A.result.path; + B.source = config.shb.hardcodedsecret.B.result.path; }; }; }; diff --git a/test/common.nix b/test/common.nix index 6af88ee..c89d2a3 100644 --- a/test/common.nix +++ b/test/common.nix @@ -146,11 +146,13 @@ in "127.0.0.1" = [ "ldap.${domain}" ]; }; - shb.hardcodedsecret.ldapUserPassword = config.shb.ldap.ldapUserPassword.request // { - content = "ldapUserPassword"; + shb.hardcodedsecret.ldapUserPassword = { + request = config.shb.ldap.ldapUserPassword.request; + settings.content = "ldapUserPassword"; }; - shb.hardcodedsecret.jwtSecret = config.shb.ldap.jwtSecret.request // { - content = "jwtSecrets"; + shb.hardcodedsecret.jwtSecret = { + request = config.shb.ldap.jwtSecret.request; + settings.content = "jwtSecrets"; }; shb.ldap = { @@ -160,8 +162,8 @@ in ldapPort = 3890; webUIListenPort = 17170; dcdomain = "dc=example,dc=com"; - ldapUserPassword.result.path = config.shb.hardcodedsecret.ldapUserPassword.path; - jwtSecret.result.path = config.shb.hardcodedsecret.jwtSecret.path; + ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; + jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; }; }; @@ -185,32 +187,38 @@ in dcdomain = config.shb.ldap.dcdomain; secrets = { - jwtSecret.result.path = config.shb.hardcodedsecret.autheliaJwtSecret.path; - ldapAdminPassword.result.path = config.shb.hardcodedsecret.ldapAdminPassword.path; - sessionSecret.result.path = config.shb.hardcodedsecret.sessionSecret.path; - storageEncryptionKey.result.path = config.shb.hardcodedsecret.storageEncryptionKey.path; - identityProvidersOIDCHMACSecret.result.path = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.path; - identityProvidersOIDCIssuerPrivateKey.result.path = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.path; + jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result; + ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result; + sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result; + storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result; + identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result; + identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result; }; }; - shb.hardcodedsecret.autheliaJwtSecret = config.shb.authelia.secrets.jwtSecret.request // { - content = "jwtSecret"; + shb.hardcodedsecret.autheliaJwtSecret = { + request = config.shb.authelia.secrets.jwtSecret.request; + settings.content = "jwtSecret"; }; - shb.hardcodedsecret.ldapAdminPassword = config.shb.authelia.secrets.ldapAdminPassword.request // { - content = "ldapUserPassword"; + shb.hardcodedsecret.ldapAdminPassword = { + request = config.shb.authelia.secrets.ldapAdminPassword.request; + settings.content = "ldapUserPassword"; }; - shb.hardcodedsecret.sessionSecret = config.shb.authelia.secrets.sessionSecret.request // { - content = "sessionSecret"; + shb.hardcodedsecret.sessionSecret = { + request = config.shb.authelia.secrets.sessionSecret.request; + settings.content = "sessionSecret"; }; - shb.hardcodedsecret.storageEncryptionKey = config.shb.authelia.secrets.storageEncryptionKey.request // { - content = "storageEncryptionKey"; + shb.hardcodedsecret.storageEncryptionKey = { + request = config.shb.authelia.secrets.storageEncryptionKey.request; + settings.content = "storageEncryptionKey"; }; - shb.hardcodedsecret.identityProvidersOIDCHMACSecret = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request // { - content = "identityProvidersOIDCHMACSecret"; + shb.hardcodedsecret.identityProvidersOIDCHMACSecret = { + request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; + settings.content = "identityProvidersOIDCHMACSecret"; }; - shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request // { - source = (pkgs.runCommand "gen-private-key" {} '' + shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = { + request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; + settings.source = (pkgs.runCommand "gen-private-key" {} '' mkdir $out ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 '') + "/private.pem"; diff --git a/test/contracts/backup.nix b/test/contracts/backup.nix index c9870f5..c53e7f3 100644 --- a/test/contracts/backup.nix +++ b/test/contracts/backup.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ pkgs, lib, ... }: let contracts = pkgs.callPackage ../../modules/contracts {}; in @@ -13,7 +13,7 @@ in ]; settings = { repository, config, ... }: { enable = true; - passphrase.result.path = config.shb.hardcodedsecret.passphrase.path; + passphrase.result = config.shb.hardcodedsecret.passphrase.result; repository = { path = repository; timerConfig = { @@ -21,16 +21,16 @@ in }; }; }; - extraConfig = { username, ... }: { + extraConfig = { username, config, ... }: { shb.hardcodedsecret.passphrase = { - owner = username; - content = "passphrase"; + request = config.shb.restic.instances."mytest".settings.passphrase.request; + settings.content = "passphrase"; }; }; }; - restic_me = contracts.test.backup { - name = "restic_me"; + restic_nonroot = contracts.test.backup { + name = "restic_nonroot"; username = "me"; providerRoot = [ "shb" "restic" "instances" "mytest" ]; modules = [ @@ -39,7 +39,7 @@ in ]; settings = { repository, config, ... }: { enable = true; - passphrase.result.path = config.shb.hardcodedsecret.passphrase.path; + passphrase.result = config.shb.hardcodedsecret.passphrase.result; repository = { path = repository; timerConfig = { @@ -47,10 +47,10 @@ in }; }; }; - extraConfig = { username, ... }: { + extraConfig = { username, config, ... }: { shb.hardcodedsecret.passphrase = { - owner = username; - content = "passphrase"; + request = config.shb.restic.instances."mytest".settings.passphrase.request; + settings.content = "passphrase"; }; }; }; diff --git a/test/contracts/databasebackup.nix b/test/contracts/databasebackup.nix index b1c18ea..aa6d3a4 100644 --- a/test/contracts/databasebackup.nix +++ b/test/contracts/databasebackup.nix @@ -10,10 +10,11 @@ in modules = [ ../../modules/blocks/postgresql.nix ../../modules/blocks/restic.nix + ../../modules/blocks/hardcodedsecret.nix ]; - settings = repository: { + settings = { repository, config, ... }: { enable = true; - passphrase.result.path = pkgs.writeText "passphrase" "PassPhrase"; + passphrase.result = config.shb.hardcodedsecret.passphrase.result; repository = { path = repository; timerConfig = { @@ -21,12 +22,17 @@ in }; }; }; - extraConfig = { username, database, ... }: { + extraConfig = { config, database, ... }: { shb.postgresql.ensures = [ { - inherit username database; + inherit database; + username = database; } ]; + shb.hardcodedsecret.passphrase = { + request = config.shb.restic.databases.postgresql.settings.passphrase.request; + settings.content = "passphrase"; + }; }; }; } diff --git a/test/contracts/secret.nix b/test/contracts/secret.nix index d029100..c594027 100644 --- a/test/contracts/secret.nix +++ b/test/contracts/secret.nix @@ -7,8 +7,8 @@ in name = "hardcoded"; modules = [ ../../modules/blocks/hardcodedsecret.nix ]; configRoot = [ "shb" "hardcodedsecret" ]; - createContent = { - content = "secretA"; + settingsCfg = secret: { + content = secret; }; }; @@ -16,8 +16,8 @@ in name = "hardcoded"; modules = [ ../../modules/blocks/hardcodedsecret.nix ]; configRoot = [ "shb" "hardcodedsecret" ]; - createContent = { - content = "secretA"; + settingsCfg = secret: { + content = secret; }; owner = "user"; group = "group"; diff --git a/test/services/forgejo.nix b/test/services/forgejo.nix index dbae04b..df26b3a 100644 --- a/test/services/forgejo.nix +++ b/test/services/forgejo.nix @@ -34,8 +34,8 @@ let enable = true; inherit domain subdomain; - adminPassword.result.path = config.shb.hardcodedsecret.forgejoAdminPassword.path; - databasePassword.result.path = config.shb.hardcodedsecret.forgejoDatabasePassword.path; + adminPassword.result = config.shb.hardcodedsecret.forgejoAdminPassword.result; + databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result; }; # Needed for gitea-runner-local to be able to ping forgejo. @@ -43,12 +43,14 @@ let "127.0.0.1" = [ "${subdomain}.${domain}" ]; }; - shb.hardcodedsecret.forgejoAdminPassword = config.shb.forgejo.adminPassword.request // { - content = adminPassword; + shb.hardcodedsecret.forgejoAdminPassword = { + request = config.shb.forgejo.adminPassword.request; + settings.content = adminPassword; }; - shb.hardcodedsecret.forgejoDatabasePassword = config.shb.forgejo.databasePassword.request // { - content = "databasePassword"; + shb.hardcodedsecret.forgejoDatabasePassword = { + request = config.shb.forgejo.databasePassword.request; + settings.content = "databasePassword"; }; }; @@ -65,12 +67,13 @@ let host = "127.0.0.1"; port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; - adminPassword.result.path = config.shb.hardcodedsecret.forgejoLdapUserPassword.path; + adminPassword.result = config.shb.hardcodedsecret.forgejoLdapUserPassword.result; }; }; - shb.hardcodedsecret.forgejoLdapUserPassword = config.shb.forgejo.ldap.adminPassword.request // { - content = "ldapUserPassword"; + shb.hardcodedsecret.forgejoLdapUserPassword = { + request = config.shb.forgejo.ldap.adminPassword.request; + settings.content = "ldapUserPassword"; }; }; @@ -79,17 +82,19 @@ let sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - sharedSecret.result.path = config.shb.hardcodedsecret.forgejoSSOPassword.path; - sharedSecretForAuthelia.result.path = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.path; + sharedSecret.result = config.shb.hardcodedsecret.forgejoSSOPassword.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.result; }; }; - shb.hardcodedsecret.forgejoSSOPassword = config.shb.forgejo.sso.sharedSecret.request // { - content = "ssoPassword"; + shb.hardcodedsecret.forgejoSSOPassword = { + request = config.shb.forgejo.sso.sharedSecret.request; + settings.content = "ssoPassword"; }; - shb.hardcodedsecret.forgejoSSOPasswordAuthelia = config.shb.forgejo.sso.sharedSecretForAuthelia.request // { - content = "ssoPassword"; + shb.hardcodedsecret.forgejoSSOPasswordAuthelia = { + request = config.shb.forgejo.sso.sharedSecretForAuthelia.request; + settings.content = "ssoPassword"; }; }; in diff --git a/test/services/jellyfin.nix b/test/services/jellyfin.nix index 8af4a96..b97873b 100644 --- a/test/services/jellyfin.nix +++ b/test/services/jellyfin.nix @@ -43,12 +43,13 @@ let host = "127.0.0.1"; port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; - adminPassword.result.path = config.shb.hardcodedsecret.jellyfinLdapUserPassword.path; + adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result; }; }; - shb.hardcodedsecret.jellyfinLdapUserPassword = config.shb.jellyfin.ldap.adminPassword.request // { - content = "ldapUserPassword"; + shb.hardcodedsecret.jellyfinLdapUserPassword = { + request = config.shb.jellyfin.ldap.adminPassword.request; + settings.content = "ldapUserPassword"; }; }; @@ -57,17 +58,19 @@ let sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - sharedSecret.result.path = config.shb.hardcodedsecret.jellyfinSSOPassword.path; - sharedSecretForAuthelia.result.path = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.path; + sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result; }; }; - shb.hardcodedsecret.jellyfinSSOPassword = config.shb.jellyfin.sso.sharedSecret.request // { - content = "ssoPassword"; + shb.hardcodedsecret.jellyfinSSOPassword = { + request = config.shb.jellyfin.sso.sharedSecret.request; + settings.content = "ssoPassword"; }; - shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = config.shb.jellyfin.sso.sharedSecretForAuthelia.request // { - content = "ssoPassword"; + shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = { + request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request; + settings.content = "ssoPassword"; }; }; in diff --git a/test/services/monitoring.nix b/test/services/monitoring.nix index a349f57..732ee89 100644 --- a/test/services/monitoring.nix +++ b/test/services/monitoring.nix @@ -30,8 +30,17 @@ let inherit subdomain domain; grafanaPort = 3000; - adminPasswordFile = pkgs.writeText "admin_password" password; - secretKeyFile = pkgs.writeText "secret_key" "secret_key"; + adminPassword.result = config.shb.hardcodedsecret."admin_password".result; + secretKey.result = config.shb.hardcodedsecret."secret_key".result; + }; + + shb.hardcodedsecret."admin_password" = { + request = config.shb.monitoring.adminPassword.request; + settings.content = password; + }; + shb.hardcodedsecret."secret_key" = { + request = config.shb.monitoring.secretKey.request; + settings.content = "secret_key_pw"; }; }; diff --git a/test/services/nextcloud.nix b/test/services/nextcloud.nix index f3eaf29..e0fcb36 100644 --- a/test/services/nextcloud.nix +++ b/test/services/nextcloud.nix @@ -132,12 +132,13 @@ let externalFqdn = "${fqdn}:8080"; adminUser = adminUser; - adminPass.result.path = config.shb.hardcodedsecret.adminPass.path; + adminPass.result = config.shb.hardcodedsecret.adminPass.result; debug = true; }; - shb.hardcodedsecret.adminPass = config.shb.nextcloud.adminPass.request // { - content = adminPass; + shb.hardcodedsecret.adminPass = { + request = config.shb.nextcloud.adminPass.request; + settings.content = adminPass; }; }; @@ -157,7 +158,7 @@ let port = config.shb.ldap.ldapPort; dcdomain = config.shb.ldap.dcdomain; adminName = "admin"; - adminPassword.result.path = config.shb.ldap.ldapUserPassword.result.path; + adminPassword.result = config.shb.ldap.ldapUserPassword.result; userGroup = "nextcloud_user"; }; }; @@ -172,18 +173,20 @@ let clientID = "nextcloud"; # adminUserGroup = "nextcloud_admin"; - secret.result.path = config.shb.hardcodedsecret.oidcSecret.path; - secretForAuthelia.result.path = config.shb.hardcodedsecret.oidcAutheliaSecret.path; + secret.result = config.shb.hardcodedsecret.oidcSecret.result; + secretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result; fallbackDefaultAuth = false; }; }; - shb.hardcodedsecret.oidcSecret = config.shb.nextcloud.apps.sso.secret.request // { - content = oidcSecret; + shb.hardcodedsecret.oidcSecret = { + request = config.shb.nextcloud.apps.sso.secret.request; + settings.content = oidcSecret; }; - shb.hardcodedsecret.oidcAutheliaSecret = config.shb.nextcloud.apps.sso.secretForAuthelia.request // { - content = oidcSecret; + shb.hardcodedsecret.oidcAutheliaSecret = { + request = config.shb.nextcloud.apps.sso.secretForAuthelia.request; + settings.content = oidcSecret; }; }; diff --git a/test/services/vaultwarden.nix b/test/services/vaultwarden.nix index 33026cd..1b5ea2c 100644 --- a/test/services/vaultwarden.nix +++ b/test/services/vaultwarden.nix @@ -51,6 +51,7 @@ let }; base = testLib.base pkgs' [ + ../../modules/blocks/hardcodedsecret.nix ../../modules/services/vaultwarden.nix ]; @@ -61,7 +62,11 @@ let inherit subdomain domain; port = 8222; - databasePasswordFile = pkgs.writeText "pwfile" "DBPASSWORDFILE"; + databasePassword.result = config.shb.hardcodedsecret.passphrase.result; + }; + shb.hardcodedsecret.passphrase = { + request = config.shb.vaultwarden.databasePassword.request; + settings.content = "PassPhrase"; }; # networking.hosts = { @@ -97,7 +102,7 @@ let request = config.shb.vaultwarden.backup; settings = { enable = true; - passphrase.result.path = config.shb.hardcodedsecret.passphrase.path; + passphrase.result = config.shb.hardcodedsecret.backupPassphrase.result; repository = { path = "/opt/repos/A"; timerConfig = { @@ -107,8 +112,9 @@ let }; }; }; - shb.hardcodedsecret.passphrase = config.shb.restic.instances."testinstance".settings.passphrase.request // { - content = "PassPhrase"; + shb.hardcodedsecret.backupPassphrase = { + request = config.shb.restic.instances."testinstance".settings.passphrase.request; + settings.content = "PassPhrase"; }; }; in