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