update secret contract and add sops module (#356)

I find these `mkProvider` and `mkRequester` functions really nice.

Also fixes doc generation related to  https://github.com/NixOS/nixpkgs/pull/353513
This commit is contained in:
Pierre Penninckx 2024-11-21 21:51:57 +01:00 committed by GitHub
parent fe137ded23
commit 5056d203d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 2710 additions and 743 deletions

View file

@ -40,6 +40,7 @@
- `shb.forgejo.databasePasswordFile` -> `shb.forgejo.databasePassword.result.path`. - `shb.forgejo.databasePasswordFile` -> `shb.forgejo.databasePassword.result.path`.
- Backup: - Backup:
- `shb.restic.instances` options has been split between `shb.restic.instances.request` and `shb.restic.instances.settings`, matching better with contracts. - `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 ## User Facing Backwards Compatible Changes

View file

@ -69,7 +69,7 @@ SHB's first goal is to provide unified [building blocks](#available-blocks)
and by extension configuration interface, for self-hosting. and by extension configuration interface, for self-hosting.
Compare the configuration for Nextcloud and Forgejo in Self Host Blocks. 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. It also does not show specific options for each service.
These are still complete snippets that configure HTTPS, These are still complete snippets that configure HTTPS,
subdomain serving the service, LDAP and SSO integration. subdomain serving the service, LDAP and SSO integration.
@ -87,14 +87,14 @@ shb.nextcloud = {
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminPasswordFile = config.sops.secrets."nextcloud/ldap_admin_password".path; adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
}; };
apps.sso = { apps.sso = {
enable = true; enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secretFile = config.sops.secrets."nextcloud/sso/secret".path; secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result;
secretFileForAuthelia = config.sops.secrets."authelia/nextcloud_sso_secret".path; secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result;
}; };
}; };
``` ```
@ -112,15 +112,15 @@ shb.forgejo = {
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminPasswordFile = config.sops.secrets."forgejo/ldap_admin_password".path; adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
}; };
sso = { sso = {
enable = true; enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secretFile = config.sops.secrets."forgejo/ssoSecret".path; secret.result = config.shb.sops.secrets."forgejo/sso/secret".result;
secretFileForAuthelia = config.sops.secrets."forgejo/authelia/ssoSecret".path; secretForAuthelia.result = config.shb.sops.secrets."forgejo/sso/secretForAuthelia".result;
}; };
}; };
``` ```

View file

@ -60,7 +60,7 @@ chmod 600 sshkey
This is only needed because git mangles with the permissions. You will not even see this change in This is only needed because git mangles with the permissions. You will not even see this change in
`git status`. `git status`.
### Deploy with Colmena {#demo-homeassitant-deploy-colmena} ### Deploy with Colmena {#demo-homeassistant-deploy-colmena}
If you deploy with Colmena, you must first build the VM and start it: If you deploy with Colmena, you must first build the VM and start it:

View file

@ -112,7 +112,7 @@ ssh -F ssh_config example
:::: {.note} :::: {.note}
This section corresponds to the `basic` section of the [Nextcloud This section corresponds to the `basic` section of the [Nextcloud
manual](services-nextcloud.html#services-nextcloud-server-usage-basic). manual](services-nextcloud.html#services-nextcloudserver-usage-basic).
:::: ::::
Assuming you already deployed the `basic` demo, now you must add the following entry to the Assuming you already deployed the `basic` demo, now you must add the following entry to the
@ -143,7 +143,7 @@ This is the admin user of Nextcloud and that's the end of the `basic` demo.
:::: {.note} :::: {.note}
This section corresponds to the `ldap` section of the [Nextcloud This section corresponds to the `ldap` section of the [Nextcloud
manual](services-nextcloud.html#services-nextcloud-server-usage-ldap). manual](services-nextcloud.html#services-nextcloudserver-usage-ldap).
:::: ::::
Assuming you already deployed the `ldap` demo, now you must add the following entry to the Assuming you already deployed the `ldap` demo, now you must add the following entry to the
@ -182,7 +182,7 @@ This is the end of the `ldap` demo.
:::: {.note} :::: {.note}
This section corresponds to the `sso` section of the [Nextcloud This section corresponds to the `sso` section of the [Nextcloud
manual](services-nextcloud.html#services-nextcloud-server-usage-oidc). manual](services-nextcloud.html#services-nextcloudserver-usage-oidc).
:::: ::::
At this point, it is assumed you already deployed the `sso` demo. There is no host to add to At this point, it is assumed you already deployed the `sso` demo. There is no host to add to

View file

@ -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). 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 ```{=include=} chapters html:into-file=//blocks-ssl.html
modules/blocks/ssl/docs/default.md modules/blocks/ssl/docs/default.md
``` ```

View file

@ -148,11 +148,14 @@ Provided contracts are:
- [SSL generator contract](contracts-ssl.html) to generate SSL certificates. - [SSL generator contract](contracts-ssl.html) to generate SSL certificates.
Two providers are implemented: self-signed and Let's Encrypt. Two providers are implemented: self-signed and Let's Encrypt.
- [Backup contract](contracts-backup.html) to backup directories. - [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. - [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. - [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 ```{=include=} chapters html:into-file=//contracts-ssl.html
modules/contracts/ssl/docs/default.md modules/contracts/ssl/docs/default.md

View file

@ -142,6 +142,11 @@ in stdenv.mkDerivation {
'@OPTIONS_JSON@' \ '@OPTIONS_JSON@' \
${individualModuleOptionsDocs [ ../modules/blocks/restic.nix ]}/share/doc/nixos/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 \ substituteInPlace ./modules/services/nextcloud-server/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@OPTIONS_JSON@' \
@ -190,6 +195,7 @@ in stdenv.mkDerivation {
nixos-render-docs manual html \ nixos-render-docs manual html \
--manpage-urls ${manpage-urls} \ --manpage-urls ${manpage-urls} \
--redirects ./redirects.json \
--media-dir media \ --media-dir media \
--revision ${lib.trivial.revisionWithDefault release} \ --revision ${lib.trivial.revisionWithDefault release} \
--stylesheet static/style.css \ --stylesheet static/style.css \

View file

@ -7,6 +7,6 @@ your local machine with minimal manual steps.
demo/homeassistant/README.md demo/homeassistant/README.md
``` ```
```{=include=} chapters html:into-file=//demo-nextcloud-server.html ```{=include=} chapters html:into-file=//demo-nextcloud.html
demo/nextcloud/README.md demo/nextcloud/README.md
``` ```

View file

@ -1,4 +1,4 @@
# All Options {#ch-options} # All Options {#all-options}
```{=include=} options ```{=include=} options
id-prefix: opt- id-prefix: opt-

View file

@ -39,14 +39,14 @@ shb.nextcloud = {
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminPasswordFile = config.sops.secrets."nextcloud/ldap_admin_password".path; adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
}; };
apps.sso = { apps.sso = {
enable = true; enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secretFile = config.sops.secrets."nextcloud/sso/secret".path; secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result;
secretFileForAuthelia = config.sops.secrets."authelia/nextcloud_sso_secret".path; secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result;
}; };
}; };
``` ```
@ -64,15 +64,15 @@ shb.forgejo = {
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminPasswordFile = config.sops.secrets."forgejo/ldap_admin_password".path; adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
}; };
sso = { sso = {
enable = true; enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secretFile = config.sops.secrets."forgejo/ssoSecret".path; secret.result = config.shb.sops.secrets."forgejo/sso/secret".result;
secretFileForAuthelia = config.sops.secrets."forgejo/authelia/ssoSecret".path; secretForAuthelia.result = config.shb.sops.secrets."forgejo/sso/secretForAuthelia".result;
}; };
}; };
``` ```

1625
docs/redirects.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -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. Setting the default this way makes all sops instances use that same file.
7. Reference the secrets in nix: 7. Reference the secrets in nix:
```nix ```nix
shb.nextcloud.adminPass.result.path = config.sops.secrets."nextcloud/adminpass".path; shb.sops.secrets."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
shb.nextcloud.adminPass.result = config.shb.sops.secrets."nextcloud/adminpass".result;
sops.secrets."nextcloud/adminpass" = config.shb.nextcloud.adminPass.request;
``` ```
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.

View file

@ -68,45 +68,69 @@ in
description = "Secrets needed by Authelia"; description = "Secrets needed by Authelia";
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
jwtSecret = contracts.secret.mkOption { jwtSecret = lib.mkOption {
description = "JWT secret."; description = "JWT secret.";
mode = "0400"; type = lib.types.submodule {
owner = cfg.autheliaUser; options = contracts.secret.mkRequester {
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
};
}; };
ldapAdminPassword = contracts.secret.mkOption { ldapAdminPassword = lib.mkOption {
description = "LDAP admin user password."; description = "LDAP admin user password.";
mode = "0400"; type = lib.types.submodule {
owner = cfg.autheliaUser; options = contracts.secret.mkRequester {
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
};
}; };
sessionSecret = contracts.secret.mkOption { sessionSecret = lib.mkOption {
description = "Session secret."; description = "Session secret.";
mode = "0400"; type = lib.types.submodule {
owner = cfg.autheliaUser; options = contracts.secret.mkRequester {
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
};
}; };
storageEncryptionKey = contracts.secret.mkOption { storageEncryptionKey = lib.mkOption {
description = "Storage encryption key."; description = "Storage encryption key.";
mode = "0400"; type = lib.types.submodule {
owner = cfg.autheliaUser; options = contracts.secret.mkRequester {
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
};
}; };
identityProvidersOIDCHMACSecret = contracts.secret.mkOption { identityProvidersOIDCHMACSecret = lib.mkOption {
description = "Identity provider OIDC HMAC secret."; description = "Identity provider OIDC HMAC secret.";
mode = "0400"; type = lib.types.submodule {
owner = cfg.autheliaUser; options = contracts.secret.mkRequester {
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
};
}; };
identityProvidersOIDCIssuerPrivateKey = contracts.secret.mkOption { identityProvidersOIDCIssuerPrivateKey = lib.mkOption {
description = '' description = ''
Identity provider OIDC issuer private key. Identity provider OIDC issuer private key.
Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048` Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048`
''; '';
mode = "0400"; type = lib.types.submodule {
owner = cfg.autheliaUser; options = contracts.secret.mkRequester {
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ]; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
};
};
}; };
}; };
}; };
@ -220,11 +244,15 @@ in
type = lib.types.str; type = lib.types.str;
description = "Username to connect to the SMTP host."; description = "Username to connect to the SMTP host.";
}; };
password = contracts.secret.mkOption { password = lib.mkOption {
description = "File containing the password to connect to the SMTP host."; description = "File containing the password to connect to the SMTP host.";
mode = "0400"; type = lib.types.submodule {
owner = cfg.autheliaUser; options = contracts.secret.mkRequester {
restartUnits = [ "authelia-${fqdn}" ]; mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
};
};
}; };
}; };
})) }))

View file

