diff options
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/modules/module-list.nix | 2 | ||||
-rw-r--r-- | nixos/modules/programs/noisetorch.nix | 25 | ||||
-rw-r--r-- | nixos/modules/services/backup/duplicity.nix | 91 | ||||
-rw-r--r-- | nixos/modules/services/games/terraria.nix | 13 | ||||
-rw-r--r-- | nixos/modules/services/networking/ghostunnel.nix | 242 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/ghostunnel.nix | 104 |
7 files changed, 460 insertions, 18 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 121696b17b817..dea0ca13565dc 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -163,6 +163,7 @@ ./programs/neovim.nix ./programs/nm-applet.nix ./programs/npm.nix + ./programs/noisetorch.nix ./programs/oblogout.nix ./programs/partition-manager.nix ./programs/plotinus.nix @@ -689,6 +690,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 diff --git a/nixos/modules/programs/noisetorch.nix b/nixos/modules/programs/noisetorch.nix new file mode 100644 index 0000000000000..5f3b0c8f5d1ee --- /dev/null +++ b/nixos/modules/programs/noisetorch.nix @@ -0,0 +1,25 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let cfg = config.programs.noisetorch; +in { + options.programs.noisetorch = { + enable = mkEnableOption "noisetorch + setcap wrapper"; + + package = mkOption { + type = types.package; + default = pkgs.noisetorch; + description = '' + The noisetorch package to use. + ''; + }; + }; + + config = mkIf cfg.enable { + security.wrappers.noisetorch = { + source = "${cfg.package}/bin/noisetorch"; + capabilities = "cap_sys_resource=+ep"; + }; + }; +} diff --git a/nixos/modules/services/backup/duplicity.nix b/nixos/modules/services/backup/duplicity.nix index a8d5642486235..1f6883ed02b79 100644 --- a/nixos/modules/services/backup/duplicity.nix +++ b/nixos/modules/services/backup/duplicity.nix @@ -1,16 +1,17 @@ -{ config, lib, pkgs, ...}: +{ config, lib, pkgs, ... }: with lib; - let cfg = config.services.duplicity; stateDirectory = "/var/lib/duplicity"; - localTarget = if hasPrefix "file://" cfg.targetUrl + localTarget = + if hasPrefix "file://" cfg.targetUrl then removePrefix "file://" cfg.targetUrl else null; -in { +in +{ options.services.duplicity = { enable = mkEnableOption "backups with duplicity"; @@ -24,7 +25,7 @@ in { include = mkOption { type = types.listOf types.str; - default = []; + default = [ ]; example = [ "/home" ]; description = '' List of paths to include into the backups. See the FILE SELECTION @@ -35,7 +36,7 @@ in { exclude = mkOption { type = types.listOf types.str; - default = []; + default = [ ]; description = '' List of paths to exclude from backups. See the FILE SELECTION section in <citerefentry><refentrytitle>duplicity</refentrytitle> @@ -82,14 +83,60 @@ in { extraFlags = mkOption { type = types.listOf types.str; - default = []; - example = [ "--full-if-older-than" "1M" ]; + default = [ ]; + example = [ "--backend-retry-delay" "100" ]; description = '' Extra command-line flags passed to duplicity. See <citerefentry><refentrytitle>duplicity</refentrytitle> <manvolnum>1</manvolnum></citerefentry>. ''; }; + + fullIfOlderThan = mkOption { + type = types.str; + default = "never"; + example = "1M"; + description = '' + If <literal>"never"</literal> (the default) always do incremental + backups (the first backup will be a full backup, of course). If + <literal>"always"</literal> always do full backups. Otherwise, this + must be a string representing a duration. Full backups will be made + when the latest full backup is older than this duration. If this is not + the case, an incremental backup is performed. + ''; + }; + + cleanup = { + maxAge = mkOption { + type = types.nullOr types.str; + default = null; + example = "6M"; + description = '' + If non-null, delete all backup sets older than the given time. Old backup sets + will not be deleted if backup sets newer than time depend on them. + ''; + }; + maxFull = mkOption { + type = types.nullOr types.int; + default = null; + example = 2; + description = '' + If non-null, delete all backups sets that are older than the count:th last full + backup (in other words, keep the last count full backups and + associated incremental sets). + ''; + }; + maxIncr = mkOption { + type = types.nullOr types.int; + default = null; + example = 1; + description = '' + If non-null, delete incremental sets of all backups sets that are + older than the count:th last full backup (in other words, keep only + old full backups and not their increments). + ''; + }; + }; }; config = mkIf cfg.enable { @@ -99,18 +146,26 @@ in { environment.HOME = stateDirectory; - serviceConfig = { - ExecStart = '' - ${pkgs.duplicity}/bin/duplicity ${escapeShellArgs ( - [ - cfg.root - cfg.targetUrl - "--archive-dir" stateDirectory - ] + script = + let + target = escapeShellArg cfg.targetUrl; + extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags); + dup = "${pkgs.duplicity}/bin/duplicity"; + in + '' + set -x + ${dup} cleanup ${target} --force ${extra} + ${lib.optionalString (cfg.cleanup.maxAge != null) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"} + ${lib.optionalString (cfg.cleanup.maxFull != null) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"} + ${lib.optionalString (cfg.cleanup.maxIncr != null) "${dup} remove-all-incr-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"} + exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${lib.escapeShellArgs ( + [ cfg.root cfg.targetUrl ] ++ concatMap (p: [ "--include" p ]) cfg.include ++ concatMap (p: [ "--exclude" p ]) cfg.exclude - ++ cfg.extraFlags)} + ++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [ "--full-if-older-than" cfg.fullIfOlderThan ]) + )} ${extra} ''; + serviceConfig = { PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = "read-only"; @@ -130,7 +185,7 @@ in { assertions = singleton { # Duplicity will fail if the last file selection option is an include. It # is not always possible to detect but this simple case can be caught. - assertion = cfg.include != [] -> cfg.exclude != [] || cfg.extraFlags != []; + assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ]; message = '' Duplicity will fail if you only specify included paths ("Because the default is to include all files, the expression is redundant. Exiting diff --git a/nixos/modules/services/games/terraria.nix b/nixos/modules/services/games/terraria.nix index 34c8ff137d6a2..9e8e5ae8759aa 100644 --- a/nixos/modules/services/games/terraria.nix +++ b/nixos/modules/services/games/terraria.nix @@ -111,6 +111,13 @@ in default = false; description = "Disables automatic Universal Plug and Play."; }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Wheter to open ports in the firewall"; + }; + dataDir = mkOption { type = types.str; default = "/var/lib/terraria"; @@ -151,5 +158,11 @@ in ${pkgs.coreutils}/bin/chgrp terraria ${cfg.dataDir}/terraria.sock ''; }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + allowedUDPPorts = [ cfg.port ]; + }; + }; } diff --git a/nixos/modules/services/networking/ghostunnel.nix b/nixos/modules/services/networking/ghostunnel.nix new file mode 100644 index 0000000000000..58a51df6cca2a --- /dev/null +++ b/nixos/modules/services/networking/ghostunnel.nix @@ -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 + ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 2d96eeacf2214..420e6eaf2e871 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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 {}; diff --git a/nixos/tests/ghostunnel.nix b/nixos/tests/ghostunnel.nix new file mode 100644 index 0000000000000..a82cff8082b75 --- /dev/null +++ b/nixos/tests/ghostunnel.nix @@ -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 + ]; +} |