Chiffrement par champ
GuideExploitationDepuis la version 1.6.0
Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : guide produit ; l'appeler : guide développeur.
Disponible depuis 1.6.0. Les champs
qu'un message déclare sensibles sont chiffrés par la passerelle à l'écriture et déchiffrés pour la
seule portée qui le peut : le courtier, ses sauvegardes et tout lecteur sans cette portée ne voient
jamais la valeur en clair. Le reste du message reste lisible en aval.
Comment. Chiffrement d'enveloppe : la passerelle tire une clé de données AES-256 (crypto/rand),
chiffre chaque champ en AES-256-GCM avec un IV aléatoire de 96 bits, et fait envelopper la clé de
données par le KMS sous une clé qui n'en sort jamais. La clé enveloppée voyage dans l'en-tête
flexway-dek de l'enregistrement. Chaque champ est lié par ses données additionnelles au tenant, au
sujet physique, au chemin du champ, à la version et à l'époque : un champ copié vers un autre
champ, un autre sujet ou un autre tenant ne se déchiffre pas. Un champ chiffré est écrit en base64
dans un champ texte, en octets dans un champ bytes. En Avro et Protobuf, le chemin traverse des
enregistrements et des messages, jamais une liste ni une table, et finit sur du texte ou des octets.
L'en-tête flexway-dek nomme aussi les champs qu'il a chiffrés, sous un code d'authentification
(HMAC-SHA256 sous une clé dérivée de la clé de données) : un lecteur déchiffre ce que l'enregistrement
dit porter, pas ce que le contrat déclare aujourd'hui. Un chemin retiré de x-flexway-encrypt se
déchiffre encore dans l'historique (tant que le schéma déclare le champ au même type) ; un chemin ajouté
ne rend pas l'historique illisible ; changer la liste fait une nouvelle clé de données. Retirer
x-flexway-encrypt entier arrête le déchiffrement : l'historique est rendu chiffré, avec son en-tête.
Le KMS - un seul genre par passerelle, les identifiants toujours par référence ou fichier monté :
# flexway.yaml - Vault transit, authentification Kubernetes (le compte de service du pod)
kms:
key: flexway-kafka # la clé transit ; flexway-kafka-{tenant} : une clé par tenant
vault_transit:
address: https://vault.security:8200 # https obligatoire : le jeton ne voyage jamais en clair
mount: transit # défaut
kubernetes: { role: flexway } # ou token: ${VAULT_TOKEN} / file:/vault/token - jamais la valeurLa politique Vault du rôle de la passerelle : update sur le chiffrement et le déchiffrement, read sur les
métadonnées des clés - jamais create. Avec create, Vault transit crée une clé absente au premier
chiffrement : une clé détruite reviendrait, vide, sous le même nom - les nouvelles écritures passeraient sous
elle, et les enregistrements effacés, qui ne s'authentifient plus, bloqueraient leurs consommateurs
(CORRUPT_MESSAGE) au lieu d'être remis avec key_destroyed.
# Politique Vault de la passerelle (flexway-kafka-{tenant} : le joker couvre la clé de chaque tenant)
path "transit/encrypt/flexway-kafka*" { capabilities = ["update"] }
path "transit/decrypt/flexway-kafka*" { capabilities = ["update"] }
path "transit/keys/flexway-kafka*" { capabilities = ["read"] }# AWS KMS : chaîne par défaut du SDK (IRSA, rôle d'instance) sans credentials_file
kms:
key: alias/flexway-kafka
aws_kms: { region: eu-west-3 }# Google Cloud KMS : identifiants par défaut (Workload Identity) sans credentials_file
kms:
key: projects/acme/locations/europe-west1/keyRings/flexway/cryptoKeys/kafka
gcp_kms: {}# local : des fichiers de clés dans un répertoire monté - tests et développement seulement
kms:
key: dev
local: { dir: /keys }Le contrat. Le message déclare ses champs chiffrés, l'opération de lecture la portée qui déchiffre :
# api/asyncapi.yaml - extrait
channels:
orders:
address: "{tenant}.orders"
parameters: { tenant: { x-flexway-bind: tenant } }
messages:
order:
payload: { $ref: "#/components/schemas/Order" }
x-flexway-encrypt: [customer.email, payment.iban] # chemins pointés ; ni index ni joker
operations:
readOrders:
action: receive
channel: { $ref: "#/channels/orders" }
x-flexway-kafka-proxy: { cluster: orders, decrypt_scope: orders:decrypt }Un lecteur dont le jeton porte orders:decrypt reçoit les champs en clair ; les autres reçoivent la
valeur chiffrée. decrypt_scope n'existe que sur une opération receive dont le message déclare
x-flexway-encrypt : sinon le contrat est refusé au chargement. Une valeur null n'est jamais chiffrée.
Réglages - chacun a un défaut et une borne, refusée au chargement au-delà :
| Clé | Défaut | Bornes | Pourquoi |
|---|---|---|---|
kms.timeout |
2 s | jusqu'à 30 s | Un appel au KMS retient la requête Kafka qui en dépend |
kms.rotation.interval |
1 h | 1 min à 7 j | Nouvelle époque, nouvelle clé de données : un effacement ou une fuite touche une fenêtre bornée |
kms.rotation.max_encryptions |
5 000 000 | 1 à 2^30 | AES-GCM à IV aléatoire reste très loin de 2^32 champs par clé |
kms.previous_keys |
aucune | 8 au plus, chacune comme kms.key |
Les clés qu'un enregistrement peut encore nommer - les anciennes valeurs de kms.key, lues, jamais écrites. Un en-tête qui nomme une autre clé est refusé (CORRUPT_MESSAGE, 2) sans appel au KMS |
kms.cache.max_keys |
10 000 | 1 à 1 000 000 | Clés de données gardées en clair en mémoire, effacées à l'éviction ; au-delà, la plus ancienne du tenant qui en tient le plus part : un tenant qui relit son historique n'évince jamais les clés des autres |
kms.cache.ttl |
1 h | 1 s à 24 h | Durée de vie d'une clé de données déchiffrée depuis son déchiffrement, servie ou non ; et intervalle auquel la clé d'écriture en cours redemande au KMS si sa clé tient. C'est le délai maximal d'un effacement (ci-dessous) |
kms.cache.negative_ttl |
5 s | 100 ms à 5 min | Un refus du KMS mémorisé : pas de tempête d'appels |
kms.tenant_calls.rate / burst |
10 / 20 | rate > 0, burst >= 1 | Appels KMS par tenant et par seconde : un tenant n'épuise jamais le KMS des autres |
KMS indisponible, clé détruite - la passerelle ne publie jamais en clair un champ déclaré chiffré et ne rend jamais le chiffré en silence à un lecteur qui détient la portée :
| Situation | Producteur | Consommateur avec la portée | Que faire |
|---|---|---|---|
| KMS injoignable, clé de données encore en cache | Servi normalement | Servi tant que la clé de l'époque lue est en cache | Rétablir le KMS ; règle kms-unavailable en alerte |
| KMS injoignable, plus de clé valide | KAFKA_STORAGE_ERROR (56), réessayable : rejoué dans delivery.timeout.ms, rien publié |
KAFKA_STORAGE_ERROR (56) sur la partition : il relit |
Rétablir le KMS avant l'échéance de delivery.timeout.ms |
| Refus d'accès du KMS (politique Vault, IAM) | Comme une panne (56) | Comme une panne (56) | Corriger la politique |
| Clé détruite, désactivée ou version retirée : effacement cryptographique | RESOURCE_NOT_FOUND (91), non réessayable ; version retirée seule : nouvelle clé de données sous la dernière version, l'écriture passe |
Enregistrement remis, champ toujours chiffré, en-tête flexway-encryption: key_destroyed - il doit lire cet en-tête avant d'utiliser le champ |
Voulu : les champs sont définitivement illisibles ; sinon restaurer la version de clé |
| Champ ou clé enveloppée qui ne s'authentifie pas (altéré) | - | CORRUPT_MESSAGE (2) sur la partition, jamais une valeur |
Chercher l'écrivain qui contourne la passerelle |
# Consommateur avec la portée (confluent-kafka, Python) : un champ effacé reste chiffré
for msg in consumer.consume(100, timeout=1.0):
headers = dict(msg.headers() or [])
if headers.get("flexway-encryption") == b"key_destroyed":
continue # effacement voulu : ne pas utiliser les champs chiffrés
handle(msg.value())Ce que chaque KMS appelle « détruite » - une clé n'est jugée effacée que sur un état explicite ; une transition, une faute de frappe ou un refus d'accès est une panne (56), jamais un effacement :
| Réponse du KMS | À l'écriture (envelopper) | À la lecture (désenvelopper) |
|---|---|---|
Vault : encryption key not found |
Panne (56) : kms.key ne nomme aucune clé |
Effacement (key_destroyed) |
Vault : version sous min_decryption_version (disallowed by policy (too old)) |
- (l'écriture prend la dernière version) | Effacement ; la clé d'écriture qui la portait est refaite |
Vault : invalid ciphertext, message authentication failed (dont une version inventée vault:v999) |
- | Invalide (CORRUPT_MESSAGE, 2) |
AWS : DisabledException ; KMSInvalidStateException d'une clé Disabled ou PendingDeletion (lu par DescribeKey) |
Effacement (91) | Effacement |
AWS : NotFoundException |
Panne (56) | Effacement (la clé a enveloppé cette donnée) |
AWS : KMSInvalidStateException d'une clé Creating, Updating, PendingImport, Unavailable, ou état illisible |
Panne (56) | Panne (56) |
GCP : FAILED_PRECONDITION qui nomme l'état DISABLED, DESTROYED ou DESTROY_SCHEDULED |
Effacement (91) | Effacement |
GCP : NOT_FOUND |
Panne (56) | Effacement |
GCP : tout autre FAILED_PRECONDITION (gestionnaire de clés externe injoignable, import en cours) |
Panne (56) | Panne (56) |
Tous : refus d'accès, délai, 5xx, réseau |
Panne (56) | Panne (56) |
Une clé en attente de suppression (AWS) ou de destruction (GCP) est désactivée - c'est l'effacement
de l'ADR ; annuler la suppression la rend lisible à nouveau, mais les enregistrements déjà remis marqués
key_destroyed ne reviennent pas.
Délai d'un effacement. Une clé détruite dans le KMS prend effet sur chaque réplique en
kms.cache.ttl au plus (1 h par défaut), plus kms.cache.negative_ttl : une clé de données lue
vit kms.cache.ttl depuis son déchiffrement, servie ou non, et la clé d'écriture redemande au KMS toutes
les kms.cache.ttl (un désenveloppement de sa propre clé enveloppée, sans permission de plus). Si le KMS
est injoignable à ce moment, la clé d'écriture continue (T2) jusqu'à kms.rotation.interval et le KMS est
réinterrogé toutes les kms.cache.negative_ttl. Raccourcir kms.cache.ttl raccourcit le délai, au prix
de plus d'appels au KMS. Une réplique démarrée après l'effacement d'une clé supprimée (introuvable)
refuse les écritures en panne (56) - le KMS ne distingue pas une clé supprimée d'une clé jamais créée.
Vérification au démarrage. Au démarrage et à chaque rechargement qui change le KMS, la passerelle
lit les métadonnées de kms.key : une clé absente, une clé transit Vault non dérivée (derived=true
obligatoire), une clé AWS non symétrique ou une clé GCP d'un autre usage refusent la configuration ;
une clé désactivée est acceptée (écritures en 91) ; un KMS injoignable ou une permission manquante ne
bloquent rien et sont journalisés. Permission de lecture à accorder : Vault read sur
<mount>/keys/<clé>, AWS kms:DescribeKey, GCP cloudkms.cryptoKeys.get (roles/cloudkms.viewer).
Un nom à {tenant} n'est pas vérifié au démarrage : la clé d'un tenant absente refuse ses écritures en
panne (56).
Effacer les données d'un tenant. Avec une clé par tenant (key: flexway-kafka-{tenant}), détruire
sa clé dans le KMS (Vault : deletion_allowed puis suppression ; AWS : ScheduleKeyDeletion ; GCP :
destruction des versions) rend ses champs définitivement illisibles, ceux des autres tenants intacts.
Avec une clé partagée, retirer une version (min_decryption_version de Vault) efface les époques
qu'elle enveloppait. La console compte ces lectures à part (key_destroyed), jamais comme une panne.
Ce qui est refusé. Un enregistrement produit avec un en-tête flexway-* (INVALID_RECORD, 87) :
ces en-têtes appartiennent à la passerelle. Un enregistrement dont le schéma ne décode pas un champ
déclaré, ou qui porte deux fois un champ déclaré (une clé JSON répétée) (INVALID_RECORD, 87). Un lot
de contrôle (bit 0x20) dans un Produce, sur tout sujet servi, chiffré ou non (INVALID_RECORD,
87) : seul le coordinateur de transactions en écrit. Un contrat qui déclare x-flexway-encrypt sans
kms dans flexway.yaml, ou avec listeners.kafka.max_response_bytes sous max_request_bytes (un
enregistrement écrit ne se relirait pas).
Limites assumées. Les champs d'un enregistrement ne sont liés qu'au tenant, au sujet, au chemin et à l'époque, pas à l'enregistrement : qui écrit dans le courtier sans la passerelle peut échanger deux champs chiffrés sous la même clé de données entre deux enregistrements du même sujet ; il ne peut ni les lire ni les déplacer vers un autre champ, sujet ou tenant. En Protobuf, un champ présent deux fois sur le fil n'en garde qu'un - le dernier, règle de fusion de Protobuf - réécrit chiffré : aucune copie en clair ne passe.
Signaux. flexway.kafka.encryption{cluster, tenant, op, outcome} (ok, kms_unavailable,
key_destroyed, invalid), flexway.kafka.kms.calls{cluster, op, outcome} (wrap, unwrap ;
budget_exhausted quand le budget du tenant refuse sans appel), flexway.kafka.dek.cache,
flexway.kafka.dek.epoch et flexway.kafka.dek.cached. /config publie le genre, l'hôte, le nom de la
clé, la rotation et le cache, jamais une référence, un secret ni une clé. Dans la console, le panneau
du cluster (Amonts) montre le KMS et ses compteurs, et les règles kms-unavailable et
kafka-key-destroyed (désactivée par défaut) alertent (Console : clusters virtuels, clients Kafka, règles).
| Symptôme | Cause | Que faire |
|---|---|---|
Refus au démarrage kms: write exactly one of ... |
Aucun ou plusieurs genres de KMS | En écrire un seul |
Refus au démarrage kms.vault_transit.token must be a reference |
Le jeton écrit en clair | ${VAULT_TOKEN} ou file:/chemin |
Producteurs en KAFKA_STORAGE_ERROR |
KMS injoignable ou refus d'accès, cache épuisé | Rétablir le KMS ou sa politique ; kms.cache.ttl donne l'autonomie |
| Lecteur avec la portée qui voit des valeurs chiffrées | En-tête key_destroyed : clé effacée |
Voulu ; sinon restaurer la version de clé |
INVALID_RECORD à la production |
Un en-tête flexway-* du client, une valeur que son schéma ne décode pas, un champ déclaré deux fois, ou un lot de contrôle |
Retirer l'en-tête ; corriger la valeur ; ne jamais marquer un lot de données comme lot de contrôle |
Refus au démarrage kms.key "...": kms key absent or unusable |
La clé n'existe pas (faute de frappe), une clé Vault non dérivée, une clé AWS non symétrique, une clé GCP d'un autre usage | Corriger kms.key, ou créer la clé (derived=true pour Vault) |
Journal kms.key could not be checked |
KMS injoignable, ou permission de lecture des métadonnées non accordée | Accorder read (Vault), kms:DescribeKey (AWS), cloudkms.cryptoKeys.get (GCP) - facultatif |
CORRUPT_MESSAGE sur une partition après un changement de kms.key |
Les enregistrements nomment l'ancienne clé | La déclarer dans kms.previous_keys |
CORRUPT_MESSAGE après un changement de schéma |
Un champ que l'historique chiffre n'existe plus au schéma, ou a changé de type | Garder le champ au schéma tant que l'historique le porte |