# Fragments de politique partagés entre passerelles

> Écrire une fois des politiques communes dans un fragment, que chaque passerelle épingle par son empreinte et dont certaines sont imposées.

Guide - Depuis la version 1.7.0 - Flexway v1.7.1 - https://flexway.io/docs/operations-policy-fragments/

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

Quand plusieurs passerelles tournent (une par département, par produit, par site), chacune recopie
les mêmes politiques : CORS de l'entreprise, en-têtes retirés, appel au service de risque. Les copies
divergent sans que personne le voie. Un **fragment de politique** est un fichier écrit une fois - par
l'équipe sécurité par exemple - que chaque passerelle **épingle par son empreinte** dans son propre
dépôt. Aucune passerelle n'attend un serveur central pour démarrer : elle lit un fichier monté, jamais
le réseau.

> [!IMPORTANT]
> **Disponible depuis `1.7.0`, en partie** - les fragments arrivent en trois étapes. Ce qui est
> décrit ici est disponible : fichier `kind: PolicyFragment`, import épinglé par `sha256`, politiques
> imposées, refus au chargement et au rechargement, `/config`, métrique, `flexway fragment digest`,
> `flexway validate`. Prévus en `1.8.0` : la **signature** d'un fragment (déclarer `verify` refuse
> le chargement d'ici là), le montage depuis un **artefact OCI**, et l'écran de la **console**.

## Étapes

1. Écrire le fragment. Il ne contient que des définitions de politiques, celles qu'il impose et ce
   qu'il exige de la passerelle qui l'importe - ni hôte, ni route, ni chaîne, ni amont, ni émetteur,
   ni import d'un autre fragment :

<!-- example: examples/policy-fragments/security-baseline.yaml -->
```yaml
# A policy fragment (1.7.0): the policies a security team writes once, for every gateway of the
# company. Each gateway copies this file into its own repository and pins it by its digest
# (flexway fragment digest security-baseline.yaml); nothing else may live here - no host, no route,
# no upstream, no import.
kind: PolicyFragment
fragment: security-baseline
version: 1.4.0                                   # what changed, for people; the digest says what runs
requires:
  flexway: ">=1.7.0"                             # the oldest gateway this fragment is written for
  upstreams: [risk]                              # the importer declares it; the fragment never does
definitions:
  strip-internal:
    type: header-remove
    phase: response
    config: { names: [server, x-powered-by] }
  trace-gateway:
    type: header-inject
    config: { set: { x-request-origin: gateway } }
  cors-corp:
    type: cors
    config: { allow_origins: ["https://app.example.com"], allow_methods: [GET, POST] }
  risk-check:
    type: external-call
    when: "request.method == 'POST'"
    config:
      upstream: risk
      path: /score
      timeout: 150ms
      on_failure: fail-closed
      deny: { when: "response.status == 403", status: 403, code: risk_refused }
enforce:
  global: [strip-internal, trace-gateway]        # at the head of every importer's chain, never removed
```

2. Calculer son empreinte - le `sha256` des octets du fichier, la valeur de `sha256sum`, sans forme
   canonique :

   ```bash
   make fragment-digest FILE=security-baseline.yaml    # ou : flexway fragment digest security-baseline.yaml
   # sha256:1c2f91d4…
   ```

3. L'importer dans chaque passerelle sous un **nom d'import** de son choix, qui préfixe ses définitions
   (`corp/cors-corp`), et l'épingler par son empreinte :

<!-- example: examples/policy-fragments/flexway.yaml -->
```yaml
# One base of policies for several gateways (1.7.0): this gateway - the orders API - imports the
# security team's fragment, pinned by the sha256 of its bytes. Its definitions are known here as
# corp/<name>; the two it enforces run first on every operation, and no local line removes them.
# A fragment file changed without its digest is refused - at start, and at a reload, where the
# configuration in force keeps serving.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
  risk:
    url: http://risk.internal:8080               # required by the fragment, declared here
policies:
  fragments:
    corp:                                        # the import name: the prefix of its definitions
      path: ./security-baseline.yaml             # a mounted file, relative to this one
      digest: sha256:1c2f91d4f7479525940a2567a49377425a41621984de8f0ec016fe2eb6c21485
  definitions:
    tenant-header:
      type: header-inject
      config: { set: { x-gateway: orders } }
  chains:
    global: [corp/cors-corp, tenant-header]      # corp/strip-internal and corp/trace-gateway come first
```

   Le second dépôt épingle **sa propre copie** du même fichier et appelle le service de risque que le
   fragment définit :

