From f772e6054e1eee84ea96fe33ea4b56e9aae238f8 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Fri, 2 Jan 2026 19:52:36 +0100 Subject: [PATCH 01/14] Enable software tpm for libvirt --- modules/virtualization.nix | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/virtualization.nix b/modules/virtualization.nix index b1cde0f..fb232cc 100644 --- a/modules/virtualization.nix +++ b/modules/virtualization.nix @@ -3,12 +3,13 @@ lib, ... }: { - environment.systemPackages = with pkgs; [ - virt-manager - ]; - virtualisation = { - libvirtd.enable = true; + libvirtd = { + enable = true; + qemu.swtpm.enable = true; + }; spiceUSBRedirection.enable = true; }; + + programs.virt-manager.enable = true; } From d4e4ecf9a9e3a8ed69de377c60122c4e610ee9ae Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 4 Jan 2026 22:17:30 +0100 Subject: [PATCH 02/14] Disallow ping on servers --- modules/network-server.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/network-server.nix b/modules/network-server.nix index 99645e8..017b1b0 100644 --- a/modules/network-server.nix +++ b/modules/network-server.nix @@ -5,5 +5,6 @@ }: { networking = { useDHCP = true; + firewall.allowPing = false; }; } From ed21c242620c6348fb9ce5555a08925791ed256b Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 4 Jan 2026 22:17:40 +0100 Subject: [PATCH 03/14] Enable ipv6 in docker --- modules/docker.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/docker.nix b/modules/docker.nix index d216ec2..2c88d1a 100644 --- a/modules/docker.nix +++ b/modules/docker.nix @@ -7,6 +7,10 @@ virtualisation = { docker = { enable = true; + daemon.settings = { + ipv6 = true; + ip6tables = true; + }; }; oci-containers.backend = "docker"; }; From cb0408abd4528d5792844c9a97e3d61cbaa1c23a Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 4 Jan 2026 22:20:20 +0100 Subject: [PATCH 04/14] Add modules for traefik and arcane --- modules/arcane.nix | 64 ++++++++++++++ modules/traefik.nix | 197 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 modules/arcane.nix create mode 100644 modules/traefik.nix diff --git a/modules/arcane.nix b/modules/arcane.nix new file mode 100644 index 0000000..160088f --- /dev/null +++ b/modules/arcane.nix @@ -0,0 +1,64 @@ +{ + config, + lib, + ... +}: +let + cfg = config.services.arcane; +in { + options.services.arcane = { + enable = lib.mkEnableOption "arcane, a modern Docker management UI"; + appUrl = lib.mkOption { + description = "External URL arcane will be reachable from, without protocol"; + type = lib.types.str; + }; + secretFile = lib.mkOption { + description = '' + Agenix secret containing the following needed environment variables in dotenv notation: + - ENCRYPTION_KEY + - JWT_SECRET + - OIDC_CLIENT_ID + - OIDC_CLIENT_SECRET + - OIDC_ISSUER_URL + - OIDC_ADMIN_CLAIM + - OIDC_ADMIN_VALUE + ''; + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers = { + arcane = { + image = "ghcr.io/getarcaneapp/arcane:v1.11.2"; + volumes = [ + "/var/run/docker.sock:/var/run/docker.sock" + ]; + environment = { + APP_URL = "https://${cfg.appUrl}"; + PUID = "1000"; + PGID = "1000"; + LOG_LEVEL = "info"; + LOG_JSON = "false"; + OIDC_ENABLED = "true"; + OIDC_SCOPES = "openid email profile groups"; + DATABASE_URL = "file:data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate"; + }; + environmentFiles = [ + cfg.secretFile.path + ]; + networks = [ + "traefik" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.arcane.middlewares" = "arcane-oidc-auth@file"; + "traefik.http.routers.arcane.rule" = "Host(`${cfg.appUrl}`)"; + "traefik.http.services.arcane.loadbalancer.server.port" = "3552"; + }; + extraOptions = [ + ''--mount=type=volume,source=arcane-data,target=/app/data,volume-driver=local'' + ]; + }; + }; + }; +} diff --git a/modules/traefik.nix b/modules/traefik.nix new file mode 100644 index 0000000..04dedfc --- /dev/null +++ b/modules/traefik.nix @@ -0,0 +1,197 @@ +{ + pkgs, + config, + lib, + ... +}: +let + + cfg = config.services.traefik-docker; + + mapOidcClientNameToEnv = stringToReplace: lib.replaceString "-" "_" (lib.toUpper stringToReplace); + + traefik-mtls-config = (pkgs.formats.yaml { }).generate "traefik-mtls-config" { + tls.options.default.clientAuth = { + caFiles = "caFiles/root_ca.crt"; + clientAuthType = "VerifyClientCertIfGiven"; + }; + }; + +in { + + options.services.traefik-docker = { + enable = lib.mkEnableOption "traefik web server hosted as OCI container"; + dashboardUrl = lib.mkOption { + description = "External URL the traefik dashboard will be reachable from, without protocol"; + type = lib.types.str; + }; + dnsSecrets = lib.mkOption { + description = "Secrets for DNS providers."; + type = lib.types.listOf lib.types.anything; + }; + mTLSCaCertSecret = lib.mkOption { + description = "Agenix secret containing the CA file to verify client certificates against."; + }; + oidcAuthProviderUrl = lib.mkOption { + description = "Provider URL of OIDC auth provider."; + type = lib.types.str; + }; + oidcClients = lib.mkOption { + example = '' + immich = { + scopes = [ + "openid" + "email" + "profile" + ]; + enableBypassUsingClientCertificate = true; + usePkce = true; + }; + ''; + description = "Attribute set of OIDC clients with their configurations."; + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + secret = lib.mkOption { + description = ''Agenix secret containing the following needed environment variables in dotenv notation: + - _OIDC_AUTH_SECRET + - _OIDC_AUTH_PROVIDER_CLIENT_ID + - _OIDC_CLIENT_SECRET + ''; + }; + scopes = lib.mkOption { + default = [ "openid" ]; + example = [ "openid" "email" "profile" "groups" ]; + description = "OIDC scopes to request from auth provider."; + type = lib.types.listOf lib.types.str; + }; + usePkce = lib.mkOption { + default = true; + description = "Whether to enable PKCE for this provider."; + type = lib.types.bool; + }; + enableBypassUsingClientCertificate = lib.mkOption { + default = false; + description = "Whether to allow bypassing OIDC protection when a verified client certificate is presented."; + type = lib.types.bool; + }; + }; + } + ); + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers = { + traefik = { + image = "traefik:v3.6.6"; + cmd = [ + "--providers.docker=true" + "--providers.docker.exposedByDefault=false" + "--providers.docker.network=traefik" + "--providers.file.directory=/dynamic-config" + "--log.level=DEBUG" + "--api=true" + "--ping=true" + "--entrypoints.web.address=:80" + "--entrypoints.websecure.address=:443" + "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=600s" + "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=600s" + "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=600s" + "--entrypoints.web.http.redirections.entrypoint.to=websecure" + "--entrypoints.websecure.asDefault=true" + "--entrypoints.websecure.http.middlewares=strip-mtls-headers@docker,pass-tls-client-cert@docker" + "--entrypoints.websecure.http.tls.certresolver=letsencrypt" + "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json" + "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" + "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=netcup" + "--experimental.plugins.traefik-oidc-auth.modulename=github.com/sevensolutions/traefik-oidc-auth" + "--experimental.plugins.traefik-oidc-auth.version=v0.17.0" + ]; + autoStart = true; + ports = [ + "80:80" + "443:443" + ]; + networks = [ + "traefik" + ]; + environment = { + OIDC_AUTH_PROVIDER_URL = cfg.oidcAuthProviderUrl; + }; + environmentFiles = lib.forEach cfg.dnsSecrets (secret: secret.path) ++ (lib.mapAttrsToList (oidcClientName: oidcClientConfig: oidcClientConfig.secret.path) cfg.oidcClients); + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.dashboard.rule" = "Host(`${cfg.dashboardUrl}`)"; + "traefik.http.routers.dashboard.service" = "dashboard@internal"; + "traefik.http.routers.dashboard.middlewares" = "traefik-dashboard-oidc-auth@file"; + "traefik.http.routers.api.rule" = "Host(`${cfg.dashboardUrl}`) && (PathPrefix(`/api`) || PathPrefix(`/oidc/callback`))"; + "traefik.http.routers.api.service" = "api@internal"; + "traefik.http.routers.api.middlewares" = "traefik-dashboard-oidc-auth@file"; + "traefik.http.middlewares.strip-mtls-headers.headers.customrequestheaders.X-Forwarded-Tls-Client-Cert" = ""; + "traefik.http.middlewares.pass-tls-client-cert.passtlsclientcert.pem" = "true"; + }; + volumes = let + oidc-config = lib.mapAttrs' ( + oidcClientName: oidcClientConfig: + lib.nameValuePair "${oidcClientName}-oidc-auth" { + plugin.traefik-oidc-auth = { + LogLevel = "INFO"; + Secret = ''{{ env "${mapOidcClientNameToEnv oidcClientName}_OIDC_AUTH_SECRET" }}''; + Provider = { + Url = ''{{ env "OIDC_AUTH_PROVIDER_URL" }}''; + ClientId = ''{{ env "${mapOidcClientNameToEnv oidcClientName}_OIDC_AUTH_PROVIDER_CLIENT_ID" }}''; + ClientSecret = ''{{ env "${mapOidcClientNameToEnv oidcClientName}_OIDC_AUTH_PROVIDER_CLIENT_SECRET" }}''; + UsePkce = oidcClientConfig.usePkce; + }; + Scopes = oidcClientConfig.scopes; + LoginUrl = ''{{ env "OIDC_AUTH_PROVIDER_URL" }}''; + } // (lib.attrsets.optionalAttrs oidcClientConfig.enableBypassUsingClientCertificate { + BypassAuthenticationRule = "HeaderRegexp(`X-Forwarded-Tls-Client-Cert`, `.+`)"; + }); + } + ) cfg.oidcClients; + traefik-oidc-authentication-config = (pkgs.formats.yaml {}).generate "traefik-oidc-auth" { + http.middlewares = oidc-config; + }; + in [ + "/var/run/docker.sock:/var/run/docker.sock" + "${traefik-oidc-authentication-config}:/dynamic-config/traefik-oidc-auth.yaml:ro" + "${traefik-mtls-config}:/dynamic-config/traefik-mtls.yaml:ro" + "${cfg.mTLSCaCertSecret.path}:/caFiles/root_ca.crt:ro" + ]; + extraOptions = [ + ''--mount=type=volume,source=certs,target=/certs,volume-driver=local'' + "--add-host=host.docker.internal:host-gateway" + "--health-cmd=wget --spider --quiet http://localhost:8080/ping" + "--health-interval=10s" + "--health-timeout=5s" + "--health-retries=3" + "--health-start-period=5s" + ]; + }; + }; + + systemd.services."docker-traefik" = { + after = [ + "docker-network-traefik.service" + ]; + requires = [ + "docker-network-traefik.service" + ]; + }; + + systemd.services."docker-network-traefik" = { + path = [ pkgs.docker ]; + serviceConfig = { + Type = "oneshot"; + }; + script = '' + docker network inspect traefik || docker network create traefik --ipv4 --ipv6 --subnet=172.18.0.0/16 --gateway=172.18.0.1 + ''; + }; + + networking.firewall.extraCommands = "iptables -t nat -I PREROUTING -s 172.18.0.0/16 -d 172.18.0.0/16 -j MASQUERADE"; + + }; +} From 5115744f46566fc17b7b1e2220d69752ba0befe6 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 4 Jan 2026 22:22:53 +0100 Subject: [PATCH 05/14] Test traefik, arcane and immich on vServer --- hosts/srv01.hf/default.nix | 41 ++++++++++++++++++++++++++++++++++++++ hosts/srv01.hf/secrets.nix | 6 ++++++ 2 files changed, 47 insertions(+) diff --git a/hosts/srv01.hf/default.nix b/hosts/srv01.hf/default.nix index feb2183..74c82c0 100644 --- a/hosts/srv01.hf/default.nix +++ b/hosts/srv01.hf/default.nix @@ -17,6 +17,8 @@ ../../modules/docker.nix ../../modules/teleport.nix ../../modules/portainer_agent.nix + ../../modules/arcane.nix + ../../modules/traefik.nix ../../modules/auto-upgrade.nix # Include the results of the hardware scan. ./hardware-configuration.nix @@ -33,6 +35,45 @@ virtualisation.oci-containers.containers.portainer_agent.environmentFiles = [ config.age.secrets."portainer-join_token".path ]; + services.traefik-docker = { + enable = true; + dashboardUrl = "traefik.juliusfr.eu"; + dnsSecrets = [ + config.age.secrets."netcup-dns" + ]; + mTLSCaCertSecret = config.age.secrets."step-ca-crt"; + oidcAuthProviderUrl = "https://login.juliusfr.eu"; + oidcClients = { + traefik-dashboard = { + secret = config.age.secrets."traefik-oidc-auth"; + }; + immich = { + secret = config.age.secrets."immich-oidc-auth"; + scopes = [ + "openid" + "email" + "profile" + ]; + enableBypassUsingClientCertificate = true; + }; + arcane = { + secret = config.age.secrets."arcane-oidc-auth"; + scopes = [ + "openid" + "email" + "profile" + "groups" + ]; + }; + }; + }; + + services.arcane = { + enable = true; + appUrl = "arcane.juliusfr.eu"; + secretFile = config.age.secrets."arcane-secrets"; + }; + systemd.network = { enable = true; networks."10-wan" = { diff --git a/hosts/srv01.hf/secrets.nix b/hosts/srv01.hf/secrets.nix index 8697e77..8dc2205 100644 --- a/hosts/srv01.hf/secrets.nix +++ b/hosts/srv01.hf/secrets.nix @@ -4,5 +4,11 @@ teleport-ca_pin.file = "${inputs.secrets}/secrets/teleport/ca_pin"; teleport-join_token.file = "${inputs.secrets}/secrets/srv01-hf/teleport_auth_token"; portainer-join_token.file = "${inputs.secrets}/secrets/srv01-hf/portainer_join_token"; + netcup-dns.file = "${inputs.secrets}/secrets/dns-management/netcup"; + traefik-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/traefik-oidc-auth"; + immich-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/immich-oidc-auth"; + arcane-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/arcane-oidc-auth"; + arcane-secrets.file = "${inputs.secrets}/secrets/srv01-hf/arcane-secrets"; + step-ca-crt.file = "${inputs.secrets}/secrets/step-ca/step-ca-crt"; }; } From b65effa878a9c705048a8eb3fb88a99c81931bd8 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 4 Jan 2026 22:23:15 +0100 Subject: [PATCH 06/14] Update flake.lock --- flake.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/flake.lock b/flake.lock index a5e3b36..95ea622 100644 --- a/flake.lock +++ b/flake.lock @@ -144,11 +144,11 @@ ] }, "locked": { - "lastModified": 1766529401, - "narHash": "sha256-OJAjJcW6ZADEzTBrvOTZanbgC8ObEWveObujtpazEbg=", + "lastModified": 1767280655, + "narHash": "sha256-YmaYMduV5ko8zURUT1VLGDbVC1L/bxHS0NsiPoZ6bBM=", "owner": "nix-community", "repo": "home-manager", - "rev": "aaf46506426cc8c53719dd20de660fc856a5561e", + "rev": "d49d2543f02dbd789ed032188c84570d929223cb", "type": "github" }, "original": { @@ -181,11 +181,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1764440730, - "narHash": "sha256-ZlJTNLUKQRANlLDomuRWLBCH5792x+6XUJ4YdFRjtO4=", + "lastModified": 1767185284, + "narHash": "sha256-ljDBUDpD1Cg5n3mJI81Hz5qeZAwCGxon4kQW3Ho3+6Q=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "9154f4569b6cdfd3c595851a6ba51bfaa472d9f3", + "rev": "40b1a28dce561bea34858287fbb23052c3ee63fe", "type": "github" }, "original": { @@ -197,11 +197,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766473571, - "narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=", + "lastModified": 1767325753, + "narHash": "sha256-yA/CuWyqm+AQo2ivGy6PlYrjZBQm7jfbe461+4HF2fo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", + "rev": "64049ca74d63e971b627b5f3178d95642e61cedd", "type": "github" }, "original": { @@ -335,11 +335,11 @@ "secrets": { "flake": false, "locked": { - "lastModified": 1758149597, - "narHash": "sha256-qUkhfFBEuDJ7nP6jcdBZzGBBhLKnXYxumBQI75DGcFc=", + "lastModified": 1767139729, + "narHash": "sha256-mkmK7wiIqwmcrU+bljxzDPqh9Ya1ITqIlBmdxYxh3nI=", "ref": "refs/heads/main", - "rev": "8404f6877e25b8cbf3f504ef1926034e8c401dbe", - "revCount": 6, + "rev": "27a126bd56c16215f80c014b8fd0b28b53605897", + "revCount": 17, "type": "git", "url": "ssh://git@git.jfreudenberger.de/JuliusFreudenberger/nix-private.git" }, From a70450af2aef5313c447c3ac6872fe5b48cffd46 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Fri, 9 Jan 2026 22:01:10 +0100 Subject: [PATCH 07/14] Add Remote-User authentication from mTLS with headers --- modules/traefik.nix | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/modules/traefik.nix b/modules/traefik.nix index 04dedfc..92b1204 100644 --- a/modules/traefik.nix +++ b/modules/traefik.nix @@ -75,6 +75,27 @@ in { description = "Whether to allow bypassing OIDC protection when a verified client certificate is presented."; type = lib.types.bool; }; + useClaimsFromUserInfo = lib.mkOption { + default = false; + description = "When enabled, an additional request to the provider's userinfo_endpoint is made to validate the token and to retrieve additional claims. The userinfo claims are merged directly into the token claims, with userinfo values overriding token values for non-security-critical claims."; + type = lib.types.bool; + }; + headers = lib.mkOption { + default = []; + description = "Headers to be added to the upstream request. Templating is possible. Documentation can be found here: https://traefik-oidc-auth.sevensolutions.cc/docs/getting-started/middleware-configuration"; + type = lib.types.listOf (lib.types.submodule { + options = { + Name = lib.mkOption { + description = "The name of the header which should be added to the upstream request."; + type = lib.types.str; + }; + Value = lib.mkOption { + description = "The value of the header, which can use Go-Templates."; + type = lib.types.str; + }; + }; + }); + }; }; } ); @@ -90,7 +111,7 @@ in { "--providers.docker.exposedByDefault=false" "--providers.docker.network=traefik" "--providers.file.directory=/dynamic-config" - "--log.level=DEBUG" + "--log.level=INFO" "--api=true" "--ping=true" "--entrypoints.web.address=:80" @@ -143,11 +164,14 @@ in { ClientId = ''{{ env "${mapOidcClientNameToEnv oidcClientName}_OIDC_AUTH_PROVIDER_CLIENT_ID" }}''; ClientSecret = ''{{ env "${mapOidcClientNameToEnv oidcClientName}_OIDC_AUTH_PROVIDER_CLIENT_SECRET" }}''; UsePkce = oidcClientConfig.usePkce; + UseClaimsFromUserInfo = oidcClientConfig.useClaimsFromUserInfo; }; Scopes = oidcClientConfig.scopes; LoginUrl = ''{{ env "OIDC_AUTH_PROVIDER_URL" }}''; } // (lib.attrsets.optionalAttrs oidcClientConfig.enableBypassUsingClientCertificate { BypassAuthenticationRule = "HeaderRegexp(`X-Forwarded-Tls-Client-Cert`, `.+`)"; + }) // (lib.attrsets.optionalAttrs ((lib.length oidcClientConfig.headers) > 0) { + Headers = oidcClientConfig.headers; }); } ) cfg.oidcClients; From cba8dea9c769288092da98f9a620381ed7f214e9 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Fri, 9 Jan 2026 22:02:18 +0100 Subject: [PATCH 08/14] Add firefly to test Remote-User authentication --- flake.lock | 8 ++++---- hosts/srv01.hf/default.nix | 11 +++++++++++ hosts/srv01.hf/secrets.nix | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 95ea622..f504243 100644 --- a/flake.lock +++ b/flake.lock @@ -335,11 +335,11 @@ "secrets": { "flake": false, "locked": { - "lastModified": 1767139729, - "narHash": "sha256-mkmK7wiIqwmcrU+bljxzDPqh9Ya1ITqIlBmdxYxh3nI=", + "lastModified": 1767562869, + "narHash": "sha256-7wNzIr1psnPLI29vUZgWEN0Tks3GFhQwsQ9P9kDkYyY=", "ref": "refs/heads/main", - "rev": "27a126bd56c16215f80c014b8fd0b28b53605897", - "revCount": 17, + "rev": "960f3efa0589a2b5314dfd55e14685432832b2fd", + "revCount": 18, "type": "git", "url": "ssh://git@git.jfreudenberger.de/JuliusFreudenberger/nix-private.git" }, diff --git a/hosts/srv01.hf/default.nix b/hosts/srv01.hf/default.nix index 74c82c0..f4889bd 100644 --- a/hosts/srv01.hf/default.nix +++ b/hosts/srv01.hf/default.nix @@ -65,6 +65,17 @@ "groups" ]; }; + firefly = { + secret = config.age.secrets."firefly-oidc-auth"; + scopes = [ + "openid" + "email" + ]; + useClaimsFromUserInfo = true; + headers = [ + { Name = "FFIII-User"; Value = "{{`{{ .claims.email }}`}}"; } + ]; + }; }; }; diff --git a/hosts/srv01.hf/secrets.nix b/hosts/srv01.hf/secrets.nix index 8dc2205..3289c1c 100644 --- a/hosts/srv01.hf/secrets.nix +++ b/hosts/srv01.hf/secrets.nix @@ -9,6 +9,7 @@ immich-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/immich-oidc-auth"; arcane-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/arcane-oidc-auth"; arcane-secrets.file = "${inputs.secrets}/secrets/srv01-hf/arcane-secrets"; + firefly-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/firefly-oidc-auth"; step-ca-crt.file = "${inputs.secrets}/secrets/step-ca/step-ca-crt"; }; } From 24cf657f9ce076e35f2ba36004e48802e20651ae Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 18 Jan 2026 22:34:11 +0100 Subject: [PATCH 09/14] Add virtiofsd to enable shared folder to qemu --- modules/virtualization.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/virtualization.nix b/modules/virtualization.nix index fb232cc..4eb21ce 100644 --- a/modules/virtualization.nix +++ b/modules/virtualization.nix @@ -6,7 +6,10 @@ virtualisation = { libvirtd = { enable = true; - qemu.swtpm.enable = true; + qemu = { + swtpm.enable = true; + vhostUserPackages = [ pkgs.virtiofsd ]; + }; }; spiceUSBRedirection.enable = true; }; From 7adb75ed324e0c03aa52f2c30ff1d442d641a899 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 8 Feb 2026 12:11:20 +0100 Subject: [PATCH 10/14] Add pangolin module --- modules/pangolin.nix | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 modules/pangolin.nix diff --git a/modules/pangolin.nix b/modules/pangolin.nix new file mode 100644 index 0000000..3da3c9e --- /dev/null +++ b/modules/pangolin.nix @@ -0,0 +1,43 @@ +{ + pkgs-unstable, + ... +}: { + + services = { + pangolin = { + enable = true; + package = pkgs-unstable.fosrl-pangolin; + openFirewall = true; + settings = { + app = { + save_logs = true; + log_failed_attempts = true; + }; + domains = { + domain1 = { + prefer_wildcard_cert = true; + }; + }; + flags = { + disable_signup_without_invite = true; + disable_user_create_org = true; + }; + }; + }; + }; + +} + +# Settings needed on the host +# +# services = { +# pangolin = { +# dnsProvider = ""; +# baseDomain = ""; +# letsEncryptEmail = ""; +# environmentFile = config.age.secrets."".path; +# }; +# traefik = { +# environmentFiles = [ config.age.secrets."".path ]; +# }; +# }; From 074a55335197e69ebaf40ebce4aceb1dcf7a8828 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 8 Feb 2026 12:17:00 +0100 Subject: [PATCH 11/14] Add newt module --- modules/newt.nix | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 modules/newt.nix diff --git a/modules/newt.nix b/modules/newt.nix new file mode 100644 index 0000000..1f8dafd --- /dev/null +++ b/modules/newt.nix @@ -0,0 +1,72 @@ +{ + pkgs, + config, + lib, + ... +}: +let + + cfg = config.services.newt-docker; + +in { + + options.services.newt-docker = { + enable = lib.mkEnableOption "Newt, user space tunnel client for Pangolin"; + pangolinEndpoint = lib.mkOption { + description = "External URL of the Pangolin instance"; + type = lib.types.str; + }; + connectionSecret = lib.mkOption { + description = "Secrets for Pangolin authentication."; + type = lib.types.anything; + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers = { + newt = { + image = "fosrl/newt:1.9.0"; + autoStart = true; + networks = [ + "pangolin" + ]; + environment = { + PANGOLIN_ENDPOINT = cfg.pangolinEndpoint; + DOCKER_SOCKET = "/var/run/docker.sock"; + }; + environmentFiles = [ cfg.connectionSecret.path ]; + volumes = [ + "/var/run/docker.sock:/var/run/docker.sock:ro" + ]; + extraOptions = [ + "--add-host=host.docker.internal:host-gateway" + ]; + }; + }; + + systemd.services."docker-pangolin" = { + after = [ + "docker-network-pangolin.service" + ]; + requires = [ + "docker-network-pangolin.service" + ]; + }; + + systemd.services."docker-network-pangolin" = { + path = [ pkgs.docker ]; + serviceConfig = { + Type = "oneshot"; + }; + script = '' + docker network inspect pangolin || docker network create pangolin --ipv4 --ipv6 --subnet=172.18.0.0/16 --gateway=172.18.0.1 + ''; + }; + + networking.firewall.extraCommands = '' + iptables -A INPUT -p icmp --source 100.89.128.0/24 -j ACCEPT + iptables -A INPUT -p tcp --source 172.18.0.0/12 --dport 22 -j ACCEPT + ''; + + }; +} From e890501a0a2107aaa4478e2ceb26e69144b5e7d6 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 8 Feb 2026 12:25:30 +0100 Subject: [PATCH 12/14] Add dockhand module --- hosts/srv01.hf/secrets.nix | 8 ++----- modules/dockhand.nix | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 modules/dockhand.nix diff --git a/hosts/srv01.hf/secrets.nix b/hosts/srv01.hf/secrets.nix index 3289c1c..a328f24 100644 --- a/hosts/srv01.hf/secrets.nix +++ b/hosts/srv01.hf/secrets.nix @@ -5,11 +5,7 @@ teleport-join_token.file = "${inputs.secrets}/secrets/srv01-hf/teleport_auth_token"; portainer-join_token.file = "${inputs.secrets}/secrets/srv01-hf/portainer_join_token"; netcup-dns.file = "${inputs.secrets}/secrets/dns-management/netcup"; - traefik-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/traefik-oidc-auth"; - immich-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/immich-oidc-auth"; - arcane-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/arcane-oidc-auth"; - arcane-secrets.file = "${inputs.secrets}/secrets/srv01-hf/arcane-secrets"; - firefly-oidc-auth.file = "${inputs.secrets}/secrets/srv01-hf/firefly-oidc-auth"; - step-ca-crt.file = "${inputs.secrets}/secrets/step-ca/step-ca-crt"; + pangolin.file = "${inputs.secrets}/secrets/srv01-hf/pangolin"; + newt.file = "${inputs.secrets}/secrets/srv01-hf/newt"; }; } diff --git a/modules/dockhand.nix b/modules/dockhand.nix new file mode 100644 index 0000000..7eeaf8e --- /dev/null +++ b/modules/dockhand.nix @@ -0,0 +1,46 @@ +{ + config, + lib, + ... +}: +let + cfg = config.services.dockhand; +in { + options.services.dockhand = { + enable = lib.mkEnableOption "dockhand, a powerful, intuitive Docker platform"; + appUrl = lib.mkOption { + description = "External URL dockhand will be reachable from, without protocol"; + type = lib.types.str; + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers = { + dockhand = { + image = "fnsys/dockhand:v1.0.12"; + volumes = [ + "/var/run/docker.sock:/var/run/docker.sock" + ]; + environment = { + PUID = "1000"; + PGID = "1000"; + }; + networks = [ + "pangolin" + ]; + labels = { + "pangolin.public-resources.dockhand.name" = "dockhand"; + "pangolin.public-resources.dockhand.full-domain" = cfg.appUrl; + "pangolin.public-resources.dockhand.protocol" = "http"; + "pangolin.public-resources.dockhand.auth.sso-enabled" = "true"; + "pangolin.public-resources.dockhand.auth.auto-login-idp" = "1"; + "pangolin.public-resources.dockhand.targets[0].method" = "http"; + }; + extraOptions = [ + ''--mount=type=volume,source=dockhand-data,target=/app/data,volume-driver=local'' + ''--group-add=131'' # docker group + ]; + }; + }; + }; +} From fd6810bd5935642af977156ccb05dcc4720e1440 Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 8 Feb 2026 12:25:48 +0100 Subject: [PATCH 13/14] Migrate srv01-hf to pangolin and dockhand --- flake.nix | 12 +++---- hosts/srv01.hf/default.nix | 66 ++++++++++++-------------------------- 2 files changed, 26 insertions(+), 52 deletions(-) diff --git a/flake.nix b/flake.nix index a0d1393..7220354 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ }; inputs = { - #nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; nixos-hardware.url = "github:NixOS/nixos-hardware/master"; home-manager = { @@ -48,7 +48,7 @@ outputs = { self, nixpkgs, - #nixpkgs-unstable, + nixpkgs-unstable, nixos-hardware, home-manager, auto-cpufreq, @@ -81,10 +81,6 @@ system = "x86_64-linux"; specialArgs = { - #pkgs-unstable = import nixpkgs-unstable { - # inherit system; - # config.allowUnfree = true; - #}; inherit inputs outputs username; }; @@ -142,6 +138,10 @@ specialArgs = { inherit inputs outputs; + pkgs-unstable = import nixpkgs-unstable { + inherit system; + config.allowUnfree = true; + }; }; modules = [ diff --git a/hosts/srv01.hf/default.nix b/hosts/srv01.hf/default.nix index f4889bd..cbfa9e5 100644 --- a/hosts/srv01.hf/default.nix +++ b/hosts/srv01.hf/default.nix @@ -17,8 +17,9 @@ ../../modules/docker.nix ../../modules/teleport.nix ../../modules/portainer_agent.nix - ../../modules/arcane.nix - ../../modules/traefik.nix + ../../modules/pangolin.nix + ../../modules/newt.nix + ../../modules/dockhand.nix ../../modules/auto-upgrade.nix # Include the results of the hardware scan. ./hardware-configuration.nix @@ -35,54 +36,27 @@ virtualisation.oci-containers.containers.portainer_agent.environmentFiles = [ config.age.secrets."portainer-join_token".path ]; - services.traefik-docker = { - enable = true; - dashboardUrl = "traefik.juliusfr.eu"; - dnsSecrets = [ - config.age.secrets."netcup-dns" - ]; - mTLSCaCertSecret = config.age.secrets."step-ca-crt"; - oidcAuthProviderUrl = "https://login.juliusfr.eu"; - oidcClients = { - traefik-dashboard = { - secret = config.age.secrets."traefik-oidc-auth"; - }; - immich = { - secret = config.age.secrets."immich-oidc-auth"; - scopes = [ - "openid" - "email" - "profile" - ]; - enableBypassUsingClientCertificate = true; - }; - arcane = { - secret = config.age.secrets."arcane-oidc-auth"; - scopes = [ - "openid" - "email" - "profile" - "groups" - ]; - }; - firefly = { - secret = config.age.secrets."firefly-oidc-auth"; - scopes = [ - "openid" - "email" - ]; - useClaimsFromUserInfo = true; - headers = [ - { Name = "FFIII-User"; Value = "{{`{{ .claims.email }}`}}"; } - ]; - }; + services = { + pangolin = { + dnsProvider = "netcup"; + baseDomain = "juliusfr.eu"; + letsEncryptEmail = "contact@jfreudenberger.de"; + environmentFile = config.age.secrets."pangolin".path; + }; + traefik = { + environmentFiles = [ config.age.secrets."netcup-dns".path ]; }; }; - services.arcane = { + services.newt-docker = { enable = true; - appUrl = "arcane.juliusfr.eu"; - secretFile = config.age.secrets."arcane-secrets"; + pangolinEndpoint = "https://pangolin.juliusfr.eu"; + connectionSecret = config.age.secrets."newt"; + }; + + services.dockhand = { + enable = true; + appUrl = "dockhand.juliusfr.eu"; }; systemd.network = { From c9216f6468a4c8053f1420be90f8392558b2dcef Mon Sep 17 00:00:00 2001 From: JuliusFreudenberger Date: Sun, 8 Feb 2026 12:28:32 +0100 Subject: [PATCH 14/14] Update flake.lock --- flake.lock | 79 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/flake.lock b/flake.lock index f504243..d4e6d06 100644 --- a/flake.lock +++ b/flake.lock @@ -30,11 +30,11 @@ ] }, "locked": { - "lastModified": 1765042799, - "narHash": "sha256-G7UJDUNcuHm1n2EuA+2iKPNISSWoUgqk85ktncJoelo=", + "lastModified": 1769608722, + "narHash": "sha256-yWUG0Emd9EuqIZ8jQ6fxqf7USw7Gtcqb4+sBhn+S+Wg=", "owner": "AdnanHodzic", "repo": "auto-cpufreq", - "rev": "2e5c725be0a7da2c749a345e09f3df3b9ef8c209", + "rev": "a11a98c46bf6a77d0c2e0ea8d87acef78507cae5", "type": "github" }, "original": { @@ -50,11 +50,11 @@ ] }, "locked": { - "lastModified": 1766150702, - "narHash": "sha256-P0kM+5o+DKnB6raXgFEk3azw8Wqg5FL6wyl9jD+G5a4=", + "lastModified": 1769524058, + "narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=", "owner": "nix-community", "repo": "disko", - "rev": "916506443ecd0d0b4a0f4cf9d40a3c22ce39b378", + "rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d", "type": "github" }, "original": { @@ -144,11 +144,11 @@ ] }, "locked": { - "lastModified": 1767280655, - "narHash": "sha256-YmaYMduV5ko8zURUT1VLGDbVC1L/bxHS0NsiPoZ6bBM=", + "lastModified": 1769580047, + "narHash": "sha256-tNqCP/+2+peAXXQ2V8RwsBkenlfWMERb+Uy6xmevyhM=", "owner": "nix-community", "repo": "home-manager", - "rev": "d49d2543f02dbd789ed032188c84570d929223cb", + "rev": "366d78c2856de6ab3411c15c1cb4fb4c2bf5c826", "type": "github" }, "original": { @@ -181,11 +181,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1767185284, - "narHash": "sha256-ljDBUDpD1Cg5n3mJI81Hz5qeZAwCGxon4kQW3Ho3+6Q=", + "lastModified": 1769302137, + "narHash": "sha256-QEDtctEkOsbx8nlFh4yqPEOtr4tif6KTqWwJ37IM2ds=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "40b1a28dce561bea34858287fbb23052c3ee63fe", + "rev": "a351494b0e35fd7c0b7a1aae82f0afddf4907aa8", "type": "github" }, "original": { @@ -197,11 +197,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767325753, - "narHash": "sha256-yA/CuWyqm+AQo2ivGy6PlYrjZBQm7jfbe461+4HF2fo=", + "lastModified": 1769598131, + "narHash": "sha256-e7VO/kGLgRMbWtpBqdWl0uFg8Y2XWFMdz0uUJvlML8o=", "owner": "nixos", "repo": "nixpkgs", - "rev": "64049ca74d63e971b627b5f3178d95642e61cedd", + "rev": "fa83fd837f3098e3e678e6cf017b2b36102c7211", "type": "github" }, "original": { @@ -221,9 +221,10 @@ "type": "github" }, "original": { - "id": "nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs", "rev": "e6f23dc08d3624daab7094b701aa3954923c6bbb", - "type": "indirect" + "type": "github" } }, "nixpkgs-stable": { @@ -244,32 +245,34 @@ }, "nixpkgs-stable_2": { "locked": { - "lastModified": 1761016216, - "narHash": "sha256-G/iC4t/9j/52i/nm+0/4ybBmAF4hzR8CNHC75qEhjHo=", + "lastModified": 1769318308, + "narHash": "sha256-Mjx6p96Pkefks3+aA+72lu1xVehb6mv2yTUUqmSet6Q=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "481cf557888e05d3128a76f14c76397b7d7cc869", + "rev": "1cd347bf3355fce6c64ab37d3967b4a2cb4b878c", "type": "github" }, "original": { - "id": "nixpkgs", - "ref": "nixos-25.05", - "type": "indirect" + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs-unstable": { "locked": { - "lastModified": 1761114652, - "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", - "owner": "NixOS", + "lastModified": 1769861584, + "narHash": "sha256-Tu85RXpHMAWmsltAEKsG1IB7JfNGbekeHh2CSR0/xG8=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", + "rev": "015e5f32a6258dc210b8e02fb47d86983959e243", "type": "github" }, "original": { - "id": "nixpkgs", - "ref": "nixos-unstable", - "type": "indirect" + "owner": "nixos", + "ref": "pull/483348/merge", + "repo": "nixpkgs", + "type": "github" } }, "pre-commit-hooks": { @@ -301,15 +304,14 @@ "flake-compat": "flake-compat_2", "nixpkgs-libvncserver": "nixpkgs-libvncserver", "nixpkgs-stable": "nixpkgs-stable_2", - "nixpkgs-unstable": "nixpkgs-unstable", "utils": "utils" }, "locked": { - "lastModified": 1764188113, - "narHash": "sha256-Oq4aCjczgnFQqFNWZ6Ablg6x9579CO8tyBktYDYiZEs=", + "lastModified": 1769870714, + "narHash": "sha256-wjwCj70iiFXoAasQto+3jTaA4wCMOAs/rdX+nsmtBrQ=", "owner": "SaumonNet", "repo": "proxmox-nixos", - "rev": "3be878a84866b9ef9214b8ea6f53630f47f4b192", + "rev": "c1f79f104930347a0b84abbca0d42884063a8c09", "type": "github" }, "original": { @@ -327,6 +329,7 @@ "lazy-apps": "lazy-apps", "nixos-hardware": "nixos-hardware", "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable", "proxmox-nixos": "proxmox-nixos", "secrets": "secrets", "systems": "systems_3" @@ -335,11 +338,11 @@ "secrets": { "flake": false, "locked": { - "lastModified": 1767562869, - "narHash": "sha256-7wNzIr1psnPLI29vUZgWEN0Tks3GFhQwsQ9P9kDkYyY=", + "lastModified": 1769426267, + "narHash": "sha256-OBHSfMHZ+sWEtigOxTfIGnkZLPOz2P7VR8+KA2KY89g=", "ref": "refs/heads/main", - "rev": "960f3efa0589a2b5314dfd55e14685432832b2fd", - "revCount": 18, + "rev": "ebefef468e16eb692df0a3d54352c94a56110a97", + "revCount": 20, "type": "git", "url": "ssh://git@git.jfreudenberger.de/JuliusFreudenberger/nix-private.git" },