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.
GuideExploitationDepuis la version 1.7.0
Sur cette page
Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la fonctionnalité côté produit : guide produit ; l'appeler : guide développeur.
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
- É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 :
# 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 removedCalculer son empreinte - le
sha256des octets du fichier, la valeur desha256sum, sans forme canonique :make fragment-digest FILE=security-baseline.yaml # ou : flexway fragment digest security-baseline.yaml # sha256:1c2f91d4…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 :
# 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 firstLe second dépôt épingle sa propre copie du même fichier et appelle le service de risque que le fragment définit :
# 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]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 souscontracts(nom de fichier -> contenu, monté dans/etc/flexway, le répertoire même deflexway.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.Dans la CI du dépôt,
make validate CONFIG=flexway.yamlcompile 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.
Prévu en 1.8
Signer un fragment et vérifier sa signature
Prévu en 1.8.0 - pas encore construit. Aujourd'hui,
policies.fragments.<import>.verifyrefuse 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.
Prévu en 1.8
Monter un fragment depuis un artefact OCI (volume d'image)
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é.
Prévu en 1.8
Console : fragments en vigueur, origine, dérive entre répliques
Prévu en 1.8.0 - pas encore construit. Aujourd'hui, ces informations se lisent dans
/configet dans la métriqueflexway.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) : 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.