From c1c024efc0c87ce9b251cca72eeb2f69575b6cb6 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Mon, 28 Aug 2023 14:08:12 -0400 Subject: [PATCH 01/16] Refactor the submodule type for services.tsnsrv.services This should allow using it in multiple places. --- nixos/default.nix | 212 +++++++++++++++++++++++----------------------- 1 file changed, 107 insertions(+), 105 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index 5cdb27c..349d811 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -4,7 +4,112 @@ lib, ... }: { - options = with lib; { + options = with lib; let + serviceSubmodule = { + options = { + authKeyPath = lib.mkOption { + description = "Path to a file containing a tailscale auth key. Make this a secret"; + type = types.path; + default = config.services.tsnsrv.defaults.authKeyPath; + }; + + ephemeral = mkOption { + description = "Delete the tailnet participant shortly after it goes offline"; + type = types.bool; + default = false; + }; + + funnel = mkOption { + description = "Serve HTTP as a funnel, meaning that it is available on the public internet."; + type = types.bool; + default = false; + }; + + insecureHTTPS = mkOption { + description = "Disable TLS certificate validation for requests from upstream. Insecure."; + type = types.bool; + default = false; + }; + + listenAddr = mkOption { + description = "Address to listen on"; + type = types.str; + default = ":443"; + }; + + package = mkOption { + description = "Package to use for this tsnsrv service."; + default = config.services.tsnsrv.defaults.package; + type = types.package; + }; + + plaintext = mkOption { + description = "Whether to serve non-TLS-encrypted plaintext HTTP"; + type = types.bool; + default = false; + }; + + upstreamUnixAddr = mkOption { + description = "Connect only to the given UNIX Domain Socket"; + type = types.nullOr types.path; + default = null; + }; + + prefixes = mkOption { + description = "URL path prefixes to allow in forwarding. Acts as an allowlist but if unset, all prefixes are allowed."; + type = types.listOf types.str; + default = []; + }; + + stripPrefix = mkOption { + description = "Strip matched prefix from request to upstream. Probably should be true when allowlisting multiple prefixes."; + type = types.bool; + default = true; + }; + + whoisTimeout = mkOption { + description = "Maximum amount of time that a requestor lookup may take."; + type = types.nullOr types.str; + default = null; + }; + + suppressWhois = mkOption { + description = "Disable passing requestor information to upstream service"; + type = types.bool; + default = false; + }; + + upstreamHeaders = mkOption { + description = "Headers to set on requests to upstream."; + type = types.attrsOf types.str; + default = {}; + }; + + suppressTailnetDialer = mkOption { + description = "Disable using the tsnet-provided dialer, which can sometimes cause issues hitting addresses outside the tailnet"; + type = types.bool; + default = false; + }; + + readHeaderTimeout = mkOption { + description = ""; + type = types.nullOr types.str; + default = null; + }; + + toURL = mkOption { + description = "URL to forward HTTP requests to"; + type = types.str; + }; + + supplementalGroups = mkOption { + description = "List of groups to run the service under (in addition to the 'tsnsrv' group)"; + type = types.listOf types.str; + default = []; + }; + }; + }; + in { services.tsnsrv.enable = mkOption { description = "Enable tsnsrv"; type = types.bool; @@ -27,110 +132,7 @@ services.tsnsrv.services = mkOption { description = "tsnsrv services"; default = {}; - type = types.attrsOf (types.submodule { - options = { - authKeyPath = lib.mkOption { - description = "Path to a file containing a tailscale auth key. Make this a secret"; - type = types.path; - default = config.services.tsnsrv.defaults.authKeyPath; - }; - - ephemeral = mkOption { - description = "Delete the tailnet participant shortly after it goes offline"; - type = types.bool; - default = false; - }; - - funnel = mkOption { - description = "Serve HTTP as a funnel, meaning that it is available on the public internet."; - type = types.bool; - default = false; - }; - - insecureHTTPS = mkOption { - description = "Disable TLS certificate validation for requests from upstream. Insecure."; - type = types.bool; - default = false; - }; - - listenAddr = mkOption { - description = "Address to listen on"; - type = types.str; - default = ":443"; - }; - - package = mkOption { - description = "Package to use for this tsnsrv service."; - default = config.services.tsnsrv.defaults.package; - type = types.package; - }; - - plaintext = mkOption { - description = "Whether to serve non-TLS-encrypted plaintext HTTP"; - type = types.bool; - default = false; - }; - - upstreamUnixAddr = mkOption { - description = "Connect only to the given UNIX Domain Socket"; - type = types.nullOr types.path; - default = null; - }; - - prefixes = mkOption { - description = "URL path prefixes to allow in forwarding. Acts as an allowlist but if unset, all prefixes are allowed."; - type = types.listOf types.str; - default = []; - }; - - stripPrefix = mkOption { - description = "Strip matched prefix from request to upstream. Probably should be true when allowlisting multiple prefixes."; - type = types.bool; - default = true; - }; - - whoisTimeout = mkOption { - description = "Maximum amount of time that a requestor lookup may take."; - type = types.nullOr types.str; - default = null; - }; - - suppressWhois = mkOption { - description = "Disable passing requestor information to upstream service"; - type = types.bool; - default = false; - }; - - upstreamHeaders = mkOption { - description = "Headers to set on requests to upstream."; - type = types.attrsOf types.str; - default = {}; - }; - - suppressTailnetDialer = mkOption { - description = "Disable using the tsnet-provided dialer, which can sometimes cause issues hitting addresses outside the tailnet"; - type = types.bool; - default = false; - }; - - readHeaderTimeout = mkOption { - description = ""; - type = types.nullOr types.str; - default = null; - }; - - toURL = mkOption { - description = "URL to forward HTTP requests to"; - type = types.str; - }; - - supplementalGroups = mkOption { - description = "List of groups to run the service under (in addition to the 'tsnsrv' group)"; - type = types.listOf types.str; - default = []; - }; - }; - }); + type = types.attrsOf (types.submodule serviceSubmodule); example = false; }; }; From e99ea5e81eb105c837dc7f652b0a5534a6f700b1 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Mon, 28 Aug 2023 16:36:26 -0400 Subject: [PATCH 02/16] Refactor the cmdline generation some more Now, a function generates the commandline args (except for -stateDir, which is by necessity different). --- nixos/default.nix | 273 +++++++++++++++++++++++----------------------- 1 file changed, 138 insertions(+), 135 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index 349d811..38049e9 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -3,113 +3,142 @@ config, lib, ... -}: { - options = with lib; let - serviceSubmodule = { - options = { - authKeyPath = lib.mkOption { - description = "Path to a file containing a tailscale auth key. Make this a secret"; - type = types.path; - default = config.services.tsnsrv.defaults.authKeyPath; - }; - - ephemeral = mkOption { - description = "Delete the tailnet participant shortly after it goes offline"; - type = types.bool; - default = false; - }; - - funnel = mkOption { - description = "Serve HTTP as a funnel, meaning that it is available on the public internet."; - type = types.bool; - default = false; - }; - - insecureHTTPS = mkOption { - description = "Disable TLS certificate validation for requests from upstream. Insecure."; - type = types.bool; - default = false; - }; - - listenAddr = mkOption { - description = "Address to listen on"; - type = types.str; - default = ":443"; - }; - - package = mkOption { - description = "Package to use for this tsnsrv service."; - default = config.services.tsnsrv.defaults.package; - type = types.package; - }; - - plaintext = mkOption { - description = "Whether to serve non-TLS-encrypted plaintext HTTP"; - type = types.bool; - default = false; - }; - - upstreamUnixAddr = mkOption { - description = "Connect only to the given UNIX Domain Socket"; - type = types.nullOr types.path; - default = null; - }; - - prefixes = mkOption { - description = "URL path prefixes to allow in forwarding. Acts as an allowlist but if unset, all prefixes are allowed."; - type = types.listOf types.str; - default = []; - }; - - stripPrefix = mkOption { - description = "Strip matched prefix from request to upstream. Probably should be true when allowlisting multiple prefixes."; - type = types.bool; - default = true; - }; - - whoisTimeout = mkOption { - description = "Maximum amount of time that a requestor lookup may take."; - type = types.nullOr types.str; - default = null; - }; - - suppressWhois = mkOption { - description = "Disable passing requestor information to upstream service"; - type = types.bool; - default = false; - }; - - upstreamHeaders = mkOption { - description = "Headers to set on requests to upstream."; - type = types.attrsOf types.str; - default = {}; - }; - - suppressTailnetDialer = mkOption { - description = "Disable using the tsnet-provided dialer, which can sometimes cause issues hitting addresses outside the tailnet"; - type = types.bool; - default = false; - }; - - readHeaderTimeout = mkOption { - description = ""; - type = types.nullOr types.str; - default = null; - }; - - toURL = mkOption { - description = "URL to forward HTTP requests to"; - type = types.str; - }; - - supplementalGroups = mkOption { - description = "List of groups to run the service under (in addition to the 'tsnsrv' group)"; - type = types.listOf types.str; - default = []; - }; +}: let + serviceSubmodule = with lib; { + options = { + authKeyPath = mkOption { + description = "Path to a file containing a tailscale auth key. Make this a secret"; + type = types.path; + default = config.services.tsnsrv.defaults.authKeyPath; + }; + + ephemeral = mkOption { + description = "Delete the tailnet participant shortly after it goes offline"; + type = types.bool; + default = false; + }; + + funnel = mkOption { + description = "Serve HTTP as a funnel, meaning that it is available on the public internet."; + type = types.bool; + default = false; + }; + + insecureHTTPS = mkOption { + description = "Disable TLS certificate validation for requests from upstream. Insecure."; + type = types.bool; + default = false; + }; + + listenAddr = mkOption { + description = "Address to listen on"; + type = types.str; + default = ":443"; + }; + + package = mkOption { + description = "Package to use for this tsnsrv service."; + default = config.services.tsnsrv.defaults.package; + type = types.package; + }; + + plaintext = mkOption { + description = "Whether to serve non-TLS-encrypted plaintext HTTP"; + type = types.bool; + default = false; + }; + + upstreamUnixAddr = mkOption { + description = "Connect only to the given UNIX Domain Socket"; + type = types.nullOr types.path; + default = null; + }; + + prefixes = mkOption { + description = "URL path prefixes to allow in forwarding. Acts as an allowlist but if unset, all prefixes are allowed."; + type = types.listOf types.str; + default = []; + }; + + stripPrefix = mkOption { + description = "Strip matched prefix from request to upstream. Probably should be true when allowlisting multiple prefixes."; + type = types.bool; + default = true; + }; + + whoisTimeout = mkOption { + description = "Maximum amount of time that a requestor lookup may take."; + type = types.nullOr types.str; + default = null; + }; + + suppressWhois = mkOption { + description = "Disable passing requestor information to upstream service"; + type = types.bool; + default = false; + }; + + upstreamHeaders = mkOption { + description = "Headers to set on requests to upstream."; + type = types.attrsOf types.str; + default = {}; + }; + + suppressTailnetDialer = mkOption { + description = "Disable using the tsnet-provided dialer, which can sometimes cause issues hitting addresses outside the tailnet"; + type = types.bool; + default = false; + }; + + readHeaderTimeout = mkOption { + description = ""; + type = types.nullOr types.str; + default = null; + }; + + toURL = mkOption { + description = "URL to forward HTTP requests to"; + type = types.str; + }; + + supplementalGroups = mkOption { + description = "List of groups to run the service under (in addition to the 'tsnsrv' group)"; + type = types.listOf types.str; + default = []; }; }; - in { + }; + + serviceArgs = { + name, + service, + }: let + readHeaderTimeout = + if service.readHeaderTimeout == null + then + if service.funnel + then "1s" + else "0s" + else service.readHeaderTimeout; + in ([ + "-name=${name}" + "-ephemeral=${lib.boolToString service.ephemeral}" + "-funnel=${lib.boolToString service.funnel}" + "-plaintext=${lib.boolToString service.plaintext}" + "-listenAddr=${service.listenAddr}" + "-stripPrefix=${lib.boolToString service.stripPrefix}" + "-authkeyPath=${service.authKeyPath}" + "-insecureHTTPS=${lib.boolToString service.insecureHTTPS}" + "-suppressTailnetDialer=${lib.boolToString service.suppressTailnetDialer}" + "-readHeaderTimeout=${readHeaderTimeout}" + ] + ++ (lib.optionals (service.whoisTimeout != null) ["-whoisTimeout" service.whoisTimeout]) + ++ (lib.optionals (service.upstreamUnixAddr != null) ["-upstreamUnixAddr" service.upstreamUnixAddr]) + ++ (map (p: "-prefix=${p}") service.prefixes) + ++ (map (h: "-upstreamHeader=${h}") (lib.mapAttrsToList (name: service: "${name}: ${service}") service.upstreamHeaders)) + ++ [service.toURL]); +in { + options = with lib; { services.tsnsrv.enable = mkOption { description = "Enable tsnsrv"; type = types.bool; @@ -141,44 +170,18 @@ users.groups.tsnsrv = {}; systemd.services = lib.mapAttrs' ( - name: value: + name: service: lib.nameValuePair "tsnsrv-${name}" { wantedBy = ["multi-user.target"]; after = ["network-online.target"]; - script = let - readHeaderTimeout = - if value.readHeaderTimeout == null - then - if value.funnel - then "1s" - else "0s" - else value.readHeaderTimeout; - args = - [ - "-name=${name}" - "-ephemeral=${lib.boolToString value.ephemeral}" - "-funnel=${lib.boolToString value.funnel}" - "-plaintext=${lib.boolToString value.plaintext}" - "-listenAddr=${value.listenAddr}" - "-stripPrefix=${lib.boolToString value.stripPrefix}" - "-authkeyPath=${value.authKeyPath}" - "-insecureHTTPS=${lib.boolToString value.insecureHTTPS}" - "-suppressTailnetDialer=${lib.boolToString value.suppressTailnetDialer}" - "-readHeaderTimeout=${readHeaderTimeout}" - ] - ++ (lib.optionals (value.whoisTimeout != null) ["-whoisTimeout" value.whoisTimeout]) - ++ (lib.optionals (value.upstreamUnixAddr != null) ["-upstreamUnixAddr" value.upstreamUnixAddr]) - ++ (map (p: "-prefix=${p}") value.prefixes) - ++ (map (h: "-upstreamHeader=${h}") (lib.mapAttrsToList (name: value: "${name}: ${value}") value.upstreamHeaders)) - ++ [value.toURL]; - in '' - exec ${value.package}/bin/tsnsrv -stateDir=$STATE_DIRECTORY/tsnet-tsnsrv ${lib.escapeShellArgs args} + script = '' + exec ${service.package}/bin/tsnsrv -stateDir=$STATE_DIRECTORY/tsnet-tsnsrv ${lib.escapeShellArgs (serviceArgs {inherit name service;})} ''; serviceConfig = { DynamicUser = true; - SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ value.supplementalGroups; + SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; StateDirectory = "tsnsrv-${name}"; StateDirectoryMode = "0700"; From 0ba118ada5efb405122d65d3806ee1566687f637 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 10:37:31 -0400 Subject: [PATCH 03/16] Basic nixos configuration for oci-container sidecars These _very likely_ won't work, but it's a start. --- nixos/default.nix | 196 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 147 insertions(+), 49 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index 38049e9..a6e02aa 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -164,56 +164,154 @@ in { type = types.attrsOf (types.submodule serviceSubmodule); example = false; }; - }; - config = lib.mkIf config.services.tsnsrv.enable { - users.groups.tsnsrv = {}; - systemd.services = - lib.mapAttrs' ( - name: service: - lib.nameValuePair - "tsnsrv-${name}" - { - wantedBy = ["multi-user.target"]; - after = ["network-online.target"]; - script = '' - exec ${service.package}/bin/tsnsrv -stateDir=$STATE_DIRECTORY/tsnet-tsnsrv ${lib.escapeShellArgs (serviceArgs {inherit name service;})} - ''; - serviceConfig = { - DynamicUser = true; - SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; - StateDirectory = "tsnsrv-${name}"; - StateDirectoryMode = "0700"; - - PrivateNetwork = false; # We need access to the internet for ts - # Activate a bunch of strictness: - DeviceAllow = ""; - LockPersonality = true; - MemoryDenyWriteExecute = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateMounts = true; - PrivateTmp = true; - PrivateUsers = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectProc = "noaccess"; - ProtectKernelModules = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelTunables = true; - RestrictNamespaces = true; - AmbientCapabilities = ""; - CapabilityBoundingSet = ""; - ProtectSystem = "strict"; - RemoveIPC = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - UMask = "0066"; + virtualisation.oci-sidecars.tsnsrv = { + enable = mkEnableOption "tsnsrv oci sidecar containers"; + + authKeyPath = mkOption { + description = "Path to a file containing a tailscale auth key. Make this a secret"; + type = types.path; + default = config.services.tsnsrv.defaults.authKeyPath; + }; + + containers = mkOption { + description = "Attrset mapping sidecar container names to their respective tsnsrv service definition. Each sidecar container will be attached to the container it belongs to, sharing its network."; + type = types.attrsOf (types.submodule { + options = { + forContainer = mkOption { + description = "The container to which to attach the sidecar."; + type = types.str; # TODO: see if we can constrain this to all the oci containers in the system definition, with types.oneOf or an appropriate check. + }; + + service = mkOption { + description = "tsnsrv service definition for the sidecar."; + type = types.submodule serviceSubmodule; }; - } - ) - config.services.tsnsrv.services; + }; + }); + }; + }; }; + + config = let + lockedDownserviceConfig = { + PrivateNetwork = false; # We need access to the internet for ts + # Activate a bunch of strictness: + DeviceAllow = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectProc = "noaccess"; + ProtectKernelModules = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelTunables = true; + RestrictNamespaces = true; + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + UMask = "0066"; + }; + in + lib.mkMerge [ + (lib.mkIf config.services.tsnsrv.enable { + users.groups.tsnsrv = {}; + systemd.services = + lib.mapAttrs' ( + name: service: + lib.nameValuePair + "tsnsrv-${name}" + { + wantedBy = ["multi-user.target"]; + after = ["network-online.target"]; + script = '' + exec ${service.package}/bin/tsnsrv -stateDir=$STATE_DIRECTORY/tsnet-tsnsrv ${lib.escapeShellArgs (serviceArgs {inherit name service;})} + ''; + serviceConfig = + { + DynamicUser = true; + SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; + StateDirectory = "tsnsrv-${name}"; + StateDirectoryMode = "0700"; + } + // lockedDownserviceConfig; + } + ) + config.services.tsnsrv.services; + }) + + (lib.mkIf config.virtualisation.oci-sidecars.tsnsrv.enable { + virtualisation.oci-containers = + lib.mapAttrs' (name: sidecar: { + inherit name; + value = let + serviceName = "${config.virtualisation.oci-containers.backend}-${name}"; + in { + imageFile = flake.packages.${pkgs.stdenv.targetPlatform.system}.tsnsrvOciImage; + image = "tsnsrv:latest"; + dependsOn = sidecar.forContainer; + volumes = [ + # The service's state dir; we have to infer /var/lib + # because the backends don't support using the + # $STATE_DIRECTORY environment variable in volume specs. + "/var/lib/${serviceName}:/state" + + # The tsnet auth key. + "${config.virtualisation.oci-sidecars.tsnsrv.authKeyPath}:${config.virtualisation.oci-sidecars.tsnsrv.authKeyPath}" + ]; + extraOptions = [ + "--network=container:${sidecar.forContainer}" + ]; + cmd = + ["-stateDir=/state"] + ++ (serviceArgs { + inherit name; + inherit (sidecar) service; + }); + }; + }) + config.virtualisation.oci-sidecars.containers; + + systemd.services = + ( + # systemd unit settings for the respective podman services: + lib.mapAttrs' (name: sidecar: let + serviceName = "${config.virtualisation.oci-containers.backend}-${name}"; + service = sidecar.service; + in { + name = serviceName; + value = + { + StateDirectory = serviceName; + StateDirectoryMode = "0700"; + DynamicUser = true; + SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; + } + // lockedDownserviceConfig; + }) + config.virtualisation.oci-sidecars.containers + ) + // ( + # systemd unit of the container we're sidecar-ing to: + # Ensure that the sidecar is up when the "main" container is up. + lib.foldAttrs (lib.mapAttrsToList (name: sidecar: let + fromServiceName = "${config.virtualisation.oci-containers.backend}-${sidecar.forContainer}"; + toServiceName = "${config.virtualisation.oci-containers.backend}-${name}"; + in { + "${fromServiceName}".unitConfig.Upholds = [toServiceName]; + }) + config.virtualisation.oci-sidecars.containers) + ); + }) + ]; } From fde092989b3c6b5f26d0affff44dda7b7e51fc7a Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 11:22:07 -0400 Subject: [PATCH 04/16] Correct a variable reference --- nixos/default.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index a6e02aa..0f5a5a7 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -280,7 +280,7 @@ in { }); }; }) - config.virtualisation.oci-sidecars.containers; + config.virtualisation.oci-sidecars.tsnsrv.containers; systemd.services = ( @@ -299,7 +299,7 @@ in { } // lockedDownserviceConfig; }) - config.virtualisation.oci-sidecars.containers + config.virtualisation.oci-sidecars.tsnsrv.containers ) // ( # systemd unit of the container we're sidecar-ing to: @@ -310,7 +310,7 @@ in { in { "${fromServiceName}".unitConfig.Upholds = [toServiceName]; }) - config.virtualisation.oci-sidecars.containers) + config.virtualisation.oci-sidecars.tsnsrv.containers) ); }) ]; From 4b5113d0c85af0c6371a9a1afda6ba75c07910d7 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 11:23:15 -0400 Subject: [PATCH 05/16] Yet another sub-key that I missed --- nixos/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/default.nix b/nixos/default.nix index 0f5a5a7..abb5484 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -251,7 +251,7 @@ in { }) (lib.mkIf config.virtualisation.oci-sidecars.tsnsrv.enable { - virtualisation.oci-containers = + virtualisation.oci-containers.containers = lib.mapAttrs' (name: sidecar: { inherit name; value = let From b06174e6a8d46053092745c2925681f8c6f504e2 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 11:24:31 -0400 Subject: [PATCH 06/16] dependsOn must be a list --- nixos/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/default.nix b/nixos/default.nix index abb5484..bf09e83 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -259,7 +259,7 @@ in { in { imageFile = flake.packages.${pkgs.stdenv.targetPlatform.system}.tsnsrvOciImage; image = "tsnsrv:latest"; - dependsOn = sidecar.forContainer; + dependsOn = [sidecar.forContainer]; volumes = [ # The service's state dir; we have to infer /var/lib # because the backends don't support using the From 5533518b751f879d4ee7218843d0005a938d410f Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 11:30:11 -0400 Subject: [PATCH 07/16] Finish the lib.foldAttrs logic --- nixos/default.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index bf09e83..434f6a5 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -304,11 +304,13 @@ in { // ( # systemd unit of the container we're sidecar-ing to: # Ensure that the sidecar is up when the "main" container is up. - lib.foldAttrs (lib.mapAttrsToList (name: sidecar: let + lib.foldAttrs (item: acc: {unitConfig.Upholds = acc.unitConfig.Upholds ++ [item];}) + {unitConfig.Upholds = [];} + (lib.mapAttrsToList (name: sidecar: let fromServiceName = "${config.virtualisation.oci-containers.backend}-${sidecar.forContainer}"; toServiceName = "${config.virtualisation.oci-containers.backend}-${name}"; in { - "${fromServiceName}".unitConfig.Upholds = [toServiceName]; + "${fromServiceName}" = toServiceName; }) config.virtualisation.oci-sidecars.tsnsrv.containers) ); From a56af8bbe035470446ea6e1daeca0029f16876e2 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 11:33:15 -0400 Subject: [PATCH 08/16] Fix serviceConfig of the sidecar --- nixos/default.nix | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index 434f6a5..65c93ac 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -290,14 +290,16 @@ in { service = sidecar.service; in { name = serviceName; - value = - { - StateDirectory = serviceName; - StateDirectoryMode = "0700"; - DynamicUser = true; - SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; - } - // lockedDownserviceConfig; + value = { + serviceConfig = + { + StateDirectory = serviceName; + StateDirectoryMode = "0700"; + DynamicUser = true; + SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; + } + // lockedDownserviceConfig; + }; }) config.virtualisation.oci-sidecars.tsnsrv.containers ) From a2b579fdb06b57d7be008092000e6be6cffd3946 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 12:44:59 -0400 Subject: [PATCH 09/16] Can't use dynamic users for the sidecar service --- nixos/default.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nixos/default.nix b/nixos/default.nix index 65c93ac..e3e32b5 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -251,6 +251,8 @@ in { }) (lib.mkIf config.virtualisation.oci-sidecars.tsnsrv.enable { + users.users.tsnsrv-sidecar.isSystemUser = true; + virtualisation.oci-containers.containers = lib.mapAttrs' (name: sidecar: { inherit name; @@ -260,6 +262,7 @@ in { imageFile = flake.packages.${pkgs.stdenv.targetPlatform.system}.tsnsrvOciImage; image = "tsnsrv:latest"; dependsOn = [sidecar.forContainer]; + user = config.users.users.tsnsrv-sidecar.name; volumes = [ # The service's state dir; we have to infer /var/lib # because the backends don't support using the @@ -295,7 +298,6 @@ in { { StateDirectory = serviceName; StateDirectoryMode = "0700"; - DynamicUser = true; SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; } // lockedDownserviceConfig; From ea2f24bdfe3aba77f8acc5c506403bfde9ed988f Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 12:47:57 -0400 Subject: [PATCH 10/16] Assign the tsnsrv group to the sidecar user --- nixos/default.nix | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index e3e32b5..5aec8bb 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -224,8 +224,9 @@ in { }; in lib.mkMerge [ + (lib.mkIf (config.services.tsnsrv.enable or config.virtualisation.oci-sidecars.tsnsrv.enable) + {users.groups.tsnsrv = {};}) (lib.mkIf config.services.tsnsrv.enable { - users.groups.tsnsrv = {}; systemd.services = lib.mapAttrs' ( name: service: @@ -251,7 +252,10 @@ in { }) (lib.mkIf config.virtualisation.oci-sidecars.tsnsrv.enable { - users.users.tsnsrv-sidecar.isSystemUser = true; + users.users.tsnsrv-sidecar = { + isSystemUser = true; + group = config.users.groups.tsnsrv.name; + }; virtualisation.oci-containers.containers = lib.mapAttrs' (name: sidecar: { From 26adf0954127e3be4a41f3c7cafe5da7c7955023 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 12:50:22 -0400 Subject: [PATCH 11/16] Don't lock down the sidecar service config for now --- nixos/default.nix | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index 5aec8bb..4ae692c 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -298,13 +298,11 @@ in { in { name = serviceName; value = { - serviceConfig = - { - StateDirectory = serviceName; - StateDirectoryMode = "0700"; - SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; - } - // lockedDownserviceConfig; + serviceConfig = { + StateDirectory = serviceName; + StateDirectoryMode = "0700"; + SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; + }; }; }) config.virtualisation.oci-sidecars.tsnsrv.containers From 91bd25cda840bc00092d18f0296930c85a27e9f3 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 12:53:34 -0400 Subject: [PATCH 12/16] Assign both user and group --- nixos/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/default.nix b/nixos/default.nix index 4ae692c..5ee79b8 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -266,7 +266,7 @@ in { imageFile = flake.packages.${pkgs.stdenv.targetPlatform.system}.tsnsrvOciImage; image = "tsnsrv:latest"; dependsOn = [sidecar.forContainer]; - user = config.users.users.tsnsrv-sidecar.name; + user = "${config.users.users.tsnsrv-sidecar.name}:${config.users.groups.tsnsrv.name}"; volumes = [ # The service's state dir; we have to infer /var/lib # because the backends don't support using the From cfae929be48050ff3f658a67d59b5f66435b5ebf Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 13:13:13 -0400 Subject: [PATCH 13/16] Add flags that allow podman to run the program --- nixos/default.nix | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index 5ee79b8..dc0f225 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -276,9 +276,19 @@ in { # The tsnet auth key. "${config.virtualisation.oci-sidecars.tsnsrv.authKeyPath}:${config.virtualisation.oci-sidecars.tsnsrv.authKeyPath}" ]; - extraOptions = [ - "--network=container:${sidecar.forContainer}" - ]; + extraOptions = + [ + "--network=container:${sidecar.forContainer}" + ] + ++ ( + if (config.virtualisation.oci-containers.backend == "podman") + then [ + "--passwd" + "--hostuser=${config.users.users.tsnsrv-sidecar.name}" + "--group-add=keep-groups" + ] + else [] + ); cmd = ["-stateDir=/state"] ++ (serviceArgs { From 16d77cfe4931d0777539b530e919693475926a4e Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 14:16:04 -0400 Subject: [PATCH 14/16] Attempt a rootless container after all This is strongly inspired by https://github.com/NixOS/nixpkgs/issues/138423 --- nixos/default.nix | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index dc0f225..2525de3 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -255,6 +255,18 @@ in { users.users.tsnsrv-sidecar = { isSystemUser = true; group = config.users.groups.tsnsrv.name; + subUidRanges = [ + { + startUid = 200000; + count = 100000; + } + ]; + subGidRanges = [ + { + startGid = 200000; + count = 100000; + } + ]; }; virtualisation.oci-containers.containers = @@ -276,19 +288,9 @@ in { # The tsnet auth key. "${config.virtualisation.oci-sidecars.tsnsrv.authKeyPath}:${config.virtualisation.oci-sidecars.tsnsrv.authKeyPath}" ]; - extraOptions = - [ - "--network=container:${sidecar.forContainer}" - ] - ++ ( - if (config.virtualisation.oci-containers.backend == "podman") - then [ - "--passwd" - "--hostuser=${config.users.users.tsnsrv-sidecar.name}" - "--group-add=keep-groups" - ] - else [] - ); + extraOptions = [ + "--network=container:${sidecar.forContainer}" + ]; cmd = ["-stateDir=/state"] ++ (serviceArgs { @@ -308,11 +310,19 @@ in { in { name = serviceName; value = { + path = ["/run/wrappers"]; serviceConfig = { + User = config.users.users.tsnsrv-sidecar.name; + Group = config.users.groups.tsnsrv.name; StateDirectory = serviceName; + RuntimeDirectory = serviceName; StateDirectoryMode = "0700"; SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; }; + environment = { + HOME = "%S/${serviceName}"; + XDG_RUNTIME_DIR = "%t/${serviceName}"; + }; }; }) config.virtualisation.oci-sidecars.tsnsrv.containers From 9ddf6040de97f62e8669a7f64a8858db257dfa5f Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 14:31:18 -0400 Subject: [PATCH 15/16] Forget everything about running as a different user It's not going to work (SO MANY bugs with non-root and even rootless containers), so let's skip all that. --- nixos/default.nix | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/nixos/default.nix b/nixos/default.nix index 2525de3..1dcf6ba 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -252,23 +252,6 @@ in { }) (lib.mkIf config.virtualisation.oci-sidecars.tsnsrv.enable { - users.users.tsnsrv-sidecar = { - isSystemUser = true; - group = config.users.groups.tsnsrv.name; - subUidRanges = [ - { - startUid = 200000; - count = 100000; - } - ]; - subGidRanges = [ - { - startGid = 200000; - count = 100000; - } - ]; - }; - virtualisation.oci-containers.containers = lib.mapAttrs' (name: sidecar: { inherit name; @@ -278,7 +261,7 @@ in { imageFile = flake.packages.${pkgs.stdenv.targetPlatform.system}.tsnsrvOciImage; image = "tsnsrv:latest"; dependsOn = [sidecar.forContainer]; - user = "${config.users.users.tsnsrv-sidecar.name}:${config.users.groups.tsnsrv.name}"; + user = config.virtualisation.oci-containers.containers.${sidecar.forContainer}.user; volumes = [ # The service's state dir; we have to infer /var/lib # because the backends don't support using the @@ -312,17 +295,10 @@ in { value = { path = ["/run/wrappers"]; serviceConfig = { - User = config.users.users.tsnsrv-sidecar.name; - Group = config.users.groups.tsnsrv.name; StateDirectory = serviceName; - RuntimeDirectory = serviceName; StateDirectoryMode = "0700"; SupplementaryGroups = [config.users.groups.tsnsrv.name] ++ service.supplementalGroups; }; - environment = { - HOME = "%S/${serviceName}"; - XDG_RUNTIME_DIR = "%t/${serviceName}"; - }; }; }) config.virtualisation.oci-sidecars.tsnsrv.containers From 3f30386eb3f38a08b8eec50b53b5f898e7138600 Mon Sep 17 00:00:00 2001 From: Andreas Fuchs Date: Tue, 29 Aug 2023 14:37:04 -0400 Subject: [PATCH 16/16] Add an optional name argument to sidecars --- nixos/default.nix | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nixos/default.nix b/nixos/default.nix index 1dcf6ba..59ea171 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -178,6 +178,12 @@ in { description = "Attrset mapping sidecar container names to their respective tsnsrv service definition. Each sidecar container will be attached to the container it belongs to, sharing its network."; type = types.attrsOf (types.submodule { options = { + name = mkOption { + description = "Name to use for the tsnet service. This defaults to the container name."; + type = types.nullOr types.str; + default = null; + }; + forContainer = mkOption { description = "The container to which to attach the sidecar."; type = types.str; # TODO: see if we can constrain this to all the oci containers in the system definition, with types.oneOf or an appropriate check. @@ -277,7 +283,10 @@ in { cmd = ["-stateDir=/state"] ++ (serviceArgs { - inherit name; + name = + if sidecar.name == null + then name + else sidecar.name; inherit (sidecar) service; }); };