flexway
FRENParler à un expert

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

Markdown

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

  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 :
# 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
  1. Calculer son empreinte - le sha256 des octets du fichier, la valeur de sha256sum, sans forme canonique :

    make fragment-digest FILE=security-baseline.yaml    # ou : flexway fragment digest security-baseline.yaml
    # sha256:1c2f91d4…
  2. 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 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 :

# 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]
  1. 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.

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

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

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 /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) : 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.

Étapes suivantes