summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Hensing <robert@roberthensing.nl>2025-01-08 14:42:51 +0100
committerRobert Hensing <robert@roberthensing.nl>2025-01-08 17:26:21 +0100
commit398f612640aee23a5aa6e895e2f701d6929ee178 (patch)
tree5d3faa1a4bbf8c0fe439f08043f10fa4e5560c4a
parentformat (diff)
downloadnixpkgs-398f612640aee23a5aa6e895e2f701d6929ee178.tar.gz
ghostunnel.services.default: init
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/ghostunnel-modular.nix120
-rw-r--r--pkgs/by-name/gh/ghostunnel/package.nix6
-rw-r--r--pkgs/by-name/gh/ghostunnel/service.nix229
4 files changed, 356 insertions, 0 deletions
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 41cbc8d658d7..fa2b8c88cfa3 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -390,6 +390,7 @@ in {
gerrit = handleTest ./gerrit.nix {};
geth = handleTest ./geth.nix {};
ghostunnel = handleTest ./ghostunnel.nix {};
+ ghostunnel-modular = runTest ./ghostunnel-modular.nix;
gitdaemon = handleTest ./gitdaemon.nix {};
gitea = handleTest ./gitea.nix { giteaPackage = pkgs.gitea; };
github-runner = handleTest ./github-runner.nix {};
diff --git a/nixos/tests/ghostunnel-modular.nix b/nixos/tests/ghostunnel-modular.nix
new file mode 100644
index 000000000000..a1f17bc03400
--- /dev/null
+++ b/nixos/tests/ghostunnel-modular.nix
@@ -0,0 +1,120 @@
+{ hostPkgs, lib, ... }:
+{
+ _class = "nixosTest";
+ name = "ghostunnel";
+ 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 =
+ { pkgs, ... }:
+ {
+ system.services."ghostunnel-plain-old" = {
+ imports = [ pkgs.ghostunnel.services.default ];
+ ghostunnel = {
+ listen = "0.0.0.0:443";
+ cert = "/root/service-cert.pem";
+ key = "/root/service-key.pem";
+ disableAuthentication = true;
+ target = "backend:80";
+ unsafeTarget = true;
+ };
+ };
+ system.services."ghostunnel-client-cert" = {
+ imports = [ pkgs.ghostunnel.services.default ];
+ ghostunnel = {
+ 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("${hostPkgs.openssl}/bin/openssl genrsa -out ca-key.pem 4096")
+ 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")
+
+ # Create service
+ cmd("${hostPkgs.openssl}/bin/openssl genrsa -out service-key.pem 4096")
+ cmd("${hostPkgs.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("${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")
+
+ # Create client
+ cmd("${hostPkgs.openssl}/bin/openssl genrsa -out client-key.pem 4096")
+ cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr")
+ cmd("echo extendedKeyUsage = clientAuth > extfile-client.cnf")
+ 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")
+
+ 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 lib.maintainers; [
+ roberth
+ ];
+}
diff --git a/pkgs/by-name/gh/ghostunnel/package.nix b/pkgs/by-name/gh/ghostunnel/package.nix
index e5946c5c682f..665184d1415f 100644
--- a/pkgs/by-name/gh/ghostunnel/package.nix
+++ b/pkgs/by-name/gh/ghostunnel/package.nix
@@ -4,6 +4,7 @@
fetchFromGitHub,
lib,
nixosTests,
+ ghostunnel,
}:
buildGoModule rec {
@@ -33,6 +34,11 @@ buildGoModule rec {
podman = nixosTests.podman-tls-ghostunnel;
};
+ passthru.services.default = {
+ imports = [ ./service.nix ];
+ ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage
+ };
+
meta = with lib; {
broken = stdenv.hostPlatform.isDarwin;
description = "TLS proxy with mutual authentication support for securing non-TLS backend applications";
diff --git a/pkgs/by-name/gh/ghostunnel/service.nix b/pkgs/by-name/gh/ghostunnel/service.nix
new file mode 100644
index 000000000000..dbc49478a6cb
--- /dev/null
+++ b/pkgs/by-name/gh/ghostunnel/service.nix
@@ -0,0 +1,229 @@
+{
+ lib,
+ config,
+ options,
+ pkgs,
+ ...
+}:
+let
+ inherit (lib)
+ concatStringsSep
+ escapeShellArg
+ mkDefault
+ mkIf
+ mkOption
+ optional
+ types
+ ;
+ cfg = config.ghostunnel;
+
+in
+{
+ # https://nixos.org/manual/nixos/unstable/#modular-services
+ _class = "service";
+ options = {
+ ghostunnel = {
+ package = mkOption {
+ description = "Package to use for ghostunnel";
+ type = types.package;
+ };
+
+ 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 `/proc/*/cmdline`.
+
+ Specify this or `cert` and `key`.
+ '';
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ cert = mkOption {
+ description = ''
+ Path to certificate (PEM with certificate chain).
+
+ Not required if `keystore` is set.
+ '';
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ key = mkOption {
+ description = ''
+ Path to certificate private key (PEM with private key).
+
+ Not required if `keystore` is set.
+ '';
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ cacert = mkOption {
+ description = ''
+ Path to CA bundle file (PEM/X509). Uses system trust store if `null`.
+ '';
+ 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 `ghostunnel server` (shell syntax)";
+ 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;
+ };
+ };
+ };
+
+ config = {
+ assertions = [
+ {
+ message = ''
+ At least one access control flag is required.
+ Set at least one of:
+ - ${options.ghostunnel.disableAuthentication}
+ - ${options.ghostunnel.allowAll}
+ - ${options.ghostunnel.allowCN}
+ - ${options.ghostunnel.allowOU}
+ - ${options.ghostunnel.allowDNS}
+ - ${options.ghostunnel.allowURI}
+ '';
+ assertion =
+ cfg.disableAuthentication
+ || cfg.allowAll
+ || cfg.allowCN != [ ]
+ || cfg.allowOU != [ ]
+ || cfg.allowDNS != [ ]
+ || cfg.allowURI != [ ];
+ }
+ ];
+
+ ghostunnel = {
+ # 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.
+ cacert = mkIf cfg.disableAuthentication (mkDefault null);
+ };
+
+ # TODO assertions
+
+ process = {
+ executable = pkgs.writeScriptBin "run-ghostunnel" ''
+ #!${pkgs.runtimeShell}
+ exec ${lib.getExe cfg.package} ${
+ concatStringsSep " " (
+ optional (cfg.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
+ ++ optional (cfg.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
+ ++ optional (cfg.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
+ ++ optional (cfg.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
+ ++ [
+ "server"
+ "--listen"
+ cfg.listen
+ "--target"
+ cfg.target
+ ]
+ ++ optional cfg.allowAll "--allow-all"
+ ++ map (v: "--allow-cn=${escapeShellArg v}") cfg.allowCN
+ ++ map (v: "--allow-ou=${escapeShellArg v}") cfg.allowOU
+ ++ map (v: "--allow-dns=${escapeShellArg v}") cfg.allowDNS
+ ++ map (v: "--allow-uri=${escapeShellArg v}") cfg.allowURI
+ ++ optional cfg.disableAuthentication "--disable-authentication"
+ ++ optional cfg.unsafeTarget "--unsafe-target"
+ ++ [ cfg.extraArguments ]
+ )
+ }
+ '';
+ };
+
+ # refine the service
+ systemd.service = {
+ after = [ "network.target" ];
+ wants = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ Restart = "always";
+ AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+ DynamicUser = true;
+ LoadCredential =
+ optional (cfg.keystore != null) "keystore:${cfg.keystore}"
+ ++ optional (cfg.cert != null) "cert:${cfg.cert}"
+ ++ optional (cfg.key != null) "key:${cfg.key}"
+ ++ optional (cfg.cacert != null) "cacert:${cfg.cacert}";
+ };
+ };
+ };
+}