@ -1,10 +1,11 @@
{ config, options, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.shb.hardcodedsecret; cfg = config.shb.hardcodedsecret;
opt = options.shb.hardcodedsecret;
contracts = pkgs.callPackage ../contracts {};
inherit (lib) mapAttrs' mkOption nameValuePair; inherit (lib) mapAttrs' mkOption nameValuePair;
inherit (lib.types) attrsOf listOf path nullOr str submodule; inherit (lib.types) attrsOf nullOr str submodule;
inherit (pkgs) writeText; inherit (pkgs) writeText;
in in
{ {
@ -16,74 +17,49 @@ in
example = lib.literalExpression '' example = lib.literalExpression ''
{ {
mySecret = { mySecret = {
user = "me"; request = {
mode = "0400"; user = "me";
restartUnits = [ "myservice.service" ]; mode = "0400";
content = "My Secrets"; restartUnits = [ "myservice.service" ];
};
settings.content = "My Secret";
}; };
} }
''; '';
type = attrsOf (submodule ({ name, ... }: { type = attrsOf (submodule ({ name, ... }: {
options = { options = contracts.secret.mkProvider {
mode = mkOption { settings = mkOption {
description = '' 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 { resultCfg = {
description = '' path = "/run/hardcodedsecrets/hardcodedsecret_${name}";
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;
}; };
}; };
})); }));
@ -92,16 +68,16 @@ in
config = { config = {
system.activationScripts = mapAttrs' (n: cfg': system.activationScripts = mapAttrs' (n: cfg':
let let
source = if cfg'.source != null source = if cfg'.settings.source != null
then cfg'.source then cfg'.settings.source
else writeText "hardcodedsecret_${n}_content" cfg'.content; else writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
in in
nameValuePair "hardcodedsecret_${n}" '' nameValuePair "hardcodedsecret_${n}" ''
mkdir -p "$(dirname "${cfg'.path}")" mkdir -p "$(dirname "${cfg'.result.path}")"
touch "${cfg'.path}" touch "${cfg'.result.path}"
chmod ${cfg'.mode} "${cfg'.path}" chmod ${cfg'.request.mode} "${cfg'.result.path}"
chown ${cfg'.owner}:${cfg'.group} "${cfg'.path}" chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}"
cp ${source} "${cfg'.path}" cp ${source} "${cfg'.result.path}"
'' ''
) cfg; ) cfg;
}; };

View file

@ -47,20 +47,28 @@ in
default = 17170; default = 17170;
}; };
ldapUserPassword = contracts.secret.mkOption { ldapUserPassword = lib.mkOption {
description = "LDAP admin user secret."; description = "LDAP admin user secret.";
mode = "0440"; type = lib.types.submodule {
owner = "lldap"; options = contracts.secret.mkRequester {
group = "lldap"; mode = "0440";
restartUnits = [ "lldap.service" ]; owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
};
}; };
jwtSecret = contracts.secret.mkOption { jwtSecret = lib.mkOption {
description = "JWT secret."; description = "JWT secret.";
mode = "0440"; type = lib.types.submodule {
owner = "lldap"; options = contracts.secret.mkRequester {
group = "lldap"; mode = "0440";
restartUnits = [ "lldap.service" ]; owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
};
}; };
restrictAccessIPRange = lib.mkOption { restrictAccessIPRange = lib.mkOption {

View file

@ -82,14 +82,28 @@ in
default = []; default = [];
}; };
adminPasswordFile = lib.mkOption { adminPassword = lib.mkOption {
type = lib.types.path; description = "Initial admin password.";
description = "File containing the initial admin password."; type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0400";
owner = "grafana";
group = "grafana";
restartUnits = [ "grafana.service" ];
};
};
}; };
secretKeyFile = lib.mkOption { secretKey = lib.mkOption {
type = lib.types.path; description = "Secret key used for signing.";
description = "File containing the 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 { smtp = lib.mkOption {
@ -159,9 +173,9 @@ in
}; };
security = { security = {
secret_key = "$__file{${cfg.secretKeyFile}}"; secret_key = "$__file{${cfg.secretKey.result.path}}";
disable_initial_admin_creation = false; # Enable when LDAP support is configured. 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 = { server = {

View file

@ -16,8 +16,8 @@ shb.monitoring = {
subdomain = "grafana"; subdomain = "grafana";
inherit domain; inherit domain;
contactPoints = [ "me@example.com" ]; contactPoints = [ "me@example.com" ];
adminPasswordFile = config.sops.secrets."monitoring/admin_password".path; adminPassword.result = config.sops.secrets."monitoring/admin_password".reuslt;
secretKeyFile = config.sops.secrets."monitoring/secret_key".path; secretKey.result = config.sops.secrets."monitoring/secret_key".result;
}; };
sops.secrets."monitoring/admin_password" = { sops.secrets."monitoring/admin_password" = {

View file

@ -6,11 +6,11 @@ let
shblib = pkgs.callPackage ../../lib {}; shblib = pkgs.callPackage ../../lib {};
contracts = pkgs.callPackage ../contracts {}; 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) 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 '' enable = mkEnableOption ''
this backup intance. this backup intance.
@ -18,13 +18,17 @@ let
but still provides the helper tool to restore snapshots but still provides the helper tool to restore snapshots
''; '';
passphrase = contracts.secret.mkOption { passphrase = lib.mkOption {
description = "Encryption key for the backup repository."; description = "Encryption key for the backup repository.";
mode = "0400"; type = lib.types.submodule {
owner = options.request.value.user; options = contracts.secret.mkRequester {
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)"; mode = "0400";
restartUnits = [ (fullName name options.settings.value.repository) ]; owner = config.request.user;
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]"; ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
restartUnits = [ (fullName name config.settings.repository) ];
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
};
};
}; };
repository = mkOption { repository = mkOption {
@ -99,7 +103,6 @@ let
}; };
repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (removePrefix "/" name); repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (removePrefix "/" name);
backupName = name: repository: "${name}_${repoSlugName repository.path}";
fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}"; fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}";
in in
{ {
@ -107,7 +110,7 @@ in
instances = mkOption { instances = mkOption {
description = "Files to backup following the [backup contract](./contracts-backup.html)."; description = "Files to backup following the [backup contract](./contracts-backup.html).";
default = {}; default = {};
type = attrsOf (submodule ({ name, options, ... }: { type = attrsOf (submodule ({ name, config, ... }: {
options = { options = {
request = mkOption { request = mkOption {
description = '' description = ''
@ -125,7 +128,7 @@ in
''; '';
type = submodule { 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. Contains the output of the Restic provider.
''; '';
default = { default = {
restoreScript = fullName name options.settings.value.repository; restoreScript = fullName name config.settings.repository;
backupService = "${fullName name options.settings.value.repository}.service"; backupService = "${fullName name config.settings.repository}.service";
}; };
defaultText = { defaultText = {
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}"; restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service"; backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
}; };
type = contracts.backup.result { type = contracts.backup.result {
restoreScript = fullName name options.settings.value.repository; restoreScript = fullName name config.settings.repository;
backupService = "${fullName name options.settings.value.repository}.service"; backupService = "${fullName name config.settings.repository}.service";
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}"; restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service"; backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
}; };
@ -157,7 +160,7 @@ in
databases = mkOption { databases = mkOption {
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html)."; description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
default = {}; default = {};
type = attrsOf (submodule ({ name, options, ... }: { type = attrsOf (submodule ({ name, config, ... }: {
options = { options = {
request = mkOption { request = mkOption {
description = '' description = ''
@ -175,7 +178,7 @@ in
''; '';
type = submodule { 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. Contains the output of the Restic provider.
''; '';
default = { default = {
restoreScript = fullName name options.settings.value.repository; restoreScript = fullName name config.settings.repository;
backupService = "${fullName name options.settings.value.repository}.service"; backupService = "${fullName name config.settings.repository}.service";
}; };
defaultText = { defaultText = {
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}"; restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service"; backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
}; };
type = contracts.databasebackup.result { type = contracts.databasebackup.result {
restoreScript = fullName name options.settings.value.repository; restoreScript = fullName name config.settings.repository;
backupService = "${fullName name options.settings.value.repository}.service"; backupService = "${fullName name config.settings.repository}.service";
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}"; restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service"; backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
}; };

View file

@ -12,7 +12,7 @@ Specific integration tests are defined in [`/test/blocks/restic.nix`](@REPO@/tes
## Provider Contracts {#blocks-restic-contract-provider} ## 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. - [backup contract](contracts-backup.html) under the [`shb.restic.instances`][instances] option.
It is tested with [contract tests][backup contract tests]. 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} ## 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. 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} ### One folder backed up manually {#blocks-restic-usage-provider-manual}
@ -55,7 +56,7 @@ shb.restic.instances."myservice" = {
settings = { settings = {
enable = true; enable = true;
passphraseFile = "<path/to/passphrase>"; passphrase.result = shb.sops.secret."passphrase".result;
repository = { repository = {
path = "/srv/backups/myservice"; 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} ### One folder backed up with contract {#blocks-restic-usage-provider-contract}
@ -89,7 +93,7 @@ shb.restic.instances."myservice" = {
settings = { settings = {
enable = true; enable = true;
passphraseFile = "<path/to/passphrase>"; passphrase.result = shb.sops.secret."passphrase".result;
repository = { repository = {
path = "/srv/backups/myservice"; 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} ### One folder backed up to S3 {#blocks-restic-usage-provider-remote}

36
modules/blocks/sops.nix Normal file
View file

@ -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.<name>.request](#blocks-sops-options-shb.sops.secret._name_.request) option.
'';
type = anything;
};
resultCfg = {
path = "/run/secrets/${name}";
pathText = "/run/secrets/<name>";
};
};
}));
};
};
}

View file

@ -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.<name>.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@
```

View file

@ -1,4 +1,4 @@
# SSL Generator Block {#ssl-block} # SSL Generator Block {#block-ssl}
This NixOS module is a block that implements the [SSL certificate generator](contracts-ssl.html) contract. This NixOS module is a block that implements the [SSL certificate generator](contracts-ssl.html) contract.
@ -14,7 +14,7 @@ It is implemented by:
[11]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.selfsigned [11]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.selfsigned
[12]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.letsencrypt [12]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.letsencrypt
## Self-Signed Certificates {#ssl-block-impl-self-signed} ## Self-Signed Certificates {#block-ssl-impl-self-signed}
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix). Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
@ -52,7 +52,7 @@ shb.certs.certs.selfsigned = {
The group has been chosen to be `nginx` to be consistent with the examples further down in this The group has been chosen to be `nginx` to be consistent with the examples further down in this
document. document.
## Let's Encrypt {#ssl-block-impl-lets-encrypt} ## Let's Encrypt {#block-ssl-impl-lets-encrypt}
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix). Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
@ -81,7 +81,7 @@ LINODE_TOKEN=XYZ...
For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/). For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/).
## Usage {#ssl-block-usage} ## Usage {#block-ssl-usage}
To use either a self-signed certificates or a Let's Encrypt generated one, we can reference the path To use either a self-signed certificates or a Let's Encrypt generated one, we can reference the path
where the certificate and the private key are located: where the certificate and the private key are located:
@ -110,16 +110,16 @@ config.shb.certs.systemdService
See also the [SSL certificate generator usage](contracts-ssl.html#ssl-contract-usage) for a more detailed usage See also the [SSL certificate generator usage](contracts-ssl.html#ssl-contract-usage) for a more detailed usage
example. example.
## Debug {#ssl-block-debug} ## Debug {#block-ssl-debug}
Each CA and Cert is generated by a systemd service whose name can be seen in the `systemdService` Each CA and Cert is generated by a systemd service whose name can be seen in the `systemdService`
option. You can then see the latest errors messages using `journalctl`. option. You can then see the latest errors messages using `journalctl`.
## Tests {#ssl-block-tests} ## Tests {#block-ssl-tests}
The self-signed implementation is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix). The self-signed implementation is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix).
## Options Reference {#ssl-block-options} ## Options Reference {#block-ssl-options}
```{=include=} options ```{=include=} options
id-prefix: blocks-ssl-options- id-prefix: blocks-ssl-options-

View file

@ -1,7 +1,7 @@
{ lib, ... }: { lib, ... }:
let let
inherit (lib) mkOption; inherit (lib) mkOption;
inherit (lib.types) anything listOf nonEmptyListOf submodule str; inherit (lib.types) listOf nonEmptyListOf submodule str;
in in
{ {
request = submodule { request = submodule {
@ -9,8 +9,6 @@ in
user = mkOption { user = mkOption {
description = '' description = ''
Unix user doing the backups. Unix user doing the backups.
Most of the time, this should be the user owning the files.
''; '';
type = str; type = str;
}; };

View file

@ -101,6 +101,6 @@ Or with another module `backupservice_2`!
- <!-- [ -->Home Assistant<!--](services-home-assistant.html). --> (no manual yet) - <!-- [ -->Home Assistant<!--](services-home-assistant.html). --> (no manual yet)
- <!-- [ -->Jellyfin<!--](services-jellyfin.html). --> (no manual yet) - <!-- [ -->Jellyfin<!--](services-jellyfin.html). --> (no manual yet)
- <!-- [ -->LLDAP<!--](blocks-ldap.html). --> (no manual yet) - <!-- [ -->LLDAP<!--](blocks-ldap.html). --> (no manual yet)
- [Nextcloud](services-nextcloud.html#services-nextcloud-server-usage-backup). - [Nextcloud](services-nextcloud.html#services-nextcloudserver-usage-backup).
- [Vaultwarden](services-vaultwarden.html#services-vaultwarden-backup). - [Vaultwarden](services-vaultwarden.html#services-vaultwarden-backup).
- <!-- [ -->*arr<!--](services-arr.html). --> (no manual yet) - <!-- [ -->*arr<!--](services-arr.html). --> (no manual yet)

View file

@ -16,7 +16,7 @@ in
"/opt/files/B" "/opt/files/B"
], ],
settings, # { repository, config } -> attrset settings, # { repository, config } -> attrset
extraConfig ? null, # { username } -> attrset extraConfig ? null, # { username, config } -> attrset
}: pkgs.testers.runNixOSTest { }: pkgs.testers.runNixOSTest {
inherit name; inherit name;
@ -41,7 +41,7 @@ in
group = "root"; group = "root";
}; };
}) })
(optionalAttrs (extraConfig != null) (extraConfig { inherit username; })) (optionalAttrs (extraConfig != null) (extraConfig { inherit username config; }))
]; ];
}; };

View file

@ -9,11 +9,10 @@ in
{ name, { name,
requesterRoot, requesterRoot,
providerRoot, providerRoot,
extraConfig ? null, # { username, database } -> attrset extraConfig ? null, # { config, database } -> attrset
modules ? [], modules ? [],
username ? "me",
database ? "me", database ? "me",
settings, # repository -> attrset settings, # { repository, config } -> attrset
}: pkgs.testers.runNixOSTest { }: pkgs.testers.runNixOSTest {
inherit name; inherit name;
@ -22,16 +21,19 @@ in
config = lib.mkMerge [ config = lib.mkMerge [
(setAttrByPath providerRoot { (setAttrByPath providerRoot {
request = (getAttrFromPath requesterRoot config).databasebackup; request = (getAttrFromPath requesterRoot config).databasebackup;
settings = settings "/opt/repos/database"; settings = settings {
inherit config;
repository = "/opt/repos/database";
};
}) })
(mkIf (username != "root") { (mkIf (database != "root") {
users.users.${username} = { users.users.${database} = {
isSystemUser = true; isSystemUser = true;
extraGroups = [ "sudoers" ]; extraGroups = [ "sudoers" ];
group = "root"; 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) machine.wait_for_open_port(5432)
def peer_cmd(cmd, db="me"): 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): def query(query):
res = machine.succeed(peer_cmd(query)) res = machine.succeed(peer_cmd(query))
@ -68,10 +70,11 @@ in
with subtest("backup"): with subtest("backup"):
print(machine.succeed("systemctl cat ${provider.backupService}")) print(machine.succeed("systemctl cat ${provider.backupService}"))
print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase"))
machine.succeed("systemctl start ${provider.backupService}") machine.succeed("systemctl start ${provider.backupService}")
with subtest("drop database"): 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")) machine.fail(peer_cmd("SELECT * FROM test"))
with subtest("restore"): with subtest("restore"):

View file

@ -1,9 +1,48 @@
{ pkgs, lib }: { 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; }; databasebackup = import ./databasebackup.nix { inherit lib; };
backup = import ./backup.nix { inherit lib; }; backup = import ./backup.nix { inherit lib; };
mount = import ./mount.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; }; ssl = import ./ssl.nix { inherit lib; };
test = { test = {
secret = import ./secret/test.nix { inherit pkgs lib; }; secret = import ./secret/test.nix { inherit pkgs lib; };

View file

@ -1,114 +1,115 @@
{ lib, ... }: { pkgs, lib, ... }:
{ let
mkOption = inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
{ description, inherit (lib.types) anything listOf submodule str;
mode ? "0400",
contractsLib = import ./default.nix { inherit pkgs lib; };
mkRequest =
{ mode ? "0400",
owner ? "root", owner ? "root",
ownerText ? null, ownerText ? null,
group ? "root", group ? "root",
restartUnits ? [], restartUnits ? [],
restartUnitsText ? null, restartUnitsText ? null,
}: lib.mkOption { }: mkOption {
inherit description; 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 = { options = {
request = lib.mkOption { mode = 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;
description = '' description = ''
Options set by the requester module Mode of the secret file.
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.
''; '';
type = lib.types.submodule { type = str;
freeformType = lib.types.anything; default = mode;
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;
};
};
};
}; };
result = lib.mkOption { owner = mkOption ({
description = '' 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 { type = str;
options = { default = owner;
path = lib.mkOption { } // optionalAttrs (ownerText != null) {
type = lib.types.path; defaultText = literalMD ownerText;
description = '' });
Path to the file containing the secret generated out of band.
This path will exist after deploying to a target host, group = mkOption {
it is not available through the nix store. 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;
} }

View file

@ -1,4 +1,4 @@
# Secret Contract {#secret-contract} # Secret Contract {#contract-secret}
This NixOS contract represents a secret file This NixOS contract represents a secret file
that must be created out of band - from outside the nix store - that must be created out of band - from outside the nix store -
@ -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 - 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. and a provider module - the one creating the secret and making it available.
## Problem Statement {#secret-contract-problem} ## Motivation {#contract-secret-motivation}
Let's provide the [ldap SHB module][ldap-module] option `ldapUserPasswordFile` Let's provide the [ldap SHB module][ldap-module] option `ldapUserPasswordFile`
with a secret managed by [sops-nix][]. with a secret managed by [sops-nix][].
@ -19,14 +19,14 @@ Without the secret contract, configuring the option would look like so:
```nix ```nix
sops.secrets."ldap/user_password" = { sops.secrets."ldap/user_password" = {
sopsFile = ./secrets.yaml;
mode = "0440"; mode = "0440";
owner = "lldap"; owner = "lldap";
group = "lldap"; group = "lldap";
restartUnits = [ "lldap.service" ]; 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 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. at the module source code.
Not a great user experience. 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 ```nix
sops.secrets."ldap/user_password" = config.shb.ldap.secret.ldapUserPassword.request // { shb.sops.secrets."ldap/user_password" = {
sopsFile = ./secrets.yaml; 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 issue is now gone as the responsibility falls
The module maintainer is now in charge of describing on the module maintainer
how the module expects the secret to be provided. for describing how the secret should be provided.
If taking advantage of the `sops.defaultSopsFile` option like so: If taking advantage of the `sops.defaultSopsFile` option like so:
@ -61,12 +63,12 @@ sops.defaultSopsFile = ./secrets.yaml;
Then the snippet above is even more simplified: Then the snippet above is even more simplified:
```nix ```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} ## Contract Reference {#contract-secret-options}
These are all the options that are expected to exist for this contract to be respected. These are all the options that are expected to exist for this contract to be respected.
@ -76,7 +78,7 @@ list-id: selfhostblocks-options
source: @OPTIONS_JSON@ source: @OPTIONS_JSON@
``` ```
## Usage {#secret-contract-usage} ## Usage {#contract-secret-usage}
A contract involves 3 parties: A contract involves 3 parties:
@ -86,24 +88,28 @@ A contract involves 3 parties:
The usage of this contract is similarly separated into 3 sections. The usage of this contract is similarly separated into 3 sections.
### Requester Module {#secret-contract-usage-requester} ### Requester Module {#contract-secret-usage-requester}
Here is an example module requesting two secrets through the `secret` contract. Here is an example module requesting two secrets through the `secret` contract.
```nix ```nix
{ config, ... }: { config, ... }:
let
inherit (lib) mkOption;
inherit (lib.types) submodule;
in
{ {
options = { options = {
myservice = lib.mkOption { myservice = mkOption {
type = lib.types.submodule { type = submodule {
options = { options = {
adminPassword = contracts.secret.mkOption { adminPassword = contracts.secret.mkRequester {
owner = "myservice"; owner = "myservice";
group = "myservice"; group = "myservice";
mode = "0440"; mode = "0440";
restartUnits = [ "myservice.service" ]; restartUnits = [ "myservice.service" ];
}; };
databasePassword = contracts.secret.mkOption { databasePassword = contracts.secret.mkRequester {
owner = "myservice"; owner = "myservice";
# group defaults to "root" # group defaults to "root"
# mode defaults to "0400" # mode defaults to "0400"
@ -122,7 +128,7 @@ Here is an example module requesting two secrets through the `secret` contract.
}; };
``` ```
### Provider Module {#secret-contract-usage-provider} ### Provider Module {#contract-secret-usage-provider}
Now, on the other side, we have a module that uses those options and provides a secret. Now, on the other side, we have a module that uses those options and provides a secret.
Let's assume such a module is available under the `secretservice` option Let's assume such a module is available under the `secretservice` option
@ -130,63 +136,67 @@ and that one can create multiple instances.
```nix ```nix
{ config, ... }: { config, ... }:
let
inherit (lib) mkOption;
inherit (lib.types) attrsOf submodule;
contracts = pkgs.callPackage ./contracts {};
in
{ {
options = { options.secretservice.secret = mkOption {
secretservice = lib.mkOption { description = "Secret following the secret contract.";
type = lib.types.attrsOf (lib.types.submodule { default = {};
options = { type = attrsOf (submodule ({ name, options, ... }: {
mode = lib.mkOption { options = contracts.secret.mkProvider {
description = "Mode of the secret file."; settings = mkOption {
type = lib.types.str; description = ''
}; Settings specific to the secrets provider.
'';
owner = lib.mkOption { type = submodule {
description = "Linux user owning the secret file."; options = {
type = lib.types.str; secretFile = lib.mkOption {
}; description = "File containing the encrypted secret.";
type = lib.types.path;
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;
}; };
}; };
});
}; resultCfg = {
path = "/run/secrets/${name}";
pathText = "/run/secrets/<name>";
};
};
}));
};
config = {
// ...
}; };
} }
``` ```
### End User {#secret-contract-usage-enduser} ### End User {#contract-secret-usage-enduser}
The end user's responsibility is now to do some plumbing. 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, 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. 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 ```nix
secretservice.adminPassword = myservice.secret.adminPassword.request // { secretservice.secret."adminPassword" = {
secretFile = ./secret.yaml; request = myservice.adminPasswor".request;
settings.secretFile = ./secret.yaml;
}; };
myservice.adminPassword.result = secretservice.secret."adminPassword".result;
secretservice.databasePassword = myservice.secret.databasePassword.request // { secretservice.secret."databasePassword" = {
secretFile = ./secret.yaml; request = myservice.databasePassword.request;
settings.secretFile = ./secret.yaml;
}; };
myservice.databasePassword.result = secretservice.service."databasePassword".result;
``` ```
Assuming the `secretservice` module accepts default options, Assuming the `secretservice` module accepts default options,
@ -195,14 +205,13 @@ the above snippet could be reduced to:
```nix ```nix
secretservice.default.secretFile = ./secret.yaml; secretservice.default.secretFile = ./secret.yaml;
secretservice.adminPassword = myservice.secret.adminPassword.request; secretservice.secret."adminPassword".request = myservice.adminPasswor".request;
secretservice.databasePassword = myservice.secret.databasePassword.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. The plumbing of request from the requester to the provider
and then the result from the provider back to the requester
```nix is quite explicit in this snippet.
myservice.secret.adminPassword.result.path = secretservice.adminPassword.result.path;
myservice.secret.databasePassword.result.path = secretservice.adminPassword.result.path;
```

View file

@ -1,21 +1,27 @@
{ pkgs, lib, ... }: { pkgs, lib, ... }:
let let
contracts = pkgs.callPackage ../. {}; contracts = pkgs.callPackage ../. {};
inherit (lib) mkOption;
inherit (lib.types) submodule;
in in
{ {
options.shb.contracts.secret = contracts.secret.mkOption { options.shb.contracts.secret = mkOption {
description = '' description = ''
Contract for secrets between a requester module Contract for secrets between a requester module
and a provider module. and a provider module.
The requester communicates to the provider The requester communicates to the provider
some properties the secret should have 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. and creates the secret as requested.
It then communicates to the requester where the secret can be found 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;
};
}; };
} }

View file

@ -9,7 +9,7 @@ let
in in
{ name, { name,
configRoot, configRoot,
createContent, # config to create a secret with value "secretA". settingsCfg, # str -> attrset
modules ? [], modules ? [],
owner ? "root", owner ? "root",
group ? "root", group ? "root",
@ -23,8 +23,11 @@ in
config = lib.mkMerge [ config = lib.mkMerge [
(setAttrByPath configRoot { (setAttrByPath configRoot {
A = { A = {
inherit owner group mode restartUnits; request = {
} // createContent; inherit owner group mode restartUnits;
};
settings = settingsCfg "secretA";
};
}) })
(mkIf (owner != "root") { (mkIf (owner != "root") {
users.users.${owner}.isNormalUser = true; users.users.${owner}.isNormalUser = true;
@ -37,26 +40,26 @@ in
testScript = { nodes, ... }: testScript = { nodes, ... }:
let let
cfg = (getAttrFromPath configRoot nodes.machine)."A"; result = (getAttrFromPath configRoot nodes.machine)."A".result;
in in
'' ''
owner = machine.succeed("stat -c '%U' ${cfg.path}").strip() owner = machine.succeed("stat -c '%U' ${result.path}").strip()
print(f"Got owner {owner}") print(f"Got owner {owner}")
if owner != "${owner}": if owner != "${owner}":
raise Exception(f"Owner should be '${owner}' but got '{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}") print(f"Got group {group}")
if group != "${group}": if group != "${group}":
raise Exception(f"Group should be '${group}' but got '{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}") print(f"Got mode {mode}")
wantedMode = str(int("${mode}")) wantedMode = str(int("${mode}"))
if mode != wantedMode: if mode != wantedMode:
raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'") 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}") print(f"Got content {content}")
if content != "secretA": if content != "secretA":
raise Exception(f"Content should be 'secretA' but got '{content}'") raise Exception(f"Content should be 'secretA' but got '{content}'")

View file

@ -1,10 +1,10 @@
# SSL Generator Contract {#ssl-contract} # SSL Generator Contract {#contract-ssl}
This NixOS contract represents an SSL certificate generator. This contract is used to decouple This NixOS contract represents an SSL certificate generator. This contract is used to decouple
generating an SSL certificate from using it. In practice, you can swap generators without updating generating an SSL certificate from using it. In practice, you can swap generators without updating
modules depending on it. modules depending on it.
## Contract Reference {#ssl-contract-options} ## Contract Reference {#contract-ssl-options}
These are all the options that are expected to exist for this contract to be respected. These are all the options that are expected to exist for this contract to be respected.
@ -14,7 +14,7 @@ list-id: selfhostblocks-options
source: @OPTIONS_JSON@ source: @OPTIONS_JSON@
``` ```
## Usage {#ssl-contract-usage} ## Usage {#contract-ssl-usage}
Let's assume a module implementing this contract is available under the `ssl` variable: Let's assume a module implementing this contract is available under the `ssl` variable:
@ -56,11 +56,11 @@ systemd.services.nginx = {
}; };
``` ```
## Provided Implementations {#ssl-contract-impl-shb} ## Provided Implementations {#contract-ssl-impl-shb}
Multiple implementation are provided out of the box at [SSL block](blocks-ssl.html). Multiple implementation are provided out of the box at [SSL block](blocks-ssl.html).
## Custom Implementation {#ssl-contract-impl-custom} ## Custom Implementation {#contract-ssl-impl-custom}
To implement this contract, you must create a module that respects this contract. The following To implement this contract, you must create a module that respects this contract. The following
snippet shows an example. snippet shows an example.

View file

@ -4,13 +4,16 @@ let
cfg = config.shb.forgejo; cfg = config.shb.forgejo;
contracts = pkgs.callPackage ../contracts {}; 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 in
{ {
options.shb.forgejo = { options.shb.forgejo = {
enable = lib.mkEnableOption "selfhostblocks.forgejo"; enable = mkEnableOption "selfhostblocks.forgejo";
subdomain = lib.mkOption { subdomain = mkOption {
type = lib.types.str; type = str;
description = '' description = ''
Subdomain under which Forgejo will be served. Subdomain under which Forgejo will be served.
@ -21,7 +24,7 @@ in
example = "forgejo"; example = "forgejo";
}; };
domain = lib.mkOption { domain = mkOption {
description = '' description = ''
Domain under which Forgejo is served. Domain under which Forgejo is served.
@ -29,75 +32,79 @@ in
<subdomain>.<domain>[:<port>] <subdomain>.<domain>[:<port>]
``` ```
''; '';
type = lib.types.str; type = str;
example = "domain.com"; example = "domain.com";
}; };
ssl = lib.mkOption { ssl = mkOption {
description = "Path to SSL files"; description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs; type = nullOr contracts.ssl.certs;
default = null; default = null;
}; };
ldap = lib.mkOption { ldap = mkOption {
description = '' description = ''
LDAP Integration. LDAP Integration.
''; '';
default = {}; default = {};
type = lib.types.nullOr (lib.types.submodule { type = nullOr (submodule {
options = { options = {
enable = lib.mkEnableOption "LDAP integration."; enable = mkEnableOption "LDAP integration.";
provider = lib.mkOption { provider = mkOption {
type = lib.types.enum [ "LLDAP" ]; type = enum [ "LLDAP" ];
description = "LDAP provider name, used for display."; description = "LDAP provider name, used for display.";
default = "LLDAP"; default = "LLDAP";
}; };
host = lib.mkOption { host = mkOption {
type = lib.types.str; type = str;
description = '' description = ''
Host serving the LDAP server. Host serving the LDAP server.
''; '';
default = "127.0.0.1"; default = "127.0.0.1";
}; };
port = lib.mkOption { port = mkOption {
type = lib.types.port; type = port;
description = '' description = ''
Port of the service serving the LDAP server. Port of the service serving the LDAP server.
''; '';
default = 389; default = 389;
}; };
dcdomain = lib.mkOption { dcdomain = mkOption {
type = lib.types.str; type = str;
description = "dc domain for ldap."; description = "dc domain for ldap.";
example = "dc=mydomain,dc=com"; example = "dc=mydomain,dc=com";
}; };
adminName = lib.mkOption { adminName = mkOption {
type = lib.types.str; type = str;
description = "Admin user of the LDAP server."; description = "Admin user of the LDAP server.";
default = "admin"; default = "admin";
}; };
adminPassword = contracts.secret.mkOption { adminPassword = mkOption {
description = "LDAP admin password."; description = "LDAP admin password.";
mode = "0440"; type = submodule {
owner = "forgejo"; options = contracts.secret.mkRequester {
group = "forgejo"; mode = "0440";
restartUnits = [ "forgejo.service" ]; owner = "forgejo";
group = "forgejo";
restartUnits = [ "forgejo.service" ];
};
};
}; };
userGroup = lib.mkOption { userGroup = mkOption {
type = lib.types.str; type = str;
description = "Group users must belong to be able to login."; description = "Group users must belong to be able to login.";
default = "forgejo_user"; default = "forgejo_user";
}; };
adminGroup = lib.mkOption { adminGroup = mkOption {
type = lib.types.str; type = str;
description = "Group users must belong to be admins."; description = "Group users must belong to be admins.";
default = "forgejo_admin"; default = "forgejo_admin";
}; };
@ -105,81 +112,97 @@ in
}); });
}; };
sso = lib.mkOption { sso = mkOption {
description = '' description = ''
Setup SSO integration. Setup SSO integration.
''; '';
default = {}; default = {};
type = lib.types.submodule { type = submodule {
options = { options = {
enable = lib.mkEnableOption "SSO integration."; enable = mkEnableOption "SSO integration.";
provider = lib.mkOption { provider = mkOption {
type = lib.types.enum [ "Authelia" ]; type = enum [ "Authelia" ];
description = "OIDC provider name, used for display."; description = "OIDC provider name, used for display.";
default = "Authelia"; default = "Authelia";
}; };
endpoint = lib.mkOption { endpoint = mkOption {
type = lib.types.str; type = str;
description = "OIDC endpoint for SSO."; description = "OIDC endpoint for SSO.";
example = "https://authelia.example.com"; example = "https://authelia.example.com";
}; };
clientID = lib.mkOption { clientID = mkOption {
type = lib.types.str; type = str;
description = "Client ID for the OIDC endpoint."; description = "Client ID for the OIDC endpoint.";
default = "forgejo"; default = "forgejo";
}; };
authorization_policy = lib.mkOption { authorization_policy = mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ]; type = enum [ "one_factor" "two_factor" ];
description = "Require one factor (password) or two factor (device) authentication."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
sharedSecret = contracts.secret.mkOption { sharedSecret = mkOption {
description = "OIDC shared secret for Forgejo."; description = "OIDC shared secret for Forgejo.";
mode = "0440"; type = submodule {
owner = "forgejo"; options = contracts.secret.mkRequester {
group = "forgejo"; mode = "0440";
restartUnits = [ "forgejo.service" ]; owner = "forgejo";
group = "forgejo";
restartUnits = [ "forgejo.service" ];
};
};
}; };
sharedSecretForAuthelia = contracts.secret.mkOption { sharedSecretForAuthelia = mkOption {
description = "OIDC shared secret for Authelia."; description = "OIDC shared secret for Authelia.";
mode = "0400"; type = submodule {
owner = "authelia"; options = contracts.secret.mkRequester {
mode = "0400";
owner = "authelia";
};
};
}; };
}; };
}; };
}; };
adminPassword = contracts.secret.mkOption { adminPassword = mkOption {
description = "File containing the Forgejo admin user password."; description = "File containing the Forgejo admin user password.";
mode = "0440"; type = submodule {
owner = "forgejo"; options = contracts.secret.mkRequester {
group = "forgejo"; mode = "0440";
restartUnits = [ "forgejo.service" ]; owner = "forgejo";
group = "forgejo";
restartUnits = [ "forgejo.service" ];
};
};
}; };
databasePassword = contracts.secret.mkOption { databasePassword = mkOption {
description = "File containing the Forgejo database password."; description = "File containing the Forgejo database password.";
mode = "0440"; type = submodule {
owner = "forgejo"; options = contracts.secret.mkRequester {
group = "forgejo"; mode = "0440";
restartUnits = [ "forgejo.service" ]; owner = "forgejo";
group = "forgejo";
restartUnits = [ "forgejo.service" ];
};
};
}; };
repositoryRoot = lib.mkOption { repositoryRoot = mkOption {
type = lib.types.nullOr lib.types.str; type = nullOr str;
description = "Path where to store the repositories. If null, uses the default under the Forgejo StateDir."; description = "Path where to store the repositories. If null, uses the default under the Forgejo StateDir.";
default = null; default = null;
example = "/srv/forgejo"; example = "/srv/forgejo";
}; };
localActionRunner = lib.mkOption { localActionRunner = mkOption {
type = lib.types.bool; type = bool;
default = true; default = true;
description = '' description = ''
Enable local action runner that runs for all labels. Enable local action runner that runs for all labels.
@ -187,8 +210,8 @@ in
}; };
hostPackages = lib.mkOption { hostPackages = mkOption {
type = lib.types.listOf lib.types.package; type = listOf package;
default = with pkgs; [ default = with pkgs; [
bash bash
coreutils coreutils
@ -199,7 +222,7 @@ in
nodejs nodejs
wget wget
]; ];
defaultText = lib.literalExpression '' defaultText = literalExpression ''
with pkgs; [ with pkgs; [
bash bash
coreutils coreutils
@ -217,7 +240,7 @@ in
''; '';
}; };
backup = lib.mkOption { backup = mkOption {
type = contracts.backup.request; type = contracts.backup.request;
description = '' description = ''
Backup configuration. This is an output option. Backup configuration. This is an output option.
@ -239,13 +262,13 @@ in
user = options.services.forgejo.user.value; user = options.services.forgejo.user.value;
sourceDirectories = [ sourceDirectories = [
options.services.forgejo.dump.backupDir.value options.services.forgejo.dump.backupDir.value
] ++ lib.optionals (cfg.repositoryRoot != null) [ ] ++ optionals (cfg.repositoryRoot != null) [
cfg.repositoryRoot cfg.repositoryRoot
]; ];
}; };
}; };
mount = lib.mkOption { mount = mkOption {
type = contracts.mount; type = contracts.mount;
description = '' description = ''
Mount configuration. This is an output option. Mount configuration. This is an output option.
@ -263,51 +286,51 @@ in
default = { path = config.services.forgejo.stateDir; }; default = { path = config.services.forgejo.stateDir; };
}; };
smtp = lib.mkOption { smtp = mkOption {
description = '' description = ''
Send notifications by smtp. Send notifications by smtp.
''; '';
default = null; default = null;
type = lib.types.nullOr (lib.types.submodule { type = nullOr (submodule {
options = { options = {
from_address = lib.mkOption { from_address = mkOption {
type = lib.types.str; type = str;
description = "SMTP address from which the emails originate."; description = "SMTP address from which the emails originate.";
example = "authelia@mydomain.com"; example = "authelia@mydomain.com";
}; };
host = lib.mkOption { host = mkOption {
type = lib.types.str; type = str;
description = "SMTP host to send the emails to."; description = "SMTP host to send the emails to.";
}; };
port = lib.mkOption { port = mkOption {
type = lib.types.port; type = port;
description = "SMTP port to send the emails to."; description = "SMTP port to send the emails to.";
default = 25; default = 25;
}; };
username = lib.mkOption { username = mkOption {
type = lib.types.str; type = str;
description = "Username to connect to the SMTP host."; description = "Username to connect to the SMTP host.";
}; };
passwordFile = lib.mkOption { passwordFile = mkOption {
type = lib.types.str; type = str;
description = "File containing the password to connect to the SMTP host."; description = "File containing the password to connect to the SMTP host.";
}; };
}; };
}); });
}; };
debug = lib.mkOption { debug = mkOption {
description = "Enable debug logging."; description = "Enable debug logging.";
type = lib.types.bool; type = bool;
default = false; default = false;
}; };
}; };
config = lib.mkMerge [ config = mkMerge [
(lib.mkIf cfg.enable { (mkIf cfg.enable {
services.forgejo = { services.forgejo = {
enable = true; enable = true;
repositoryRoot = lib.mkIf (cfg.repositoryRoot != null) cfg.repositoryRoot; repositoryRoot = mkIf (cfg.repositoryRoot != null) cfg.repositoryRoot;
settings = { settings = {
server = { server = {
DOMAIN = cfg.domain; DOMAIN = cfg.domain;
@ -328,10 +351,10 @@ in
}; };
# 1 lower than default, to solve conflict between shb.postgresql and nixpkgs' forgejo module. # 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 # 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 = [{ shb.nginx.vhosts = [{
inherit (cfg) domain subdomain ssl; inherit (cfg) domain subdomain ssl;
@ -339,7 +362,7 @@ in
}]; }];
}) })
(lib.mkIf cfg.enable { (mkIf cfg.enable {
services.forgejo.database = { services.forgejo.database = {
type = "postgres"; type = "postgres";
@ -347,7 +370,7 @@ in
}; };
}) })
(lib.mkIf cfg.enable { (mkIf cfg.enable {
services.forgejo.dump = { services.forgejo.dump = {
enable = true; enable = true;
type = "tar.gz"; type = "tar.gz";
@ -358,12 +381,12 @@ in
# For Forgejo setup: https://github.com/lldap/lldap/blob/main/example_configs/gitea.md # For Forgejo setup: https://github.com/lldap/lldap/blob/main/example_configs/gitea.md
# For cli info: https://docs.gitea.com/usage/command-line # 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 # 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! # The delimiter in the `cut` command is a TAB!
systemd.services.forgejo.preStart = let systemd.services.forgejo.preStart = let
provider = "SHB-${cfg.ldap.provider}"; provider = "SHB-${cfg.ldap.provider}";
in '' 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}"... echo "Trying to find existing ldap configuration for ${provider}"...
set +e -o pipefail set +e -o pipefail
@ -417,7 +440,7 @@ in
# For Authelia to Forgejo integration: https://www.authelia.com/integration/openid-connect/gitea/ # 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 Forgejo config: https://forgejo.org/docs/latest/admin/config-cheat-sheet
# For cli info: https://docs.gitea.com/usage/command-line # 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 = { services.forgejo.settings = {
oauth2 = { oauth2 = {
ENABLED = true; ENABLED = true;
@ -430,7 +453,7 @@ in
}; };
service = { service = {
# DISABLE_REGISTRATION = lib.mkForce false; # DISABLE_REGISTRATION = mkForce false;
# ALLOW_ONLY_EXTERNAL_REGISTRATION = false; # ALLOW_ONLY_EXTERNAL_REGISTRATION = false;
SHOW_REGISTRATION_BUTTON = false; SHOW_REGISTRATION_BUTTON = false;
}; };
@ -440,7 +463,7 @@ in
systemd.services.forgejo.preStart = let systemd.services.forgejo.preStart = let
provider = "SHB-${cfg.sso.provider}"; provider = "SHB-${cfg.sso.provider}";
in '' 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}"... echo "Trying to find existing sso configuration for ${provider}"...
set +e -o pipefail set +e -o pipefail
@ -468,7 +491,7 @@ in
fi fi
''; '';
shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [ shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [
(let (let
provider = "SHB-${cfg.sso.provider}"; provider = "SHB-${cfg.sso.provider}";
in { in {
@ -482,15 +505,15 @@ in
]; ];
}) })
(lib.mkIf cfg.enable { (mkIf cfg.enable {
systemd.services.forgejo.preStart = '' 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 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 $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 = { services.forgejo.settings.mailer = {
ENABLED = true; ENABLED = true;
SMTP_ADDR = "${cfg.smtp.host}:${toString cfg.smtp.port}"; SMTP_ADDR = "${cfg.smtp.host}:${toString cfg.smtp.port}";
@ -502,13 +525,13 @@ in
}) })
# https://wiki.nixos.org/wiki/Forgejo#Runner # https://wiki.nixos.org/wiki/Forgejo#Runner
(lib.mkIf cfg.enable { (mkIf cfg.enable {
services.forgejo.settings.actions = { services.forgejo.settings.actions = {
ENABLED = true; ENABLED = true;
DEFAULT_ACTIONS_URL = "github"; DEFAULT_ACTIONS_URL = "github";
}; };
services.gitea-actions-runner = lib.mkIf cfg.localActionRunner { services.gitea-actions-runner = mkIf cfg.localActionRunner {
package = pkgs.forgejo-actions-runner; package = pkgs.forgejo-actions-runner;
instances.local = { instances.local = {
enable = true; enable = true;
@ -530,9 +553,9 @@ in
# This combined with the next statement takes care of # This combined with the next statement takes care of
# automatically registering a forgejo runner. # 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' ${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 echo -n TOKEN= > /run/forgejo/forgejo-runner-token
$actions generate-runner-token >> /run/forgejo/forgejo-runner-token $actions generate-runner-token >> /run/forgejo/forgejo-runner-token
''); '');

View file

@ -15,7 +15,7 @@ LDAP and SSO integration as well as one local runner.
- Declarative [local runner](#services-forgejo-options-shb.forgejo.localActionRunner) Configuration. - 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 [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). - 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} ## Usage {#services-forgejo-usage}
@ -44,7 +44,7 @@ Then you can use that secret:
shb.forgejo.adminPasswordFile = config.sops.secrets."forgejo/adminPasswordFile".path; 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, This will set up a Forgejo service that runs on the NixOS target machine,
reachable at `http://forgejo.example.com`. 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), 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`. 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 ```nix
shb.certs = { shb.certs.certs.letsencrypt."example.com" = {
cas.selfsigned.myca = { domain = "example.com";
name = "My CA"; group = "nginx";
}; reloadServices = [ "nginx.service" ];
certs.selfsigned = { adminEmail = "myemail@mydomain.com";
foregejo = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "forgejo.example.com";
};
};
}; };
``` ```
Then you can tell Forgejo to use those certificates. Then you can tell Forgejo to use those certificates.
```nix ```nix
shb.certs.certs.letsencrypt."example.com".extraDomains = [ "forgejo.example.com" ];
shb.forgejo = { shb.forgejo = {
ssl = config.shb.certs.certs.selfsigned.forgejo; ssl = config.shb.certs.certs.selfsigned.forgejo;
}; };
@ -87,7 +84,7 @@ shb.forgejo = {
### With LDAP Support {#services-forgejo-usage-ldap} ### With LDAP Support {#services-forgejo-usage-ldap}
:::: {.note} :::: {.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. so please follow that first.
:::: ::::
@ -99,12 +96,18 @@ shb.ldap = {
enable = true; enable = true;
domain = "example.com"; domain = "example.com";
subdomain = "ldap"; subdomain = "ldap";
ssl = config.shb.certs.certs.letsencrypt."example.com";
ldapPort = 3890; ldapPort = 3890;
webUIListenPort = 17170; webUIListenPort = 17170;
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
ldapUserPasswordFile = <path/to/ldapUserPasswordSecret>; ldapUserPassword.result = config.shb.sops.secrets."ldap/userPassword".result;
jwtSecretFile = <path/to/ldapJwtSecret>; 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 We also need to configure the `forgejo` service
@ -116,7 +119,12 @@ shb.forgejo.ldap
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminPasswordFile = <path/to/ldapUserPasswordSecret>; 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} ### With SSO Support {#services-forgejo-usage-sso}
:::: {.note} :::: {.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. 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, We then need to setup the SSO provider,
here Authelia thanks to the corresponding SHB block: here Authelia thanks to the corresponding SHB block:
@ -166,21 +155,31 @@ shb.authelia = {
enable = true; enable = true;
domain = "example.com"; domain = "example.com";
subdomain = "auth"; subdomain = "auth";
ssl = config.shb.certs.certs.selfsigned.auth; ssl = config.shb.certs.certs.letsencrypt."example.com";
ldapHostname = "127.0.0.1"; ldapHostname = "127.0.0.1";
ldapPort = config.shb.ldap.ldapPort; ldapPort = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
secrets = { secrets = {
jwtSecretFile = <path/to/autheliaJwtSecret>; jwtSecret.result = config.shb.sops.secrets."authelia/jwt_secret".result;
ldapAdminPasswordFile = <path/to/ldapUserPasswordSecret>; ldapAdminPassword.result = config.shb.sops.secrets."authelia/ldap_admin_password".result;
sessionSecretFile = <path/to/autheliaSessionSecret>; sessionSecret.result = config.shb.sops.secrets."authelia/session_secret".result;
storageEncryptionKeyFile = <path/to/autheliaStorageEncryptionKeySecret>; storageEncryptionKey.result = config.shb.sops.secrets."authelia/storage_encryption_key".result;
identityProvidersOIDCHMACSecretFile = <path/to/providersOIDCHMACSecret>; identityProvidersOIDCHMACSecret.result = config.shb.sops.secrets."authelia/hmac_secret".result;
identityProvidersOIDCIssuerPrivateKeyFile = <path/to/providersOIDCIssuerSecret>; 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 The `shb.authelia.secrets.ldapAdminPasswordFile` must be the same

View file

@ -67,12 +67,16 @@ in
default = "jellyfin_admin"; default = "jellyfin_admin";
}; };
adminPassword = contracts.secret.mkOption { adminPassword = lib.mkOption {
description = "LDAP admin password."; description = "LDAP admin password.";
mode = "0440"; type = lib.types.submodule {
owner = "jellyfin"; options = contracts.secret.mkRequester {
group = "jellyfin"; mode = "0440";
restartUnits = [ "jellyfin.service" ]; owner = "jellyfin";
group = "jellyfin";
restartUnits = [ "jellyfin.service" ];
};
};
}; };
}; };
}; };
@ -121,18 +125,26 @@ in
default = "one_factor"; default = "one_factor";
}; };
sharedSecret = contracts.secret.mkOption { sharedSecret = lib.mkOption {
description = "OIDC shared secret for Jellyfin."; description = "OIDC shared secret for Jellyfin.";
mode = "0440"; type = lib.types.submodule {
owner = "jellyfin"; options = contracts.secret.mkRequester {
group = "jellyfin"; mode = "0440";
restartUnits = [ "jellyfin.service" ]; owner = "jellyfin";
group = "jellyfin";
restartUnits = [ "jellyfin.service" ];
};
};
}; };
sharedSecretForAuthelia = contracts.secret.mkOption { sharedSecretForAuthelia = lib.mkOption {
description = "OIDC shared secret for Authelia."; description = "OIDC shared secret for Authelia.";
mode = "0400"; type = lib.types.submodule {
owner = config.shb.authelia.autheliaUser; options = contracts.secret.mkRequester {
mode = "0400";
owner = config.shb.authelia.autheliaUser;
};
};
}; };
}; };
}; };

View file

@ -95,13 +95,18 @@ in
default = "root"; default = "root";
}; };
adminPass = contracts.secret.mkOption { adminPass = lib.mkOption {
description = "Nextcloud admin password."; description = "Nextcloud admin password.";
mode = "0400"; type = lib.types.submodule {
owner = "nextcloud"; options = contracts.secret.mkRequester {
restartUnits = [ "phpfpm-nextcloud.service" ]; mode = "0400";
owner = "nextcloud";
restartUnits = [ "phpfpm-nextcloud.service" ];
};
};
}; };
maxUploadSize = lib.mkOption { maxUploadSize = lib.mkOption {
default = "4G"; default = "4G";
type = lib.types.str; type = lib.types.str;
@ -374,13 +379,18 @@ in
default = "admin"; default = "admin";
}; };
adminPassword = contracts.secret.mkOption { adminPassword = lib.mkOption {
description = "LDAP server admin password."; description = "LDAP server admin password.";
mode = "0400"; type = lib.types.submodule {
owner = "nextcloud"; options = contracts.secret.mkRequester {
restartUnits = [ "phpfpm-nextcloud.service" ]; mode = "0400";
owner = "nextcloud";
restartUnits = [ "phpfpm-nextcloud.service" ];
};
};
}; };
userGroup = lib.mkOption { userGroup = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Group users must belong to to be able to login to Nextcloud."; description = "Group users must belong to to be able to login to Nextcloud.";
@ -441,19 +451,29 @@ in
default = "one_factor"; default = "one_factor";
}; };
secret = contracts.secret.mkOption { secret = lib.mkOption {
description = "OIDC shared secret."; description = "OIDC shared secret.";
mode = "0400"; type = lib.types.submodule {
owner = "nextcloud"; options = contracts.secret.mkRequester {
restartUnits = [ "phpfpm-nextcloud.service" ]; 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."; description = "OIDC shared secret. Content must be the same as `secretFile` option.";
mode = "0400"; type = lib.types.submodule {
owner = "authelia"; options = contracts.secret.mkRequester {
mode = "0400";
owner = "authelia";
};
};
}; };
fallbackDefaultAuth = lib.mkOption { fallbackDefaultAuth = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
description = '' description = ''

View file

@ -1,27 +1,27 @@
# Nextcloud Server Service {#services-nextcloud-server} # Nextcloud Server Service {#services-nextcloudserver}
Defined in [`/modules/services/nextcloud-server.nix`](@REPO@/modules/services/nextcloud-server.nix). Defined in [`/modules/services/nextcloud-server.nix`](@REPO@/modules/services/nextcloud-server.nix).
This NixOS module is a service that sets up a [Nextcloud Server](https://nextcloud.com/). This NixOS module is a service that sets up a [Nextcloud Server](https://nextcloud.com/).
It is based on the nixpkgs Nextcloud server and provides opinionated defaults. It is based on the nixpkgs Nextcloud server and provides opinionated defaults.
## Features {#services-nextcloud-server-features} ## Features {#services-nextcloudserver-features}
- Declarative [Apps](#services-nextcloud-server-options-shb.nextcloud.apps) Configuration - no need - Declarative [Apps](#services-nextcloudserver-options-shb.nextcloud.apps) Configuration - no need
to configure those with the UI. to configure those with the UI.
- [LDAP](#services-nextcloud-server-usage-ldap) app: - [LDAP](#services-nextcloudserver-usage-ldap) app:
enables app and sets up integration with an existing LDAP server, in this case LLDAP. enables app and sets up integration with an existing LDAP server, in this case LLDAP.
- [OIDC](#services-nextcloud-server-usage-oidc) app: - [SSO](#services-nextcloudserver-usage-oidc) app:
enables app and sets up integration with an existing OIDC server, in this case Authelia. enables app and sets up integration with an existing SSO server, in this case Authelia.
- [Preview Generator](#services-nextcloud-server-usage-previewgenerator) app: - [Preview Generator](#services-nextcloudserver-usage-previewgenerator) app:
enables app and sets up required cron job. enables app and sets up required cron job.
- [External Storage](#services-nextcloud-server-usage-externalstorage) app: - [External Storage](#services-nextcloudserver-usage-externalstorage) app:
enables app and optionally configures one local mount. enables app and optionally configures one local mount.
This enables having data living on separate hard drives. This enables having data living on separate hard drives.
- [Only Office](#services-nextcloud-server-usage-onlyoffice) app: - [Only Office](#services-nextcloudserver-usage-onlyoffice) app:
enables app and sets up Only Office service. enables app and sets up Only Office service.
- Any other app through the - Any other app through the
[shb.nextcloud.extraApps](#services-nextcloud-server-options-shb.nextcloud.extraApps) option. [shb.nextcloud.extraApps](#services-nextcloudserver-options-shb.nextcloud.extraApps) option.
- Access through subdomain using reverse proxy. - Access through subdomain using reverse proxy.
- Forces Nginx as the reverse proxy. (This is hardcoded in the upstream nixpkgs module). - Forces Nginx as the reverse proxy. (This is hardcoded in the upstream nixpkgs module).
- Sets good defaults for trusted proxies settings, chunk size, opcache php options. - Sets good defaults for trusted proxies settings, chunk size, opcache php options.
@ -40,15 +40,15 @@ It is based on the nixpkgs Nextcloud server and provides opinionated defaults.
- By default automatically disables maintenance mode on start. - By default automatically disables maintenance mode on start.
- By default automatically launches repair mode with expensive migrations on start. - By default automatically launches repair mode with expensive migrations on start.
- Access to advanced options not exposed here thanks to how NixOS modules work. - Access to advanced options not exposed here thanks to how NixOS modules work.
- Has a [demo](#services-nextcloud-server-demo). - Has a [demo](#services-nextcloudserver-demo).
[dataDir]: ./services-nextcloud.html#services-nextcloud-server-options-shb.nextcloud.dataDir [dataDir]: ./services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dataDir
## Usage {#services-nextcloud-server-usage} ## Usage {#services-nextcloudserver-usage}
### Nextcloud through HTTP {#services-nextcloud-server-usage-basic} ### Nextcloud through HTTP {#services-nextcloudserver-usage-basic}
[HTTP]: #services-nextcloud-server-usage-basic [HTTP]: #services-nextcloudserver-usage-basic
:::: {.note} :::: {.note}
This section corresponds to the `basic` section of the [Nextcloud This section corresponds to the `basic` section of the [Nextcloud
@ -66,10 +66,10 @@ shb.nextcloud = {
domain = "example.com"; domain = "example.com";
subdomain = "n"; subdomain = "n";
defaultPhoneRegion = "US"; 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. This assumes secrets are setup with SOPS as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual.
@ -82,9 +82,9 @@ We will set that up in the next section.
You can now login as the admin user using the username `admin` You can now login as the admin user using the username `admin`
and the password defined in `sops.secrets."nextcloud/adminpass"`. and the password defined in `sops.secrets."nextcloud/adminpass"`.
### Nextcloud through HTTPS {#services-nextcloud-server-usage-https} ### Nextcloud through HTTPS {#services-nextcloudserver-usage-https}
[HTTPS]: #services-nextcloud-server-usage-https [HTTPS]: #services-nextcloudserver-usage-https
To setup HTTPS, we will get our certificates from Let's Encrypt using the HTTP method. To setup HTTPS, we will get our certificates from Let's Encrypt using the HTTP method.
This is the easiest way to get started and does not require you to programmatically This is the easiest way to get started and does not require you to programmatically
@ -94,7 +94,7 @@ Under the hood, we use the Self Host Block [SSL contract](./contracts-ssl.html).
It allows the end user to choose how to generate the certificates. It allows the end user to choose how to generate the certificates.
If you want other options to generate the certificate, follow the SSL contract link. If you want other options to generate the certificate, follow the SSL contract link.
Building upon the [Basic Configuration](#services-nextcloud-server-usage-basic) above, we add: Building upon the [Basic Configuration](#services-nextcloudserver-usage-basic) above, we add:
```nix ```nix
shb.certs.certs.letsencrypt."example.com" = { shb.certs.certs.letsencrypt."example.com" = {
@ -111,17 +111,17 @@ shb.nextcloud = {
}; };
``` ```
### Choose Nextcloud Version {#services-nextcloud-server-usage-version} ### Choose Nextcloud Version {#services-nextcloudserver-usage-version}
Self Host Blocks is conservative in the version of Nextcloud it's using. Self Host Blocks is conservative in the version of Nextcloud it's using.
To choose the version and upgrade at the time of your liking, To choose the version and upgrade at the time of your liking,
just use the [version](#services-nextcloud-server-options-shb.nextcloud.version) option: just use the [version](#services-nextcloudserver-options-shb.nextcloud.version) option:
```nix ```nix
shb.nextcloud.version = 29; shb.nextcloud.version = 29;
``` ```
### Mount Point {#services-nextcloud-server-usage-mount-point} ### Mount Point {#services-nextcloudserver-usage-mount-point}
If the `dataDir` exists in a mount point, If the `dataDir` exists in a mount point,
it is highly recommended to make the various Nextcloud services wait on the mount point before starting. it is highly recommended to make the various Nextcloud services wait on the mount point before starting.
@ -134,9 +134,9 @@ fileSystems."/var".device = "...";
shb.nextcloud.mountPointServices = [ "var.mount" ]; shb.nextcloud.mountPointServices = [ "var.mount" ];
``` ```
### With LDAP Support {#services-nextcloud-server-usage-ldap} ### With LDAP Support {#services-nextcloudserver-usage-ldap}
[LDAP]: #services-nextcloud-server-usage-ldap [LDAP]: #services-nextcloudserver-usage-ldap
:::: {.note} :::: {.note}
This section corresponds to the `ldap` section of the [Nextcloud This section corresponds to the `ldap` section of the [Nextcloud
@ -154,15 +154,18 @@ shb.ldap = {
enable = true; enable = true;
domain = "example.com"; domain = "example.com";
subdomain = "ldap"; subdomain = "ldap";
ssl = config.shb.certs.certs.letsencrypt."example.com";
ldapPort = 3890; ldapPort = 3890;
webUIListenPort = 17170; webUIListenPort = 17170;
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
ldapUserPassword.result.path = config.sops.secrets."ldap/userPassword".path; ldapUserPassword.result = config.shb.sops.secrets."ldap/userPassword".result;
jwtSecret.result.path = config.sops.secrets."ldap/jwtSecret".path; jwtSecret.result = config.shb.sops.secrets."ldap/jwtSecret".result;
}; };
sops.secrets."ldap/userPassword" = config.shb.ldap.userPassword.request; shb.certs.certs.letsencrypt."example.com".extraDomains = [ "ldap.example.com" ];
sops.secrets."ldap/jwtSecret" = config.shb.ldap.jwtSecret.request;
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 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; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminName = "admin"; adminName = "admin";
adminPassword.result.path = config.sops.secrets."nextcloud/ldapUserPassword".path adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/adminPassword".result
userGroup = "nextcloud_user"; userGroup = "nextcloud_user";
}; };
sops.secrets."nextcloud/ldapUserPassword" = config.shb.nextcloud.adminPasswordFile.request // { shb.sops.secrets."nextcloud/ldap/adminPassword" = {
key = "ldap/userPassword"; 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 it once so it is known to Nextcloud, then logout,
login with the admin Nextcloud user and promote that new user to admin level. 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-nextcloudserver-usage-oidc}
:::: {.note} :::: {.note}
This section corresponds to the `sso` section of the [Nextcloud This section corresponds to the `sso` section of the [Nextcloud
@ -230,26 +234,28 @@ shb.authelia = {
port = 587; port = 587;
username = "postmaster@mg.example.com"; username = "postmaster@mg.example.com";
from_address = "authelia@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 = { secrets = {
jwtSecret.result.path = config.sops.secrets."authelia/jwt_secret".path; jwtSecret.result = config.shb.sops.secrets."authelia/jwt_secret".result;
ldapAdminPassword.result.path = config.sops.secrets."authelia/ldap_admin_password".path; ldapAdminPassword.result = config.shb.sops.secrets."authelia/ldap_admin_password".result;
sessionSecret.result.path = config.sops.secrets."authelia/session_secret".path; sessionSecret.result = config.shb.sops.secrets."authelia/session_secret".result;
storageEncryptionKey.result.path = config.sops.secrets."authelia/storage_encryption_key".path; storageEncryptionKey.result = config.shb.sops.secrets."authelia/storage_encryption_key".result;
identityProvidersOIDCHMACSecret.result.path = config.sops.secrets."authelia/hmac_secret".path; identityProvidersOIDCHMACSecret.result = config.shb.sops.secrets."authelia/hmac_secret".result;
identityProvidersOIDCIssuerPrivateKey.result.path = config.sops.secrets."authelia/private_key".path; identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secrets."authelia/private_key".result;
}; };
}; };
sops.secrets."authelia/jwt_secret" = config.shb.authelia.secrets.jwtSecret.request; shb.certs.certs.letsencrypt."example.com".extraDomains = [ "auth.example.com" ];
sops.secrets."authelia/ldap_admin_password" = config.shb.authelia.secrets.ldapAdminPassword.request;
sops.secrets."authelia/session_secret" = config.shb.authelia.secrets.sessionSecret.request; shb.sops.secrets."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request;
sops.secrets."authelia/storage_encryption_key" = config.shb.authelia.secrets.storageEncryptionKey.request; shb.sops.secrets."authelia/ldap_admin_password".request = config.shb.authelia.secrets.ldapAdminPassword.request;
sops.secrets."authelia/hmac_secret" = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; shb.sops.secrets."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request;
sops.secrets."authelia/private_key" = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; shb.sops.secrets."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request;
sops.secrets."authelia/smtp_password" = config.shb.authelia.smtp.password.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`. The secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`.
@ -263,13 +269,14 @@ shb.nextcloud.apps.sso = {
clientID = "nextcloud"; clientID = "nextcloud";
fallbackDefaultAuth = false; fallbackDefaultAuth = false;
secret.result.path = config.sops.secrets."nextcloud/sso/secret".path; secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result;
secretForAuthelia.result.path = config.sops.secrets."nextcloud/sso/secretForAuthelia".path; secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result;
}; };
sops.secret."nextcloud/sso/secret" = config.shb.nextcloud.apps.sso.secret.request; shb.sops.secret."nextcloud/sso/secret".request = config.shb.nextcloud.apps.sso.secret.request;
sops.secret."nextcloud/sso/secretForAuthelia" = config.shb.nextcloud.apps.sso.secretForAuthelia.request // { shb.sops.secret."nextcloud/sso/secretForAuthelia" = {
key = "nextcloud/sso/secret"; request = config.shb.nextcloud.apps.sso.secretForAuthelia.request;
settings.key = "nextcloud/sso/secret";
}; };
``` ```
@ -282,7 +289,7 @@ secrets have the same content.
Setting the `fallbackDefaultAuth` to `false` means the only way to login is through Authelia. Setting the `fallbackDefaultAuth` to `false` means the only way to login is through Authelia.
If this does not work for any reason, you can let users login through Nextcloud directly by setting this option to `true`. If this does not work for any reason, you can let users login through Nextcloud directly by setting this option to `true`.
### Tweak PHPFpm Config {#services-nextcloud-server-usage-phpfpm} ### Tweak PHPFpm Config {#services-nextcloudserver-usage-phpfpm}
For instances with more users, or if you feel the pages are loading slowly, For instances with more users, or if you feel the pages are loading slowly,
you can tweak the `php-fpm` pool settings. you can tweak the `php-fpm` pool settings.
@ -304,7 +311,7 @@ I don't have a good heuristic for what are good values here but what I found
is that you don't want too high of a `max_children` value is that you don't want too high of a `max_children` value
to avoid I/O strain on the hard drives, especially if you use spinning drives. to avoid I/O strain on the hard drives, especially if you use spinning drives.
### Tweak PostgreSQL Settings {#services-nextcloud-server-usage-postgres} ### Tweak PostgreSQL Settings {#services-nextcloudserver-usage-postgres}
These settings will impact all databases since the NixOS Postgres module These settings will impact all databases since the NixOS Postgres module
configures only one Postgres instance. configures only one Postgres instance.
@ -346,7 +353,7 @@ shb.nextcloud.postgresSettings = {
}; };
``` ```
### Backup {#services-nextcloud-server-usage-backup} ### Backup {#services-nextcloudserver-usage-backup}
Backing up Nextcloud data files using the [Restic block](blocks-restic.html) is done like so: Backing up Nextcloud data files using the [Restic block](blocks-restic.html) is done like so:
@ -378,7 +385,7 @@ Note that this will backup the whole PostgreSQL instance,
not just the Nextcloud database. not just the Nextcloud database.
This limitation will be lifted in the future. This limitation will be lifted in the future.
### Enable Preview Generator App {#services-nextcloud-server-usage-previewgenerator} ### Enable Preview Generator App {#services-nextcloudserver-usage-previewgenerator}
The following snippet installs and enables the [Preview The following snippet installs and enables the [Preview
Generator](https://apps.nextcloud.com/apps/previewgenerator) application as well as creates the Generator](https://apps.nextcloud.com/apps/previewgenerator) application as well as creates the
@ -403,7 +410,7 @@ You can opt-out with:
shb.nextcloud.apps.previewgenerator.recommendedSettings = false; shb.nextcloud.apps.previewgenerator.recommendedSettings = false;
``` ```
### Enable External Storage App {#services-nextcloud-server-usage-externalstorage} ### Enable External Storage App {#services-nextcloudserver-usage-externalstorage}
The following snippet installs and enables the [External The following snippet installs and enables the [External
Storage](https://docs.nextcloud.com/server/28/go.php?to=admin-external-storage) application. Storage](https://docs.nextcloud.com/server/28/go.php?to=admin-external-storage) application.
@ -440,7 +447,7 @@ which is well suited for randomly accessing small files like thumbnails.
On the other side, a spinning hard drive can store more data On the other side, a spinning hard drive can store more data
which is well suited for storing user data. which is well suited for storing user data.
### Enable OnlyOffice App {#services-nextcloud-server-usage-onlyoffice} ### Enable OnlyOffice App {#services-nextcloudserver-usage-onlyoffice}
The following snippet installs and enables the [Only The following snippet installs and enables the [Only
Office](https://apps.nextcloud.com/apps/onlyoffice) application as well as sets up an Only Office Office](https://apps.nextcloud.com/apps/onlyoffice) application as well as sets up an Only Office
@ -462,7 +469,7 @@ nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg)
]; ];
``` ```
### Enable Monitoring {#services-nextcloud-server-server-usage-monitoring} ### Enable Monitoring {#services-nextcloudserver-server-usage-monitoring}
Enable the [monitoring block](./blocks-monitoring.html). Enable the [monitoring block](./blocks-monitoring.html).
A [Grafana dashboard][] for overall server performance will be created A [Grafana dashboard][] for overall server performance will be created
@ -470,7 +477,7 @@ and the Nextcloud metrics will automatically appear there.
[Grafana dashboard]: ./blocks-monitoring.html#blocks-monitoring-performance-dashboard [Grafana dashboard]: ./blocks-monitoring.html#blocks-monitoring-performance-dashboard
### Enable Tracing {#services-nextcloud-server-server-usage-tracing} ### Enable Tracing {#services-nextcloudserver-server-usage-tracing}
You can enable tracing with: You can enable tracing with:
@ -485,7 +492,7 @@ but that's not the case yet.
[my blog post]: http://blog.tiserbox.com/posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html [my blog post]: http://blog.tiserbox.com/posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html
### Appdata Location {#services-nextcloud-server-server-usage-appdata} ### Appdata Location {#services-nextcloudserver-server-usage-appdata}
The appdata folder is a special folder located under the `shb.nextcloud.dataDir` directory. The appdata folder is a special folder located under the `shb.nextcloud.dataDir` directory.
It is named `appdata_<instanceid>` with the Nextcloud's instance ID as a suffix. It is named `appdata_<instanceid>` with the Nextcloud's instance ID as a suffix.
@ -496,7 +503,7 @@ For performance reasons, it is recommended to store this folder on a fast drive
that is optimized for randomized read and write access. that is optimized for randomized read and write access.
The best would be either an SSD or an NVMe drive. The best would be either an SSD or an NVMe drive.
The best way to solve this is to use the [External Storage app](#services-nextcloud-server-usage-externalstorage). The best way to solve this is to use the [External Storage app](#services-nextcloudserver-usage-externalstorage).
If you have an existing installation and put Nextcloud's `shb.nextcloud.dataDir` folder on a HDD with spinning disks, If you have an existing installation and put Nextcloud's `shb.nextcloud.dataDir` folder on a HDD with spinning disks,
then the appdata folder is also located on spinning drives. then the appdata folder is also located on spinning drives.
@ -514,16 +521,16 @@ mount --bind /srv/sdd/appdata_nextcloud /var/lib/nextcloud/data/appdata_ocxvky2f
Note that you can re-generate a new appdata folder Note that you can re-generate a new appdata folder
by issuing the command `nextcloud-occ config:system:delete instanceid`. by issuing the command `nextcloud-occ config:system:delete instanceid`.
## Demo {#services-nextcloud-server-demo} ## Demo {#services-nextcloudserver-demo}
Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or
without LDAP integration on a VM with minimal manual steps. without LDAP integration on a VM with minimal manual steps.
## Maintenance {#services-nextcloud-server-maintenance} ## Maintenance {#services-nextcloudserver-maintenance}
On the command line, the `occ` tool is called `nextcloud-occ`. On the command line, the `occ` tool is called `nextcloud-occ`.
## Debug {#services-nextcloud-server-debug} ## Debug {#services-nextcloudserver-debug}
In case of an issue, check the logs for any systemd service mentioned in this section. In case of an issue, check the logs for any systemd service mentioned in this section.
@ -540,10 +547,10 @@ Access the database with `sudo -u nextcloud psql`.
Access Redis with `sudo -u nextcloud redis-cli -s /run/redis-nextcloud/redis.sock`. Access Redis with `sudo -u nextcloud redis-cli -s /run/redis-nextcloud/redis.sock`.
## Options Reference {#services-nextcloud-server-options} ## Options Reference {#services-nextcloudserver-options}
```{=include=} options ```{=include=} options
id-prefix: services-nextcloud-server-options- id-prefix: services-nextcloudserver-options-
list-id: selfhostblocks-service-nextcloud-options list-id: selfhostblocks-service-nextcloud-options
source: @OPTIONS_JSON@ source: @OPTIONS_JSON@
``` ```

View file

@ -45,9 +45,16 @@ in
example = "https://authelia.example.com"; example = "https://authelia.example.com";
}; };
databasePasswordFile = lib.mkOption { databasePassword = lib.mkOption {
type = lib.types.path; description = "File containing the Vaultwarden database password.";
description = "File containing the password to connect to the postgresql database."; type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0440";
owner = "vaultwarden";
group = "postgres";
restartUnits = [ "vaultwarden.service" "postgresql.service" ];
};
};
}; };
smtp = lib.mkOption { smtp = lib.mkOption {
@ -88,9 +95,15 @@ in
description = "Auth mechanism."; description = "Auth mechanism.";
default = "Login"; default = "Login";
}; };
passwordFile = lib.mkOption { password = lib.mkOption {
type = lib.types.str;
description = "File containing the password to connect to the SMTP host."; description = "File containing the password to connect to the SMTP host.";
type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0400";
owner = "vaultwarden";
restartUnits = [ "vaultwarden.service" ];
};
};
}; };
}; };
}); });
@ -187,10 +200,10 @@ in
systemd.services.vaultwarden.preStart = systemd.services.vaultwarden.preStart =
shblib.replaceSecrets { shblib.replaceSecrets {
userConfig = { 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"; DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
} // lib.optionalAttrs (cfg.smtp != null) { } // lib.optionalAttrs (cfg.smtp != null) {
SMTP_PASSWORD.source = cfg.smtp.passwordFile; SMTP_PASSWORD.source = cfg.smtp.password.result.path;
}; };
resultPath = "${dataFolder}/vaultwarden.env"; resultPath = "${dataFolder}/vaultwarden.env";
generator = name: v: pkgs.writeText "template" (lib.generators.toINIWithGlobalSection {} { globalSection = v; }); generator = name: v: pkgs.writeText "template" (lib.generators.toINIWithGlobalSection {} { globalSection = v; });
@ -224,7 +237,7 @@ in
{ {
username = "vaultwarden"; username = "vaultwarden";
database = "vaultwarden"; database = "vaultwarden";
passwordFile = builtins.toString cfg.databasePasswordFile; passwordFile = cfg.databasePassword.result.path;
} }
]; ];
# TODO: make this work. # TODO: make this work.

View file

@ -33,8 +33,17 @@ in
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
subdomain = "ldap"; subdomain = "ldap";
domain = "machine.com"; domain = "machine.com";
ldapUserPassword.result.path = pkgs.writeText "user_password" ldapAdminPassword; ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result.path = pkgs.writeText "jwt_secret" "securejwtsecret"; 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 = { shb.authelia = {
@ -45,12 +54,12 @@ in
ldapPort = config.shb.ldap.ldapPort; ldapPort = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
secrets = { secrets = {
jwtSecret.result.path = config.shb.hardcodedsecret.autheliaJwtSecret.path; jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result;
ldapAdminPassword.result.path = config.shb.hardcodedsecret.ldapAdminPassword.path; ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result.path = config.shb.hardcodedsecret.sessionSecret.path; sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result.path = config.shb.hardcodedsecret.storageEncryptionKey.path; storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result.path = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.path; identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result.path = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.path; identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
}; };
oidcClients = [ oidcClients = [
@ -73,23 +82,29 @@ in
]; ];
}; };
shb.hardcodedsecret.autheliaJwtSecret = config.shb.authelia.secrets.jwtSecret.request // { shb.hardcodedsecret.autheliaJwtSecret = {
content = "jwtSecret"; request = config.shb.authelia.secrets.jwtSecret.request;
settings.content = "jwtSecret";
}; };
shb.hardcodedsecret.ldapAdminPassword = config.shb.authelia.secrets.ldapAdminPassword.request // { shb.hardcodedsecret.ldapAdminPassword = {
content = ldapAdminPassword; request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.content = ldapAdminPassword;
}; };
shb.hardcodedsecret.sessionSecret = config.shb.authelia.secrets.sessionSecret.request // { shb.hardcodedsecret.sessionSecret = {
content = "sessionSecret"; request = config.shb.authelia.secrets.sessionSecret.request;
settings.content = "sessionSecret";
}; };
shb.hardcodedsecret.storageEncryptionKey = config.shb.authelia.secrets.storageEncryptionKey.request // { shb.hardcodedsecret.storageEncryptionKey = {
content = "storageEncryptionKey"; request = config.shb.authelia.secrets.storageEncryptionKey.request;
settings.content = "storageEncryptionKey";
}; };
shb.hardcodedsecret.identityProvidersOIDCHMACSecret = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request // { shb.hardcodedsecret.identityProvidersOIDCHMACSecret = {
content = "identityProvidersOIDCHMACSecret"; request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
settings.content = "identityProvidersOIDCHMACSecret";
}; };
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request // { shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
source = (pkgs.runCommand "gen-private-key" {} '' request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source = (pkgs.runCommand "gen-private-key" {} ''
mkdir $out mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem"; '') + "/private.pem";

View file

@ -1,6 +1,8 @@
{ pkgs, lib, ... }: { pkgs, lib, ... }:
let let
pkgs' = pkgs; pkgs' = pkgs;
password = "securepassword";
in in
{ {
auth = pkgs.testers.runNixOSTest { auth = pkgs.testers.runNixOSTest {
@ -15,6 +17,7 @@ in
shb.ssl.enable = lib.mkEnableOption "ssl"; shb.ssl.enable = lib.mkEnableOption "ssl";
}; };
} }
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/ldap.nix ../../modules/blocks/ldap.nix
]; ];
@ -23,10 +26,19 @@ in
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
subdomain = "ldap"; subdomain = "ldap";
domain = "example.com"; domain = "example.com";
ldapUserPassword.result.path = pkgs.writeText "user_password" "securepw"; ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result.path = pkgs.writeText "jwt_secret" "securejwtsecret"; jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
debug = true; 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 networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port
}; };
@ -63,7 +75,7 @@ in
+ """ -H "Content-type: application/json" """ + """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """ + """ -H "Host: ldap.example.com" """
+ " http://server/auth/simple/login " + " http://server/auth/simple/login "
+ """ -d '{"username": "admin", "password": "securepw"}' """ + """ -d '{"username": "admin", "password": "${password}"}' """
))['token'] ))['token']
data = json.loads(client.succeed( data = json.loads(client.succeed(

View file

@ -19,23 +19,32 @@ let
]; ];
shb.hardcodedsecret.A = { shb.hardcodedsecret.A = {
owner = "root"; request = {
group = "keys"; owner = "root";
mode = "0440"; group = "keys";
content = "secretA"; mode = "0440";
};
settings.content = "secretA";
}; };
shb.hardcodedsecret.B = { shb.hardcodedsecret.B = {
owner = "root"; request = {
group = "keys"; owner = "root";
mode = "0440"; group = "keys";
content = "secretB"; mode = "0440";
};
settings.content = "secretB";
};
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."testinstance".settings.passphrase.request;
settings.content = "secretB";
}; };
shb.restic.instances."testinstance" = { shb.restic.instances."testinstance" = {
settings = { settings = {
enable = true; enable = true;
passphrase.result.path = pkgs.writeText "passphrase" "PassPhrase"; passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = { repository = {
path = "/opt/repos/A"; path = "/opt/repos/A";
@ -46,8 +55,8 @@ let
# Those are not needed by the repository but are still included # Those are not needed by the repository but are still included
# so we can test them in the hooks section. # so we can test them in the hooks section.
secrets = { secrets = {
A.source = config.shb.hardcodedsecret.A.path; A.source = config.shb.hardcodedsecret.A.result.path;
B.source = config.shb.hardcodedsecret.B.path; B.source = config.shb.hardcodedsecret.B.result.path;
}; };
}; };
}; };

View file

@ -146,11 +146,13 @@ in
"127.0.0.1" = [ "ldap.${domain}" ]; "127.0.0.1" = [ "ldap.${domain}" ];
}; };
shb.hardcodedsecret.ldapUserPassword = config.shb.ldap.ldapUserPassword.request // { shb.hardcodedsecret.ldapUserPassword = {
content = "ldapUserPassword"; request = config.shb.ldap.ldapUserPassword.request;
settings.content = "ldapUserPassword";
}; };
shb.hardcodedsecret.jwtSecret = config.shb.ldap.jwtSecret.request // { shb.hardcodedsecret.jwtSecret = {
content = "jwtSecrets"; request = config.shb.ldap.jwtSecret.request;
settings.content = "jwtSecrets";
}; };
shb.ldap = { shb.ldap = {
@ -160,8 +162,8 @@ in
ldapPort = 3890; ldapPort = 3890;
webUIListenPort = 17170; webUIListenPort = 17170;
dcdomain = "dc=example,dc=com"; dcdomain = "dc=example,dc=com";
ldapUserPassword.result.path = config.shb.hardcodedsecret.ldapUserPassword.path; ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result.path = config.shb.hardcodedsecret.jwtSecret.path; jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
}; };
}; };
@ -185,32 +187,38 @@ in
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
secrets = { secrets = {
jwtSecret.result.path = config.shb.hardcodedsecret.autheliaJwtSecret.path; jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result;
ldapAdminPassword.result.path = config.shb.hardcodedsecret.ldapAdminPassword.path; ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result.path = config.shb.hardcodedsecret.sessionSecret.path; sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result.path = config.shb.hardcodedsecret.storageEncryptionKey.path; storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result.path = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.path; identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result.path = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.path; identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
}; };
}; };
shb.hardcodedsecret.autheliaJwtSecret = config.shb.authelia.secrets.jwtSecret.request // { shb.hardcodedsecret.autheliaJwtSecret = {
content = "jwtSecret"; request = config.shb.authelia.secrets.jwtSecret.request;
settings.content = "jwtSecret";
}; };
shb.hardcodedsecret.ldapAdminPassword = config.shb.authelia.secrets.ldapAdminPassword.request // { shb.hardcodedsecret.ldapAdminPassword = {
content = "ldapUserPassword"; request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.content = "ldapUserPassword";
}; };
shb.hardcodedsecret.sessionSecret = config.shb.authelia.secrets.sessionSecret.request // { shb.hardcodedsecret.sessionSecret = {
content = "sessionSecret"; request = config.shb.authelia.secrets.sessionSecret.request;
settings.content = "sessionSecret";
}; };
shb.hardcodedsecret.storageEncryptionKey = config.shb.authelia.secrets.storageEncryptionKey.request // { shb.hardcodedsecret.storageEncryptionKey = {
content = "storageEncryptionKey"; request = config.shb.authelia.secrets.storageEncryptionKey.request;
settings.content = "storageEncryptionKey";
}; };
shb.hardcodedsecret.identityProvidersOIDCHMACSecret = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request // { shb.hardcodedsecret.identityProvidersOIDCHMACSecret = {
content = "identityProvidersOIDCHMACSecret"; request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
settings.content = "identityProvidersOIDCHMACSecret";
}; };
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request // { shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
source = (pkgs.runCommand "gen-private-key" {} '' request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source = (pkgs.runCommand "gen-private-key" {} ''
mkdir $out mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem"; '') + "/private.pem";

View file

@ -1,4 +1,4 @@
{ pkgs, ... }: { pkgs, lib, ... }:
let let
contracts = pkgs.callPackage ../../modules/contracts {}; contracts = pkgs.callPackage ../../modules/contracts {};
in in
@ -13,7 +13,7 @@ in
]; ];
settings = { repository, config, ... }: { settings = { repository, config, ... }: {
enable = true; enable = true;
passphrase.result.path = config.shb.hardcodedsecret.passphrase.path; passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = { repository = {
path = repository; path = repository;
timerConfig = { timerConfig = {
@ -21,16 +21,16 @@ in
}; };
}; };
}; };
extraConfig = { username, ... }: { extraConfig = { username, config, ... }: {
shb.hardcodedsecret.passphrase = { shb.hardcodedsecret.passphrase = {
owner = username; request = config.shb.restic.instances."mytest".settings.passphrase.request;
content = "passphrase"; settings.content = "passphrase";
}; };
}; };
}; };
restic_me = contracts.test.backup { restic_nonroot = contracts.test.backup {
name = "restic_me"; name = "restic_nonroot";
username = "me"; username = "me";
providerRoot = [ "shb" "restic" "instances" "mytest" ]; providerRoot = [ "shb" "restic" "instances" "mytest" ];
modules = [ modules = [
@ -39,7 +39,7 @@ in
]; ];
settings = { repository, config, ... }: { settings = { repository, config, ... }: {
enable = true; enable = true;
passphrase.result.path = config.shb.hardcodedsecret.passphrase.path; passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = { repository = {
path = repository; path = repository;
timerConfig = { timerConfig = {
@ -47,10 +47,10 @@ in
}; };
}; };
}; };
extraConfig = { username, ... }: { extraConfig = { username, config, ... }: {
shb.hardcodedsecret.passphrase = { shb.hardcodedsecret.passphrase = {
owner = username; request = config.shb.restic.instances."mytest".settings.passphrase.request;
content = "passphrase"; settings.content = "passphrase";
}; };
}; };
}; };

View file

@ -10,10 +10,11 @@ in
modules = [ modules = [
../../modules/blocks/postgresql.nix ../../modules/blocks/postgresql.nix
../../modules/blocks/restic.nix ../../modules/blocks/restic.nix
../../modules/blocks/hardcodedsecret.nix
]; ];
settings = repository: { settings = { repository, config, ... }: {
enable = true; enable = true;
passphrase.result.path = pkgs.writeText "passphrase" "PassPhrase"; passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = { repository = {
path = repository; path = repository;
timerConfig = { timerConfig = {
@ -21,12 +22,17 @@ in
}; };
}; };
}; };
extraConfig = { username, database, ... }: { extraConfig = { config, database, ... }: {
shb.postgresql.ensures = [ 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";
};
}; };
}; };
} }

View file

@ -7,8 +7,8 @@ in
name = "hardcoded"; name = "hardcoded";
modules = [ ../../modules/blocks/hardcodedsecret.nix ]; modules = [ ../../modules/blocks/hardcodedsecret.nix ];
configRoot = [ "shb" "hardcodedsecret" ]; configRoot = [ "shb" "hardcodedsecret" ];
createContent = { settingsCfg = secret: {
content = "secretA"; content = secret;
}; };
}; };
@ -16,8 +16,8 @@ in
name = "hardcoded"; name = "hardcoded";
modules = [ ../../modules/blocks/hardcodedsecret.nix ]; modules = [ ../../modules/blocks/hardcodedsecret.nix ];
configRoot = [ "shb" "hardcodedsecret" ]; configRoot = [ "shb" "hardcodedsecret" ];
createContent = { settingsCfg = secret: {
content = "secretA"; content = secret;
}; };
owner = "user"; owner = "user";
group = "group"; group = "group";

View file

@ -34,8 +34,8 @@ let
enable = true; enable = true;
inherit domain subdomain; inherit domain subdomain;
adminPassword.result.path = config.shb.hardcodedsecret.forgejoAdminPassword.path; adminPassword.result = config.shb.hardcodedsecret.forgejoAdminPassword.result;
databasePassword.result.path = config.shb.hardcodedsecret.forgejoDatabasePassword.path; databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result;
}; };
# Needed for gitea-runner-local to be able to ping forgejo. # Needed for gitea-runner-local to be able to ping forgejo.
@ -43,12 +43,14 @@ let
"127.0.0.1" = [ "${subdomain}.${domain}" ]; "127.0.0.1" = [ "${subdomain}.${domain}" ];
}; };
shb.hardcodedsecret.forgejoAdminPassword = config.shb.forgejo.adminPassword.request // { shb.hardcodedsecret.forgejoAdminPassword = {
content = adminPassword; request = config.shb.forgejo.adminPassword.request;
settings.content = adminPassword;
}; };
shb.hardcodedsecret.forgejoDatabasePassword = config.shb.forgejo.databasePassword.request // { shb.hardcodedsecret.forgejoDatabasePassword = {
content = "databasePassword"; request = config.shb.forgejo.databasePassword.request;
settings.content = "databasePassword";
}; };
}; };
@ -65,12 +67,13 @@ let
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminPassword.result.path = config.shb.hardcodedsecret.forgejoLdapUserPassword.path; adminPassword.result = config.shb.hardcodedsecret.forgejoLdapUserPassword.result;
}; };
}; };
shb.hardcodedsecret.forgejoLdapUserPassword = config.shb.forgejo.ldap.adminPassword.request // { shb.hardcodedsecret.forgejoLdapUserPassword = {
content = "ldapUserPassword"; request = config.shb.forgejo.ldap.adminPassword.request;
settings.content = "ldapUserPassword";
}; };
}; };
@ -79,17 +82,19 @@ let
sso = { sso = {
enable = true; enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result.path = config.shb.hardcodedsecret.forgejoSSOPassword.path; sharedSecret.result = config.shb.hardcodedsecret.forgejoSSOPassword.result;
sharedSecretForAuthelia.result.path = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.path; sharedSecretForAuthelia.result = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.result;
}; };
}; };
shb.hardcodedsecret.forgejoSSOPassword = config.shb.forgejo.sso.sharedSecret.request // { shb.hardcodedsecret.forgejoSSOPassword = {
content = "ssoPassword"; request = config.shb.forgejo.sso.sharedSecret.request;
settings.content = "ssoPassword";
}; };
shb.hardcodedsecret.forgejoSSOPasswordAuthelia = config.shb.forgejo.sso.sharedSecretForAuthelia.request // { shb.hardcodedsecret.forgejoSSOPasswordAuthelia = {
content = "ssoPassword"; request = config.shb.forgejo.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
}; };
}; };
in in

