Skip to content

Commit 3d163d7

Browse files
committed
ghostunnel.services.default: init
1 parent a936535 commit 3d163d7

File tree

4 files changed

+341
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)