Skip to content

Commit 398f612

Browse files
committed
ghostunnel.services.default: init
1 parent 30c57d5 commit 398f612

File tree

4 files changed

+356
-0
lines changed

4 files changed

+356
-0
lines changed

nixos/tests/all-tests.nix

+1
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ in {
390390
gerrit = handleTest ./gerrit.nix {};
391391
geth = handleTest ./geth.nix {};
392392
ghostunnel = handleTest ./ghostunnel.nix {};
393+
ghostunnel-modular = runTest ./ghostunnel-modular.nix;
393394
gitdaemon = handleTest ./gitdaemon.nix {};
394395
gitea = handleTest ./gitea.nix { giteaPackage = pkgs.gitea; };
395396
github-runner = handleTest ./github-runner.nix {};

nixos/tests/ghostunnel-modular.nix

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
{ hostPkgs, lib, ... }:
2+
{
3+
_class = "nixosTest";
4+
name = "ghostunnel";
5+
nodes = {
6+
backend =
7+
{ pkgs, ... }:
8+
{
9+
services.nginx.enable = true;
10+
services.nginx.virtualHosts."backend".root = pkgs.runCommand "webroot" { } ''
11+
mkdir $out
12+
echo hi >$out/hi.txt
13+
'';
14+
networking.firewall.allowedTCPPorts = [ 80 ];
15+
};
16+
service =
17+
{ pkgs, ... }:
18+
{
19+
system.services."ghostunnel-plain-old" = {
20+
imports = [ pkgs.ghostunnel.services.default ];
21+
ghostunnel = {
22+
listen = "0.0.0.0:443";
23+
cert = "/root/service-cert.pem";
24+
key = "/root/service-key.pem";
25+
disableAuthentication = true;
26+
target = "backend:80";
27+
unsafeTarget = true;
28+
};
29+
};
30+
system.services."ghostunnel-client-cert" = {
31+
imports = [ pkgs.ghostunnel.services.default ];
32+
ghostunnel = {
33+
listen = "0.0.0.0:1443";
34+
cert = "/root/service-cert.pem";
35+
key = "/root/service-key.pem";
36+
cacert = "/root/ca.pem";
37+
target = "backend:80";
38+
allowCN = [ "client" ];
39+
unsafeTarget = true;
40+
};
41+
};
42+
networking.firewall.allowedTCPPorts = [
43+
443
44+
1443
45+
];
46+
};
47+
client =
48+
{ pkgs, ... }:
49+
{
50+
environment.systemPackages = [
51+
pkgs.curl
52+
];
53+
};
54+
};
55+
56+
testScript = ''
57+
58+
# prepare certificates
59+
60+
def cmd(command):
61+
print(f"+{command}")
62+
r = os.system(command)
63+
if r != 0:
64+
raise Exception(f"Command {command} failed with exit code {r}")
65+
66+
# Create CA
67+
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out ca-key.pem 4096")
68+
cmd("${hostPkgs.openssl}/bin/openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj '/C=NL/ST=Zuid-Holland/L=The Hague/O=Stevige Balken en Planken B.V./OU=OpSec/CN=Certificate Authority' -out ca.pem")
69+
70+
# Create service
71+
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out service-key.pem 4096")
72+
cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=service' -sha256 -new -key service-key.pem -out service.csr")
73+
cmd("echo subjectAltName = DNS:service,IP:127.0.0.1 >> extfile.cnf")
74+
cmd("echo extendedKeyUsage = serverAuth >> extfile.cnf")
75+
cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in service.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out service-cert.pem -extfile extfile.cnf")
76+
77+
# Create client
78+
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out client-key.pem 4096")
79+
cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr")
80+
cmd("echo extendedKeyUsage = clientAuth > extfile-client.cnf")
81+
cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile extfile-client.cnf")
82+
83+
cmd("ls -al")
84+
85+
start_all()
86+
87+
# Configuration
88+
service.copy_from_host("ca.pem", "/root/ca.pem")
89+
service.copy_from_host("service-cert.pem", "/root/service-cert.pem")
90+
service.copy_from_host("service-key.pem", "/root/service-key.pem")
91+
client.copy_from_host("ca.pem", "/root/ca.pem")
92+
client.copy_from_host("service-cert.pem", "/root/service-cert.pem")
93+
client.copy_from_host("client-cert.pem", "/root/client-cert.pem")
94+
client.copy_from_host("client-key.pem", "/root/client-key.pem")
95+
96+
backend.wait_for_unit("nginx.service")
97+
service.wait_for_unit("multi-user.target")
98+
service.wait_for_unit("multi-user.target")
99+
client.wait_for_unit("multi-user.target")
100+
101+
# Check assumptions before the real test
102+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter http://backend/hi.txt) <(echo hi)'")
103+
104+
# Plain old simple TLS can connect, ignoring cert
105+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --insecure https://service/hi.txt) <(echo hi)'")
106+
107+
# Plain old simple TLS provides correct signature with its cert
108+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service/hi.txt) <(echo hi)'")
109+
110+
# Client can authenticate with certificate
111+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cert /root/client-cert.pem --key /root/client-key.pem --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
112+
113+
# Client must authenticate with certificate
114+
client.fail("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
115+
'';
116+
117+
meta.maintainers = with lib.maintainers; [
118+
roberth
119+
];
120+
}

pkgs/by-name/gh/ghostunnel/package.nix

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
fetchFromGitHub,
55
lib,
66
nixosTests,
7+
ghostunnel,
78
}:
89

910
buildGoModule rec {
@@ -33,6 +34,11 @@ buildGoModule rec {
3334
podman = nixosTests.podman-tls-ghostunnel;
3435
};
3536

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

0 commit comments

Comments
 (0)