diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..53b93d4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,39 @@ +# Keycloak SSO Rollout (Server) + +## Compatible services to cover (assume up-to-date versions) +- Gitea (OAuth2/OIDC) +- Nextcloud (Social Login app) +- Paperless-ngx (OIDC) +- Mealie (OIDC v1+) +- Jellyfin (OIDC plugin) +- Kavita (OIDC-capable builds) +- Readeck (OIDC-capable builds) +- Audiobookshelf (OIDC-capable builds) +- Matrix Synapse – intentionally excluded (see below) but natively OIDC if needed + +## Explicit exclusions (no SSO for now) +- Syncplay +- Matrix/Synapse +- Arr stack (sonarr, radarr, lidarr, prowlarr, bazarr) +- qbittorrent +- sabnzbd +- metube +- multi-scrobbler +- microbin +- ryot +- maloja +- plex +- atticd + +## Phased rollout plan +1) Base identity + - Add Keycloak deployment/module and realm/client defaults. +2) Gateway/proxy auth + - Add oauth2-proxy (Keycloak provider) + nginx auth_request for non-OIDC apps (e.g., homepage-dashboard, stash). +3) Native OIDC wiring + - Configure native OIDC services (Gitea, Nextcloud, Paperless, Mealie, Jellyfin/Kavita/Readeck/Audiobookshelf) with Keycloak clients. +4) Per-service rollout + - Enable per app in priority order; document client IDs/secrets and callback URLs. +5) Verification + - Smoke-test login flows and cache any needed public keys/metadata. + diff --git a/config/overlay.nix b/config/overlay.nix index dea5260..e8da2f2 100644 --- a/config/overlay.nix +++ b/config/overlay.nix @@ -38,7 +38,7 @@ _final: prev: { waybar = prev.waybar.overrideAttrs (old: { mesonFlags = old.mesonFlags ++ [ "-Dexperimental=true" ]; }); - qbittorrent = prev.qbittorrent.overrideAttrs (old: rec { + qbittorrent = prev.qbittorrent.overrideAttrs (_old: rec { version = "5.1.3"; src = prev.fetchFromGitHub { owner = "qbittorrent"; diff --git a/config/users.nix b/config/users.nix index 873e143..c73aa80 100644 --- a/config/users.nix +++ b/config/users.nix @@ -1,5 +1,4 @@ -{ ... }: -{ +_: { users.users = { sonarr = { uid = 274; diff --git a/hosts/server/toggles.nix b/hosts/server/toggles.nix index 5ebea53..1c6a32e 100644 --- a/hosts/server/toggles.nix +++ b/hosts/server/toggles.nix @@ -81,5 +81,7 @@ in "audiobookshelf" "vaultwarden" "readeck" + "keycloak" + "oauth2-proxy" ]; } diff --git a/modules/servers/gitea.nix b/modules/servers/gitea.nix index d69c8a3..86f3981 100644 --- a/modules/servers/gitea.nix +++ b/modules/servers/gitea.nix @@ -30,6 +30,10 @@ in FROM = config.my.smtpemail; SENDMAIL_PATH = "${pkgs.msmtp}/bin/msmtp"; }; + service = { + DISABLE_REGISTRATION = true; + ALLOW_ONLY_EXTERNAL_REGISTRATION = true; + }; }; database = { socket = config.my.postgresSocket; diff --git a/modules/servers/keycloak.nix b/modules/servers/keycloak.nix new file mode 100644 index 0000000..c7e3e5c --- /dev/null +++ b/modules/servers/keycloak.nix @@ -0,0 +1,44 @@ +{ + lib, + config, + inputs, + ... +}: +let + setup = import ../factories/mkserver.nix { inherit lib config; }; + cfg = config.my.servers.keycloak; +in +{ + options.my.servers.keycloak = setup.mkOptions "keycloak" "auth" 8090; + config = lib.mkIf (cfg.enable && config.my.secureHost) { + sops.secrets.postgres-password.sopsFile = ../../secrets/secrets.yaml; + sops.secrets.keycloak = { + sopsFile = ../../secrets/env.yaml; + restartUnits = [ "keycloak.service" ]; + }; + services.keycloak = { + inherit (cfg) enable; + database = { + type = "postgresql"; + host = "localhost"; + createLocally = false; + username = "keycloak"; + name = "keycloak"; + passwordFile = config.sops.secrets.postgres-password.path; + }; + settings = { + hostname = cfg.host; + hostname-strict = true; + hostname-strict-https = false; + http-enabled = true; + http-port = cfg.port; + http-host = cfg.ip; + proxy-headers = "xforwarded"; + }; + }; + systemd.services.keycloak.serviceConfig.EnvironmentFile = config.sops.secrets.keycloak.path; + services.nginx.virtualHosts.${cfg.host} = lib.mkIf (cfg.enableProxy && config.my.enableProxy) ( + inputs.self.lib.proxyReverseFix cfg + ); + }; +} diff --git a/modules/servers/mealie.nix b/modules/servers/mealie.nix index eef34ef..e040273 100644 --- a/modules/servers/mealie.nix +++ b/modules/servers/mealie.nix @@ -17,7 +17,7 @@ in TZ = config.my.timeZone; DEFAULT_GROUP = "Home"; BASE_URL = cfg.url; - API_DOCS = "false"; + API_DOCS = "true"; ALLOW_SIGNUP = "false"; DB_ENGINE = "postgres"; POSTGRES_URL_OVERRIDE = "postgresql://${cfg.name}:@/${cfg.name}?host=${config.my.postgresSocket}"; @@ -25,6 +25,13 @@ in WEB_CONCURRENCY = "1"; SMTP_HOST = "smtp.gmail.com"; SMTP_PORT = "587"; + OIDC_AUTH_ENABLED = "true"; + OIDC_SIGNUP_ENABLED = "true"; + OIDC_CLIENT_ID = "mealie"; + OIDC_ADMIN_GROUP = "/admins"; + OIDC_USER_CLAIM = "email"; + OIDC_PROVIDER_NAME = "keycloak"; + OIDC_SIGNING_ALGORITHM = "RS256"; }; credentialsFile = config.sops.secrets.mealie.path; }; diff --git a/modules/servers/oauth2-proxy.nix b/modules/servers/oauth2-proxy.nix new file mode 100644 index 0000000..1cf8b77 --- /dev/null +++ b/modules/servers/oauth2-proxy.nix @@ -0,0 +1,51 @@ +{ + lib, + config, + ... +}: +let + setup = import ../factories/mkserver.nix { inherit lib config; }; + cfg = config.my.servers.oauth2-proxy; +in +{ + options.my.servers.oauth2-proxy = setup.mkOptions "oauth2-proxy" "auth-proxy" 4180; + config = lib.mkIf (cfg.enable && config.my.secureHost) { + sops.secrets.oauth2-proxy = { + sopsFile = ../../secrets/env.yaml; + restartUnits = [ "oauth2-proxy.service" ]; + }; + sops.secrets.oauth2-proxy-cookie = { + sopsFile = ../../secrets/secrets.yaml; + restartUnits = [ "oauth2-proxy.service" ]; + }; + services.oauth2-proxy = { + inherit (cfg) enable; + provider = "keycloak-oidc"; + clientID = "oauth2-proxy"; + keyFile = config.sops.secrets.oauth2-proxy.path; + oidcIssuerUrl = "${config.my.servers.keycloak.url}/realms/homelab"; + httpAddress = "${cfg.ip}:${toString cfg.port}"; + email.domains = [ "*" ]; + cookie = { + name = "_oauth2_proxy"; + secure = true; + expire = "168h"; + refresh = "1h"; + domain = ".lebubu.org"; + secret = config.sops.secrets.oauth2-proxy-cookie.path; + }; + extraConfig = { + skip-auth-route = [ "^/ping$" ]; + set-xauthrequest = true; + pass-access-token = true; + pass-user-headers = true; + request-logging = true; + auth-logging = true; + session-store-type = "cookie"; + skip-provider-button = true; + code-challenge-method = "S256"; + whitelist-domain = [ ".lebubu.org" ]; + }; + }; + }; +} diff --git a/modules/servers/paperless.nix b/modules/servers/paperless.nix index 5763677..7d9fe4b 100644 --- a/modules/servers/paperless.nix +++ b/modules/servers/paperless.nix @@ -1,21 +1,28 @@ { lib, config, ... }: let cfg = config.my.servers.paperless; + port = config.services.paperless.port; in { options.my.servers.paperless.enable = lib.mkEnableOption "Paperless-ngx document management system"; config = lib.mkIf (cfg.enable && config.my.servers.postgres.enable) { - networking.firewall.allowedTCPPorts = [ config.services.paperless.port ]; + networking.firewall.allowedTCPPorts = [ port ]; services.paperless = { inherit (cfg) enable; - address = "0.0.0.0"; + address = config.my.ips.server; consumptionDirIsPublic = true; consumptionDir = "/srv/pool/scans/"; settings = { + PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL = "http"; + PAPERLESS_URL = "http://${config.my.ips.server}:${builtins.toString port}"; PAPERLESS_DBENGINE = "postgress"; PAPERLESS_DBNAME = "paperless"; PAPERLESS_DBHOST = config.my.postgresSocket; PAPERLESS_TIME_ZONE = config.my.timeZone; + PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect"; + PAPERLESS_ACCOUNT_ALLOW_SIGNUPS = false; + PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS = true; + PAPERLESS_SOCIAL_AUTO_SIGNUP = true; PAPERLESS_CONSUMER_IGNORE_PATTERN = builtins.toJSON [ ".DS_STORE/*" "desktop.ini" diff --git a/modules/servers/postgres.nix b/modules/servers/postgres.nix index 4978528..188ea56 100644 --- a/modules/servers/postgres.nix +++ b/modules/servers/postgres.nix @@ -40,6 +40,7 @@ let "sonarqube" "gitea" "atticd" + "keycloak" ]; in { diff --git a/modules/servers/qbittorrent.nix b/modules/servers/qbittorrent.nix index fa9d0c6..87b9393 100644 --- a/modules/servers/qbittorrent.nix +++ b/modules/servers/qbittorrent.nix @@ -7,10 +7,6 @@ }: let inherit (inputs) qbit_manage; - pkgsU = import inputs.nixpkgs-unstable { - system = "x86_64-linux"; - config.allowUnfree = true; - }; vuetorrent = pkgs.fetchzip { url = "https://github.com/VueTorrent/VueTorrent/releases/download/v2.31.0/vuetorrent.zip"; sha256 = "sha256-kVDnDoCoJlY2Ew71lEMeE67kNOrKTJEMqNj2OfP01qw="; diff --git a/modules/services/sound.nix b/modules/services/sound.nix index a562251..a391b93 100644 --- a/modules/services/sound.nix +++ b/modules/services/sound.nix @@ -1,7 +1,6 @@ { config, lib, - inputs, ... }: { diff --git a/secrets/env.yaml b/secrets/env.yaml index 131252b..b7d0d03 100644 --- a/secrets/env.yaml +++ b/secrets/env.yaml @@ -2,7 +2,7 @@ gitea: ENC[AES256_GCM,data:8o+U4qFdyIhCPNlYyflQIuLHsQHtbT6G/a0OyCUeg9DtIeABXNVFh shiori: ENC[AES256_GCM,data:tV7+1GusZvcli8dM86xOD71dc2mzcyfQwMeTh//LDb0=,iv:ED9wR6QjQgwd9Ll/UC5FK3CyYK3b0RniC/D6Y0nGEOI=,tag:X/aopMc2vhnRW2iTphFflQ==,type:str] flame: ENC[AES256_GCM,data:XsYRsA2xs+juWje2Od2Yl2xIvU0OS8xMrtwtcK/0NyyRrg==,iv:FR8lHsNQNCaOy4P+7BsIjNCz+H38i5RlwLYQ4fpB2+w=,tag:61EV7H04pcr1bSX4nSvlpw==,type:str] ryot: ENC[AES256_GCM,data:VMWf3VqcUdyJu2Ygd3XmoqGNWY/W/VJ4213ej0FrA95kAoX+S+j0+4a4B65NtW9UheDSxD1swTXebyenJCIN/tEZwH2wj9I12akNNvSDpt/LG3d1/BZ62cvLCb5n9vyE/vcXgJVfPUqmc67pYDWLpEV/vkKjpqwNH4Y8vnapVo1ytIgsjkTuBb7VFbnRPvYs6J1M0rnaTtkVhOBoRxv+Xg3pWYCgFEXdM/Pg/WKqdHpyh+tJqR74Z91Mwv6G56ZYEDQmAp+Cn+Kk2zZ+t44UAu1SQOgYXPLep+4/PgWw/vQMuyN7GNNP6TrsX3g+ONtJtkdmGu6ArcfbRAky4vM14DxlQP4xSjYSu+FDWGJL/J4TMw6IVDuw/TDVNpMrhBmZdPujYLUW1c6GCCEchBknNfw/Wt+NyTjOzCmZLVw760jY05Fa9kcW2kz+P0iAGTviY7yJZWDctP6PrVNtG1cXc4noJqV/uJ9sQmuGWCiTzaCIIZEhwRKnvjpvZNisKPhx4tctZMWm8l9gKO/TJC/SHMIhvEazmH4v0AzCiRUzdTfnWQZGTNenDrCUetztPh/UUJbLZjhFBH3QR26w/3I5oNpUzUDhfDhcEYtfWuB7ckbkXT8nyYMfe0OR16yJTfQCdnIPBhAUi1g1ZV3jFg+OhYWxk73lPiqC1ADRNh01L1k90PMMWtLXXm6aQ28cB+iQTvvgKbDrr76U8bXoZUyEl30waOQ2HT6nDG61OBUtQHTu6/cFhfhrnU6poAD/k+L7SyqcBoMYAZJN6Us1y3SKhV/3mXVKjRwSl5XZSW+ZpcRe/Cg4bonxFBYsZyY3VjK0LC4Cj8ijh4LpYWrGWtVmWOt/gg7UQPTd81A=,iv:Oa2pvfDpfPr3pqeAg2kYIzjf8KUK9ckMfbVymM78FyE=,tag:XyjYEvWo46BliYXdDH8QrQ==,type:str] -mealie: ENC[AES256_GCM,data:RjKqDs70lWhGN0LXPp3feQfW/WtfJlR6vX++0hwGtqcA3iepEh2Ab/36YRKbsVRBkglp0u18MusTmP0LSHUpzgCn/c/5ZzzRLGL83K3aQRlg8JtdTvzvEnLQSdE=,iv:GEfa8LwpOhkqWtLk0I5F14zkHcnFjVhVaHeLSFlDkN4=,tag:lkGcFn91hVxraMHCKF7rXQ==,type:str] +mealie: ENC[AES256_GCM,data:JmubDEnMp/djzsO6AQOyJkTKZYAUbTMoeIKGQ423MZfGbMVld0vmVSR0C1l8J4VFhW3HLGsoDdg4yRHpckgyqrN+VVFPovsDUZS71VnTNSP02CuJCjmqt4p6VGnB2wBlPKHx51VwFiVO3Q3WbwGivE3XjqQaF7mZuQOAuNjNOW7qinh062/d78uzU3c4s5eD8HBiq+3Q2O+Mj5CUW+PA580ikxur+/tGusE5TniqX8A56Fo8McTU/2w7YoA6f5UaFTHDdmDTwF7mhhxd/70k0hoeb8iQkIapvPFVME6osBHlF43wDhRS+OAFb0ZuMumf1g==,iv:Ynpbqb7Np5SPBCqVuIh9rxeE5nVIoNZNddvllrPOXZs=,tag:u/P1kaEEnfkHHj2Sul0Bww==,type:str] maloja: ENC[AES256_GCM,data:yCwokfD4I1Boy2NOhOTLA3dWgUVOdSzWKIEdYC0klvYu41IGcM8bM65uYFmiOtk+jHgt6j3kO/pBBlC4w/iTElphTqFyFRGdBN4fNRntAhMzqOszBZII,iv:Vf9hfNwSTBkh2cXV7Y2fv4NA8kng2M1i7BtTXJvy4u4=,tag:KLc8sP6N2/Pp/9069E3aPQ==,type:str] multi-scrobbler: ENC[AES256_GCM,data:ce3dd0PKm6eyD2AqWmw+8iex/tBHgMhG8ASoOMkT3c9k6kiZabpTTFTkcouMO+s42P+qjWQAUJcJlDdYVYJZbAqw8nnxLrtYmKoBknSbbWijlR//CpgfwuuAWIyGQAGVPliuxz+lR+1cf/G2mXM+FJIfp8Sliak3v/nGg3ry0bdjbOLVoBM4rS90Jrq98ZuBrjlFVhcJTKkEHtgDv8N56wWbPL/r3cTlS9MoEu2ulCSLvfu/snr8HqJ5yssAGQ==,iv:jOJulX6o3t+W6DrD6sU7amDH7JQP/JFGBI9IM8m/sXU=,tag:jFZoLpYFXj+xplbypf3nvw==,type:str] vaultwarden: ENC[AES256_GCM,data:NituIOyGrYALEkuwKT0RRS1gvi3wjC6ZSAfUIejfi8xoePE6vSNztJTGsRSIh4sJnRrQIiDuKTmRKZDM6AtX/oEBsNW8MVq+lWAq/vtcO7fuTriySEungmpXhQwRZD6NsXE+9283P3s6RshpA4iipmENiW2v2/uxkIXxtTguUxfX0psWYtF6mx5/hpaoNZ523OB69m6veAxD6Pmnj+pTOAORGXHldoNrxNc35WBDdndjAZICyO873tbs22VJOWD9a66BNxtfwIPYoFkuPO6QG3nnFfyPSQ==,iv:rmDJbrP+NQ5HGdRCWSYfymP8dU9WJdMEhAg80eupgeY=,tag:kdNzgWjgeqaTCjqUCc4uWw==,type:str] @@ -11,6 +11,8 @@ cloudflare-api: ENC[AES256_GCM,data:iNUMlY8rz5yHVitpK4HGaFSK7j+c8Pm7rOQMOQGmSJ3a synapse: ENC[AES256_GCM,data:IR0pFwQBEM4O8mzzYXrPe2FjulSUGuitzLDLms2uovr6gEU82mCkRO/UCQOybNm03iOQeXX0Whz739kpYSGSInEyx69BNG/etH+bMu+GbYeMdrTEyXHSa7kcH4Ug,iv:Vn2ILYXnCj+Op/E2kWoxV+2ZtlxYJxO6XK3Ql41KW6w=,tag:9wogJFLlmfM5PRgPdwFlcw==,type:str] readeck: ENC[AES256_GCM,data:TsIkHLji37dDHQRt78SquBhoSREHDgvgbc6+M1k2MLrgMGJ/Ejfy5AZXCIp/Qj5sXDzKP4j6Y6xFvGLswCqe02XjqGCpX13gZVCFPuKr8Nq051Xg,iv:Rc/pjYP+Vd/DvLCYsfJjDrnAlAiUlZOcNeeYzE6O3UY=,tag:OvR+CXMmrUFbsrHvduhnjA==,type:str] lidarr-mb-gap: ENC[AES256_GCM,data:bNzD9Nf9BWAPkm0Yk0J4MJbmo908QX9VsD+40Rngnfec9nzH4vZ2DrelxRllgT1kgnXMQzvoSgNhBwkDN4fgX73hz1FjkytTwahlO0wcY6R+tw4aokh0QYy0TVx5pZ4u1FEQOAp3IMgBsP8HOqaL/NEsEo3yb0K9iC3AfFihkLDJmVh26Pg=,iv:go0qS7/BcfcAMPkAdGWCoL61gNqBG5lWDev++y9DJ/I=,tag:LgtEyTZH8NfhfrKTcAigZw==,type:str] +keycloak: ENC[AES256_GCM,data:BmwZxuJaOB8F7zmBNAf42lkw36s5TepimtdyT2xjdGVyuHgRHbTZqeVen7/0II39qrJjko4agZJgToIZ1uhaC/gpGSoHZlib3rJozPCqmBc42nO6SOtpIO8=,iv:kPModK85937/liNk6iLIRiQ/G5yB7S7h24ZzPb8A1zo=,tag:lWvDQAHVRiBz8XZUoADKvw==,type:str] +oauth2-proxy: ENC[AES256_GCM,data:MnAMX4adm8joZGaxZhgMDGf/15U2tk3dE/0dHFwETIi4JdpNvG/PUHTWGmXJrUnRrFxdZaOtGUzAMF47,iv:eEoo0YM+wt2/pCcONHM9YPRj/q4fC9OQZr+ckRsmhjY=,tag:AevxpvvRt13T5w5xwzay5w==,type:str] sops: age: - recipient: age1lufn6t35gs4wgevyr2gud4eec7lvkn7pgnnv4tja64ww3hef7gqq8fas37 @@ -49,7 +51,7 @@ sops: QXRUYWtGcWZCVW11U3VYRktuUjlCbDgKsTK4WhUza/JuoDTU3uATa6fq/8eYzxtb 9BUK1ddzx9Mghea9XBMS17YGtGmW800OsLBomb3SINnOFvejcnKf8Q== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-11-11T23:18:34Z" - mac: ENC[AES256_GCM,data:i3U364pjZB5Y61Wf7ETbXhNWyfH1gw0oyPcNyT+nCIJmePh8JWiP9hnHmZfLS1BKkI2powQdezbz9R0XDvU7g2SkV8EsWmn/h3rFwbopUZbeRQ2SCoX7LGFez74l1oTPQjL8zWJVdrUtfAFgbZKSEWuz7rsDieKBVhIJwWaeePY=,iv:N4z+X3eD6jH+zQfY24qec+U6wkfhLGPm4MzY8T2Km/A=,tag:yluW5YSKMZ4Kk+wcXbkj8Q==,type:str] + lastmodified: "2025-12-10T19:51:43Z" + mac: ENC[AES256_GCM,data:2U3Q6V1RL4xqPQbTvAZ76J/q8buGZTlZpSx5Alj2C1txarbHgeEkoHCmnkK6c3KQPD1qoBwuAhLd1z3FOTujmQERW+ptStShj03dNX+qW+hTHKrhJ6VKDuN5euOa1MkABO2LT1ylHNLahOlht5wYLP8JPoNyLuBtAAsZ1bZwHtY=,iv:BW+JF27xdbWpjcje2Px5XSLtjMp2zvtTl7q/+ihFxIE=,tag:LjRx1DCafMh9JPuPVkOGYQ==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0 diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index d1437bc..e5c2704 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -14,6 +14,8 @@ stash: unpackerr: sonarr-api: ENC[AES256_GCM,data:74/aSs7Q2tcDh9hPGm88h2hIneOcJ+P9HaCqoeuL6FQ=,iv:1AOpCii7ED1EyOFNCzvgRp5tR2ic1U6oRi7yg0hUcLk=,tag:k1miUivDQPxRgBWhXi9f+w==,type:str] radarr-api: ENC[AES256_GCM,data:bZiJNk/ewREIBss+z4daVwL1UyI4rt8GxVmC/bpTNvc=,iv:li2kMzOgdWtLLr4l244P082Z0jwDB2aEC6iRYt3o/HY=,tag:mi9SY/pT2qTIzR/ngp8bGg==,type:str] +postgres-password: ENC[AES256_GCM,data:V0g4T1cLUFnTN94zZZR83/KVJFUDGEWVEn6nyijnver4QCELUFkNr99s9g==,iv:1ymHA0JaVC2/aHdg4TmJmuKOG8JGZRRvynrgQIGdTss=,tag:xsCVpc+HBaNeswYvzo0PaA==,type:str] +oauth2-proxy-cookie: ENC[AES256_GCM,data:eWEgnIGcdq1aRXWokmVO9DDb+t2oAxNCwFeyOUITzHQ=,iv:x5CROKQ5arUMESWQsroC15xbtMA6/HvnArhBiGwAx6k=,tag:U5yYk1ztExZsou7gVvA8Og==,type:str] sops: age: - recipient: age1lufn6t35gs4wgevyr2gud4eec7lvkn7pgnnv4tja64ww3hef7gqq8fas37 @@ -52,7 +54,7 @@ sops: RmRyZldlMjUwMEdUUEpDS2JSa2tDTTAKp/pT+0cNnCuKVL+Z0fEMiw1PL9PB/nSM QWVTo0Mt8Y6X0Xt0EAi9G5AYxADZ/mmEWPxB7RFgVAiMKtor5Gy1zw== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-10-02T20:02:38Z" - mac: ENC[AES256_GCM,data:DnbkeF+evVTMhYTg3OU528cRQ+jBiUl7Q7JZxyGRL6USjB2OdIRxqnnCH8L36K2hSAIkKQ/kojyJs+8Pgkx5uD/qsCbGlNT9pSBU1qPdSBxqJsVPxHZmkuf/QxGtE4pgV/50xJMrVyzAetWPZuxcYVfWAPszxDZcR5XDuD+Yjk4=,iv:i2Vt6nv6etIgaaoxsbVlxEnIhIx4adOQZFeyGM/4Saw=,tag:jugPmHU78lap7Hy7RJd9pg==,type:str] + lastmodified: "2025-12-10T10:25:19Z" + mac: ENC[AES256_GCM,data:nltQOPjhpJ0+xPBpA8SZOxbV9HeahxS7xG6I+sdYHhNxPsjYnpyTlIf281NdnRaefcGbtcsXDBo3sDeiOjL6zfknQ88nMEyR0tFNXAjb0K1aPAtDfwoZR69hftWafJi9RWNCEFg0W3L/CSLPCB57Xqr3NSKtDeftCBcJ1kYpXmQ=,iv:loSoBoLIId6TNxh5PHrmYO9tVaF/HIJpE4U7fMphqCQ=,tag:WWZ3Fq5dB3eRK4jhKWUGNg==,type:str] unencrypted_suffix: _unencrypted - version: 3.10.2 + version: 3.11.0