nixos/ghostunnel: init

This commit is contained in:
Robert Hensing 2021-05-20 01:15:28 +02:00
parent 76993d0b76
commit dc9cb63de4
5 changed files with 351 additions and 0 deletions

View File

@ -689,6 +689,7 @@
./services/networking/gale.nix
./services/networking/gateone.nix
./services/networking/gdomap.nix
./services/networking/ghostunnel.nix
./services/networking/git-daemon.nix
./services/networking/gnunet.nix
./services/networking/go-neb.nix

View File

@ -0,0 +1,242 @@
{ config, lib, pkgs, ... }:
let
inherit (lib)
attrValues
concatMap
concatStringsSep
escapeShellArg
literalExample
mapAttrs'
mkDefault
mkEnableOption
mkIf
mkOption
nameValuePair
optional
types
;
mainCfg = config.services.ghostunnel;
module = { config, name, ... }:
{
options = {
listen = mkOption {
description = ''
Address and port to listen on (can be HOST:PORT, unix:PATH).
'';
type = types.str;
};
target = mkOption {
description = ''
Address to forward connections to (can be HOST:PORT or unix:PATH).
'';
type = types.str;
};
keystore = mkOption {
description = ''
Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
NB: storepass is not supported because it would expose credentials via <code>/proc/*/cmdline</code>.
Specify this or <code>cert</code> and <code>key</code>.
'';
type = types.nullOr types.str;
default = null;
};
cert = mkOption {
description = ''
Path to certificate (PEM with certificate chain).
Not required if <code>keystore</code> is set.
'';
type = types.nullOr types.str;
default = null;
};
key = mkOption {
description = ''
Path to certificate private key (PEM with private key).
Not required if <code>keystore</code> is set.
'';
type = types.nullOr types.str;
default = null;
};
cacert = mkOption {
description = ''
Path to CA bundle file (PEM/X509). Uses system trust store if <code>null</code>.
'';
type = types.nullOr types.str;
};
disableAuthentication = mkOption {
description = ''
Disable client authentication, no client certificate will be required.
'';
type = types.bool;
default = false;
};
allowAll = mkOption {
description = ''
If true, allow all clients, do not check client cert subject.
'';
type = types.bool;
default = false;
};
allowCN = mkOption {
description = ''
Allow client if common name appears in the list.
'';
type = types.listOf types.str;
default = [];
};
allowOU = mkOption {
description = ''
Allow client if organizational unit name appears in the list.
'';
type = types.listOf types.str;
default = [];
};
allowDNS = mkOption {
description = ''
Allow client if DNS subject alternative name appears in the list.
'';
type = types.listOf types.str;
default = [];
};
allowURI = mkOption {
description = ''
Allow client if URI subject alternative name appears in the list.
'';
type = types.listOf types.str;
default = [];
};
extraArguments = mkOption {
description = "Extra arguments to pass to <code>ghostunnel server</code>";
type = types.separatedString " ";
default = "";
};
unsafeTarget = mkOption {
description = ''
If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
This is meant to protect against accidental unencrypted traffic on
untrusted networks.
'';
type = types.bool;
default = false;
};
# Definitions to apply at the root of the NixOS configuration.
atRoot = mkOption {
internal = true;
};
};
# Clients should not be authenticated with the public root certificates
# (afaict, it doesn't make sense), so we only provide that default when
# client cert auth is disabled.
config.cacert = mkIf config.disableAuthentication (mkDefault null);
config.atRoot = {
assertions = [
{ message = ''
services.ghostunnel.servers.${name}: At least one access control flag is required.
Set at least one of:
- services.ghostunnel.servers.${name}.disableAuthentication
- services.ghostunnel.servers.${name}.allowAll
- services.ghostunnel.servers.${name}.allowCN
- services.ghostunnel.servers.${name}.allowOU
- services.ghostunnel.servers.${name}.allowDNS
- services.ghostunnel.servers.${name}.allowURI
'';
assertion = config.disableAuthentication
|| config.allowAll
|| config.allowCN != []
|| config.allowOU != []
|| config.allowDNS != []
|| config.allowURI != []
;
}
];
systemd.services."ghostunnel-server-${name}" = {
after = [ "network.target" ];
wants = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "always";
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
DynamicUser = true;
LoadCredential = optional (config.keystore != null) "keystore:${config.keystore}"
++ optional (config.cert != null) "cert:${config.cert}"
++ optional (config.key != null) "key:${config.key}"
++ optional (config.cacert != null) "cacert:${config.cacert}";
};
script = concatStringsSep " " (
[ "${mainCfg.package}/bin/ghostunnel" ]
++ optional (config.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
++ optional (config.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
++ optional (config.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
++ optional (config.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
++ [
"server"
"--listen ${config.listen}"
"--target ${config.target}"
] ++ optional config.allowAll "--allow-all"
++ map (v: "--allow-cn=${escapeShellArg v}") config.allowCN
++ map (v: "--allow-ou=${escapeShellArg v}") config.allowOU
++ map (v: "--allow-dns=${escapeShellArg v}") config.allowDNS
++ map (v: "--allow-uri=${escapeShellArg v}") config.allowURI
++ optional config.disableAuthentication "--disable-authentication"
++ optional config.unsafeTarget "--unsafe-target"
++ [ config.extraArguments ]
);
};
};
};
in
{
options = {
services.ghostunnel.enable = mkEnableOption "ghostunnel";
services.ghostunnel.package = mkOption {
description = "The ghostunnel package to use.";
type = types.package;
default = pkgs.ghostunnel;
defaultText = literalExample ''pkgs.ghostunnel'';
};
services.ghostunnel.servers = mkOption {
description = ''
Server mode ghostunnels (TLS listener -> plain TCP/UNIX target)
'';
type = types.attrsOf (types.submodule module);
default = {};
};
};
config = mkIf mainCfg.enable {
assertions = lib.mkMerge (map (v: v.atRoot.assertions) (attrValues mainCfg.servers));
systemd = lib.mkMerge (map (v: v.atRoot.systemd) (attrValues mainCfg.servers));
};
meta.maintainers = with lib.maintainers; [
roberth
];
}

View File

@ -135,6 +135,7 @@ in
fsck = handleTest ./fsck.nix {};
ft2-clone = handleTest ./ft2-clone.nix {};
gerrit = handleTest ./gerrit.nix {};
ghostunnel = handleTest ./ghostunnel.nix {};
gitdaemon = handleTest ./gitdaemon.nix {};
gitea = handleTest ./gitea.nix {};
gitlab = handleTest ./gitlab.nix {};

104
nixos/tests/ghostunnel.nix Normal file
View File

@ -0,0 +1,104 @@
{ pkgs, ... }: import ./make-test-python.nix {
nodes = {
backend = { pkgs, ... }: {
services.nginx.enable = true;
services.nginx.virtualHosts."backend".root = pkgs.runCommand "webroot" {} ''
mkdir $out
echo hi >$out/hi.txt
'';
networking.firewall.allowedTCPPorts = [ 80 ];
};
service = { ... }: {
services.ghostunnel.enable = true;
services.ghostunnel.servers."plain-old" = {
listen = "0.0.0.0:443";
cert = "/root/service-cert.pem";
key = "/root/service-key.pem";
disableAuthentication = true;
target = "backend:80";
unsafeTarget = true;
};
services.ghostunnel.servers."client-cert" = {
listen = "0.0.0.0:1443";
cert = "/root/service-cert.pem";
key = "/root/service-key.pem";
cacert = "/root/ca.pem";
target = "backend:80";
allowCN = ["client"];
unsafeTarget = true;
};
networking.firewall.allowedTCPPorts = [ 443 1443 ];
};
client = { pkgs, ... }: {
environment.systemPackages = [
pkgs.curl
];
};
};
testScript = ''
# prepare certificates
def cmd(command):
print(f"+{command}")
r = os.system(command)
if r != 0:
raise Exception(f"Command {command} failed with exit code {r}")
# Create CA
cmd("${pkgs.openssl}/bin/openssl genrsa -out ca-key.pem 4096")
cmd("${pkgs.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")
# Create service
cmd("${pkgs.openssl}/bin/openssl genrsa -out service-key.pem 4096")
cmd("${pkgs.openssl}/bin/openssl req -subj '/CN=service' -sha256 -new -key service-key.pem -out service.csr")
cmd("echo subjectAltName = DNS:service,IP:127.0.0.1 >> extfile.cnf")
cmd("echo extendedKeyUsage = serverAuth >> extfile.cnf")
cmd("${pkgs.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")
# Create client
cmd("${pkgs.openssl}/bin/openssl genrsa -out client-key.pem 4096")
cmd("${pkgs.openssl}/bin/openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr")
cmd("echo extendedKeyUsage = clientAuth > extfile-client.cnf")
cmd("${pkgs.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")
cmd("ls -al")
start_all()
# Configuration
service.copy_from_host("ca.pem", "/root/ca.pem")
service.copy_from_host("service-cert.pem", "/root/service-cert.pem")
service.copy_from_host("service-key.pem", "/root/service-key.pem")
client.copy_from_host("ca.pem", "/root/ca.pem")
client.copy_from_host("service-cert.pem", "/root/service-cert.pem")
client.copy_from_host("client-cert.pem", "/root/client-cert.pem")
client.copy_from_host("client-key.pem", "/root/client-key.pem")
backend.wait_for_unit("nginx.service")
service.wait_for_unit("multi-user.target")
service.wait_for_unit("multi-user.target")
client.wait_for_unit("multi-user.target")
# Check assumptions before the real test
client.succeed("bash -c 'diff <(curl -v --no-progress-meter http://backend/hi.txt) <(echo hi)'")
# Plain old simple TLS can connect, ignoring cert
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --insecure https://service/hi.txt) <(echo hi)'")
# Plain old simple TLS provides correct signature with its cert
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service/hi.txt) <(echo hi)'")
# Client can authenticate with certificate
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)'")
# Client must authenticate with certificate
client.fail("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
'';
meta.maintainers = with pkgs.lib.maintainers; [
roberth
];
}

View File

@ -2,6 +2,7 @@
buildGoModule,
fetchFromGitHub,
lib,
nixosTests,
}:
buildGoModule rec {
@ -23,4 +24,6 @@ buildGoModule rec {
license = licenses.asl20;
maintainers = with maintainers; [ roberth ];
};
passthru.tests.nixos = nixosTests.ghostunnel;
}