diff --git a/stacks/matrix.poneyworld.net/README.md b/stacks/matrix.poneyworld.net/README.md new file mode 100644 index 0000000..41803db --- /dev/null +++ b/stacks/matrix.poneyworld.net/README.md @@ -0,0 +1,204 @@ +# Matrix — matrix.poneyworld.net + +Homeserver Synapse et ses satellites. Huit conteneurs : + +| Service | Rôle | Exposé sur | +|---|---|---| +| `synapse` | le homeserver | `synapse.poneyworld.net` (fédération) | +| `db` | PostgreSQL 18 | — | +| `redis` | Valkey, cache interne de Synapse (bloc `redis:` du homeserver) | — | +| `nginx` | sert `/_matrix` (vers `synapse:8008`) et `/.well-known/matrix/` | `matrix.poneyworld.net` (API client) | +| `web` | Element Web | `chat.poneyworld.net` | +| `livekit` | SFU pour Element Call | `livekit.opytex.org` | +| `lk-jwt` | délivre les jetons LiveKit à Element | `livekit.opytex.org/sfu/get`, `/get_token`, `/healthz` | +| `coturn` | serveur TURN (appels 1-à-1) | ports 3478 / 5349 / 49160-49200 | + +Deux entrées différentes, à ne pas confondre : `matrix.poneyworld.net` est le +`server_name` et l'API client, `synapse.poneyworld.net` est la cible de +délégation annoncée par `/.well-known/matrix/server`, empruntée par les +serveurs fédérés. Les deux sont vivantes. + +## Déploiement + +Depuis ce dossier, sur chioggia : + +```bash +sops exec-env secrets.env.yaml 'docker compose up -d' +``` + +Les secrets sont déclarés `${VAR:?}` : **toute** commande `docker compose` +échoue sans l'enveloppe `sops exec-env`, y compris `stop`, `logs`, `ps` et +`exec`. Pour en enchaîner plusieurs, une seule enveloppe suffit : + +```bash +sops exec-env secrets.env.yaml ' + docker compose stop synapse && + docker compose exec -T db psql -U synapse synapse -c "..." && + docker compose up -d +' +``` + +Après une rotation de secret, `docker compose up -d` répond « up to date » et +garde l'ancienne valeur. Il faut `--force-recreate` sur le service concerné. + +## Le nom de projet + +`name: matrix_poneyworld`, en tête du compose, ne suit pas le nom du dossier. +C'est délibéré et c'est porteur : les volumes s'appellent +`matrix_poneyworld_matrix_data` et `matrix_poneyworld_matrix_db`. + +**Retirer cette ligne, ou la modifier, rendrait invisibles les médias, les +clés de signature du serveur, `homeserver.yaml` et toute la base — la stack +repartirait sur un homeserver vide, sans la moindre erreur.** + +## Où vit vraiment la configuration + +Presque rien de la configuration de Synapse n'est dans ce dépôt : elle est +dans `/data/homeserver.yaml`, à l'intérieur du volume `matrix_data`. + +Les variables `SYNAPSE_*` de l'`env_file` ne sont lues que par le mode +`generate` de l'image, au tout premier démarrage. Elles sont **inertes au +runtime** : `SYNAPSE_ENABLE_REGISTRATION=yes` y cohabite sans conséquence avec +un `enable_registration: False` effectif dans `homeserver.yaml`. Ne pas s'y +fier pour lire l'état du serveur. + +Corollaire pour les secrets : `turn_shared_secret`, `macaroon_secret_key`, +`registration_shared_secret` et le mot de passe de la base sont dans +`homeserver.yaml`, donc **hors SOPS et hors dépôt**. Une rotation se fait des +deux côtés (voir plus bas). + +## Secrets + +Cinq valeurs dans `secrets.env.yaml`, injectées par `sops exec-env` : + +| Variable | Consommé par | +|---|---| +| `POSTGRES_PASSWORD` | `db` | +| `LIVEKIT_KEY`, `LIVEKIT_SECRET` | `livekit` (via `LIVEKIT_KEYS`) et `lk-jwt` | +| `TURN_STATIC_AUTH_SECRET`, `TURN_CLI_PASSWORD` | `coturn` | + +`LIVEKIT_KEY` est un identifiant plus qu'un secret, et la convention du dépôt +laisserait ce genre de valeur en clair. Il est chiffré quand même : il ne se +tourne jamais seul, toujours en paire avec son secret, et le séparer sur deux +mécanismes rendrait la rotation facile à rater à moitié. + +```bash +sops edit secrets.env.yaml +``` + +### Deux mécanismes différents pour deux logiciels + +Ni coturn ni LiveKit ne lisent une variable d'environnement depuis leur +fichier de configuration — un fichier monté n'est pas interpolé par Compose. +C'est le même problème que la clé LAPI de CrowdSec sur Traefik, mais les deux +n'offrent pas la même sortie. + +**LiveKit** accepte la paire par variable d'environnement : `LIVEKIT_KEYS`, au +format `clé: secret`. `livekit.yaml` n'a donc pas de bloc `keys:`, et la +valeur est composée dans `environment:` du compose, où l'interpolation +`${...}` s'applique. `lk-jwt` consomme les deux mêmes variables. + +**coturn** n'offre rien d'équivalent : pas d'interpolation dans +`turnserver.conf`, pas d'option `...File`, pas de lecture d'environnement. La +seule voie sans bricolage est la ligne de commande : + +```yaml +command: + - -c + - /etc/coturn/turnserver.conf + - --static-auth-secret=${TURN_STATIC_AUTH_SECRET:?} + - --cli-password=${TURN_CLI_PASSWORD:?} +``` + +Le compromis est explicite : les valeurs ne sont pas dans un fichier +versionnable, mais elles sont visibles dans `docker inspect coturn` et dans la +table des processus de l'hôte. C'est strictement mieux qu'un fichier +versionné, ce n'est pas aussi bon qu'un secret Compose. + +`turnserver.conf` ne doit donc jamais reprendre de lignes +`static-auth-secret=` ou `cli-password=` : ce serait à la fois un secret en +clair dans le dépôt et une seconde source concurrente. + +### Rotation + +`turn_shared_secret` ne se change **pas seul** : la même valeur doit être +identique dans `homeserver.yaml` (Synapse la signe) et côté coturn (il la +vérifie). Deux valeurs différentes ne produisent aucune erreur au démarrage — +les appels échouent simplement à établir un relais. + +```bash +sops edit secrets.env.yaml # TURN_STATIC_AUTH_SECRET +docker compose exec synapse vi /data/homeserver.yaml # turn_shared_secret +sops exec-env secrets.env.yaml 'docker compose up -d --force-recreate coturn' +docker compose restart synapse +``` + +`macaroon_secret_key` (dans `homeserver.yaml`) signe les jetons d'accès : le +changer **déconnecte tous les utilisateurs**, sur tous leurs appareils. Ce +n'est pas une opération anodine, c'est une opération annoncée. + +`registration_shared_secret` (dans `homeserver.yaml`) n'est utilisé que par +`register_new_matrix_user`. Il se change librement. + +`POSTGRES_PASSWORD` ne se change pas seul non plus : PostgreSQL ignore la +variable sur un `PGDATA` déjà initialisé. Il faut un `ALTER USER` dans la base +**et** la même valeur dans le bloc `database:` de `homeserver.yaml`. + +## Volumes et sauvegarde + +| Volume | Contenu | Sauvegarde | +|---|---|---| +| `matrix_poneyworld_matrix_data` | médias, clé de signature du serveur, `homeserver.yaml` | label `backup.enable=true` + `MANUAL_VOLUMES` | +| `matrix_poneyworld_matrix_db` | base PostgreSQL | dump SQL (`backup.type=postgres` sur `db`) | + +Les labels de volume ne s'appliquent qu'à la création : en ajouter ou en +retirer un ne changerait rien au volume réel, mais ferait diverger sa +déclaration, et Compose proposerait alors de le **recréer**. Ne pas toucher au +bloc `volumes:` du compose. + +### Pourquoi `POSTGRES_DB` est indispensable + +`backup_docker.sh` détermine la base à dumper ainsi : + +```bash +db_name=$(docker exec "$container" printenv POSTGRES_DB 2>/dev/null || echo "postgres") +``` + +Sans `POSTGRES_DB` dans l'environnement du conteneur `db`, `printenv` sort en +erreur, le repli s'applique, et le script dumpe `postgres` — la base système, +vide — avec succès et sans le moindre avertissement. Le dump obtenu fait +quelques centaines d'octets et ne contient aucune table. + +`POSTGRES_DB=synapse` dans l'`env_file` est ce qui garantit que le dump porte +sur la bonne base. La variable n'est lue par PostgreSQL qu'à l'initialisation +du cluster : elle est sans effet sur une base existante, sa seule fonction ici +est d'être visible par `printenv`. + +Vérification, après tout changement touchant `db` : + +```bash +zcat /other/backups/db-dumps/matrix_poneyworld.sql.gz | grep -c "CREATE TABLE" +``` + +Un `0` signifie que la sauvegarde est vide. + +## Défauts connus + +1. **`external-ip` de coturn pointe sur une autre machine.** + `coturn/turnserver.conf` annonce `91.121.90.228`, alors que chioggia est en + `188.165.240.182` (l'adresse que `livekit.yaml` déclare en `node_ip`). + coturn annonce donc aux clients une adresse qui n'est pas la sienne. + +2. **UDP 3478 n'est pas publié.** `ports:` déclare `3478:3478`, sans `/udp` — + donc TCP seul. Un client TURN parle UDP en premier. Combiné au point 1, + coturn est très probablement inopérant. + +3. **Trois images en tag flottant** : `nginx:latest`, + `livekit/livekit-server:latest`, `ghcr.io/element-hq/lk-jwt-service:latest`. + La convention du dépôt veut une version en dur, et Renovate ne sait rien + faire d'un `latest`. + +Références : + +- https://element-hq.github.io/synapse/latest/setup/turn/coturn.html +- https://docs.livekit.io/home/self-hosting/deployment/ diff --git a/stacks/matrix.poneyworld.net/coturn/turnserver.conf b/stacks/matrix.poneyworld.net/coturn/turnserver.conf new file mode 100644 index 0000000..cee7781 --- /dev/null +++ b/stacks/matrix.poneyworld.net/coturn/turnserver.conf @@ -0,0 +1,14 @@ +# Serveur TURN de matrix.poneyworld.net. +# +# `static-auth-secret` et `cli-password` ne sont PAS ici : coturn n'interpole +# aucune variable dans ce fichier et n'a pas d'option `...File`. Ils sont +# passés en `command:` par docker-compose.yml, alimentés par SOPS. +use-auth-secret +realm=matrix.poneyworld.net +listening-port=3478 +tls-listening-port=5349 +min-port=49160 +max-port=49200 +verbose +allow-loopback-peers +external-ip=91.121.90.228 diff --git a/stacks/matrix.poneyworld.net/docker-compose.yml b/stacks/matrix.poneyworld.net/docker-compose.yml new file mode 100644 index 0000000..6d5f6f5 --- /dev/null +++ b/stacks/matrix.poneyworld.net/docker-compose.yml @@ -0,0 +1,155 @@ +# Le nom de projet ne suit PAS le dossier : les volumes existants sont +# préfixés `matrix_poneyworld_`. Retirer cette ligne ferait repartir Synapse +# sur une base et un magasin de médias vides. +name: matrix_poneyworld + +services: + synapse: + image: matrixdotorg/synapse:v1.148.0 + restart: unless-stopped + env_file: + - env_file + volumes: + - matrix_data:/data + networks: + - default + - traefik-proxy + - smtp + labels: + - traefik.enable=true + - traefik.docker.network=traefik-proxy + - traefik.http.routers.synapse.entrypoints=web-secure + - traefik.http.routers.synapse.rule=Host(`synapse.poneyworld.net`) + - traefik.http.routers.synapse.tls.certresolver=letsencrypt + - traefik.http.services.synapse.loadbalancer.server.port=8008 + + db: + image: postgres:18 + restart: unless-stopped + env_file: + - env_file + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?} + POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C + volumes: + - matrix_db:/var/lib/postgresql + networks: + - default + labels: + backup.enable: true + backup.type: postgres + + redis: + image: valkey/valkey:8.1 + restart: unless-stopped + networks: + - default + + nginx: + image: nginx:latest + restart: unless-stopped + volumes: + - ./nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf:ro + - ./nginx/www:/var/www/:ro + networks: + - default + - traefik-proxy + labels: + - traefik.enable=true + - traefik.docker.network=traefik-proxy + - traefik.http.routers.matrix_nginx.entrypoints=web-secure + - traefik.http.routers.matrix_nginx.rule=Host(`matrix.poneyworld.net`) + - traefik.http.routers.matrix_nginx.tls.certresolver=letsencrypt + - traefik.http.services.matrix_nginx.loadbalancer.server.port=80 + + web: + image: vectorim/element-web:v1.12.11 + restart: unless-stopped + volumes: + - ./element/config.json:/app/config.json:ro + networks: + - default + - traefik-proxy + labels: + - traefik.enable=true + - traefik.docker.network=traefik-proxy + - traefik.http.routers.element.entrypoints=web-secure + - traefik.http.routers.element.rule=Host(`chat.poneyworld.net`) + - traefik.http.routers.element.tls.certresolver=letsencrypt + - traefik.http.services.element.loadbalancer.server.port=80 + + livekit: + image: livekit/livekit-server:latest + command: --config /etc/livekit.yaml + restart: unless-stopped + environment: + # `keys:` a été retiré de livekit.yaml : la paire arrive ici, où + # l'interpolation ${...} de Compose s'applique. + LIVEKIT_KEYS: "${LIVEKIT_KEY:?}: ${LIVEKIT_SECRET:?}" + volumes: + - ./livekit/livekit.yaml:/etc/livekit.yaml:ro + ports: + - "7881:7881/tcp" + - "50100-50200:50100-50200/udp" + networks: + - default + - traefik-proxy + labels: + - traefik.enable=true + - traefik.docker.network=traefik-proxy + - traefik.http.routers.livekit.entrypoints=web-secure + - traefik.http.routers.livekit.rule=Host(`livekit.opytex.org`) + - traefik.http.routers.livekit.tls.certresolver=letsencrypt + - traefik.http.services.livekit.loadbalancer.server.port=7880 + + lk-jwt: + image: ghcr.io/element-hq/lk-jwt-service:latest + restart: unless-stopped + environment: + LIVEKIT_JWT_BIND: ":8081" + LIVEKIT_URL: wss://livekit.opytex.org + LIVEKIT_KEY: ${LIVEKIT_KEY:?} + LIVEKIT_SECRET: ${LIVEKIT_SECRET:?} + LIVEKIT_FULL_ACCESS_HOMESERVERS: matrix.poneyworld.net + networks: + - default + - traefik-proxy + labels: + - traefik.enable=true + - traefik.docker.network=traefik-proxy + - traefik.http.routers.lkjwt.entrypoints=web-secure + - traefik.http.routers.lkjwt.rule=Host(`livekit.opytex.org`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/get_token`) || PathPrefix(`/healthz`)) + - traefik.http.routers.lkjwt.tls.certresolver=letsencrypt + - traefik.http.services.lkjwt.loadbalancer.server.port=8081 + + coturn: + image: coturn/coturn:4.9 + restart: unless-stopped + # Les deux secrets ne peuvent pas venir du fichier de conf : coturn n'y + # interpole rien et n'offre aucune option `...File`. Voir README.md. + command: + - -c + - /etc/coturn/turnserver.conf + - --static-auth-secret=${TURN_STATIC_AUTH_SECRET:?} + - --cli-password=${TURN_CLI_PASSWORD:?} + volumes: + - ./coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro + # 3478 et 5349 sont publiés en TCP seul, sans /udp. Voir README.md. + ports: + - "3478:3478" + - "5349:5349" + - "49160-49200:49160-49200/udp" + networks: + - default + +volumes: + matrix_data: + labels: + backup.enable: true + matrix_db: + +networks: + traefik-proxy: + external: true + smtp: + external: true diff --git a/stacks/matrix.poneyworld.net/element/config.json b/stacks/matrix.poneyworld.net/element/config.json new file mode 100644 index 0000000..b797734 --- /dev/null +++ b/stacks/matrix.poneyworld.net/element/config.json @@ -0,0 +1,65 @@ +{ + "default_server_config": { + "m.homeserver": { + "base_url": "https://matrix.poneyworld.net", + "server_name": "matrix.poneyworld.net" + }, + "m.identity_server": { + "base_url": "https://vector.im" + } + }, + "disable_custom_urls": false, + "disable_guests": false, + "disable_login_language_selector": false, + "disable_3pid_login": false, + "brand": "Element", + "integrations_ui_url": "https://scalar.vector.im/", + "integrations_rest_url": "https://scalar.vector.im/api", + "integrations_widgets_urls": [ + "https://scalar.vector.im/_matrix/integrations/v1", + "https://scalar.vector.im/api", + "https://scalar-staging.vector.im/_matrix/integrations/v1", + "https://scalar-staging.vector.im/api", + "https://scalar-staging.riot.im/scalar/api" + ], + "bug_report_endpoint_url": "https://riot.im/bugreports/submit", + "defaultCountryCode": "FR", + "showLabsSettings": false, + "features": { + "feature_new_spinner": "labs", + "feature_pinning": "labs", + "feature_custom_status": "labs", + "feature_custom_tags": "labs", + "feature_state_counters": "labs" + }, + "default_federate": true, + "default_theme": "light", + "roomDirectory": { + "servers": [ + "matrix.org" + ] + }, + "welcomeUserId": "@riot-bot:matrix.org", + "piwik": { + "url": "https://piwik.riot.im/", + "whitelistedHSUrls": [ + "https://matrix.org" + ], + "whitelistedISUrls": [ + "https://vector.im", + "https://matrix.org" + ], + "siteId": 1 + }, + "enable_presence_by_hs_url": { + "https://matrix.org": false, + "https://matrix-client.matrix.org": false + }, + "settingDefaults": { + "breadcrumbs": true + }, + "element_call": { + "url": "https://call.element.io", + "use_exclusively": true + } +} diff --git a/stacks/matrix.poneyworld.net/env_file b/stacks/matrix.poneyworld.net/env_file new file mode 100644 index 0000000..c411ead --- /dev/null +++ b/stacks/matrix.poneyworld.net/env_file @@ -0,0 +1,21 @@ +# Synapse — configuration non sensible. +# +# La configuration réelle du homeserver n'est PAS ici : elle vit dans +# /data/homeserver.yaml, à l'intérieur du volume matrix_data, hors dépôt. +# Les variables SYNAPSE_* ci-dessous ne sont lues que par le mode `generate` +# de l'image, au tout premier démarrage. Elles sont inertes au runtime : +# SYNAPSE_ENABLE_REGISTRATION=yes cohabite ainsi avec un +# `enable_registration: False` effectif dans homeserver.yaml. +SYNAPSE_HOST=synapse.poneyworld.net +SYNAPSE_NO_TLS=true +SYNAPSE_REPORT_STATS=no +SYNAPSE_ENABLE_REGISTRATION=yes +PROTOCOL=http + +# Lues par le conteneur `db`, et par le script de sauvegarde qui fait +# `docker exec printenv POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD`. +# Sans POSTGRES_DB, le script retombe sur `postgres` et dumpe une base vide +# en signalant un succès. Voir README.md. +# POSTGRES_PASSWORD n'est pas ici, il vient de SOPS. +POSTGRES_USER=synapse +POSTGRES_DB=synapse diff --git a/stacks/matrix.poneyworld.net/livekit/livekit.yaml b/stacks/matrix.poneyworld.net/livekit/livekit.yaml new file mode 100644 index 0000000..2c5f1e8 --- /dev/null +++ b/stacks/matrix.poneyworld.net/livekit/livekit.yaml @@ -0,0 +1,13 @@ +port: 7880 +bind_addresses: + - "0.0.0.0" +rtc: + tcp_port: 7881 + # udp_port: 7882 + port_range_start: 50100 + port_range_end: 50200 + #use_external_ip: true + enable_loopback_candidate: false + node_ip: 188.165.240.182 +# `keys:` est absent volontairement : la paire clé/secret arrive par la +# variable d'environnement LIVEKIT_KEYS, alimentée par SOPS. diff --git a/stacks/matrix.poneyworld.net/nginx/matrix.conf b/stacks/matrix.poneyworld.net/nginx/matrix.conf new file mode 100644 index 0000000..34e0f6a --- /dev/null +++ b/stacks/matrix.poneyworld.net/nginx/matrix.conf @@ -0,0 +1,17 @@ +server { + listen 80 default_server; + server_name matrix.poneyworld.net; + + # Traefik -> nginx -> synapse + location /_matrix { + proxy_pass http://synapse:8008; + proxy_set_header X-Forwarded-For $remote_addr; + client_max_body_size 128m; + } + + location /.well-known/matrix/ { + root /var/www/; + default_type application/json; + add_header Access-Control-Allow-Origin *; + } +} diff --git a/stacks/matrix.poneyworld.net/nginx/www/.well-known/matrix/client b/stacks/matrix.poneyworld.net/nginx/www/.well-known/matrix/client new file mode 100644 index 0000000..228f5ad --- /dev/null +++ b/stacks/matrix.poneyworld.net/nginx/www/.well-known/matrix/client @@ -0,0 +1,11 @@ +{ + "m.homeserver": { + "base_url": "https://matrix.poneyworld.net" + }, + "org.matrix.msc4143.rtc_foci": [ + { + "type": "livekit", + "livekit_service_url": "https://livekit.opytex.org" + } + ] +} diff --git a/stacks/matrix.poneyworld.net/nginx/www/.well-known/matrix/server b/stacks/matrix.poneyworld.net/nginx/www/.well-known/matrix/server new file mode 100644 index 0000000..f9f467f --- /dev/null +++ b/stacks/matrix.poneyworld.net/nginx/www/.well-known/matrix/server @@ -0,0 +1,3 @@ +{ + "m.server": "synapse.poneyworld.net:443" +} diff --git a/stacks/matrix.poneyworld.net/secrets.env.yaml b/stacks/matrix.poneyworld.net/secrets.env.yaml new file mode 100644 index 0000000..adf4ebe --- /dev/null +++ b/stacks/matrix.poneyworld.net/secrets.env.yaml @@ -0,0 +1,29 @@ +POSTGRES_PASSWORD: ENC[AES256_GCM,data:ooetYEZloaW9P13No7yOyuQ5CNOWRzCucgOjHLYlr8LJo09iJmbx1U8OiJU=,iv:v5SDUiN756EF+sIPx7OSCR/wYQ+isir9ZUaA5fzQikk=,tag:yA0Gso5WkoD8SirMeHlYsQ==,type:str] +LIVEKIT_KEY: ENC[AES256_GCM,data:PCL+ADpEEBbLl6G2eMdxZv/J1zU=,iv:uU1WOFWaxAxOOOzmzuSAhRMO4owTy3coWej5orDOdFE=,tag:BiN36C/iBuWlo0+HX1xlEg==,type:str] +LIVEKIT_SECRET: ENC[AES256_GCM,data:4W6H6uuxR9jiws00rAYSjKzoNKGohqTldKauF/HDpDJxutPF+rYb8rBWmao+bJZy9syYgAkYUDXTqoh6puTexw==,iv:qPsKxpblHRnB75lnQR0T3q5a/cdhZP+BPX47tmUYS6Y=,tag:UnsqIKMtnCNxYZ0GJAdrjA==,type:str] +TURN_STATIC_AUTH_SECRET: ENC[AES256_GCM,data:TunW0WK0Aurb2+G6gn2H9/gUUBx+YbdMqv487mIqENMQ8GVFgttY7q2194kbWkogLF0zaVTZChJsc8H66+wZmg==,iv:SYcOWq7oPRwgJmzPctwNkr+3TyRArI+jj1Smb9so30E=,tag:SJuSsncV/HIZ3+28lvHvaw==,type:str] +TURN_CLI_PASSWORD: ENC[AES256_GCM,data:U6NjXIyFiQh0LlqJpC3MUUxrKHGr7i5Mzf8ovJ6d4VdQn3HwacykQnIY7Bf5fCjMHfGS8YXUlrnx+qmOFw6iaA==,iv:kUsUkflYTHjLTSKlmMkVOrOifeDIyRIb/bVo9jsWZ+k=,tag:6TzxAAo8bhXOzFFojzxkgw==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIL3JiNjB6Zkt0MWREWlp6 + RDYvQjRzSXJaWWJEWkpROU56YzhVTE9jYzBNCmE4dEt3cDNncWhQYlFqRFlVVHVL + cGJ0eVo3WEFSVUtNcTJKbENtN1dBeE0KLS0tIDNzVW1hUlpQYmFEMUxyb3hWSUNl + TTN6YjFabjZ6QTVtZ3Nwc01WbEs1ckkKD/uwrElZd6/xNbyO/RdPegbDDaS0Kler + 0CJwZRBumyLirDMVDOMTB8uJqqwCQLppGL8yLsZFMUaNdR3GJpLmFw== + -----END AGE ENCRYPTED FILE----- + recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOYjYwNTE0RVdQMm5wOHpv + RnVrOXRWTUQ0MjRuQlI3cU5hMWNLaGdQb21zCmxzS3lzRjgybWxVQ2xGTStKbVRx + V1RFREY1TVJzVTl2Rk5ZQ0QrY3U3ZVUKLS0tIGMvUUZpd01ROWhuRTh6RWdNS0V5 + K0Zwd0NMWGJKUnJyaEZDMlM1SENzQ28K4tbrrogUL9hLBM49FIT6cQLxyG5/dqj8 + fifz17xNS4S2YRCOVNal+AI4JXM9yw9hcoB4tt3ileEYcVYl9hzcHg== + -----END AGE ENCRYPTED FILE----- + recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj + lastmodified: "2026-08-19T15:05:29Z" + mac: ENC[AES256_GCM,data:NDhgv/WRYYXsTBtiuaW/EwUeNL/C+013QQuVK68SDMc8gEGgLRHuBCqUSrW5VnnOGia2i86fNWo2QCm2rNwaeg1LnqLS5IbG6NtsFLQeMXJUzvs8GE0H++kWYOVs3CHf84Z/xuITjVT92de7Y6heOyaClsYunavj0JM5/OavWjU=,iv:qTkntUHabClwfP5/rtkvo3g7Miv/pAQGpCEfwo+xSqg=,tag:lJ13YwEsb6Z1DmHCmKQ3lQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3