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 verifyChainPathAssert = n: c: {
11 assertion = c.verifyHostname == null || (c.verifyChain || c.verifyPeer);
12 message = "stunnel: \"${n}\" client configuration - hostname verification " +
13 "is not possible without either verifyChain or verifyPeer enabled";
14 };
15
16 serverConfig = {
17 options = {
18 accept = mkOption {
19 type = types.either types.str types.int;
20 description = ''
21 On which [host:]port stunnel should listen for incoming TLS connections.
22 Note that unlike other softwares stunnel ipv6 address need no brackets,
23 so to listen on all IPv6 addresses on port 1234 one would use ':::1234'.
24 '';
25 };
26
27 connect = mkOption {
28 type = types.int;
29 description = "To which port the decrypted connection should be forwarded.";
30 };
31
32 cert = mkOption {
33 type = types.path;
34 description = "File containing both the private and public keys.";
35 };
36 };
37 };
38
39 clientConfig = {
40 options = {
41 accept = mkOption {
42 type = types.str;
43 description = "IP:Port on which connections should be accepted.";
44 };
45
46 connect = mkOption {
47 type = types.str;
48 description = "IP:Port destination to connect to.";
49 };
50
51 verifyChain = mkOption {
52 type = types.bool;
53 default = true;
54 description = "Check if the provided certificate has a valid certificate chain (against CAPath).";
55 };
56
57 verifyPeer = mkOption {
58 type = types.bool;
59 default = false;
60 description = "Check if the provided certificate is contained in CAPath.";
61 };
62
63 CAPath = mkOption {
64 type = types.nullOr types.path;
65 default = null;
66 description = "Path to a directory containing certificates to validate against.";
67 };
68
69 CAFile = mkOption {
70 type = types.nullOr types.path;
71 default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
72 description = "Path to a file containing certificates to validate against.";
73 };
74
75 verifyHostname = mkOption {
76 type = with types; nullOr str;
77 default = null;
78 description = "If set, stunnel checks if the provided certificate is valid for the given hostname.";
79 };
80 };
81 };
82
83
84in
85
86{
87
88 ###### interface
89
90 options = {
91
92 services.stunnel = {
93
94 enable = mkOption {
95 type = types.bool;
96 default = false;
97 description = "Whether to enable the stunnel TLS tunneling service.";
98 };
99
100 user = mkOption {
101 type = with types; nullOr str;
102 default = "nobody";
103 description = "The user under which stunnel runs.";
104 };
105
106 group = mkOption {
107 type = with types; nullOr str;
108 default = "nogroup";
109 description = "The group under which stunnel runs.";
110 };
111
112 logLevel = mkOption {
113 type = types.enum [ "emerg" "alert" "crit" "err" "warning" "notice" "info" "debug" ];
114 default = "info";
115 description = "Verbosity of stunnel output.";
116 };
117
118 fipsMode = mkOption {
119 type = types.bool;
120 default = false;
121 description = "Enable FIPS 140-2 mode required for compliance.";
122 };
123
124 enableInsecureSSLv3 = mkOption {
125 type = types.bool;
126 default = false;
127 description = "Enable support for the insecure SSLv3 protocol.";
128 };
129
130
131 servers = mkOption {
132 description = "Define the server configuations.";
133 type = with types; attrsOf (submodule serverConfig);
134 example = {
135 fancyWebserver = {
136 accept = 443;
137 connect = 8080;
138 cert = "/path/to/pem/file";
139 };
140 };
141 default = { };
142 };
143
144 clients = mkOption {
145 description = "Define the client configurations.";
146 type = with types; attrsOf (submodule clientConfig);
147 example = {
148 foobar = {
149 accept = "0.0.0.0:8080";
150 connect = "nixos.org:443";
151 verifyChain = false;
152 };
153 };
154 default = { };
155 };
156 };
157 };
158
159
160 ###### implementation
161
162 config = mkIf cfg.enable {
163
164 assertions = concatLists [
165 (singleton {
166 assertion = (length (attrValues cfg.servers) != 0) || ((length (attrValues cfg.clients)) != 0);
167 message = "stunnel: At least one server- or client-configuration has to be present.";
168 })
169
170 (mapAttrsToList verifyChainPathAssert cfg.clients)
171 ];
172
173 environment.systemPackages = [ pkgs.stunnel ];
174
175 environment.etc."stunnel.cfg".text = ''
176 ${ if cfg.user != null then "setuid = ${cfg.user}" else "" }
177 ${ if cfg.group != null then "setgid = ${cfg.group}" else "" }
178
179 debug = ${cfg.logLevel}
180
181 ${ optionalString cfg.fipsMode "fips = yes" }
182 ${ optionalString cfg.enableInsecureSSLv3 "options = -NO_SSLv3" }
183
184 ; ----- SERVER CONFIGURATIONS -----
185 ${ lib.concatStringsSep "\n"
186 (lib.mapAttrsToList
187 (n: v: ''
188 [${n}]
189 accept = ${toString v.accept}
190 connect = ${toString v.connect}
191 cert = ${v.cert}
192
193 '')
194 cfg.servers)
195 }
196
197 ; ----- CLIENT CONFIGURATIONS -----
198 ${ lib.concatStringsSep "\n"
199 (lib.mapAttrsToList
200 (n: v: ''
201 [${n}]
202 client = yes
203 accept = ${v.accept}
204 connect = ${v.connect}
205 verifyChain = ${yesNo v.verifyChain}
206 verifyPeer = ${yesNo v.verifyPeer}
207 ${optionalString (v.CAPath != null) "CApath = ${v.CAPath}"}
208 ${optionalString (v.CAFile != null) "CAFile = ${v.CAFile}"}
209 ${optionalString (v.verifyHostname != null) "checkHost = ${v.verifyHostname}"}
210 OCSPaia = yes
211
212 '')
213 cfg.clients)
214 }
215 '';
216
217 systemd.services.stunnel = {
218 description = "stunnel TLS tunneling service";
219 after = [ "network.target" ];
220 wants = [ "network.target" ];
221 wantedBy = [ "multi-user.target" ];
222 restartTriggers = [ config.environment.etc."stunnel.cfg".source ];
223 serviceConfig = {
224 ExecStart = "${pkgs.stunnel}/bin/stunnel ${config.environment.etc."stunnel.cfg".source}";
225 Type = "forking";
226 };
227 };
228
229 meta.maintainers = with maintainers; [
230 # Server side
231 lschuermann
232 # Client side
233 das_j
234 ];
235 };
236
237}