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