Administration du stockage client (brique E-13)
Version anglaise : storage-administration. Référence API : control-plane-v1 section "Client object storage". Page de la brique : E-13-object-storage.
La plateforme n'impose jamais de stockage. Le client déclare son propre bucket ou conteneur dans un profil de stockage ; la plateforme découvre ce que le stockage offre réellement (versioning, Object Lock, legal hold, rétention, multipart, URL présignées, chiffrement, KMS, classes de stockage, cycle de vie, réplication, intégrité) et refuse tout usage qui exige une capacité non prouvée. Le WORM n'est jamais simulé.
1. Stockages et authentifications pris en charge
Fournisseur (provider) | API utilisée | Authentification (auth.method) | URL présignées |
|---|---|---|---|
aws_s3 | REST S3, URL virtual hosted (https://<bucket>.s3.<région>.amazonaws.com) | sigv4 (clé d'accès, jeton de session facultatif) | SigV4 en paramètres, 7 jours au plus |
ibm_cos | REST S3 | sigv4 avec clés HMAC, ou ibm_iam : clé d'API échangée sur https://iam.cloud.ibm.com/identity/token contre un jeton bearer, en-tête ibm-service-instance-id tiré de auth.service_instance_id | clés HMAC uniquement (ibm_iam : capacité presign à false) |
seaweedfs, s3_compatible | REST S3, URL path style | sigv4 | SigV4 en paramètres |
azure_blob | REST Blob, version 2023-11-03 | shared_key (clé de compte), sas (jeton SAS), entra_client_secret (identifiants client Microsoft Entra ID, auth.tenant_id, auth.client_id), workload_identity (fichier de jeton fédéré AZURE_FEDERATED_TOKEN_FILE, profils de plateforme uniquement) | SAS de service (clé partagée) ou SAS de délégation utilisateur (Entra) ; aucune avec un identifiant SAS |
gcs | API JSON | service_account (assertion JWT signée par la clé, échangée sur https://oauth2.googleapis.com/token) | URL signée V4 (GOOG4-RSA-SHA256), 7 jours au plus |
Chaque appel passe par la garde de sortie (eai.security.egress) : aucun SDK fournisseur, aucune variable de proxy, aucune redirection, adresses de métadonnées cloud toujours refusées. Un point de terminaison privé (SeaweedFS sur site, appliance IBM COS) n'est joignable que si l'exploitant l'inscrit dans EAI_EGRESS_PRIVATE_ALLOWLIST ; la restriction du tenant egress_allowed_hosts s'applique aussi aux profils de tenant. Hors dev et test, les points de terminaison, les points de jeton et les URL présignées doivent être en https ; EAI_STORAGE_CA_BUNDLE ajoute l'autorité de certification d'un point interne.
2. Enregistrer l'identifiant, puis créer le profil
Un profil contient une référence, jamais un secret : les profils de tenant utilisent openbao://<chemin> (secret écrit avec POST /v1/secrets), les profils de plateforme env://EAI_CRED_<NOM> (variable injectée par l'exploitant, par exemple depuis un Secret Kubernetes). La valeur du secret est un objet JSON avec les champs de la méthode, ou la valeur brute de son champ unique.
| Méthode | Valeur du secret |
|---|---|
sigv4 | {"access_key_id": "...", "secret_access_key": "...", "session_token": "..."} (jeton de session facultatif) |
ibm_iam | la clé d'API, ou {"api_key": "..."} |
shared_key | la clé de compte (base64), ou {"account_key": "..."} |
sas | le jeton SAS, ou {"sas_token": "..."} |
entra_client_secret | le secret client, ou {"client_secret": "..."} |
service_account | le fichier de clé du compte de service Google (client_email, private_key, private_key_id, token_uri) |
workload_identity | aucun (credential_ref absent) |
curl -X POST <control-plane-url>/v1/secrets -H "Authorization: Bearer $TOKEN" \
-d '{"path": "storage/audit", "value": "{\"access_key_id\": \"AKIA...\", \"secret_access_key\": \"...\"}"}'
curl -X POST <control-plane-url>/v1/storage/profiles -H "Authorization: Bearer $TOKEN" -d '{
"name": "audit-evidence", "provider": "aws_s3", "region": "eu-west-3", "bucket": "bank-audit-worm",
"key_prefix": "eai/", "auth": {"method": "sigv4"}, "credential_ref": "openbao://storage/audit",
"encryption": {"mode": "sse-kms", "kms_key_id": "arn:aws:kms:eu-west-3:111122223333:key/..."},
"purposes": ["audit_worm"]}'
Un champ secret dans auth, options ou encryption est refusé (422 storage_secret_in_profile). Rôles : tenant_admin gère les profils du tenant, platform_admin les profils de plateforme ("scope": "platform", invisibles des tenants).
3. Vérifier les capacités
POST /v1/storage/profiles/{id}/verify (tenant_admin, security_officer) interroge le stockage par de vrais appels et enregistre chaque capacité avec sa méthode et son heure : supported à true (prouvée), false (absence prouvée) ou null (non déterminée : refusée comme une capacité absente). Avec probe_write (par défaut), un objet sonde est écrit sous .eai-probe/ avec le chiffrement, la classe de stockage et le legal hold configurés, relu, comparé par sha256 puis supprimé ; la sonde est omise sur un bucket doté d'une rétention par défaut ou d'une politique d'immutabilité, car elle ne pourrait pas être supprimée.
| Capacité | S3 (AWS, IBM COS, SeaweedFS) | Azure Blob | Google Cloud Storage |
|---|---|---|---|
object_lock (WORM qualifié) | GetObjectLockConfiguration Enabled | conteneur x-ms-immutable-storage-with-versioning-enabled (immutabilité au niveau version) | bucket objectRetention.mode: Enabled ou politique de rétention verrouillée |
retention, legal_hold | Object Lock (legal hold posé, relu et levé par la sonde) | immutabilité au niveau version, politique du conteneur | politique de rétention ou rétention d'objet ; temporary hold posé et levé par la sonde |
versioning | GetBucketVersioning | impliqué par l'immutabilité au niveau version, sinon x-ms-version-id de la sonde | bucket versioning.enabled |
multipart | envoi multipart ouvert puis annulé | Put Block et Put Block List (sonde) | session résumable ouverte puis annulée |
presign | GET présigné d'une clé absente répondu 404 | GET SAS répondu 404 | GET signé répondu 404 |
sse, kms | GetBucketEncryption, en-têtes de réponse de la sonde | x-ms-request-server-encrypted, encryption scope | clé KMS par défaut du bucket |
storage_classes | classe de stockage de la sonde | niveau d'accès de la sonde | classe de stockage par défaut |
lifecycle, replication | GetBucketLifecycleConfiguration, GetBucketReplication | plan de gestion seulement (inconnu), SKU du compte (GRS, GZRS) | règles de cycle de vie, type d'emplacement (dual-region, multi-region) |
integrity | aller-retour sha256 de la sonde, empreinte SigV4 et Content-MD5 vérifiés par le serveur | sha256 de la sonde, Content-MD5 par bloc | sha256 de la sonde, md5Hash vérifié par le serveur |
Une vérification plus ancienne que EAI_STORAGE_CAPABILITY_MAX_AGE_SECONDS (1 jour par défaut) ne compte plus : vérifiez à nouveau, par exemple depuis une tâche planifiée de l'exploitant. Un changement de point de terminaison, de bucket, d'identifiant, de chiffrement ou d'options efface les capacités. GET /v1/storage/profiles/{id}/checks conserve chaque vérification comme preuve.
Le statut vaut verified, degraded (une capacité exigée par les usages ou par required_capabilities manque, listée dans missing_capabilities) ou failed (error : storage_credential_invalid, storage_auth_failed, storage_unreachable, storage_backend_error, egress_denied).
4. URL présignées
POST /v1/storage/profiles/{id}/presign (tenant_admin, developer, agent) {"scope": "read" | "write", "key", "expires_seconds"} renvoie l'URL une seule fois. La clé doit commencer par l'un des presign_prefixes du profil (403 storage_key_out_of_scope) et elle est placée sous key_prefix ; l'expiration est bornée par EAI_STORAGE_PRESIGN_MAX_SECONDS (422 storage_presign_expiry_invalid) ; la capacité presign doit être vérifiée (409 storage_capability_missing). Une URL de lecture ne permet pas d'écrire, et inversement (la méthode est signée). Seuls le sha256 de l'URL, sa clé, sa portée et son expiration sont conservés (storage_presigned_grants) et audités (storage.presign.issue).
5. Exports WORM de l'audit (AUDI-011)
Les exports signés de l'audit (POST /v1/audit/export et le job audit_worm du worker) partent vers le profil du tenant ayant l'usage audit_worm, sinon vers le profil de plateforme nommé par EAI_AUDIT_WORM_STORAGE_PROFILE. L'export est refusé en 409 storage_capability_missing tant que object_lock et retention ne sont pas vérifiées sur ce profil ; chaque objet est écrit en mode conformité jusqu'à retain_until et n'est accepté que si le stockage renvoie lui-même cette rétention (sinon 502 storage_worm_not_honoured, l'objet étant supprimé). Le manifeste porte alors worm.qualified: true, le profil, la rétention relue et l'identifiant de version ; audit_exports.worm_qualified vaut true. Sans profil, le stockage historique EAI_S3_* est utilisé et le manifeste indique worm.qualified: false : ce n'est pas une immutabilité qualifiée.
Ajouter l'usage audit_worm à un profil vérifié sans Object Lock est refusé. Pour qualifier un bucket : AWS et IBM COS, créez le bucket avec Object Lock ; SeaweedFS, créez-le avec x-amz-bucket-object-lock-enabled: true ; Azure, activez l'immutabilité au niveau version sur le conteneur ; Google Cloud Storage, activez la rétention d'objet sur le bucket.
6. Politique, audit et isolement
Chaque action est d'abord décidée par le moteur de politiques (storage.profile.create, storage.profile.update, storage.profile.delete, storage.profile.verify, storage.presign.read, storage.presign.write) puis auditée (storage.profile.*, storage.presign.issue, storage.capability.refused). Les tables storage_profiles, storage_capability_checks et storage_presigned_grants ont une sécurité au niveau ligne forcée : un tenant ne voit jamais le profil d'un autre tenant (404), ni les profils de plateforme.
7. Tests et émulateurs
bash tools/emulators/fetch.sh installe dans /tmp/toolchains des émulateurs épinglés et vérifiés par sha256 : SeaweedFS weed 4.51 (Apache-2.0), Azurite 3.37.0 (MIT, npm, chaque dépendance épinglée par tools/emulators/azurite/package-lock.json, télémétrie désactivée par les tests), fake-gcs-server 1.56.1 (BSD-2-Clause, outil de test seulement, non distribué). Les tests services/control-plane/tests/test_storage_*.py les démarrent sur des ports libres et échouent avec la commande ci-dessus quand un binaire manque.
Ce qu'un émulateur ne peut pas prouver est testé contre une simulation fidèle et reste à valider sur le cloud réel du client (BLOCKED tant qu'aucun compte de test n'existe) : AWS S3 lui-même (le code S3 tourne contre SeaweedFS et les exemples de signature AWS), les jetons IBM IAM, les jetons Microsoft Entra ID (Azurite contrôle l'audience du jeton, pas sa signature), l'immutabilité Azure au niveau version, les jetons Google OAuth2, la vérification des signatures V4 et la rétention d'objet de Google Cloud Storage (fake-gcs-server ne contrôle ni l'une ni l'autre).
8. Procédures dans le portail
L'écran est Données et connaissances > Profils de stockage (portal/src/screens/kit-data.tsx), avec les
onglets Profils, Objets d'un profil et URL présignée.
-
Enregistrer l'identifiant. Le profil ne contient qu'une référence : le secret va d'abord dans OpenBao.
Écran : Sécurité et gouvernance > Secrets, ou
POST /v1/secretscomme dans la section 2.Résultat attendu : le chemin du secret, par exemple
storage/audit, utilisé commeopenbao://storage/audit.En cas d'échec : 403
forbiddensignale que votre rôle ne peut pas écrire de secrets. -
Créer le profil. Il déclare le bucket ou le conteneur du client, ses usages et son chiffrement.
Écran : Profils de stockage > Profils > Nouveau profil : nom, fournisseur, bucket, portée, point de terminaison, région, préfixe de clé,
auth,credential_ref, usages. API :POST /v1/storage/profiles.Résultat attendu : le profil est listé ; ses capacités ne sont pas encore vérifiées.
En cas d'échec : 422
storage_secret_in_profile(un champ secret dansauth,optionsouencryption), 409storage_profile_exists(nom déjà pris), 422storage_tls_required(point de terminaison en http horsdevettest), 403storage_scope_forbidden(portéeplatformsansplatform_admin). -
Vérifier les capacités. Seules les capacités vérifiées comptent : vérifiez avant tout usage.
Écran : Profils, action verify sur le profil, cochez
probe_writepour écrire l'objet sonde. API :POST /v1/storage/profiles/{id}/verify.Résultat attendu : statut
verified; l'action checks liste chaque vérification.En cas d'échec : le statut
degradedlistemissing_capabilities; le statutfaileddonneerror(voir le tableau ci-dessous). -
Émettre une URL présignée. Une application cliente lit ou écrit alors un objet directement, pour une durée bornée.
Écran : Profils de stockage > URL présignée : profil, portée
readouwrite, clé, expiration. API :POST /v1/storage/profiles/{id}/presign.Résultat attendu : l'URL, affichée une seule fois.
En cas d'échec : 403
storage_key_out_of_scope, 422storage_presign_expiry_invalid, 409storage_capability_missing, 422storage_prefix_required(le profil a plusieurspresign_prefixeset la clé ne commence par aucun). -
Désactiver ou supprimer un profil. Un profil désactivé refuse tout usage jusqu'à sa réactivation.
Écran : Profils, action disable, enable ou delete.
Résultat attendu : le nouveau statut s'affiche dans la liste.
En cas d'échec : 409
storage_profile_disabledquand un profil désactivé est utilisé ; 409storage_object_protectedquand un objet d'un usage de preuve est supprimé par l'API.
9. Dépannage
| Statut | Code | Cause | Correction |
|---|---|---|---|
| 422 | storage_secret_in_profile | un champ secret dans le profil | déplacez le secret dans OpenBao, donnez credential_ref |
| 422 | storage_credential_invalid | la valeur du secret ne correspond pas à la méthode | écrivez les champs de la méthode (section 2) |
| 502 | storage_auth_failed | le stockage a refusé l'identifiant | renouvelez la clé chez le fournisseur, réécrivez-la |
| 502 | storage_unreachable | point de terminaison injoignable | vérifiez le réseau et EAI_EGRESS_PRIVATE_ALLOWLIST pour un point privé |
| 422 | storage_tls_required | point de terminaison en http hors dev et test | utilisez https, ajoutez l'autorité avec EAI_STORAGE_CA_BUNDLE |
| 409 | storage_capability_missing | capacité non vérifiée (présignature, Object Lock, rétention) | activez-la sur le bucket, vérifiez à nouveau |
| 502 | storage_worm_not_honoured | le stockage n'a pas renvoyé la rétention | activez Object Lock ou l'immutabilité (section 5) |
| 403 | storage_key_out_of_scope | clé hors des presign_prefixes | utilisez une clé sous un préfixe autorisé |
| 404 | storage_profile_not_found | profil inconnu ou d'un autre tenant | vérifiez l'identifiant |
10. Étapes suivantes
- Sécurité des fichiers
- Référence API
- Page de la brique : E-13-object-storage