View file

@ -43,12 +43,13 @@ let
host = "127.0.0.1"; host = "127.0.0.1";
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminPassword.result.path = config.shb.hardcodedsecret.jellyfinLdapUserPassword.path; adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result;
}; };
}; };
shb.hardcodedsecret.jellyfinLdapUserPassword = config.shb.jellyfin.ldap.adminPassword.request // { shb.hardcodedsecret.jellyfinLdapUserPassword = {
content = "ldapUserPassword"; request = config.shb.jellyfin.ldap.adminPassword.request;
settings.content = "ldapUserPassword";
}; };
}; };
@ -57,17 +58,19 @@ let
sso = { sso = {
enable = true; enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result.path = config.shb.hardcodedsecret.jellyfinSSOPassword.path; sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result;
sharedSecretForAuthelia.result.path = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.path; sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result;
}; };
}; };
shb.hardcodedsecret.jellyfinSSOPassword = config.shb.jellyfin.sso.sharedSecret.request // { shb.hardcodedsecret.jellyfinSSOPassword = {
content = "ssoPassword"; request = config.shb.jellyfin.sso.sharedSecret.request;
settings.content = "ssoPassword";
}; };
shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = config.shb.jellyfin.sso.sharedSecretForAuthelia.request // { shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = {
content = "ssoPassword"; request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
}; };
}; };
in in

