nixpkgs/nixos/modules/services/networking/stunnel.nix
shelvacu 1a4575f9db
nixos/modules: Add security.pki.caBundle option and make all services use it for CA bundles (#352244)
Previously some modules used `config.environment.etc."ssl/certs/ca-certificates.crt".source`, some used `"/etc/ssl/certs/ca-certificates.crt"`, and some used `"${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"`. These were all bad in one way or another:

- `config.environment.etc."ssl/certs/ca-certificates.crt".source` relies on `source` being set; if `text` is set instead this breaks, introducing a weird undocumented requirement
- `"/etc/ssl/certs/ca-certificates.crt"` is probably okay but very un-nix. It's a magic string, and the path doesn't change when the file changes (and so you can't trigger service reloads, for example, when the contents change in a new system activation)
- `"${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"` silently doesn't include the options from `security.pki`

Co-authored-by: Shelvacu <git@shelvacu.com>
2025-03-08 08:41:08 +00:00

234 lines
6.2 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.stunnel;
yesNo = val: if val then "yes" else "no";
verifyRequiredField = type: field: n: c: {
assertion = lib.hasAttr field c;
message = "stunnel: \"${n}\" ${type} configuration - Field ${field} is required.";
};
verifyChainPathAssert = n: c: {
assertion = (c.verifyHostname or null) == null || (c.verifyChain || c.verifyPeer);
message =
"stunnel: \"${n}\" client configuration - hostname verification "
+ "is not possible without either verifyChain or verifyPeer enabled";
};
removeNulls = lib.mapAttrs (_: lib.filterAttrs (_: v: v != null));
mkValueString =
v:
if v == true then
"yes"
else if v == false then
"no"
else
lib.generators.mkValueStringDefault { } v;
generateConfig =
c:
lib.generators.toINI {
mkSectionName = lib.id;
mkKeyValue = k: v: "${k} = ${mkValueString v}";
} (removeNulls c);
in
{
###### interface
options = {
services.stunnel = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the stunnel TLS tunneling service.";
};
user = lib.mkOption {
type = with lib.types; nullOr str;
default = "nobody";
description = "The user under which stunnel runs.";
};
group = lib.mkOption {
type = with lib.types; nullOr str;
default = "nogroup";
description = "The group under which stunnel runs.";
};
logLevel = lib.mkOption {
type = lib.types.enum [
"emerg"
"alert"
"crit"
"err"
"warning"
"notice"
"info"
"debug"
];
default = "info";
description = "Verbosity of stunnel output.";
};
fipsMode = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable FIPS 140-2 mode required for compliance.";
};
enableInsecureSSLv3 = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable support for the insecure SSLv3 protocol.";
};
servers = lib.mkOption {
description = ''
Define the server configurations.
See "SERVICE-LEVEL OPTIONS" in {manpage}`stunnel(8)`.
'';
type =
with lib.types;
attrsOf (
attrsOf (
nullOr (oneOf [
bool
int
str
])
)
);
example = {
fancyWebserver = {
accept = 443;
connect = 8080;
cert = "/path/to/pem/file";
};
};
default = { };
};
clients = lib.mkOption {
description = ''
Define the client configurations.
By default, verifyChain and OCSPaia are enabled and CAFile is set to `security.pki.caBundle`.
See "SERVICE-LEVEL OPTIONS" in {manpage}`stunnel(8)`.
'';
type =
with lib.types;
attrsOf (
attrsOf (
nullOr (oneOf [
bool
int
str
])
)
);
apply =
let
applyDefaults =
c:
{
CAFile = config.security.pki.caBundle;
OCSPaia = true;
verifyChain = true;
}
// c;
setCheckHostFromVerifyHostname =
c:
# To preserve backward-compatibility with the old NixOS stunnel module
# definition, allow "verifyHostname" as an alias for "checkHost".
c
// {
checkHost = c.checkHost or c.verifyHostname or null;
verifyHostname = null; # Not a real stunnel configuration setting
};
forceClient = c: c // { client = true; };
in
lib.mapAttrs (_: c: forceClient (setCheckHostFromVerifyHostname (applyDefaults c)));
example = {
foobar = {
accept = "0.0.0.0:8080";
connect = "nixos.org:443";
verifyChain = false;
};
};
default = { };
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = lib.concatLists [
(lib.singleton {
assertion =
(lib.length (lib.attrValues cfg.servers) != 0) || ((lib.length (lib.attrValues cfg.clients)) != 0);
message = "stunnel: At least one server- or client-configuration has to be present.";
})
(lib.mapAttrsToList verifyChainPathAssert cfg.clients)
(lib.mapAttrsToList (verifyRequiredField "client" "accept") cfg.clients)
(lib.mapAttrsToList (verifyRequiredField "client" "connect") cfg.clients)
(lib.mapAttrsToList (verifyRequiredField "server" "accept") cfg.servers)
(lib.mapAttrsToList (verifyRequiredField "server" "cert") cfg.servers)
(lib.mapAttrsToList (verifyRequiredField "server" "connect") cfg.servers)
];
environment.systemPackages = [ pkgs.stunnel ];
environment.etc."stunnel.cfg".text = ''
${lib.optionalString (cfg.user != null) "setuid = ${cfg.user}"}
${lib.optionalString (cfg.group != null) "setgid = ${cfg.group}"}
debug = ${cfg.logLevel}
${lib.optionalString cfg.fipsMode "fips = yes"}
${lib.optionalString cfg.enableInsecureSSLv3 "options = -NO_SSLv3"}
; ----- SERVER CONFIGURATIONS -----
${generateConfig cfg.servers}
; ----- CLIENT CONFIGURATIONS -----
${generateConfig cfg.clients}
'';
systemd.services.stunnel = {
description = "stunnel TLS tunneling service";
after = [ "network.target" ];
wants = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [ config.environment.etc."stunnel.cfg".source ];
serviceConfig = {
ExecStart = "${pkgs.stunnel}/bin/stunnel ${config.environment.etc."stunnel.cfg".source}";
Type = "forking";
};
};
meta.maintainers = with lib.maintainers; [
# Server side
lschuermann
# Client side
das_j
];
};
}