<!-- example: examples/policy-fragments/partners/flexway.yaml -->
```yaml
# The second repository (1.7.0): the partners API pins the same fragment, copied into it, and calls
# the risk service the fragment defines on its own operation. Same file, same digest, same enforced
# head - each gateway decides alone, nothing is pushed to it.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://partners.internal:8080
  risk:
    url: http://risk.internal:8080
policies:
  fragments:
    corp:
      path: ./security-baseline.yaml             # its own copy: a fragment is read from this directory or below
      digest: sha256:1c2f91d4f7479525940a2567a49377425a41621984de8f0ec016fe2eb6c21485
  chains:
    global: [corp/risk-check]
```

4. Monter le fichier à côté de `flexway.yaml`, dans son répertoire ou en dessous - jamais au-dessus
   (`..` refusé), ni un chemin absolu hors de ce répertoire ; un fichier ordinaire de 1 Mio au plus.
   Avec le chart, c'est un fichier de plus sous `contracts` (nom de fichier -> contenu, monté dans
   `/etc/flexway`, le répertoire même de `flexway.yaml`) ; le chemin est alors plat
   (`./security-baseline.yaml`). Le fichier arrive par la même pull request que son empreinte :
   copié à la main, par Renovate ou par la CI de l'équipe qui publie le fragment.

5. Dans la CI du dépôt, `make validate CONFIG=flexway.yaml` compile le fragment avec la configuration
   qui l'importe et imprime une ligne par fragment (`fragment corp: security-baseline@1.4.0 sha256:…`).

## Vérifier

| Où | Ce qu'on y lit |
| --- | --- |
| `/config` (port d'administration) -> `fragments` | Par import : `fragment`, `version`, `digest`, `path`, `signature` (`none` : l'empreinte seule), `requires`, `requires_unchecked` (`true` : une construction de développement l'a chargé sans comparer `requires.flexway`, avertissement au journal), `definitions`, `enforced`, `unused` |
| `/config` -> `chains` | Chaque étape venue d'un fragment porte `origin` (`import`, `fragment`, `version`, `enforced`) ; une imposée est attachée par `policies.fragments` et passe en tête |
| Métrique `flexway.config.fragment.info` | `1` par fragment, labels `import`, `fragment`, `version`, `digest` (12 caractères) : deux répliques sur deux empreintes donnent deux séries |

Un fichier de fragment modifié sans changer l'empreinte doit être **refusé** : `last_reload.result:
rejected`, `digest mismatch` dans `last_reload.cause`, et la chaîne en cours continue de servir.

## Signer un fragment et vérifier sa signature

> [!NOTE]
> Prévu en 1.8

> **Prévu en 1.8.0 - pas encore construit.** Aujourd'hui, `policies.fragments.<import>.verify`
> **refuse le chargement** : seule l'empreinte épingle un fragment.

L'équipe qui publie le fragment le signera avec `cosign sign-blob --bundle` ; chaque passerelle
importatrice déclarera sous `verify` le bundle et la clé publique attendue, et vérifiera la signature
**hors ligne**, au chargement et au rechargement, sans interroger de journal de transparence. Une
signature sans clé (émetteur et identité attendus, racine de confiance fournie) est prévue au même
jalon. L'empreinte restera obligatoire : la signature dit qui a écrit le fragment, l'empreinte ce qui
tourne. C'est la signature qui protégera contre une équipe importatrice qui ré-épingle une autre
empreinte dans son propre dépôt.

## Monter un fragment depuis un artefact OCI (volume d'image)

> [!NOTE]
> Prévu en 1.8

> **Prévu en 1.8.0 - pas encore construit.** Aujourd'hui, le fragment est un fichier copié dans le
> dépôt de chaque passerelle et monté avec sa configuration.

Le chart montera un fragment publié en artefact OCI comme **volume d'image** Kubernetes
(`volumes[].image`), désigné par son digest - jamais par un tag seul -, ou depuis une `ConfigMap`. La
passerelle continuera de ne lire qu'un fichier monté : elle ne contacte jamais le registre. Le volume
d'image demande Kubernetes `1.36` ou plus (sinon la porte `ImageVolume`) et un changement d'empreinte
recrée les pods ; la copie dans le dépôt reste la voie par défaut, et celle à retenir pour un site
isolé.

## Console : fragments en vigueur, origine, dérive entre répliques

> [!NOTE]
> Prévu en 1.8

> **Prévu en 1.8.0 - pas encore construit.** Aujourd'hui, ces informations se lisent dans `/config`
> et dans la métrique `flexway.config.fragment.info` (section « Vérifier » ci-dessus).

La console montrera, pour chaque passerelle, les fragments en vigueur (version, empreinte, signature,
politiques imposées), l'origine de chaque étape du pipeline, et la dérive entre répliques ou avec le
dépôt - deux empreintes servies en même temps -, avec une règle d'alerte `fragment-drift`. Elle ne
modifie jamais la configuration : une nouvelle empreinte passe toujours par une pull request relue.

## À savoir

### Les clés

| Clé | Règle | Défaut |
| --- | --- | --- |
| `policies.fragments` | Nom d'import (minuscules, chiffres, `-`, 63 au plus) -> fichier épinglé | absent : aucun fragment |
| `policies.fragments.<import>.path` | Fichier ordinaire monté (1 Mio au plus), dans le répertoire de `flexway.yaml` ou en dessous : `..` et chemin absolu hors de ce répertoire refusés | obligatoire |
| `policies.fragments.<import>.digest` | `sha256:` et 64 caractères hexadécimaux minuscules | obligatoire |
| `policies.fragments.<import>.verify` | Signature : pas encore construite - déclarée, elle refuse le chargement | absent |
| Fichier : `kind` | Toujours `PolicyFragment` | obligatoire |
| Fichier : `fragment` | Nom du fragment (minuscules, chiffres, `-`) ; une passerelle n'en importe qu'une version | obligatoire |
| Fichier : `version` | Version sémantique (`1.4.0`), lue par les personnes et `/config` ; l'**empreinte** dit ce qui tourne | obligatoire |
| Fichier : `requires.flexway` | `>=MAJEURE.MINEURE.CORRECTIF` ; une construction de développement (`dev`) la satisfait toujours, le dit au journal (`Warn`) et dans `/config` (`requires_unchecked`) ; une version servie qui n'est pas sémantique est refusée | toute version |
| Fichier : `requires.upstreams` | Amonts que l'importateur doit déclarer (ceux qu'un `external-call` nomme) | aucun |
| Fichier : `definitions` | Au moins une politique, écrite comme dans `policies.definitions`, d'un type que la passerelle exécute (un type inconnu est refusé, jamais ignoré) ; un nom en ASCII : lettres, chiffres, `.`, `_`, `-` | obligatoire |
| Fichier : `enforce.global` | Politiques imposées, en tête de chaque chaîne de l'importateur (écouteur HTTP) | rien d'imposé |

Un secret s'écrit en référence (`${VAR}`, `file:/chemin`), résolue chez l'importateur ; une valeur en
clair sous une clé de secret est refusée. Ancres, alias et clés de fusion YAML sont refusés dans un
fragment et sous `policies.fragments` : chaque valeur s'écrit là où elle s'applique. Le schéma du fichier est publié avec les autres
([référence de configuration](https://flexway.io/docs/operations-configuration-reference/)) : `policy-fragment.schema.json`.

### Ce qui est imposé

Les politiques de `enforce.global` passent **avant** `global`, dans l'ordre du fragment, puis les
fragments dans l'ordre de `policies.fragments`. Ensuite, rien ne change : `global -> hôte -> routes ->
opération`, la plus précise gagne. Les chaînes - imposées comprises - s'appliquent aux requêtes que la
passerelle admet sur son écouteur HTTP (REST, gRPC, outils MCP, inférence, portes HTTP du plan
asynchrone) ; l'**écouteur Kafka natif** n'exécute aucune chaîne : une imposée ne s'y applique pas.

| Cas | Effet |
| --- | --- |
| Une définition importée nommée par une chaîne locale | Comme une définition locale |
| `!corp/strip-internal` (retrait d'une imposée), à n'importe quel niveau | **Refus au chargement**, qui nomme la chaîne ou l'opération |
| Une instance locale d'un type à instance unique (`cors`, `rate-limit`, `quota`, `cache`, `rewrite-path`, `transform-body`, `map-status`) là où une imposée de ce type s'applique | **Refus au chargement** : une imposée n'est jamais remplacée |
| Une politique non imposée (`header-*`, `query-*`, `set_headers` d'un `external-call`) qui pose, renomme ou retire, dans la même phase, un en-tête (casse ignorée) ou un paramètre qu'une imposée pose ou retire | **Refus au chargement** : une ligne locale ne défait jamais une imposée |
| Deux imposées d'un même type à instance unique, de deux fragments | **Refus au chargement**, nommée ailleurs ou non |
| Une imposée aussi nommée par une chaîne locale | Exécutée une fois, à sa place imposée |
| Une définition importée jamais nommée | Chargée et signalée (`unused` dans `/config`), jamais une erreur |

Ce que le lot 1 garantit : un fichier changé hors revue ne s'applique jamais (empreinte), et aucune
ligne locale ne retire, ne remplace ni ne défait une imposée. Il ne protège **pas** contre l'équipe qui
importe et qui ré-épingle une autre empreinte ou retire l'import dans son propre dépôt : c'est le rôle
de la signature (`verify`, à venir, avec une clé que détient l'équipe sécurité) et d'une garde
d'admission dans votre outil GitOps.

### Monter de version, revenir en arrière

Monter de version : l'équipe qui publie change le fichier et sa `version` ; dans chaque dépôt
importateur, une pull request copie le fichier et change l'**empreinte** ; après la fusion, le
rechargement habituel l'applique, sans redémarrage. Revenir en arrière : rétablir l'ancien fichier et
son ancienne empreinte, par une pull request.

### Refus au chargement et au rechargement

Tout écart refuse la configuration **en bloc** ; tous les problèmes sont dits d'un coup. Au
démarrage, la passerelle ne démarre pas ; au rechargement, la configuration en cours **continue de
servir** et le refus est dit (journal, `flexway.config.reloads{result="rejected"}`, `last_reload` de
`/config`). La passerelle relit les fichiers de fragment avec la configuration toutes les
`server.reload.interval` : un fichier changé à côté d'un `flexway.yaml` inchangé est vu et refusé.

| Écart | Message (extrait) |
| --- | --- |
| Fichier absent ou illisible | `policies.fragments.corp: the fragment file cannot be read` |
| Empreinte différente | `digest mismatch for ./security-baseline.yaml - pinned sha256:…, read sha256:…` |
| Hôte, route, chaîne, amont dans le fragment | `hosts: a fragment holds kind, fragment, version, requires, definitions, enforce only` |
| Import d'un autre fragment | `fragments: a fragment imports no fragment` |
| Deux imports du même `fragment` | `fragment security-baseline is already imported as corp` |
| `/` dans un nom local | `a local name holds no /` |
| Amont exigé non déclaré | `requires.upstreams names "risk", which upstreams does not declare` |
| Passerelle trop ancienne | `requires flexway >=1.8.0, and this gateway is v1.7.0` |
| Retrait ou remplacement d'une imposée | `... which fragment corp enforces - an enforced policy is never removed` |
| Ligne locale qui défait une imposée | `"mine" writes the header Server in phase response, which "corp/strip-internal" ... sets or removes` |
| Deux imposées du même type à instance unique | `two enforced cors policies, "corp/cors" (fragment corp) and "team/lax" (fragment team)` |
| Type de politique inconnu dans un fragment | `... is not a policy type this version executes - an imported policy is never skipped` |
| Chemin hors du répertoire, fichier non ordinaire ou de plus de 1 Mio | `never above it (..)`, `outside flexway.yaml's directory`, `not a regular file`, `a fragment may hold` |
| Ancre, alias ou clé de fusion | `anchors, aliases and merge keys are refused` |
| Version servie non sémantique | `this gateway's version "..." cannot be compared` |

*Disponible depuis `1.7.0`.*
