1{ config, lib, pkgs, ... }:
2let
3 inherit (lib)
4 attrValues
5 concatMap
6 concatStringsSep
7 escapeShellArg
8 literalExpression
9 mapAttrs'
10 mkDefault
11 mkEnableOption
12 mkPackageOption
13 mkIf
14 mkOption
15 nameValuePair
16 optional
17 types
18 ;
19
20 mainCfg = config.services.ghostunnel;
21
22 module = { config, name, ... }:
23 {
24 options = {
25
26 listen = mkOption {
27 description = ''
28 Address and port to listen on (can be HOST:PORT, unix:PATH).
29 '';
30 type = types.str;
31 };
32
33 target = mkOption {
34 description = ''
35 Address to forward connections to (can be HOST:PORT or unix:PATH).
36 '';
37 type = types.str;
38 };
39
40 keystore = mkOption {
41 description = ''
42 Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
43
44 NB: storepass is not supported because it would expose credentials via `/proc/*/cmdline`.
45
46 Specify this or `cert` and `key`.
47 '';
48 type = types.nullOr types.str;
49 default = null;
50 };
51
52 cert = mkOption {
53 description = ''
54 Path to certificate (PEM with certificate chain).
55
56 Not required if `keystore` is set.
57 '';
58 type = types.nullOr types.str;
59 default = null;
60 };
61
62 key = mkOption {
63 description = ''
64 Path to certificate private key (PEM with private key).
65
66 Not required if `keystore` is set.
67 '';
68 type = types.nullOr types.str;
69 default = null;
70 };
71
72 cacert = mkOption {
73 description = ''
74 Path to CA bundle file (PEM/X509). Uses system trust store if `null`.
75 '';
76 type = types.nullOr types.str;
77 };
78
79 disableAuthentication = mkOption {
80 description = ''
81 Disable client authentication, no client certificate will be required.
82 '';
83 type = types.bool;
84 default = false;
85 };
86
87 allowAll = mkOption {
88 description = ''
89 If true, allow all clients, do not check client cert subject.
90 '';
91 type = types.bool;
92 default = false;
93 };
94
95 allowCN = mkOption {
96 description = ''
97 Allow client if common name appears in the list.
98 '';
99 type = types.listOf types.str;
100 default = [];
101 };
102
103 allowOU = mkOption {
104 description = ''
105 Allow client if organizational unit name appears in the list.
106 '';
107 type = types.listOf types.str;
108 default = [];
109 };
110
111 allowDNS = mkOption {
112 description = ''
113 Allow client if DNS subject alternative name appears in the list.
114 '';
115 type = types.listOf types.str;
116 default = [];
117 };
118
119 allowURI = mkOption {
120 description = ''
121 Allow client if URI subject alternative name appears in the list.
122 '';
123 type = types.listOf types.str;
124 default = [];
125 };
126
127 extraArguments = mkOption {
128 description = "Extra arguments to pass to `ghostunnel server`";
129 type = types.separatedString " ";
130 default = "";
131 };
132
133 unsafeTarget = mkOption {
134 description = ''
135 If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
136
137 This is meant to protect against accidental unencrypted traffic on
138 untrusted networks.
139 '';
140 type = types.bool;
141 default = false;
142 };
143
144 # Definitions to apply at the root of the NixOS configuration.
145 atRoot = mkOption {
146 internal = true;
147 };
148 };
149
150 # Clients should not be authenticated with the public root certificates
151 # (afaict, it doesn't make sense), so we only provide that default when
152 # client cert auth is disabled.
153 config.cacert = mkIf config.disableAuthentication (mkDefault null);
154
155 config.atRoot = {
156 assertions = [
157 { message = ''
158 services.ghostunnel.servers.${name}: At least one access control flag is required.
159 Set at least one of:
160 - services.ghostunnel.servers.${name}.disableAuthentication
161 - services.ghostunnel.servers.${name}.allowAll
162 - services.ghostunnel.servers.${name}.allowCN
163 - services.ghostunnel.servers.${name}.allowOU
164 - services.ghostunnel.servers.${name}.allowDNS
165 - services.ghostunnel.servers.${name}.allowURI
166 '';
167 assertion = config.disableAuthentication
168 || config.allowAll
169 || config.allowCN != []
170 || config.allowOU != []
171 || config.allowDNS != []
172 || config.allowURI != []
173 ;
174 }
175 ];
176
177 systemd.services."ghostunnel-server-${name}" = {
178 after = [ "network.target" ];
179 wants = [ "network.target" ];
180 wantedBy = [ "multi-user.target" ];
181 serviceConfig = {
182 Restart = "always";
183 AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
184 DynamicUser = true;
185 LoadCredential = optional (config.keystore != null) "keystore:${config.keystore}"
186 ++ optional (config.cert != null) "cert:${config.cert}"
187 ++ optional (config.key != null) "key:${config.key}"
188 ++ optional (config.cacert != null) "cacert:${config.cacert}";
189 };
190 script = concatStringsSep " " (
191 [ "${mainCfg.package}/bin/ghostunnel" ]
192 ++ optional (config.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
193 ++ optional (config.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
194 ++ optional (config.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
195 ++ optional (config.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
196 ++ [
197 "server"
198 "--listen ${config.listen}"
199 "--target ${config.target}"
200 ] ++ optional config.allowAll "--allow-all"
201 ++ map (v: "--allow-cn=${escapeShellArg v}") config.allowCN
202 ++ map (v: "--allow-ou=${escapeShellArg v}") config.allowOU
203 ++ map (v: "--allow-dns=${escapeShellArg v}") config.allowDNS
204 ++ map (v: "--allow-uri=${escapeShellArg v}") config.allowURI
205 ++ optional config.disableAuthentication "--disable-authentication"
206 ++ optional config.unsafeTarget "--unsafe-target"
207 ++ [ config.extraArguments ]
208 );
209 };
210 };
211 };
212
213in
214{
215
216 options = {
217 services.ghostunnel.enable = mkEnableOption "ghostunnel";
218
219 services.ghostunnel.package = mkPackageOption pkgs "ghostunnel" { };
220
221 services.ghostunnel.servers = mkOption {
222 description = ''
223 Server mode ghostunnels (TLS listener -> plain TCP/UNIX target)
224 '';
225 type = types.attrsOf (types.submodule module);
226 default = {};
227 };
228 };
229
230 config = mkIf mainCfg.enable {
231 assertions = lib.mkMerge (map (v: v.atRoot.assertions) (attrValues mainCfg.servers));
232 systemd = lib.mkMerge (map (v: v.atRoot.systemd) (attrValues mainCfg.servers));
233 };
234
235 meta.maintainers = with lib.maintainers; [
236 roberth
237 ];
238}