View file

@ -30,8 +30,17 @@ let
inherit subdomain domain; inherit subdomain domain;
grafanaPort = 3000; grafanaPort = 3000;
adminPasswordFile = pkgs.writeText "admin_password" password; adminPassword.result = config.shb.hardcodedsecret."admin_password".result;
secretKeyFile = pkgs.writeText "secret_key" "secret_key"; 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";
}; };
}; };

View file

@ -132,12 +132,13 @@ let
externalFqdn = "${fqdn}:8080"; externalFqdn = "${fqdn}:8080";
adminUser = adminUser; adminUser = adminUser;
adminPass.result.path = config.shb.hardcodedsecret.adminPass.path; adminPass.result = config.shb.hardcodedsecret.adminPass.result;
debug = true; debug = true;
}; };
shb.hardcodedsecret.adminPass = config.shb.nextcloud.adminPass.request // { shb.hardcodedsecret.adminPass = {
content = adminPass; request = config.shb.nextcloud.adminPass.request;
settings.content = adminPass;
}; };
}; };
@ -157,7 +158,7 @@ let
port = config.shb.ldap.ldapPort; port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain; dcdomain = config.shb.ldap.dcdomain;
adminName = "admin"; adminName = "admin";
adminPassword.result.path = config.shb.ldap.ldapUserPassword.result.path; adminPassword.result = config.shb.ldap.ldapUserPassword.result;
userGroup = "nextcloud_user"; userGroup = "nextcloud_user";
}; };
}; };
@ -172,18 +173,20 @@ let
clientID = "nextcloud"; clientID = "nextcloud";
# adminUserGroup = "nextcloud_admin"; # adminUserGroup = "nextcloud_admin";
secret.result.path = config.shb.hardcodedsecret.oidcSecret.path; secret.result = config.shb.hardcodedsecret.oidcSecret.result;
secretForAuthelia.result.path = config.shb.hardcodedsecret.oidcAutheliaSecret.path; secretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
fallbackDefaultAuth = false; fallbackDefaultAuth = false;
}; };
}; };
shb.hardcodedsecret.oidcSecret = config.shb.nextcloud.apps.sso.secret.request // { shb.hardcodedsecret.oidcSecret = {
content = oidcSecret; request = config.shb.nextcloud.apps.sso.secret.request;
settings.content = oidcSecret;
}; };
shb.hardcodedsecret.oidcAutheliaSecret = config.shb.nextcloud.apps.sso.secretForAuthelia.request // { shb.hardcodedsecret.oidcAutheliaSecret = {
content = oidcSecret; request = config.shb.nextcloud.apps.sso.secretForAuthelia.request;
settings.content = oidcSecret;
}; };
}; };

