flexway
FRENParler à un expert

Chiffrement par champ

GuideExploitationDepuis la version 1.6.0

Markdown

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 valeur

La 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

Étapes suivantes