# Chiffrement par champ

> 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…

Guide - Depuis la version 1.6.0 - Flexway v1.6.3 - https://flexway.io/docs/operations-kafka-encryption/

> Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : [guide produit](https://flexway.io/docs/product-overview/) ; l'appeler : [guide développeur](https://flexway.io/docs/developer-overview/).

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é :

```yaml
# 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`.

```hcl
# 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"] }
```

```yaml
# 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 }
```

```yaml
# 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: {}
```

```yaml
# 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 :

```yaml
# 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 |

```python
# 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](https://flexway.io/docs/operations-kafka-clients/#console--clusters-virtuels-clients-kafka-r%C3%A8gles)).

| 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 |
