diff --git a/flake.lock b/flake.lock index 3348a05..78c0194 100644 --- a/flake.lock +++ b/flake.lock @@ -30,11 +30,11 @@ ] }, "locked": { - "lastModified": 1772058043, - "narHash": "sha256-m1cmQgb6tBcHkndKZ8BSsw6PRNJMG89FZwoYVOuKi34=", + "lastModified": 1774779250, + "narHash": "sha256-n7zH1dk+mcUt59i5FDSF3q6G398NzKVt/E/lM2DZY8A=", "owner": "AdnanHodzic", "repo": "auto-cpufreq", - "rev": "5d600d710bb2aa331e1a4370e08476bcdea1cab5", + "rev": "a620b9971963730978a0e5fc9d92bf613218d21e", "type": "github" }, "original": { @@ -50,11 +50,11 @@ ] }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1776613567, + "narHash": "sha256-gC9Cp5ibBmGD5awCA9z7xy6MW6iJufhazTYJOiGlCUI=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "32f4236bfc141ae930b5ba2fb604f561fed5219d", "type": "github" }, "original": { @@ -144,11 +144,11 @@ ] }, "locked": { - "lastModified": 1774559029, - "narHash": "sha256-deix7yg3j6AhjMPnFDCmWB3f83LsajaaULP5HH2j34k=", + "lastModified": 1775425411, + "narHash": "sha256-KY6HsebJHEe5nHOWP7ur09mb0drGxYSzE3rQxy62rJo=", "owner": "nix-community", "repo": "home-manager", - "rev": "a0bb0d11514f92b639514220114ac8063c72d0a3", + "rev": "0d02ec1d0a05f88ef9e74b516842900c41f0f2fe", "type": "github" }, "original": { @@ -181,11 +181,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1774465523, - "narHash": "sha256-4v7HPm63Q90nNn4fgkgKsjW1AH2Klw7XzPtHJr562nM=", + "lastModified": 1776983936, + "narHash": "sha256-ZOQyNqSvJ8UdrrqU1p7vaFcdL53idK+LOM8oRWEWh6o=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "de895be946ad1d8aafa0bb6dfc7e7e0e9e466a29", + "rev": "2096f3f411ce46e88a79ae4eafcfc9df8ed41c61", "type": "github" }, "original": { @@ -197,11 +197,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1774388614, - "narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=", + "lastModified": 1776734388, + "narHash": "sha256-vl3dkhlE5gzsItuHoEMVe+DlonsK+0836LIRDnm6MXQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e", + "rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac", "type": "github" }, "original": { @@ -261,11 +261,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1774386573, - "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", "type": "github" }, "original": { @@ -307,11 +307,11 @@ "utils": "utils" }, "locked": { - "lastModified": 1769870714, - "narHash": "sha256-wjwCj70iiFXoAasQto+3jTaA4wCMOAs/rdX+nsmtBrQ=", + "lastModified": 1775228763, + "narHash": "sha256-8fKOEOouCaPZLBTdWPS+uU0bxsPp1OmfloDNoNSiu8w=", "owner": "SaumonNet", "repo": "proxmox-nixos", - "rev": "c1f79f104930347a0b84abbca0d42884063a8c09", + "rev": "e803cb839e5e5207fa37d92bc6ac7290f4dba633", "type": "github" }, "original": { @@ -338,11 +338,11 @@ "secrets": { "flake": false, "locked": { - "lastModified": 1774571252, - "narHash": "sha256-NU/vfItTMSjaRTXe0UDzbWR8UnhkBUFU47OpqEpxKb4=", + "lastModified": 1777076192, + "narHash": "sha256-N7n2OPN2IRWwL73Cr6mc5nNhucHmMeFas9hQ/NF0bFg=", "ref": "refs/heads/main", - "rev": "7965907ae885d77acb3c4ecc11cee096a12af868", - "revCount": 25, + "rev": "34ff1c4b0460a2e103a8fec183f53f274dc123ed", + "revCount": 32, "type": "git", "url": "ssh://git@git.jfreudenberger.de/JuliusFreudenberger/nix-private.git" }, diff --git a/flake.nix b/flake.nix index 3fa246c..74a3034 100644 --- a/flake.nix +++ b/flake.nix @@ -167,6 +167,24 @@ ]; }; + srv03 = nixpkgs.lib.nixosSystem rec { + system = "x86_64-linux"; + + specialArgs = { + inherit inputs outputs; + pkgs-unstable = import nixpkgs-unstable { + inherit system; + config.allowUnfree = true; + }; + }; + + modules = [ + disko.nixosModules.disko + agenix.nixosModules.default + ./hosts/srv03 + ]; + }; + }; homeConfigurations = { diff --git a/hosts/srv03/default.nix b/hosts/srv03/default.nix new file mode 100644 index 0000000..b56205c --- /dev/null +++ b/hosts/srv03/default.nix @@ -0,0 +1,91 @@ +{ inputs, outputs, config, lib, pkgs, ... }: + +{ + imports = + [ + ./secrets.nix + + ../../modules/disko/legacy-full-ext4-swap.nix + + ../../users/julius/nixos-server.nix + ../../modules/nix.nix + ../../modules/locale.nix + ../../modules/server-cli.nix + ../../modules/sshd.nix + ../../modules/qemu-guest.nix + ../../modules/docker.nix + ../../modules/traefik.nix + ../../modules/pocket-id.nix + ../../modules/auto-upgrade.nix + "${inputs.secrets}/modules/opkssh.nix" + # Include the results of the hardware scan. + ./hardware-configuration.nix + ]; + + #services.openssh.openFirewall = false; + + services = { + traefik-docker = { + enable = true; + dashboardUrl = "traefik.netbird.jfreudenberger.de"; + dnsChallengeProvider = "inwx"; + dnsSecrets = [ + config.age.secrets.inwx + ]; + }; + + pocket-id-docker.enable = true; + pocket-id = { + settings = { + APP_URL = "https://login.jfreudenberger.de"; + TRUST_PROXY = true; + }; + environmentFile = config.age.secrets.pocket-id.path; + }; + }; + + systemd.network = { + enable = true; + networks."10-wan" = { + matchConfig.Name = "enp1s0"; + networkConfig.DHCP = "no"; + address = [ + "46.224.47.24/32" + "2a01:4f8:c013:bf68::1/64" + ]; + routes = [ + { Gateway = "172.31.1.1"; GatewayOnLink = true; } + { Gateway = "fe80::1"; GatewayOnLink = true; } + ]; + dns = [ "9.9.9.9" ]; + }; + }; + + boot = { + tmp.cleanOnBoot = true; + growPartition = true; + kernelParams = [ "console=ttyS0" ]; + loader = { + grub.enable = true; + }; + }; + + # Disable classic networking configuration + networking.useDHCP = lib.mkForce false; + + networking.hostName = "srv03"; # Define your hostname. + + # This option defines the first version of NixOS you have installed on this particular machine, + # and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions. + # Most users should NEVER change this value after the initial install, for any reason, + # even if you've upgraded your system to a new NixOS release. + # This value does NOT affect the Nixpkgs version your packages and OS are pulled from, + # so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how + # to actually do that. + # This value being lower than the current NixOS release does NOT mean your system is + # out of date, out of support, or vulnerable. + # Do NOT change this value unless you have manually inspected all the changes it would make to your configuration, + # and migrated your data accordingly. + # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . + system.stateVersion = "25.05"; # Did you read the comment? +} diff --git a/hosts/srv03/hardware-configuration.nix b/hosts/srv03/hardware-configuration.nix new file mode 100644 index 0000000..729ee98 --- /dev/null +++ b/hosts/srv03/hardware-configuration.nix @@ -0,0 +1,24 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.enp1s0.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/hosts/srv03/secrets.nix b/hosts/srv03/secrets.nix new file mode 100644 index 0000000..2a119d0 --- /dev/null +++ b/hosts/srv03/secrets.nix @@ -0,0 +1,7 @@ +{ inputs, ... }: +{ + age.secrets = { + inwx.file = "${inputs.secrets}/secrets/dns-management/inwx"; + pocket-id.file = "${inputs.secrets}/secrets/srv03/pocket-id"; + }; +} diff --git a/modules/administration.nix b/modules/administration.nix index 233f194..a68b589 100644 --- a/modules/administration.nix +++ b/modules/administration.nix @@ -7,6 +7,7 @@ remmina teleport_17.client + opkssh ]; } diff --git a/modules/disko/legacy-full-ext4-swap.nix b/modules/disko/legacy-full-ext4-swap.nix new file mode 100644 index 0000000..c06727f --- /dev/null +++ b/modules/disko/legacy-full-ext4-swap.nix @@ -0,0 +1,45 @@ +{ + disko.devices = { + disk = { + sda = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + MBR = { + type = "EF02"; # for grub MBR + size = "1M"; + priority = 1; # Needs to be first partition + }; + ESP = { + size = "1G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + end = "-1G"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + encryptedSwap = { + size = "100%"; + content = { + type = "swap"; + randomEncryption = true; + priority = 100; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/modules/dockhand.nix b/modules/dockhand.nix index 7eeaf8e..1e8f57b 100644 --- a/modules/dockhand.nix +++ b/modules/dockhand.nix @@ -38,7 +38,7 @@ in { }; extraOptions = [ ''--mount=type=volume,source=dockhand-data,target=/app/data,volume-driver=local'' - ''--group-add=131'' # docker group + ''--group-add=${config.ids.gids.docker}'' ]; }; }; diff --git a/modules/laptop.nix b/modules/laptop.nix index 9a95899..49e7492 100644 --- a/modules/laptop.nix +++ b/modules/laptop.nix @@ -4,7 +4,7 @@ ... }: { - boot.kernelPackages = pkgs.linuxPackages_latest; + boot.kernelPackages = pkgs.linuxKernel.packages.linux_zen; services.logind.settings.Login = { HandleLidSwitch= "suspend-then-hibernate"; diff --git a/modules/pocket-id.nix b/modules/pocket-id.nix new file mode 100644 index 0000000..48ac9cb --- /dev/null +++ b/modules/pocket-id.nix @@ -0,0 +1,58 @@ +{ + config, + lib, + ... +}: +let + + cfg = config.services.pocket-id-docker; + pocketidCfg = config.services.pocket-id; + version = "2.6.2"; + +in { + + options.services.pocket-id-docker = { + enable = lib.mkEnableOption "Pocket ID server hosted as OCI container"; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers = { + pocket-id = { + image = "ghcr.io/pocket-id/pocket-id:v${version}"; + autoStart = true; + networks = [ + "webproxy" + ]; + environment = { + APP_URL = pocketidCfg.settings.APP_URL; + TRUST_PROXY = lib.boolToString pocketidCfg.settings.TRUST_PROXY; + ANALYTICS_DISABLED = lib.boolToString pocketidCfg.settings.ANALYTICS_DISABLED; + }; + environmentFiles = [ pocketidCfg.environmentFile ]; + extraOptions = [ + ''--mount=type=volume,source=data,target=/app/data,volume-driver=local'' + "--health-cmd=/app/pocket-id healthcheck" + "--health-interval=1m30s" + "--health-timeout=5s" + "--health-retries=2" + "--health-start-period=10s" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.pocket-id.rule" = "Host(`${lib.removePrefix "https://" pocketidCfg.settings.APP_URL}`)"; + "traefik.http.routers.pocket-id.entrypoints" = "websecure"; + }; + }; + }; + + systemd.services."docker-pocket-id" = { + after = [ + "docker-traefik.service" + ]; + requires = [ + "docker-traefik.service" + ]; + }; + + }; +} diff --git a/modules/traefik-oidc.nix b/modules/traefik-oidc.nix new file mode 100644 index 0000000..92b1204 --- /dev/null +++ b/modules/traefik-oidc.nix @@ -0,0 +1,221 @@ +{ + 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; + }; + 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; + }; + }; + }); + }; + }; + } + ); + }; + }; + + 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=INFO" + "--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; + 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; + 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"; + + }; +} diff --git a/modules/traefik.nix b/modules/traefik.nix index 92b1204..8bea41f 100644 --- a/modules/traefik.nix +++ b/modules/traefik.nix @@ -7,15 +7,7 @@ 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"; - }; - }; + version = "3.6.14"; in { @@ -29,105 +21,40 @@ in { 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."; + dnsChallengeProvider = lib.mkOption { + description = "Name of provider for DNS challenge."; 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; - }; - 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; - }; - }; - }); - }; - }; - } - ); - }; }; config = lib.mkIf cfg.enable { virtualisation.oci-containers.containers = { traefik = { - image = "traefik:v3.6.6"; + image = "traefik:v${version}"; cmd = [ "--providers.docker=true" + "--providers.docker.endpoint=http://docker-socket-proxy:2375" "--providers.docker.exposedByDefault=false" - "--providers.docker.network=traefik" + "--providers.docker.network=webproxy" "--providers.file.directory=/dynamic-config" "--log.level=INFO" "--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.websecure.transport.respondingTimeouts.readTimeout=0" + "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0" + "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0" + "--serverstransport.forwardingtimeouts.responseheadertimeout=0s" + "--serverstransport.forwardingtimeouts.idleconntimeout=0s" "--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.email=contact@jfreudenberger.de" "--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" + "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=${cfg.dnsChallengeProvider}" + "--providers.file.filename=/dynamic-config/providers.yaml" ]; autoStart = true; ports = [ @@ -135,54 +62,17 @@ in { "443:443" ]; networks = [ - "traefik" + "webproxy" + "docker-socket" ]; - 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"; - }; + environmentFiles = lib.forEach cfg.dnsSecrets (secret: secret.path); 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; - 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; - traefik-oidc-authentication-config = (pkgs.formats.yaml {}).generate "traefik-oidc-auth" { - http.middlewares = oidc-config; + traefik-providers-config = (pkgs.formats.yaml {}).generate "traefik-providers-config" { + tcp.serversTransports.pp-v2.proxyProtocol.version = 2; }; 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" + "${traefik-providers-config}:/dynamic-config/providers.yaml:ro" ]; extraOptions = [ ''--mount=type=volume,source=certs,target=/certs,volume-driver=local'' @@ -194,24 +84,49 @@ in { "--health-start-period=5s" ]; }; + docker-socket-proxy = { + image = "tecnativa/docker-socket-proxy:v0.4.2"; + autoStart = true; + networks = [ + "docker-socket" + ]; + environment = { + CONTAINERS = "1"; + }; + volumes = [ + "/var/run/docker.sock:/var/run/docker.sock:ro" + ]; + }; }; systemd.services."docker-traefik" = { after = [ - "docker-network-traefik.service" + "docker-network-webproxy.service" + "docker-network-docker-socket.service" ]; requires = [ - "docker-network-traefik.service" + "docker-network-webproxy.service" + "docker-network-docker-socket.service" ]; }; - systemd.services."docker-network-traefik" = { + systemd.services."docker-network-webproxy" = { 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 + docker network inspect webproxy || docker network create webproxy --ipv4 --ipv6 --subnet=172.18.0.0/16 --gateway=172.18.0.1 + ''; + }; + + systemd.services."docker-network-docker-socket" = { + path = [ pkgs.docker ]; + serviceConfig = { + Type = "oneshot"; + }; + script = '' + docker network inspect docker-socket || docker network create docker-socket --ipv4 --ipv6 --subnet=172.19.0.0/16 --gateway=172.19.0.1 ''; };