View file

@ -51,6 +51,7 @@ let
}; };
base = testLib.base pkgs' [ base = testLib.base pkgs' [
../../modules/blocks/hardcodedsecret.nix
../../modules/services/vaultwarden.nix ../../modules/services/vaultwarden.nix
]; ];
@ -61,7 +62,11 @@ let
inherit subdomain domain; inherit subdomain domain;
port = 8222; 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 = { # networking.hosts = {
@ -97,7 +102,7 @@ let
request = config.shb.vaultwarden.backup; request = config.shb.vaultwarden.backup;
settings = { settings = {
enable = true; enable = true;
passphrase.result.path = config.shb.hardcodedsecret.passphrase.path; passphrase.result = config.shb.hardcodedsecret.backupPassphrase.result;
repository = { repository = {
path = "/opt/repos/A"; path = "/opt/repos/A";
timerConfig = { timerConfig = {
@ -107,8 +112,9 @@ let
}; };
}; };
}; };
shb.hardcodedsecret.passphrase = config.shb.restic.instances."testinstance".settings.passphrase.request // { shb.hardcodedsecret.backupPassphrase = {
content = "PassPhrase"; request = config.shb.restic.instances."testinstance".settings.passphrase.request;
settings.content = "PassPhrase";
}; };
}; };
in in