use contract for ssl block
This commit is contained in:
parent
909ebe405a
commit
adc09acc49
21 changed files with 790 additions and 144 deletions
|
@ -26,6 +26,10 @@ Self Host Blocks provides at least one implementation for each block and allows
|
||||||
implementation if you want to, as long as it passes the tests. You can then use blocks to improve
|
implementation if you want to, as long as it passes the tests. You can then use blocks to improve
|
||||||
services you already have deployed.
|
services you already have deployed.
|
||||||
|
|
||||||
|
```{=include=} chapters html:into-file=//blocks-ssl.html
|
||||||
|
modules/blocks/ssl/docs/default.md
|
||||||
|
```
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//blocks-backup.html
|
```{=include=} chapters html:into-file=//blocks-backup.html
|
||||||
modules/blocks/backup/docs/default.md
|
modules/blocks/backup/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
|
@ -68,21 +68,15 @@ let
|
||||||
|
|
||||||
optionsDocs = buildOptionsDocs {
|
optionsDocs = buildOptionsDocs {
|
||||||
modules = allModules ++ [ scrubbedModule ];
|
modules = allModules ++ [ scrubbedModule ];
|
||||||
variablelistId = "selfhostblocks-block-backup-options";
|
|
||||||
includeModuleSystemOptions = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
backupOptionsDocs = buildOptionsDocs {
|
|
||||||
modules = [ ../modules/blocks/backup.nix scrubbedModule ];
|
|
||||||
variablelistId = "selfhostblocks-options";
|
variablelistId = "selfhostblocks-options";
|
||||||
includeModuleSystemOptions = false;
|
includeModuleSystemOptions = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
nextcloudOptionsDocs = buildOptionsDocs {
|
individualModuleOptionsDocs = path: (buildOptionsDocs {
|
||||||
modules = [ ../modules/services/nextcloud-server.nix scrubbedModule ];
|
modules = [ path scrubbedModule ];
|
||||||
variablelistId = "selfhostblocks-options";
|
variablelistId = "selfhostblocks-options";
|
||||||
includeModuleSystemOptions = false;
|
includeModuleSystemOptions = false;
|
||||||
};
|
}).optionsJSON;
|
||||||
|
|
||||||
nmd = import nmdsrc {
|
nmd = import nmdsrc {
|
||||||
inherit lib;
|
inherit lib;
|
||||||
|
@ -135,22 +129,27 @@ in stdenv.mkDerivation {
|
||||||
'@OPTIONS_JSON@' \
|
'@OPTIONS_JSON@' \
|
||||||
${optionsDocs.optionsJSON}/share/doc/nixos/options.json
|
${optionsDocs.optionsJSON}/share/doc/nixos/options.json
|
||||||
|
|
||||||
|
substituteInPlace ./modules/blocks/ssl/docs/default.md \
|
||||||
|
--replace \
|
||||||
|
'@OPTIONS_JSON@' \
|
||||||
|
${individualModuleOptionsDocs ../modules/blocks/ssl.nix}/share/doc/nixos/options.json
|
||||||
|
|
||||||
substituteInPlace ./modules/blocks/backup/docs/default.md \
|
substituteInPlace ./modules/blocks/backup/docs/default.md \
|
||||||
--replace \
|
--replace \
|
||||||
'@OPTIONS_JSON@' \
|
'@OPTIONS_JSON@' \
|
||||||
${backupOptionsDocs.optionsJSON}/share/doc/nixos/options.json
|
${individualModuleOptionsDocs ../modules/blocks/backup.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@' \
|
||||||
${nextcloudOptionsDocs.optionsJSON}/share/doc/nixos/options.json
|
${individualModuleOptionsDocs ../modules/services/nextcloud-server.nix}/share/doc/nixos/options.json
|
||||||
|
|
||||||
find . -name "*.md" -print0 | \
|
find . -name "*.md" -print0 | \
|
||||||
while IFS= read -r -d ''' f; do
|
while IFS= read -r -d ''' f; do
|
||||||
substituteInPlace "''${f}" \
|
substituteInPlace "''${f}" \
|
||||||
--replace \
|
--replace \
|
||||||
'@REPO@' \
|
'@REPO@' \
|
||||||
"${ghRoot}"
|
"${ghRoot}" 2>/dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
nixos-render-docs manual html \
|
nixos-render-docs manual html \
|
||||||
|
|
|
@ -58,6 +58,8 @@
|
||||||
release = "0.0.1";
|
release = "0.0.1";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
lib.contracts = pkgs.callPackage ./modules/contracts {};
|
||||||
|
|
||||||
checks =
|
checks =
|
||||||
let
|
let
|
||||||
importFiles = files:
|
importFiles = files:
|
||||||
|
@ -96,6 +98,7 @@
|
||||||
// (vm_test "postgresql" ./test/vm/postgresql.nix)
|
// (vm_test "postgresql" ./test/vm/postgresql.nix)
|
||||||
// (vm_test "monitoring" ./test/vm/monitoring.nix)
|
// (vm_test "monitoring" ./test/vm/monitoring.nix)
|
||||||
// (vm_test "nextcloud" ./test/vm/nextcloud.nix)
|
// (vm_test "nextcloud" ./test/vm/nextcloud.nix)
|
||||||
|
// (vm_test "ssl" ./test/vm/ssl.nix)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.authelia;
|
cfg = config.shb.authelia;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
autheliaCfg = config.services.authelia.instances.${fqdn};
|
autheliaCfg = config.services.authelia.instances.${fqdn};
|
||||||
|
@ -35,6 +37,12 @@ in
|
||||||
example = "mydomain.com";
|
example = "mydomain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
ldapEndpoint = lib.mkOption {
|
ldapEndpoint = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Endpoint for LDAP authentication backend.";
|
description = "Endpoint for LDAP authentication backend.";
|
||||||
|
@ -293,9 +301,9 @@ in
|
||||||
lib.mkBefore (lib.concatStringsSep "\n" (map mkCfg cfg.oidcClients));
|
lib.mkBefore (lib.concatStringsSep "\n" (map mkCfg cfg.oidcClients));
|
||||||
|
|
||||||
services.nginx.virtualHosts.${fqdn} = {
|
services.nginx.virtualHosts.${fqdn} = {
|
||||||
forceSSL = lib.mkIf config.shb.ssl.enable true;
|
forceSSL = !(isNull cfg.ssl);
|
||||||
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem";
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem";
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
# Taken from https://github.com/authelia/authelia/issues/178
|
# Taken from https://github.com/authelia/authelia/issues/178
|
||||||
# TODO: merge with config from https://matwick.ca/authelia-nginx-sso/
|
# TODO: merge with config from https://matwick.ca/authelia-nginx-sso/
|
||||||
locations."/".extraConfig = ''
|
locations."/".extraConfig = ''
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.ldap;
|
cfg = config.shb.ldap;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -33,6 +35,12 @@ in
|
||||||
default = 3890;
|
default = 3890;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
webUIListenPort = lib.mkOption {
|
webUIListenPort = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
description = "Port on which the web UI is exposed.";
|
description = "Port on which the web UI is exposed.";
|
||||||
|
@ -69,9 +77,9 @@ in
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
virtualHosts.${fqdn} = {
|
virtualHosts.${fqdn} = {
|
||||||
forceSSL = lib.mkIf config.shb.ssl.enable true;
|
forceSSL = !(isNull cfg.ssl);
|
||||||
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem";
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem";
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.monitoring;
|
cfg = config.shb.monitoring;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -21,6 +23,12 @@ in
|
||||||
example = "mydomain.com";
|
example = "mydomain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
grafanaPort = lib.mkOption {
|
grafanaPort = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
description = "Port where Grafana listens to HTTP requests.";
|
description = "Port where Grafana listens to HTTP requests.";
|
||||||
|
@ -362,9 +370,10 @@ in
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
virtualHosts.${fqdn} = {
|
virtualHosts.${fqdn} = {
|
||||||
forceSSL = lib.mkIf config.shb.ssl.enable true;
|
forceSSL = !(isNull cfg.ssl);
|
||||||
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem";
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem";
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
|
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
|
proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.nginx;
|
cfg = config.shb.nginx;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = c: "${c.subdomain}.${c.domain}";
|
fqdn = c: "${c.subdomain}.${c.domain}";
|
||||||
|
|
||||||
autheliaConfig = lib.types.submodule {
|
autheliaConfig = lib.types.submodule {
|
||||||
|
@ -19,6 +21,12 @@ let
|
||||||
example = "mydomain.com";
|
example = "mydomain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
authEndpoint = lib.mkOption {
|
authEndpoint = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Auth endpoint for SSO.";
|
description = "Auth endpoint for SSO.";
|
||||||
|
@ -102,9 +110,9 @@ in
|
||||||
let
|
let
|
||||||
vhostCfg = c: {
|
vhostCfg = c: {
|
||||||
${fqdn c} = {
|
${fqdn c} = {
|
||||||
forceSSL = lib.mkIf config.shb.ssl.enable true;
|
forceSSL = !(isNull c.ssl);
|
||||||
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${c.domain}/cert.pem";
|
sslCertificate = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.cert;
|
||||||
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${c.domain}/key.pem";
|
sslCertificateKey = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.key;
|
||||||
|
|
||||||
# Taken from https://github.com/authelia/authelia/issues/178
|
# Taken from https://github.com/authelia/authelia/issues/178
|
||||||
locations."/".extraConfig = ''
|
locations."/".extraConfig = ''
|
||||||
|
|
|
@ -1,26 +1,148 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.ssl;
|
cfg = config.shb.certs;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.ssl = {
|
options.shb.certs = {
|
||||||
enable = lib.mkEnableOption "selfhostblocks.ssl";
|
systemdService = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Systemd oneshot service used to generate the Certificate Authority bundle.
|
||||||
|
'';
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "shb-ca-bundle.service";
|
||||||
|
};
|
||||||
|
cas.selfsigned = lib.mkOption {
|
||||||
|
description = "Generate a self-signed Certificate Authority.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.attrsOf (lib.types.submodule ({ config, ...}: {
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = ''
|
||||||
|
Certificate Authority Name. You can put what you want here, it will be displayed by the
|
||||||
|
browser.
|
||||||
|
'';
|
||||||
|
default = "Self Host Blocks Certificate";
|
||||||
|
};
|
||||||
|
|
||||||
|
paths = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Paths where CA certs will be located.
|
||||||
|
|
||||||
|
This option is the contract output of the `shb.certs.cas` SSL block.
|
||||||
|
'';
|
||||||
|
type = contracts.ssl.certs-paths;
|
||||||
|
default = rec {
|
||||||
|
key = "/var/lib/certs/cas/${config._module.args.name}.key";
|
||||||
|
cert = "/var/lib/certs/cas/${config._module.args.name}.cert";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemdService = lib.mkOption {
|
||||||
|
description = "Systemd oneshot service used to generate the certs.";
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "shb-certs-ca-${config._module.args.name}.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
certs.selfsigned = lib.mkOption {
|
||||||
|
description = "Generate self-signed certificates signed by a Certificate Authority.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
|
||||||
|
options = {
|
||||||
|
ca = lib.mkOption {
|
||||||
|
type = lib.types.nullOr contracts.ssl.cas;
|
||||||
|
description = ''
|
||||||
|
CA used to generate this certificate. Only used for self-signed.
|
||||||
|
|
||||||
|
This contract input takes the contract output of the `shb.certs.cas` SSL block.
|
||||||
|
'';
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
domain = lib.mkOption {
|
domain = lib.mkOption {
|
||||||
description = "Domain to ask a wildcard certificate for.";
|
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "domain.com";
|
description = ''
|
||||||
|
Domain to generate a certificate for. This can be a wildcard domain like
|
||||||
|
`*.example.com`.
|
||||||
|
'';
|
||||||
|
example = "example.com";
|
||||||
|
};
|
||||||
|
|
||||||
|
paths = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Paths where certs will be located.
|
||||||
|
|
||||||
|
This option is the contract output of the `shb.certs.certs` SSL block.
|
||||||
|
'';
|
||||||
|
type = contracts.ssl.certs-paths;
|
||||||
|
default = rec {
|
||||||
|
key = "/var/lib/certs/selfsigned/${config._module.args.name}.key";
|
||||||
|
cert = "/var/lib/certs/selfsigned/${config._module.args.name}.cert";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemdService = lib.mkOption {
|
||||||
|
description = "Systemd oneshot service used to generate the certs.";
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "shb-certs-cert-selfsigned-${config._module.args.name}.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
certs.letsencrypt = lib.mkOption {
|
||||||
|
description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/).";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
|
||||||
|
options = {
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = ''
|
||||||
|
Domain to generate a certificate for. This can be a wildcard domain like
|
||||||
|
`*.example.com`.
|
||||||
|
'';
|
||||||
|
example = "example.com";
|
||||||
|
};
|
||||||
|
|
||||||
|
paths = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Paths where certs will be located.
|
||||||
|
|
||||||
|
This option is the contract output of the `shb.certs.certs` SSL block.
|
||||||
|
'';
|
||||||
|
type = contracts.ssl.certs-paths;
|
||||||
|
default = {
|
||||||
|
key = "/var/lib/acme/${config._module.args.name}/key.pem";
|
||||||
|
cert = "/var/lib/acme/${config._module.args.name}/cert.pem";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemdService = lib.mkOption {
|
||||||
|
description = "Systemd oneshot service used to generate the certs.";
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service";
|
||||||
};
|
};
|
||||||
|
|
||||||
dnsProvider = lib.mkOption {
|
dnsProvider = lib.mkOption {
|
||||||
description = "DNS provider to use. See https://go-acme.github.io/lego/dns/ for the list of supported providers.";
|
description = "DNS provider to use. See https://go-acme.github.io/lego/dns/ for the list of supported providers.";
|
||||||
type = lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
example = "linode";
|
example = "linode";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dnsResolver = lib.mkOption {
|
||||||
|
description = "IP of a DNS server used to resolve hostnames.";
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "8.8.8.8";
|
||||||
|
};
|
||||||
|
|
||||||
credentialsFile = lib.mkOption {
|
credentialsFile = lib.mkOption {
|
||||||
type = lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
description = ''
|
description = ''
|
||||||
Credentials file location for the chosen DNS provider.
|
Credentials file location for the chosen DNS provider.
|
||||||
|
|
||||||
|
@ -35,16 +157,18 @@ in
|
||||||
You can put non-secret environment variables here too or use shb.ssl.additionalcfg instead.
|
You can put non-secret environment variables here too or use shb.ssl.additionalcfg instead.
|
||||||
'';
|
'';
|
||||||
example = "/run/secrets/ssl";
|
example = "/run/secrets/ssl";
|
||||||
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
additionalCfg = lib.mkOption {
|
additionalEnvironment = lib.mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
description = ''Additional environment variables used to configure the DNS provider.
|
description = ''
|
||||||
|
Additional environment variables used to configure the DNS provider.
|
||||||
|
|
||||||
For secrets, use shb.ssl.credentialsFile instead.
|
For secrets, use shb.ssl.credentialsFile instead.
|
||||||
|
|
||||||
See the chose provider's [documentation](https://go-acme.github.io/lego/dns/) for available
|
See the chosen provider's [documentation](https://go-acme.github.io/lego/dns/) for
|
||||||
options.
|
available options.
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
{
|
{
|
||||||
|
@ -54,10 +178,12 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dnsResolver = lib.mkOption {
|
makeAvailableToUser = lib.mkOption {
|
||||||
description = "IP of a DNS server used to resolve hostnames.";
|
type = lib.types.nullOr lib.types.str;
|
||||||
type = lib.types.str;
|
description = ''
|
||||||
default = "8.8.8.8";
|
Make all certificates available to given user.
|
||||||
|
'';
|
||||||
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
adminEmail = lib.mkOption {
|
adminEmail = lib.mkOption {
|
||||||
|
@ -71,28 +197,142 @@ in
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
}));
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
users.users.${config.services.nginx.user} = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "nginx";
|
|
||||||
extraGroups = [ config.security.acme.defaults.group ];
|
|
||||||
};
|
|
||||||
users.groups.nginx = {};
|
|
||||||
|
|
||||||
security.acme = {
|
|
||||||
acceptTerms = true;
|
|
||||||
certs."${cfg.domain}" = {
|
|
||||||
extraDomainNames = ["*.${cfg.domain}"];
|
|
||||||
};
|
|
||||||
defaults = {
|
|
||||||
email = cfg.adminEmail;
|
|
||||||
inherit (cfg) dnsProvider dnsResolver;
|
|
||||||
credentialsFile = cfg.credentialsFile;
|
|
||||||
enableDebugLogs = cfg.debug;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services."acme-${cfg.domain}".environment = cfg.additionalCfg;
|
config =
|
||||||
};
|
let
|
||||||
|
filterProvider = provider: lib.attrsets.filterAttrs (k: i: i.provider == provider);
|
||||||
|
|
||||||
|
serviceName = lib.strings.removeSuffix ".service";
|
||||||
|
in
|
||||||
|
lib.mkMerge [
|
||||||
|
# Config for self-signed CA.
|
||||||
|
{
|
||||||
|
systemd.services = lib.mapAttrs' (_name: caCfg:
|
||||||
|
lib.nameValuePair (serviceName caCfg.systemdService) {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
wants = [ config.shb.certs.systemdService ];
|
||||||
|
before = [ config.shb.certs.systemdService ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
serviceConfig.RuntimeDirectory = serviceName caCfg.systemdService;
|
||||||
|
# serviceConfig.User = "nextcloud";
|
||||||
|
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
|
||||||
|
script = ''
|
||||||
|
cd $RUNTIME_DIRECTORY
|
||||||
|
|
||||||
|
cat >ca.template <<EOF
|
||||||
|
organization = "${caCfg.name}"
|
||||||
|
cn = "${caCfg.name}"
|
||||||
|
expiration_days = 365
|
||||||
|
ca
|
||||||
|
cert_signing_key
|
||||||
|
crl_signing_key
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p "$(dirname -- "${caCfg.paths.key}")"
|
||||||
|
${pkgs.gnutls}/bin/certtool \
|
||||||
|
--generate-privkey \
|
||||||
|
--key-type rsa \
|
||||||
|
--sec-param High \
|
||||||
|
--outfile ${caCfg.paths.key}
|
||||||
|
chmod 666 ${caCfg.paths.key}
|
||||||
|
|
||||||
|
mkdir -p "$(dirname -- "${caCfg.paths.cert}")"
|
||||||
|
${pkgs.gnutls}/bin/certtool \
|
||||||
|
--generate-self-signed \
|
||||||
|
--load-privkey ${caCfg.paths.key} \
|
||||||
|
--template ca.template \
|
||||||
|
--outfile ${caCfg.paths.cert}
|
||||||
|
chmod 666 ${caCfg.paths.cert}
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
) cfg.cas.selfsigned;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
systemd.services.${serviceName config.shb.certs.systemdService} = (lib.mkIf (cfg.cas.selfsigned != {}) {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
script = ''
|
||||||
|
mkdir -p /etc/ssl/certs
|
||||||
|
|
||||||
|
rm -f /etc/ssl/certs/ca-certificates.crt
|
||||||
|
for file in ${lib.concatStringsSep " " (lib.mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)}; do
|
||||||
|
cat "$file" >> /etc/ssl/certs/ca-certificates.crt
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
# Config for self-signed cert.
|
||||||
|
{
|
||||||
|
systemd.services = lib.mapAttrs' (_name: certCfg:
|
||||||
|
lib.nameValuePair (serviceName certCfg.systemdService) {
|
||||||
|
after = [ certCfg.ca.systemdService ];
|
||||||
|
requires = [ certCfg.ca.systemdService ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig.RuntimeDirectory = serviceName certCfg.systemdService;
|
||||||
|
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
|
||||||
|
script = ''
|
||||||
|
cd $RUNTIME_DIRECTORY
|
||||||
|
|
||||||
|
# server cert template
|
||||||
|
cat >server.template <<EOF
|
||||||
|
organization = "An example company"
|
||||||
|
cn = "${certCfg.domain}"
|
||||||
|
expiration_days = 30
|
||||||
|
dns_name = "${certCfg.domain}"
|
||||||
|
encryption_key
|
||||||
|
signing_key
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p "$(dirname -- "${certCfg.paths.key}")"
|
||||||
|
${pkgs.gnutls}/bin/certtool \
|
||||||
|
--generate-privkey \
|
||||||
|
--key-type rsa \
|
||||||
|
--sec-param High \
|
||||||
|
--outfile ${certCfg.paths.key}
|
||||||
|
chmod 666 ${certCfg.paths.key}
|
||||||
|
|
||||||
|
mkdir -p "$(dirname -- "${certCfg.paths.cert}")"
|
||||||
|
${pkgs.gnutls}/bin/certtool \
|
||||||
|
--generate-certificate \
|
||||||
|
--load-privkey ${certCfg.paths.key} \
|
||||||
|
--load-ca-privkey ${certCfg.ca.paths.key} \
|
||||||
|
--load-ca-certificate ${certCfg.ca.paths.cert} \
|
||||||
|
--template server.template \
|
||||||
|
--outfile ${certCfg.paths.cert}
|
||||||
|
chmod 666 ${certCfg.paths.cert}
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
# serviceConfig.User = "nextcloud";
|
||||||
|
}
|
||||||
|
) cfg.certs.selfsigned;
|
||||||
|
}
|
||||||
|
# Config for Let's Encrypt cert.
|
||||||
|
{
|
||||||
|
users.users = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
|
||||||
|
${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [
|
||||||
|
config.security.acme.defaults.group
|
||||||
|
];
|
||||||
|
}) cfg.certs.letsencrypt);
|
||||||
|
|
||||||
|
security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != {}) true;
|
||||||
|
|
||||||
|
security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
|
||||||
|
"${name}" = {
|
||||||
|
extraDomainNames = [ certCfg.domain ];
|
||||||
|
email = certCfg.adminEmail;
|
||||||
|
inherit (certCfg) dnsProvider dnsResolver;
|
||||||
|
credentialsFile = certCfg.credentialsFile;
|
||||||
|
enableDebugLogs = certCfg.debug;
|
||||||
|
};
|
||||||
|
}) cfg.certs.letsencrypt);
|
||||||
|
|
||||||
|
systemd.services = lib.mkMerge (lib.mapAttrsToList (name: certCfg: {
|
||||||
|
"acme-${certCfg.domain}".environment = certCfg.additionalEnvironment;
|
||||||
|
}) cfg.certs.letsencrypt);
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
159
modules/blocks/ssl/docs/default.md
Normal file
159
modules/blocks/ssl/docs/default.md
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
# SSL Block {#ssl-block}
|
||||||
|
|
||||||
|
This NixOS module is a block that provides a contract to generate TLS certificates.
|
||||||
|
|
||||||
|
It is implemented by:
|
||||||
|
- [`shb.certs.cas.selfsigned`][10] and [`shb.certs.certs.selfsigned`][11]: Generates self-signed certificates,
|
||||||
|
including self-signed CA thanks to the [certtool][1] package.
|
||||||
|
- [`shb.certs.certs.letsencrypt`][12]: Requests certificates from [Let's Encrypt][2].
|
||||||
|
|
||||||
|
[1]: https://search.nixos.org/packages?channel=23.11&show=gnutls&from=0&size=50&sort=relevance&type=packages&query=certtool
|
||||||
|
[2]: https://letsencrypt.org/
|
||||||
|
|
||||||
|
[10]: blocks-ssl.html#blocks-ssl-options-shb.certs.cas.selfsigned
|
||||||
|
[11]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.selfsigned
|
||||||
|
[12]: blocks-ssl.html#blocks-ssl-options-shb.certs.certs.letsencrypt
|
||||||
|
|
||||||
|
## Contract {#ssl-block-contract}
|
||||||
|
|
||||||
|
The contract for this block is defined in [`/modules/contracts/ssl.nix`](@REPO@/modules/contracts/ssl.nix).
|
||||||
|
|
||||||
|
Every module implementing this contract provides the following options:
|
||||||
|
|
||||||
|
- `paths.cert`: Path to the cert file.
|
||||||
|
- `paths.key`: Path to the key file.
|
||||||
|
- `systemdService`: Systemd oneshot service used to generate the certificate.
|
||||||
|
The Systemd service file name must include the `.service` suffix. Downstream users of the
|
||||||
|
certificate can use this option to wait for the certificate to be generated.
|
||||||
|
|
||||||
|
## Implementations {#ssl-block-impl}
|
||||||
|
|
||||||
|
This sections explains how to generate certificates using the SSL block implementations.
|
||||||
|
|
||||||
|
### Self-Signed Certificates {#ssl-block-impl-self-signed}
|
||||||
|
|
||||||
|
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
|
||||||
|
|
||||||
|
To use self-signed certificates, we must first generate at least one Certificate Authority (CA):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.certs.cas.selfsigned.myca = {
|
||||||
|
name = "My CA";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Every CA defined this way will be concatenated into the file `/etc/ssl/certs/ca-certificates.cert`
|
||||||
|
which means those CAs and all certificates generated by those CAs will be automatically trusted.
|
||||||
|
|
||||||
|
We can then generate one or more certificates signed by that CA:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.certs.certs.selfsigned = {
|
||||||
|
"example.com" = {
|
||||||
|
ca = config.shb.certs.cas.selfsigned.myca;
|
||||||
|
|
||||||
|
domain = "example.com";
|
||||||
|
};
|
||||||
|
"www.example.com" = {
|
||||||
|
ca = config.shb.certs.cas.selfsigned.myca;
|
||||||
|
|
||||||
|
domain = "www.example.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Let's Encrypt {#ssl-block-impl-lets-encrypt}
|
||||||
|
|
||||||
|
Defined in [`/modules/blocks/ssl.nix`](@REPO@/modules/blocks/ssl.nix).
|
||||||
|
|
||||||
|
We can ask Let's Encrypt to generate a certificate with:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.certs.certs.letsencrypt."example.com" = {
|
||||||
|
domain = "example.com";
|
||||||
|
dnsProvider = "linode";
|
||||||
|
adminEmail = "admin@example.com";
|
||||||
|
credentialsFile = /path/to/secret/file;
|
||||||
|
additionalEnvironment = {
|
||||||
|
LINODE_HTTP_TIMEOUT = "10";
|
||||||
|
LINODE_POLLING_INTERVAL = "10";
|
||||||
|
LINODE_PROPAGATION_TIMEOUT = "240";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The credential file's content would be a key-value pair:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
LINODE_TOKEN=XYZ...
|
||||||
|
```
|
||||||
|
|
||||||
|
For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/).
|
||||||
|
|
||||||
|
## Usage {#ssl-block-usage}
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.shb.certs.<implementation>.<name>.paths.cert
|
||||||
|
config.shb.certs.<implementation>.<name>.paths.key
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.shb.certs.selfsigned."example.com".paths.cert
|
||||||
|
config.shb.certs.selfsigned."example.com".paths.key
|
||||||
|
```
|
||||||
|
We can then configure Nginx to use those certificates:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.nginx.virtualHosts."example.com" =
|
||||||
|
let
|
||||||
|
cert = config.shb.certs.selfsigned."example.com";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
onlySSL = true;
|
||||||
|
sslCertificate = cert.paths.cert;
|
||||||
|
sslCertificateKey = cert.paths.key;
|
||||||
|
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 'It works!';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
To make sure the Nginx webserver can find the generated file, we will make it wait for the
|
||||||
|
certificate to the generated:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
systemd.services.nginx = {
|
||||||
|
after = [ config.shb.certs.selfsigned."example.com".systemdService ];
|
||||||
|
requires = [ config.shb.certs.selfsigned."example.com".systemdService ];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
If needed, we can also wait on the CA bundle to be generated by waiting for the Systemd service:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.shb.certs.systemdService
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debug {#ssl-block-debug}
|
||||||
|
|
||||||
|
Each CA and Cert is generated by a systemd service whose name can be seen in `systemdService`
|
||||||
|
options below. You can then see the latest errors messages using `journalctl`.
|
||||||
|
|
||||||
|
## Tests {#ssl-block-tests}
|
||||||
|
|
||||||
|
This block is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix).
|
||||||
|
|
||||||
|
## Options Reference {#ssl-block-options}
|
||||||
|
|
||||||
|
```{=include=} options
|
||||||
|
id-prefix: blocks-ssl-options-
|
||||||
|
list-id: selfhostblocks-options
|
||||||
|
source: @OPTIONS_JSON@
|
||||||
|
```
|
4
modules/contracts/default.nix
Normal file
4
modules/contracts/default.nix
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{ lib }:
|
||||||
|
{
|
||||||
|
ssl = import ./ssl.nix { inherit lib; };
|
||||||
|
}
|
58
modules/contracts/ssl.nix
Normal file
58
modules/contracts/ssl.nix
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{ lib }:
|
||||||
|
rec {
|
||||||
|
certs-paths = lib.types.submodule {
|
||||||
|
freeformType = lib.types.anything;
|
||||||
|
|
||||||
|
options = {
|
||||||
|
cert = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Path to the cert file.";
|
||||||
|
};
|
||||||
|
key = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Path to the key file.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
cas = lib.types.submodule {
|
||||||
|
freeformType = lib.types.anything;
|
||||||
|
|
||||||
|
options = {
|
||||||
|
paths = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Paths where the files for the CA will be located.
|
||||||
|
|
||||||
|
This option is the contract output of the `shb.certs.cas` SSL block.
|
||||||
|
'';
|
||||||
|
type = certs-paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemdService = lib.mkOption {
|
||||||
|
description = "Systemd oneshot service used to generate the CA.";
|
||||||
|
type = lib.types.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
certs = lib.types.submodule {
|
||||||
|
freeformType = lib.types.anything;
|
||||||
|
|
||||||
|
options = {
|
||||||
|
paths = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Paths where the files for the certificate will be located.
|
||||||
|
|
||||||
|
This option is the contract output of the `shb.certs.certs` SSL block.
|
||||||
|
'';
|
||||||
|
type = certs-paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemdService = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Systemd oneshot service used to generate the certificate. The name must include the
|
||||||
|
`.service` suffix.
|
||||||
|
'';
|
||||||
|
type = lib.types.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.arr;
|
cfg = config.shb.arr;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
apps = {
|
apps = {
|
||||||
radarr = {
|
radarr = {
|
||||||
defaultPort = 7001;
|
defaultPort = 7001;
|
||||||
|
@ -146,6 +148,12 @@ let
|
||||||
example = "example.com";
|
example = "example.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
description = "Port on which ${name} listens to incoming requests.";
|
description = "Port on which ${name} listens to incoming requests.";
|
||||||
|
@ -304,7 +312,7 @@ config.xml" templatedSettings) "${config.services.radarr.dataDir}/config.xml" (
|
||||||
c = cfg.${name};
|
c = cfg.${name};
|
||||||
in
|
in
|
||||||
lib.mkIf (c.authEndpoint != null) {
|
lib.mkIf (c.authEndpoint != null) {
|
||||||
inherit (c) subdomain domain authEndpoint;
|
inherit (c) subdomain domain authEndpoint ssl;
|
||||||
upstream = "http://127.0.0.1:${toString c.port}";
|
upstream = "http://127.0.0.1:${toString c.port}";
|
||||||
autheliaRules = [
|
autheliaRules = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.deluge;
|
cfg = config.shb.deluge;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -21,6 +23,12 @@ in
|
||||||
example = "mydomain.com";
|
example = "mydomain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
daemonPort = lib.mkOption {
|
daemonPort = lib.mkOption {
|
||||||
type = lib.types.int;
|
type = lib.types.int;
|
||||||
description = "Deluge daemon port";
|
description = "Deluge daemon port";
|
||||||
|
@ -256,7 +264,7 @@ in
|
||||||
|
|
||||||
shb.nginx.autheliaProtect = lib.mkIf config.shb.authelia.enable [
|
shb.nginx.autheliaProtect = lib.mkIf config.shb.authelia.enable [
|
||||||
{
|
{
|
||||||
inherit (cfg) subdomain domain authEndpoint;
|
inherit (cfg) subdomain domain authEndpoint ssl;
|
||||||
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
|
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
|
||||||
autheliaRules = [{
|
autheliaRules = [{
|
||||||
domain = fqdn;
|
domain = fqdn;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.hledger;
|
cfg = config.shb.hledger;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -21,6 +23,12 @@ in
|
||||||
example = "mydomain.com";
|
example = "mydomain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.int;
|
type = lib.types.int;
|
||||||
description = "HLedger port";
|
description = "HLedger port";
|
||||||
|
@ -74,7 +82,7 @@ in
|
||||||
|
|
||||||
shb.nginx.autheliaProtect = [
|
shb.nginx.autheliaProtect = [
|
||||||
{
|
{
|
||||||
inherit (cfg) subdomain domain authEndpoint;
|
inherit (cfg) subdomain domain authEndpoint ssl;
|
||||||
upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
|
upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
|
||||||
autheliaRules = [{
|
autheliaRules = [{
|
||||||
domain = fqdn;
|
domain = fqdn;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.home-assistant;
|
cfg = config.shb.home-assistant;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
ldap_auth_script_repo = pkgs.fetchFromGitHub {
|
ldap_auth_script_repo = pkgs.fetchFromGitHub {
|
||||||
|
@ -33,6 +35,12 @@ in
|
||||||
example = "mydomain.com";
|
example = "mydomain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
ldap = lib.mkOption {
|
ldap = lib.mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html)
|
LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html)
|
||||||
|
@ -193,10 +201,12 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx.virtualHosts."${fqdn}" = {
|
services.nginx.virtualHosts."${fqdn}" = {
|
||||||
forceSSL = lib.mkIf config.shb.ssl.enable true;
|
|
||||||
http2 = true;
|
http2 = true;
|
||||||
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem";
|
|
||||||
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem";
|
forceSSL = !(isNull cfg.ssl);
|
||||||
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
|
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
'';
|
'';
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.jellyfin;
|
cfg = config.shb.jellyfin;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
template = file: newPath: replacements:
|
template = file: newPath: replacements:
|
||||||
|
@ -33,6 +35,12 @@ in
|
||||||
example = "domain.com";
|
example = "domain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
ldapHost = lib.mkOption {
|
ldapHost = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "host serving the LDAP server";
|
description = "host serving the LDAP server";
|
||||||
|
@ -108,10 +116,12 @@ in
|
||||||
|
|
||||||
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
|
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
|
||||||
services.nginx.virtualHosts."${fqdn}" = {
|
services.nginx.virtualHosts."${fqdn}" = {
|
||||||
forceSSL = true;
|
forceSSL = !(isNull cfg.ssl);
|
||||||
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
|
|
||||||
http2 = true;
|
http2 = true;
|
||||||
sslCertificate = "/var/lib/acme/${cfg.domain}/cert.pem";
|
|
||||||
sslCertificateKey = "/var/lib/acme/${cfg.domain}/key.pem";
|
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
# The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
|
# The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
|
||||||
client_max_body_size 20M;
|
client_max_body_size 20M;
|
||||||
|
|
|
@ -5,6 +5,8 @@ let
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
# Make sure to bump both nextcloudPkg and nextcloudApps at the same time.
|
# Make sure to bump both nextcloudPkg and nextcloudApps at the same time.
|
||||||
nextcloudPkg = pkgs.nextcloud27;
|
nextcloudPkg = pkgs.nextcloud27;
|
||||||
nextcloudApps = pkgs.nextcloud27Packages.apps;
|
nextcloudApps = pkgs.nextcloud27Packages.apps;
|
||||||
|
@ -27,6 +29,12 @@ in
|
||||||
example = "domain.com";
|
example = "domain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
externalFqdn = lib.mkOption {
|
externalFqdn = lib.mkOption {
|
||||||
description = "External fqdn used to access Nextcloud. Defaults to <subdomain>.<domain>. This should only be set if you include the port when accessing Nextcloud.";
|
description = "External fqdn used to access Nextcloud. Defaults to <subdomain>.<domain>. This should only be set if you include the port when accessing Nextcloud.";
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
@ -146,6 +154,12 @@ in
|
||||||
default = "oo";
|
default = "oo";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
localNetworkIPRange = lib.mkOption {
|
localNetworkIPRange = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Local network range, to restrict access to Open Office to only those IPs.";
|
description = "Local network range, to restrict access to Open Office to only those IPs.";
|
||||||
|
@ -363,14 +377,14 @@ in
|
||||||
webfinger = true;
|
webfinger = true;
|
||||||
|
|
||||||
# Very important for a bunch of scripts to load correctly. Otherwise you get Content-Security-Policy errors. See https://docs.nextcloud.com/server/13/admin_manual/configuration_server/harden_server.html#enable-http-strict-transport-security
|
# Very important for a bunch of scripts to load correctly. Otherwise you get Content-Security-Policy errors. See https://docs.nextcloud.com/server/13/admin_manual/configuration_server/harden_server.html#enable-http-strict-transport-security
|
||||||
https = config.shb.ssl.enable;
|
https = !(isNull cfg.ssl);
|
||||||
|
|
||||||
extraApps = if isNull cfg.extraApps then {} else cfg.extraApps nextcloudApps;
|
extraApps = if isNull cfg.extraApps then {} else cfg.extraApps nextcloudApps;
|
||||||
extraAppsEnable = true;
|
extraAppsEnable = true;
|
||||||
appstoreEnable = true;
|
appstoreEnable = true;
|
||||||
|
|
||||||
extraOptions = let
|
extraOptions = let
|
||||||
protocol = if config.shb.ssl.enable then "https" else "http";
|
protocol = if !(isNull cfg.ssl) then "https" else "http";
|
||||||
in {
|
in {
|
||||||
"overwrite.cli.url" = "${protocol}://${fqdn}";
|
"overwrite.cli.url" = "${protocol}://${fqdn}";
|
||||||
"overwritehost" = if (isNull cfg.externalFqdn) then fqdn else cfg.externalFqdn;
|
"overwritehost" = if (isNull cfg.externalFqdn) then fqdn else cfg.externalFqdn;
|
||||||
|
@ -382,6 +396,9 @@ in
|
||||||
"overwritecondaddr" = ""; # We need to set it to empty otherwise overwriteprotocol does not work.
|
"overwritecondaddr" = ""; # We need to set it to empty otherwise overwriteprotocol does not work.
|
||||||
"debug" = cfg.debug;
|
"debug" = cfg.debug;
|
||||||
"filelocking.debug" = cfg.debug;
|
"filelocking.debug" = cfg.debug;
|
||||||
|
|
||||||
|
# Use persistent SQL connections.
|
||||||
|
"dbpersistent" = "true";
|
||||||
};
|
};
|
||||||
|
|
||||||
phpOptions = {
|
phpOptions = {
|
||||||
|
@ -396,7 +413,7 @@ in
|
||||||
"opcache.max_accelerated_files" = "10000";
|
"opcache.max_accelerated_files" = "10000";
|
||||||
"opcache.memory_consumption" = "128";
|
"opcache.memory_consumption" = "128";
|
||||||
"opcache.revalidate_freq" = "1";
|
"opcache.revalidate_freq" = "1";
|
||||||
"openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
|
"openssl.cafile" = "/etc/ssl/certs/ca-certificates.cert";
|
||||||
short_open_tag = "Off";
|
short_open_tag = "Off";
|
||||||
|
|
||||||
output_buffering = "Off";
|
output_buffering = "Off";
|
||||||
|
@ -424,9 +441,9 @@ in
|
||||||
|
|
||||||
services.nginx.virtualHosts.${fqdn} = {
|
services.nginx.virtualHosts.${fqdn} = {
|
||||||
# listen = [ { addr = "0.0.0.0"; port = 443; } ];
|
# listen = [ { addr = "0.0.0.0"; port = 443; } ];
|
||||||
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem";
|
forceSSL = !(isNull cfg.ssl);
|
||||||
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem";
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
forceSSL = lib.mkIf config.shb.ssl.enable true;
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
|
|
||||||
# From [1] this should fix downloading of big files. [2] seems to indicate that buffering
|
# From [1] this should fix downloading of big files. [2] seems to indicate that buffering
|
||||||
# happens at multiple places anyway, so disabling one place should be okay.
|
# happens at multiple places anyway, so disabling one place should be okay.
|
||||||
|
@ -491,9 +508,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx.virtualHosts."${cfg.apps.onlyoffice.subdomain}.${cfg.domain}" = {
|
services.nginx.virtualHosts."${cfg.apps.onlyoffice.subdomain}.${cfg.domain}" = {
|
||||||
sslCertificate = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/cert.pem";
|
forceSSL = !(isNull cfg.ssl);
|
||||||
sslCertificateKey = lib.mkIf config.shb.ssl.enable "/var/lib/acme/${cfg.domain}/key.pem";
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
forceSSL = lib.mkIf config.shb.ssl.enable true;
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
|
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
allow ${cfg.apps.onlyoffice.localNetworkIPRange};
|
allow ${cfg.apps.onlyoffice.localNetworkIPRange};
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
let
|
let
|
||||||
cfg = config.shb.vaultwarden;
|
cfg = config.shb.vaultwarden;
|
||||||
|
|
||||||
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
template = file: newPath: replacements:
|
template = file: newPath: replacements:
|
||||||
|
@ -33,6 +35,12 @@ in
|
||||||
example = "mydomain.com";
|
example = "mydomain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ssl = lib.mkOption {
|
||||||
|
description = "Path to SSL files";
|
||||||
|
type = lib.types.nullOr contracts.ssl.certs;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
description = "Port on which vaultwarden service listens.";
|
description = "Port on which vaultwarden service listens.";
|
||||||
|
@ -164,7 +172,7 @@ in
|
||||||
|
|
||||||
shb.nginx.autheliaProtect = [
|
shb.nginx.autheliaProtect = [
|
||||||
{
|
{
|
||||||
inherit (cfg) subdomain domain authEndpoint;
|
inherit (cfg) subdomain domain authEndpoint ssl;
|
||||||
upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}";
|
upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}";
|
||||||
autheliaRules = [
|
autheliaRules = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -89,6 +89,7 @@ in
|
||||||
authEndpoint = "https://oidc.example.com";
|
authEndpoint = "https://oidc.example.com";
|
||||||
subdomain = "radarr";
|
subdomain = "radarr";
|
||||||
upstream = "http://127.0.0.1:7001";
|
upstream = "http://127.0.0.1:7001";
|
||||||
|
ssl = null;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
users.users.radarr.extraGroups = [ "media" ];
|
users.users.radarr.extraGroups = [ "media" ];
|
||||||
|
@ -162,6 +163,7 @@ in
|
||||||
authEndpoint = "https://oidc.example.com";
|
authEndpoint = "https://oidc.example.com";
|
||||||
subdomain = "radarr";
|
subdomain = "radarr";
|
||||||
upstream = "http://127.0.0.1:7001";
|
upstream = "http://127.0.0.1:7001";
|
||||||
|
ssl = null;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
users.users.radarr.extraGroups = [ "media" ];
|
users.users.radarr.extraGroups = [ "media" ];
|
||||||
|
|
|
@ -18,9 +18,11 @@ let
|
||||||
services = anyOpt {};
|
services = anyOpt {};
|
||||||
shb.authelia = anyOpt {};
|
shb.authelia = anyOpt {};
|
||||||
shb.backup = anyOpt {};
|
shb.backup = anyOpt {};
|
||||||
shb.ssl = anyOpt {};
|
systemd = anyOpt {};
|
||||||
|
users = anyOpt {};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
../../modules/blocks/ssl.nix
|
||||||
../../modules/blocks/nginx.nix
|
../../modules/blocks/nginx.nix
|
||||||
m
|
m
|
||||||
];
|
];
|
||||||
|
@ -46,36 +48,32 @@ in
|
||||||
|
|
||||||
testAuth = {
|
testAuth = {
|
||||||
expected = {
|
expected = {
|
||||||
shb.backup = {};
|
nginx.enable = true;
|
||||||
shb.nginx = {
|
nginx.virtualHosts."my.example.com" = {
|
||||||
accessLog = false;
|
|
||||||
autheliaProtect = [{
|
|
||||||
authEndpoint = "hello";
|
|
||||||
autheliaRules = [{}];
|
|
||||||
subdomain = "my";
|
|
||||||
domain = "example.com";
|
|
||||||
upstream = "http://127.0.0.1:1234";
|
|
||||||
}];
|
|
||||||
debugLog = false;
|
|
||||||
};
|
|
||||||
services.nginx.enable = true;
|
|
||||||
services.nginx.virtualHosts."my.example.com" = {
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
locations."/" = {};
|
locations."/" = {};
|
||||||
locations."/authelia" = {};
|
locations."/authelia" = {};
|
||||||
sslCertificate = "/var/lib/acme/example.com/cert.pem";
|
sslCertificate = "/var/lib/certs/selfsigned/example.com.cert";
|
||||||
sslCertificateKey = "/var/lib/acme/example.com/key.pem";
|
sslCertificateKey = "/var/lib/certs/selfsigned/example.com.key";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
expr = testConfig {
|
expr = (testConfig ({ config, ... }: {
|
||||||
shb.ssl.enable = true;
|
shb.certs.cas.selfsigned.myca = {};
|
||||||
|
|
||||||
|
shb.certs.certs.selfsigned."example.com" = {
|
||||||
|
ca = config.shb.certs.cas.selfsigned.myca;
|
||||||
|
|
||||||
|
domain = "example.com";
|
||||||
|
};
|
||||||
|
|
||||||
shb.nginx.autheliaProtect = [{
|
shb.nginx.autheliaProtect = [{
|
||||||
subdomain = "my";
|
subdomain = "my";
|
||||||
domain = "example.com";
|
domain = "example.com";
|
||||||
|
ssl = config.shb.certs.certs.selfsigned."example.com";
|
||||||
upstream = "http://127.0.0.1:1234";
|
upstream = "http://127.0.0.1:1234";
|
||||||
authEndpoint = "hello";
|
authEndpoint = "hello";
|
||||||
autheliaRules = [{}];
|
autheliaRules = [{}];
|
||||||
}];
|
}];
|
||||||
};
|
})).services;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
76
test/vm/ssl.nix
Normal file
76
test/vm/ssl.nix
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
{
|
||||||
|
test = pkgs.nixosTest {
|
||||||
|
name = "ssl-test";
|
||||||
|
|
||||||
|
nodes.server = { config, pkgs, ... }: {
|
||||||
|
imports = [
|
||||||
|
../../modules/blocks/ssl.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
shb.certs = {
|
||||||
|
cas.selfsigned = {
|
||||||
|
myca = {
|
||||||
|
name = "My CA";
|
||||||
|
};
|
||||||
|
myotherca = {
|
||||||
|
name = "My Other CA";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
certs.selfsigned = {
|
||||||
|
mycert = {
|
||||||
|
ca = config.shb.certs.cas.selfsigned.myca;
|
||||||
|
|
||||||
|
domain = "example.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# The configuration below is to create a webserver that uses the server certificate.
|
||||||
|
networking.hosts."127.0.0.1" = [ "example.com" ];
|
||||||
|
|
||||||
|
services.nginx.enable = true;
|
||||||
|
services.nginx.virtualHosts."example.com" =
|
||||||
|
{
|
||||||
|
onlySSL = true;
|
||||||
|
sslCertificate = config.shb.certs.certs.selfsigned.mycert.paths.cert;
|
||||||
|
sslCertificateKey = config.shb.certs.certs.selfsigned.mycert.paths.key;
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 'It works!';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
systemd.services.nginx = {
|
||||||
|
after = [ config.shb.certs.certs.selfsigned.mycert.systemdService ];
|
||||||
|
requires = [ config.shb.certs.certs.selfsigned.mycert.systemdService ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
|
||||||
|
testScript = { nodes, ... }:
|
||||||
|
let
|
||||||
|
myca = nodes.server.shb.certs.cas.selfsigned.myca;
|
||||||
|
myotherca = nodes.server.shb.certs.cas.selfsigned.myotherca;
|
||||||
|
mycert = nodes.server.shb.certs.certs.selfsigned.mycert;
|
||||||
|
in
|
||||||
|
''
|
||||||
|
start_all()
|
||||||
|
|
||||||
|
# Make sure certs are generated.
|
||||||
|
server.wait_for_file("${myca.paths.key}")
|
||||||
|
server.wait_for_file("${myca.paths.cert}")
|
||||||
|
server.wait_for_file("${myotherca.paths.key}")
|
||||||
|
server.wait_for_file("${myotherca.paths.cert}")
|
||||||
|
server.wait_for_file("${mycert.paths.key}")
|
||||||
|
server.wait_for_file("${mycert.paths.cert}")
|
||||||
|
|
||||||
|
# Wait for jkkk
|
||||||
|
server.require_unit_state("${nodes.server.shb.certs.systemdService}", "inactive")
|
||||||
|
|
||||||
|
with subtest("Certificate is trusted in curl"):
|
||||||
|
machine.wait_for_unit("nginx")
|
||||||
|
machine.wait_for_open_port(443)
|
||||||
|
machine.succeed("curl --fail-with-body -v https://example.com")
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue