1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.stunnel;
8 yesNo = val: if val then "yes" else "no";
9
10 verifyRequiredField = type: field: n: c: {
11 assertion = hasAttr field c;
12 message = "stunnel: \"${n}\" ${type} configuration - Field ${field} is required.";
13 };
14
15 verifyChainPathAssert = n: c: {
16 assertion = (c.verifyHostname or null) == null || (c.verifyChain || c.verifyPeer);
17 message = "stunnel: \"${n}\" client configuration - hostname verification " +
18 "is not possible without either verifyChain or verifyPeer enabled";
19 };
20
21 removeNulls = mapAttrs (_: filterAttrs (_: v: v != null));
22 mkValueString = v:
23 if v == true then "yes"
24 else if v == false then "no"
25 else generators.mkValueStringDefault {} v;
26 generateConfig = c:
27 generators.toINI {
28 mkSectionName = id;
29 mkKeyValue = k: v: "${k} = ${mkValueString v}";
30 } (removeNulls c);
31
32in
33
34{
35
36 ###### interface
37
38 options = {
39
40 services.stunnel = {
41
42 enable = mkOption {
43 type = types.bool;
44 default = false;
45 description = lib.mdDoc "Whether to enable the stunnel TLS tunneling service.";
46 };
47
48 user = mkOption {
49 type = with types; nullOr str;
50 default = "nobody";
51 description = lib.mdDoc "The user under which stunnel runs.";
52 };
53
54 group = mkOption {
55 type = with types; nullOr str;
56 default = "nogroup";
57 description = lib.mdDoc "The group under which stunnel runs.";
58 };
59
60 logLevel = mkOption {
61 type = types.enum [ "emerg" "alert" "crit" "err" "warning" "notice" "info" "debug" ];
62 default = "info";
63 description = lib.mdDoc "Verbosity of stunnel output.";
64 };
65
66 fipsMode = mkOption {
67 type = types.bool;
68 default = false;
69 description = lib.mdDoc "Enable FIPS 140-2 mode required for compliance.";
70 };
71
72 enableInsecureSSLv3 = mkOption {
73 type = types.bool;
74 default = false;
75 description = lib.mdDoc "Enable support for the insecure SSLv3 protocol.";
76 };
77
78
79 servers = mkOption {
80 description = lib.mdDoc ''
81 Define the server configurations.
82
83 See "SERVICE-LEVEL OPTIONS" in {manpage}`stunnel(8)`.
84 '';
85 type = with types; attrsOf (attrsOf (nullOr (oneOf [bool int str])));
86 example = {
87 fancyWebserver = {
88 accept = 443;
89 connect = 8080;
90 cert = "/path/to/pem/file";
91 };
92 };
93 default = { };
94 };
95
96 clients = mkOption {
97 description = lib.mdDoc ''
98 Define the client configurations.
99
100 By default, verifyChain and OCSPaia are enabled and a CAFile is provided from pkgs.cacert.
101
102 See "SERVICE-LEVEL OPTIONS" in {manpage}`stunnel(8)`.
103 '';
104 type = with types; attrsOf (attrsOf (nullOr (oneOf [bool int str])));
105
106 apply = let
107 applyDefaults = c:
108 {
109 CAFile = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
110 OCSPaia = true;
111 verifyChain = true;
112 } // c;
113 setCheckHostFromVerifyHostname = c:
114 # To preserve backward-compatibility with the old NixOS stunnel module
115 # definition, allow "verifyHostname" as an alias for "checkHost".
116 c // {
117 checkHost = c.checkHost or c.verifyHostname or null;
118 verifyHostname = null; # Not a real stunnel configuration setting
119 };
120 forceClient = c: c // { client = true; };
121 in mapAttrs (_: c: forceClient (setCheckHostFromVerifyHostname (applyDefaults c)));
122
123 example = {
124 foobar = {
125 accept = "0.0.0.0:8080";
126 connect = "nixos.org:443";
127 verifyChain = false;
128 };
129 };
130 default = { };
131 };
132 };
133 };
134
135
136 ###### implementation
137
138 config = mkIf cfg.enable {
139
140 assertions = concatLists [
141 (singleton {
142 assertion = (length (attrValues cfg.servers) != 0) || ((length (attrValues cfg.clients)) != 0);
143 message = "stunnel: At least one server- or client-configuration has to be present.";
144 })
145
146 (mapAttrsToList verifyChainPathAssert cfg.clients)
147 (mapAttrsToList (verifyRequiredField "client" "accept") cfg.clients)
148 (mapAttrsToList (verifyRequiredField "client" "connect") cfg.clients)
149 (mapAttrsToList (verifyRequiredField "server" "accept") cfg.servers)
150 (mapAttrsToList (verifyRequiredField "server" "cert") cfg.servers)
151 (mapAttrsToList (verifyRequiredField "server" "connect") cfg.servers)
152 ];
153
154 environment.systemPackages = [ pkgs.stunnel ];
155
156 environment.etc."stunnel.cfg".text = ''
157 ${ optionalString (cfg.user != null) "setuid = ${cfg.user}" }
158 ${ optionalString (cfg.group != null) "setgid = ${cfg.group}" }
159
160 debug = ${cfg.logLevel}
161
162 ${ optionalString cfg.fipsMode "fips = yes" }
163 ${ optionalString cfg.enableInsecureSSLv3 "options = -NO_SSLv3" }
164
165 ; ----- SERVER CONFIGURATIONS -----
166 ${ generateConfig cfg.servers }
167
168 ; ----- CLIENT CONFIGURATIONS -----
169 ${ generateConfig cfg.clients }
170 '';
171
172 systemd.services.stunnel = {
173 description = "stunnel TLS tunneling service";
174 after = [ "network.target" ];
175 wants = [ "network.target" ];
176 wantedBy = [ "multi-user.target" ];
177 restartTriggers = [ config.environment.etc."stunnel.cfg".source ];
178 serviceConfig = {
179 ExecStart = "${pkgs.stunnel}/bin/stunnel ${config.environment.etc."stunnel.cfg".source}";
180 Type = "forking";
181 };
182 };
183
184 meta.maintainers = with maintainers; [
185 # Server side
186 lschuermann
187 # Client side
188 das_j
189 ];
190 };
191
192}