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