Fédération de source externe — Climscore (lecture seule)¶
Sprint 82 A3 — première source de données client externe fédérée par le moteur de requête AKKO.
Cette page documente comment AKKO fédère une base PostgreSQL existante d'un client sans jamais toucher à ses données. Le déploiement de référence utilise la base climatique Climscore hébergée dans une stack Docker Compose séparée sur le même hôte Netcup, mais le schéma s'applique à n'importe quelle source externe.
La promesse — et la seule chose que le client doit accepter — c'est un accès en lecture seule. AKKO n'écrit jamais, ne modifie jamais, ne supprime jamais les données en amont. Trois couches d'application indépendantes s'empilent pour qu'une erreur de configuration isolée ne casse pas cette promesse.
Architecture¶
+-------------------- Cluster AKKO (k3s, namespace akko) ----------------+
| |
| alice (cockpit) -> ADEN/Trino -> climscore.public.communes (SELECT) |
| | |
| v |
| Trino coordinator/workers --> Service akko-climscore-postgres:15432 |
| |
| +-----------+ hostNetwork=true |
| | pod socat | écoute hôte:15432 +--------- forwards -> |
| +-----------+ | |
+-------------------------------------------------|---------------------+
v
+---------------------- Hôte Netcup (Linux) ----------+
| Bridge Docker climscore_climscore (172.18.0.0/16) |
| 172.18.0.2:5432 climscore-postgres-1 (postgis) |
| rôle akko_readonly GRANT SELECT UNIQUEMENT |
+-----------------------------------------------------+
Trois ressources Kubernetes sont créées dans le namespace akko, toutes conditionnées par global.climscore.enabled (voir helm/akko/templates/climscore-readonly-federation.yaml) :
| Ressource | Rôle |
|---|---|
Deployment akko-climscore-proxy |
alpine/socat qui tourne avec hostNetwork: true et fait le pont entre les pods k3s (10.42.0.0/16) et le bridge Docker qui héberge le conteneur PostGIS climscore. La chaîne iptables DOCKER rejette l'ingress des pods vers 172.18.0.0/16 directement, donc le proxy vit dans le namespace réseau de l'hôte où le routage est ouvert. |
Service akko-climscore-postgres |
Façade ClusterIP sur le port 15432. Le catalogue Trino utilise le nom DNS akko-climscore-postgres.akko.svc.cluster.local — aucune IP d'hôte ne fuit dans le catalogue. |
Secret akko-climscore-readonly-creds |
Contient username, password, host, port, database. Monté dans Trino en variables d'env (CLIMSCORE_RO_*) et lu par le bloc catalogue climscore.properties. |
Le catalogue Trino climscore est ajouté dans additionalCatalogs de helm/examples/values-netcup.yaml :
connector.name=postgresql
connection-url=jdbc:postgresql://${ENV:CLIMSCORE_RO_HOST}:${ENV:CLIMSCORE_RO_PORT}/${ENV:CLIMSCORE_RO_DB}
connection-user=${ENV:CLIMSCORE_RO_USER}
connection-password=${ENV:CLIMSCORE_RO_PASS}
case-insensitive-name-matching=false
Défense en profondeur — trois couches read-only indépendantes¶
Couche 1 — Grants Postgres (principale)¶
Le rôle PostgreSQL akko_readonly est créé hors-bande sur la base en amont avec SELECT uniquement sur le schéma public. Le moteur de base rejette chaque INSERT, UPDATE, DELETE, TRUNCATE, DROP, ALTER, et CREATE peu importe quel client l'émet. C'est la couche que le client audite — il possède la base, il possède les grants.
Script de provisioning (one-shot, idempotent) :
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'akko_readonly') THEN
CREATE USER akko_readonly WITH PASSWORD '<random-strong-pass>';
ELSE
ALTER USER akko_readonly WITH PASSWORD '<random-strong-pass>';
END IF;
END$$;
GRANT CONNECT ON DATABASE climscore TO akko_readonly;
GRANT USAGE ON SCHEMA public TO akko_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO akko_readonly;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO akko_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO akko_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO akko_readonly;
REVOKE CREATE ON SCHEMA public FROM akko_readonly;
REVOKE CREATE ON DATABASE climscore FROM akko_readonly;
Vérification :
psql -U akko_readonly -d climscore -c 'SELECT count(*) FROM communes' # OK
psql -U akko_readonly -d climscore -c 'CREATE TABLE x()' # ERROR: permission denied
psql -U akko_readonly -d climscore -c 'INSERT INTO communes ...' # ERROR: permission denied
Couche 2 — Marqueur Helm¶
global.climscore.readOnly: true est défini dans values.yaml et explicitement réaffirmé dans values-netcup.yaml. La valeur :
- est écrite dans le fichier de données OPA
climscore.json(data.akko.climscore.read_only) - est affichée dans le panneau "Sources externes" du cockpit en badge vert
- est visible dans
helm get values akko -n akkopour l'opérateur de garde
C'est de la documentation qui mord — passer à false exige un overlay Helm revu par le pipeline sécurité.
Couche 3 — Blocage OPA des tentatives d'écriture¶
La policy OPA côté Trino (helm/akko/charts/akko-opa/templates/configmap.yaml, package trino) porte un garde _climscore_write_attempt qui s'active dès qu'un appelant cible le catalogue climscore avec une opération d'écriture ou un DDL destructif. Le garde est ajouté en AND sur chaque règle allow qui octroie des privilèges d'écriture :
_climscore_catalog_targeted if {
input.action.resource.table.catalogName == "climscore"
}
_climscore_write_op if {
input.action.operation in write_ops
}
_climscore_write_op if {
input.action.operation in destructive_ops
}
_climscore_write_attempt if {
_climscore_catalog_targeted
_climscore_write_op
not _climscore_user_exempt
}
allow if {
has_role("admin")
not _climscore_write_attempt # même les admins ne peuvent PAS écrire sur climscore
}
L'exemption se fait en ajoutant des noms d'utilisateur à global.climscore.opaWriteAllowList. Vide par défaut — même le rôle admin ne peut pas écrire sur le catalogue climscore via Trino sans un opt-in opérateur explicite.
Cette couche attrape une classe d'erreur que les deux autres rateraient : un changement futur qui étendrait accidentellement le grant akko_readonly ou qui basculerait le catalogue vers un autre connection-user. Il faudrait casser à la fois le grant DB et le garde OPA pour qu'un trafic d'écriture atteigne la source en amont.
Cas d'usage — "laisse-moi voir tes données sans y toucher"¶
L'objection la plus fréquente à une vente de moteur de requête fédéré est :
"Et si votre moteur corrompait notre base de production ?"
La garantie en trois couches répond à cette objection :
| Peur du client | Mitigation AKKO |
|---|---|
| "AKKO écrit par erreur" | Postgres rejette les écritures — mauvais privilège côté moteur de base. |
| "Quelqu'un exploite un rôle admin" | Couche OPA 3 bloque le DML sur climscore.* pour chaque rôle. |
| "Un futur déploiement autorise les écritures" | Helm value readOnly: true + revue PR sur opaWriteAllowList. |
Le client garde son secret de connection-string chez nous, scope le rôle DB lui-même, et audite l'utilisateur Postgres AKKO. Les opérateurs AKKO ne peuvent pas élargir ce scope unilatéralement.
Opérations¶
Onboarder une nouvelle source externe¶
- Le client crée un rôle Postgres
<src>_readonlyavecGRANT SELECTuniquement. - L'opérateur pré-crée le Secret
akko-<src>-readonly-credsavecusername,password,host,port,database. - Ajouter le bloc catalogue dans
helm/examples/values-netcup.yamladditionalCatalogs. - Activer
global.<src>.enabled: truedans le même overlay. helm upgrade akko helm/akko -n akko -f helm/examples/values-netcup.yaml.- Validation :
kubectl -n akko exec deploy/akko-trino-coordinator -- trino --execute "SHOW TABLES FROM <src>.public".
Rotation du mot de passe lecture seule¶
kubectl -n akko delete secret akko-climscore-readonly-creds
kubectl -n akko create secret generic akko-climscore-readonly-creds \
--from-literal=username=akko_readonly \
--from-literal=password="<new-random>" \
--from-literal=host=akko-climscore-postgres \
--from-literal=port=15432 \
--from-literal=database=climscore
psql -U postgres -d climscore -c "ALTER USER akko_readonly WITH PASSWORD '<new-random>'"
kubectl -n akko rollout restart deploy/akko-trino-coordinator deploy/akko-trino-worker
Vérifications de santé¶
# Le proxy est vivant
kubectl -n akko get pod -l app.kubernetes.io/name=akko-climscore-proxy
# Trino voit le catalogue
kubectl -n akko exec deploy/akko-trino-coordinator -- trino --execute "SHOW CATALOGS LIKE 'climscore'"
# La lecture fonctionne
kubectl -n akko exec deploy/akko-trino-coordinator -- trino --execute "SELECT count(*) FROM climscore.public.communes"
# L'écriture est bloquée (doit échouer)
kubectl -n akko exec deploy/akko-trino-coordinator -- trino --execute "CREATE TABLE climscore.public.akko_should_fail (id int)"
Liens¶
feedback_readonly_external_source_defense_in_depth.md— rationale design.gotcha_postgres_externalname_host_docker_internal.md— pourquoi on déploie un proxy socat hostNetwork au lieu d'un simple ExternalName.feedback_dont_touch_climscore.md— STRICT boundary opérationnelle sur la stack docker compose climscore.