diff --git a/ansible/playbooks/tasks/integrations/nextcloud.yml b/ansible/playbooks/tasks/integrations/nextcloud.yml index 0f6bee08..c6ece3cd 100644 --- a/ansible/playbooks/tasks/integrations/nextcloud.yml +++ b/ansible/playbooks/tasks/integrations/nextcloud.yml @@ -1,8 +1,32 @@ --- -- ansible.builtin.include_tasks: _write-secret-and-restart.yml - vars: - secret_name: nextcloud-sso-env - secret_data: - OIDC_CLIENT_ID: "{{ oidc_client_credentials.client_id }}" - OIDC_CLIENT_SECRET: "{{ oidc_client_credentials.client_secret }}" - deployment_name: nextcloud +- name: "[{{ client_spec.slug }}] Configure local Nextcloud OIDC" + ansible.builtin.command: + argv: + - docker + - exec + - --user=33:33 + - -e + - "OIDC_CLIENT_ID={{ oidc_client_credentials.client_id }}" + - -e + - "OIDC_CLIENT_SECRET={{ oidc_client_credentials.client_secret }}" + - nextcloud + - /bin/sh + - -ec + - | + occ() { + php /var/www/html/occ "$@" + } + + occ app:install user_oidc || true + occ app:enable user_oidc + occ user_oidc:provider cute.haus \ + --clientid="$OIDC_CLIENT_ID" \ + --clientsecret="$OIDC_CLIENT_SECRET" \ + --discoveryuri="https://id.cute.haus/.well-known/openid-configuration" \ + --scope="openid email profile" \ + --mapping-uid=preferred_username \ + --mapping-email=email \ + --mapping-display-name=name \ + --unique-uid=0 + delegate_to: jubilife + no_log: true diff --git a/k8s/flux/apps/nextcloud.yaml b/k8s/flux/apps/nextcloud.yaml index ca2b469f..99284cef 100644 --- a/k8s/flux/apps/nextcloud.yaml +++ b/k8s/flux/apps/nextcloud.yaml @@ -4,6 +4,7 @@ metadata: name: nextcloud namespace: flux-system spec: + suspend: true dependsOn: - name: pg-shared - name: valkey-nextcloud diff --git a/nix/modules/hosts/nixos/jubilife/backups.nix b/nix/modules/hosts/nixos/jubilife/backups.nix index 077231e0..e0840d0c 100644 --- a/nix/modules/hosts/nixos/jubilife/backups.nix +++ b/nix/modules/hosts/nixos/jubilife/backups.nix @@ -11,7 +11,10 @@ _: { "${dataDirectory}/immich/postgres" ]; garage.paths = ["${dataDirectory}/garage"]; - nextcloud.paths = ["${dataDirectory}/nextcloud/html"]; + nextcloud.paths = [ + "${dataDirectory}/nextcloud/html" + "${dataDirectory}/postgres" + ]; paperless.paths = ["${dataDirectory}/paperless"]; plex = { exclude = ["${dataDirectory}/plex/Library/Application Support/Plex Media Server/Plug-in Support/Databases"]; diff --git a/nix/modules/hosts/nixos/jubilife/containers.nix b/nix/modules/hosts/nixos/jubilife/containers.nix index d8058c88..f32e61d1 100644 --- a/nix/modules/hosts/nixos/jubilife/containers.nix +++ b/nix/modules/hosts/nixos/jubilife/containers.nix @@ -21,6 +21,8 @@ _: { "d /mnt/Data/immich/ml-cache 0755 root root - -" "d /mnt/Data/immich/postgres 0700 999 999 - -" "d /mnt/Data/nextcloud/html 0750 33 33 - -" + "d /mnt/Data/nextcloud/valkey 0750 999 999 - -" + "d /mnt/Data/postgres 0700 999 999 - -" "d /mnt/Data/paperless 0750 1000 1000 - -" "d /mnt/Data/paperless/data 0750 1000 1000 - -" "d /mnt/Data/paperless/consume 0750 1000 1000 - -" @@ -40,11 +42,53 @@ _: { "docker-immich.service" ]; }; + + postgresPassword.sopsFile = "${inputs.self}/secrets/postgres.yaml"; + + nextcloudAdminPassword = { + sopsFile = "${inputs.self}/k8s/flux/secrets/nextcloud-env.sops.yaml"; + key = "data/NEXTCLOUD_ADMIN_PASSWORD"; + restartUnits = ["docker-nextcloud.service"]; + }; + + nextcloudPostgresPassword = { + sopsFile = "${inputs.self}/k8s/flux/secrets/nextcloud-env.sops.yaml"; + key = "data/POSTGRES_PASSWORD"; + }; + + nextcloudSmtpPassword = { + sopsFile = "${inputs.self}/k8s/flux/secrets/nextcloud-env.sops.yaml"; + key = "data/SMTP_PASSWORD"; + restartUnits = ["docker-nextcloud.service"]; + }; + + nextcloudObjectStoreKey = { + sopsFile = "${inputs.self}/k8s/flux/secrets/nextcloud-garage-env.sops.yaml"; + key = "stringData/OBJECTSTORE_S3_KEY"; + restartUnits = ["docker-nextcloud.service"]; + }; + + nextcloudObjectStoreSecret = { + sopsFile = "${inputs.self}/k8s/flux/secrets/nextcloud-garage-env.sops.yaml"; + key = "stringData/OBJECTSTORE_S3_SECRET"; + restartUnits = ["docker-nextcloud.service"]; + }; }; sops.templates = { immich-postgres-environment.content = "POSTGRES_PASSWORD=${config.sops.placeholder.immichDbPassword}"; immich-server-environment.content = "DB_PASSWORD=${config.sops.placeholder.immichDbPassword}"; + postgres-environment.content = '' + POSTGRES_PASSWORD=${config.sops.placeholder.postgresPassword} + NEXTCLOUD_POSTGRES_PASSWORD=${config.sops.placeholder.nextcloudPostgresPassword} + ''; + nextcloud-environment.content = '' + NEXTCLOUD_ADMIN_PASSWORD=${config.sops.placeholder.nextcloudAdminPassword} + POSTGRES_PASSWORD=${config.sops.placeholder.nextcloudPostgresPassword} + SMTP_PASSWORD=${config.sops.placeholder.nextcloudSmtpPassword} + OBJECTSTORE_S3_KEY=${config.sops.placeholder.nextcloudObjectStoreKey} + OBJECTSTORE_S3_SECRET=${config.sops.placeholder.nextcloudObjectStoreSecret} + ''; }; virtualisation.oci-containers.containers = { @@ -175,6 +219,94 @@ _: { "/run/immich/config.json:/etc/immich/config.json:ro" ]; }; + + postgres = { + image = "docker.io/library/postgres:18.6@sha256:cd78ca58eb75f929698e117a589488ccb2bd45107247fe02400b50ff6c418324"; + networks = ["nextcloud"]; + environment = { + POSTGRES_DB = "postgres"; + POSTGRES_USER = "postgres"; + }; + environmentFiles = [config.sops.templates.postgres-environment.path]; + volumes = [ + "/mnt/Data/postgres:/var/lib/postgresql/data" + "${pkgs.writeShellScript "postgres-init-nextcloud" '' + set -eu + psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \\ + --set=nextcloud_postgres_password="$NEXTCLOUD_POSTGRES_PASSWORD" <<'SQL' + CREATE ROLE nextcloud LOGIN PASSWORD :'nextcloud_postgres_password'; + CREATE DATABASE nextcloud OWNER nextcloud; + SQL + ''}:/docker-entrypoint-initdb.d/10-nextcloud:ro" + ]; + }; + + nextcloud-valkey = { + image = "valkey/valkey:9-alpine@sha256:ee91f7a174ac4d6a6b0685b3a60e321f0a9dbbb691f9b0e285be2ba1d1be8328"; + networks = ["nextcloud"]; + cmd = ["valkey-server" "--maxmemory" "256mb" "--maxmemory-policy" "volatile-lru"]; + volumes = ["/mnt/Data/nextcloud/valkey:/data"]; + }; + + nextcloud = { + image = "docker.io/library/nextcloud:34-apache@sha256:6eb64bc58acd8dd751415f15c470d397fe922cefbd154e48480f5fcd9ce3a8e5"; + dependsOn = [ + "postgres" + "nextcloud-valkey" + ]; + networks = ["nextcloud"]; + environment = { + APACHE_DISABLE_REWRITE_IP = "1"; + MAIL_DOMAIN = "aly.social"; + MAIL_FROM_ADDRESS = "admin"; + OBJECTSTORE_S3_AUTOCREATE = "false"; + OBJECTSTORE_S3_BUCKET = "aly-nextcloud"; + OBJECTSTORE_S3_HOST = "10.254.0.1"; + OBJECTSTORE_S3_PORT = "3900"; + OBJECTSTORE_S3_REGION = "garage"; + OBJECTSTORE_S3_SSL = "false"; + OBJECTSTORE_S3_USEPATH_STYLE = "true"; + OVERWRITECLIURL = "https://nextcloud.cute.haus"; + OVERWRITEHOST = "nextcloud.cute.haus"; + OVERWRITEPROTOCOL = "https"; + PHP_MEMORY_LIMIT = "512M"; + PHP_UPLOAD_LIMIT = "10G"; + POSTGRES_DB = "nextcloud"; + POSTGRES_HOST = "postgres"; + POSTGRES_USER = "nextcloud"; + REDIS_HOST = "nextcloud-valkey"; + REDIS_HOST_PORT = "6379"; + SMTP_AUTHTYPE = "LOGIN"; + SMTP_HOST = "smtp.resend.com"; + SMTP_NAME = "resend"; + SMTP_PORT = "465"; + SMTP_SECURE = "ssl"; + TRUSTED_PROXIES = "10.42.0.0/16"; + NEXTCLOUD_ADMIN_USER = "alyraffauf"; + NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.cute.haus"; + }; + environmentFiles = [config.sops.templates.nextcloud-environment.path]; + extraOptions = ["--memory=4g"]; + ports = ["10.254.0.1:8080:80"]; + volumes = ["/mnt/Data/nextcloud/html:/var/www/html"]; + }; + + nextcloud-cron = { + image = "docker.io/library/nextcloud:34-apache@sha256:6eb64bc58acd8dd751415f15c470d397fe922cefbd154e48480f5fcd9ce3a8e5"; + dependsOn = ["nextcloud"]; + networks = ["nextcloud"]; + cmd = ["/bin/sh" "-c" "while true; do php -f /var/www/html/cron.php || true; sleep 60; done"]; + environment = { + POSTGRES_DB = "nextcloud"; + POSTGRES_HOST = "postgres"; + POSTGRES_USER = "nextcloud"; + REDIS_HOST = "nextcloud-valkey"; + REDIS_HOST_PORT = "6379"; + }; + environmentFiles = [config.sops.templates.nextcloud-environment.path]; + extraOptions = ["--user=33:33"]; + volumes = ["/mnt/Data/nextcloud/html:/var/www/html"]; + }; }; systemd.services = { @@ -204,6 +336,32 @@ _: { }; }; + docker-network-nextcloud = { + after = ["docker.service"]; + requires = ["docker.service"]; + + before = [ + "docker-postgres.service" + "docker-nextcloud-valkey.service" + "docker-nextcloud.service" + "docker-nextcloud-cron.service" + ]; + + requiredBy = [ + "docker-postgres.service" + "docker-nextcloud-valkey.service" + "docker-nextcloud.service" + "docker-nextcloud-cron.service" + ]; + + path = [pkgs.docker]; + script = "docker network inspect nextcloud >/dev/null 2>&1 || docker network create nextcloud"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + }; + docker-dizquetv.unitConfig.RequiresMountsFor = ["/mnt/Data"]; docker-plex.unitConfig.RequiresMountsFor = [ @@ -226,6 +384,35 @@ _: { docker-immich-machine-learning.unitConfig.RequiresMountsFor = ["/mnt/Data"]; docker-immich-postgres.unitConfig.RequiresMountsFor = ["/mnt/Data"]; + + docker-postgres.unitConfig.RequiresMountsFor = ["/mnt/Data"]; + docker-nextcloud-valkey.unitConfig.RequiresMountsFor = ["/mnt/Data"]; + + # The migration creates this only after pg_dump has been restored locally. + docker-nextcloud = { + preStart = '' + config_file=/mnt/Data/nextcloud/html/config/config.php + + if ${pkgs.gnugrep}/bin/grep -Fq "pg-shared-rw.cnpg-system.svc" "$config_file"; then + ${pkgs.gnused}/bin/sed -i \ + -e "s/pg-shared-rw\.cnpg-system\.svc/postgres/" \ + -e "s/valkey\.nextcloud\.svc/nextcloud-valkey/" \ + "$config_file" + elif ! ${pkgs.gnugrep}/bin/grep -Fq "'dbhost' => 'postgres'" "$config_file"; then + echo "Nextcloud database host is neither the cluster nor local PostgreSQL" >&2 + exit 1 + fi + ''; + unitConfig = { + ConditionPathExists = "/mnt/Data/nextcloud/.local-postgres-ready"; + RequiresMountsFor = ["/mnt/Data"]; + }; + }; + + docker-nextcloud-cron.unitConfig = { + ConditionPathExists = "/mnt/Data/nextcloud/.local-postgres-ready"; + RequiresMountsFor = ["/mnt/Data"]; + }; }; }; } diff --git a/secrets/postgres.yaml b/secrets/postgres.yaml new file mode 100644 index 00000000..5b4a8f76 --- /dev/null +++ b/secrets/postgres.yaml @@ -0,0 +1,106 @@ +postgresPassword: ENC[AES256_GCM,data:3B2+IiXMtQhSmWbAx2k8JEf6n34/CZuTAi2CsmXX2/4OTOqhhw9J3k0lOeRLnzfJTuwLH0u64iKlOrNhq0vl9Q==,iv:e4IB+fRHTUEPtJDOBW1BdQwRa6JevLaclNQ5ox2c70A=,tag:qXEOpSwP8e+b5XbCVRuDfQ==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrQ3NJYXNxK0NWWlc3YUZN + TCs1T0h1alhMY0NEc1hZeWYva011TWVaZzFjCjl1YWRXeFFHTXhic29uSHZXV1Jp + UThVYk50a3ZwR0xzaitjTW9kNFNFeVEKLS0tIFRmalcyZURMaUZyN3dTM0EwVzB6 + UnlJSlBEZ2FkaE5NRFFjcnhWc0Y0N00KyA1Bbu8PrR2hd1oUZU4acWUEpThRnuue + vU3y6m5PO78ZAJg6UavFmKfvl9n44sN/94cla0kEwFoG0YnJtTqWuA== + -----END AGE ENCRYPTED FILE----- + recipient: age1kny9hv9t64ge379mk977slfx8deqy3df0c4cydh0lxu4ffczkftqpe2x6g + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmQVU0U2pyeGVGeFVhUUd6 + emg1dDBXaUlwaldwd2FXVmtOek1YeUpGTTNvCi8yMHNWclNuY1RwQmNxQnVxZXhU + TVVwNjNvS3h5eklQRExwMG1lUWFYRFEKLS0tIFV2UWlmNTVIVFo1MEx5NVZkRlJa + Q1JNdFZsYnFZaG5EVlIxWk1aRzZYa1UKM5ip9jRX0lohA29vJj2ZnH7i0t8dG7kM + RMZRwR/xkYJhHUH2RJhk87BSd4VQOqXKRQA9GBC/TDESzg9uXUz7jQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1e5a0aaueyk34klry2qk094zrwfsvga0uft93mwweye30xhqhvfzs3jgqku + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoSVRObW1YRWZlNkNiU0Vv + aEdiMGZudm85QVprUXhKRG5TNHI1ZTVFd3lJCnJMK2RRUWYyT0tTcjY3Y0pDTWhJ + SW96ZzZLMlBTRDdGU1BwQkh6ZUdnNk0KLS0tIG9QSDhXb0g3MWVzdmNBMjhwSUdk + NHF3S3AyanhMd25jb2xXcHg1SVlqbGMKyxd4HIoS0bl9dGJUyAEp9IwejlVgOaYx + ryyjiTkLLU4pQS2f7u72WnRU/3iI/IIwRkj33YhjFuV3gh2LrIMytg== + -----END AGE ENCRYPTED FILE----- + recipient: age1j9jju3whwj72udhge7v4c77gk6v6w2cq8zjngzyppwtptfsz499qyywu9a + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEZHd0T0dlT1hFTXBGQjJx + WnlNYjA0VXF3Tk1vTWNsY1ZsellmSnpOcEU0CmNJVUtkdEhsTnN1UnpWallFWHUz + cU9ScjJ4SmYxZ3FsLzBVNHpwbEFkOVkKLS0tIFN1WUZqQXltbzdMclcva0oxeVNH + c1FIRmFKQndHS3luTGJZUUxLQkkwVUUK79utwmOjx8Nx5jcPO8RPTQs37q17ImTX + c609D4u9/QQ2E83byEYccZjZzF6lH3K9umTSKXkO2yrr9ZBvKcRkcA== + -----END AGE ENCRYPTED FILE----- + recipient: age162sx8kppp90mjxw88eqs78hgnpkddf8k9kgtksmmexzvcdxknpkspmkgaf + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuRUNIbytzNkgwRS9ycXov + OEYzWm1CZGUyMVNYbXdPRWNHL1pVRlp5TnhzClpXMEtPbVhXYXYzaWo5Q24zL3Ju + Yklab0hTa2l2VTF1SXU3TVl2NDVwV00KLS0tIDI1UXJkN0I5VUpjaDlqejFuanZG + VzEvRUZGMERJS1pZMi9Va0Z4dXlFTmsKOPfiduCbBaTng7hQI7Lywtu/RjXwOANN + mr3xaDmppztpG5o38n8OTbGRnxcFGaLhTv+LBWC1nUxj1oOrr9pn7w== + -----END AGE ENCRYPTED FILE----- + recipient: age1hhvzccxzrysq6ak2ktuks00vy3smr6rhey9swhncq8850epftpjqt3c7f3 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRMnBwcVBhMDZOUkErMGo2 + YmV4cTdMZVJZd2t1RnQwZHdCY0FSdThIMG44Cms0RFFLd1lVeEFKWFRJUDlzelUx + OXlxeVJqa3c3VVFaSEFSeHRLcThSZ00KLS0tIE1MSm1vWDRIT0xUWWx5T1F3RVc4 + cXJuaitxNUlYdHhTOEkwSGZRTmRPVjQKghn6q/lS3mNnafi1yz7N95QmF9wCqKoU + jGJyje03ZuppR46yV4dtgoPVe3cR3W7TQDOMca6FbP+0QDVqALOGeA== + -----END AGE ENCRYPTED FILE----- + recipient: age1zj2aq2a2v3ngcer7s3jz6pufhh8ula8pw56etmy28dy25wrkk3vq59hu2s + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSUDhpTDNhRkN2Rzl3NmQ3 + M051TzREZ1B4Qmt5dVk5SHJrTjFpWUU0L1VFCjJhQWZCMUJNeGxORWJGUmJBR0xo + aG85WlhjSGlYRllRc0VIS3JQWmMySnMKLS0tIDl6T2gwc3FFZWU0anVqUlRhYkhK + VEVOOEppMGNWT0YvU1MrTndFZ1VvazgKnrDTnGIOm4D82m8HXW/jDEtbT+aWFHVg + 0UgrPHn+FcmdZsuSKYibRzvUw2PDh1vU6z7QLrNPjvdr9oNWiVIZTA== + -----END AGE ENCRYPTED FILE----- + recipient: age15td0jk5e2qeq2pexvxguhuuaxuaa7nhql7mhx9jr79caska6t4pqdv5spl + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5YUhyZVJmWmQyQU90MGZr + ZGU4U2htSHZhYStuMmxtWGNIQy9Hd0xxVzIwCnVVb3MyNjd2Y3JQc3UyQUgzSjJt + QU9TSjlDdEMyNEdTaGxkc3ZoY1F5Uk0KLS0tIG52cnk0MW9qbTl2NHM0ME9DcXRW + Zmo3SllzWjhtVStNd2xZQ2JKZGRzejAKBhO8iBs7wdeKVbsxjykssusYrr+DWwAI + Xr8FUvWjaFhR3eRKP8wpdcUR+L71h7t3vhU2s2/gmk7RmadRjhZy4Q== + -----END AGE ENCRYPTED FILE----- + recipient: age157en2pt286a22pxzyugt84ak6rn2at89tpma88juqfgj84lme5csq4999g + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzSHBpb3hsMHVOQSs0V0JX + OS9ISWhsNUlZZm14dmdNS0dIUlQzSCtYNWowCjhhNmlEYmVLeVRTMEtkcEwwbjBv + QVlGbFFqUGdyRTBKOHdKNm1KdkkrcXcKLS0tIDNkMWtQOVdTSVl6eHNKdnBiQTJS + WHZ6b3lBb1NxYjdsL1gyT3UxU3JpMnMK2neomJjwNgvzNZyRZFIIAmgxAst1X4gI + QNG056CFZkJT+cHyWggy14PBBjdTZhOJtK07YOnLtd9Zni+M/18C5Q== + -----END AGE ENCRYPTED FILE----- + recipient: age1xwlrtfu3du4jxg2l6eemzqeed8tflhdkjn0q5j9pzpecshtp3cpsh60l4j + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxMHZFTnpwSlRKa1plbEVj + emE0a20vRndaZWFDT1BWdnJVdU8wUi9saUc0CmdkeERhMU0vc3ZxOHpkMjlCd3Bl + Q29WekZ2ZlcxTnN1T1F1Zk9MVkd0NzgKLS0tIDcyVlcxTWtYbHUxaEtmMTF1eVVG + RU9CWEkvd2hQaUFrOE01WXB6eWJpWHcKq8bhfk5LCnQrQjw3hi1y+foPNvguDhZA + 04u96J+UQUmzIxcI1cavIx1d8iB7+R4DKsWL9VpKkNJ+cr5C2je8hA== + -----END AGE ENCRYPTED FILE----- + recipient: age1kgp3x48wn0dmzwqp0vdpfsdw42rej0ypkj0qavs7c2t2tgdud5zqcmhl40 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6cFJWdHdPSHI0VWFpcTAy + QWkxNi9mZXpnckh6eVB3VEd3R2lVaVd2aXlRCmxNVXVma29NZ0dpV2hvWlYzOFJz + VW93MmlLalhYb1hIWWRvMS8wUks0OEUKLS0tIG1OeVNIU1lyUC9lVUdvT1NtUVll + MmdDaWFjL28rZmhMVVpPQms4bi9LNzgK6RNhCBuBrcoWPjd0YcMcPUBVx3KwXfaM + DZuMCbR6FteTnc4A/Jm1KGQ1RPDmfwvptlTSgdL2PxOO728BCG2TJw== + -----END AGE ENCRYPTED FILE----- + recipient: age1stsuakfyj3gkee4sm328ky2crmymyuzwjjmxrpvnm754sdt2zvnshf68jx + lastmodified: "2026-08-17T00:26:22Z" + mac: ENC[AES256_GCM,data:W2rOLmagKMc3nQB0HMKX15lTjl1xVVVWCsO/cztq2/lvrFAsf75eXiBdm+C4grnmk8355FGWCDOVtDDdrrmk311sPTh4aBiI/NDY05w2iH1zcqbZB2+3a3CeShWuAueUjyWxEzcUNqELc/bOPavJTONSRjQqK1raNSMYFtfe0HQ=,iv:VBau5owUnDWg4sImkcCfG3vQzsk3iM55b9wzkL93xL4=,tag:7m0x4QEZN+uhwwISz72rTw==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3