Configurations cibles des prochaines versions
GuideExploitationDepuis la version 1.6.0
Sur cette page
- Appareils MQTT : X.509 ou jeton, topic lié au tenant et au certificat
- Sparkplug B
- OPC UA PubSub JSON sur MQTT
- Plans lus d'une claim, plan par défaut
- Budgets d'inférence par utilisateur et par cas d'usage
- Webhook entrant Stripe et rotation du secret
- Webhook générique HMAC
- Standard Webhooks, mémoire de rejeu partagée
- Listes d'adresses : liste noire globale et ip-filter
- Compression négociée
- Coalescence des lectures en vol
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.
Cette page montre, pour chaque cas d'usage prévu, la forme cible d'un flexway.yaml complet et
l'extrait de contrat qui va avec, et la version qui doit le livrer : 1.7.0 (appareils et industrie) et
1.8.0 (besoins des entreprises). Rien de ce qui suit n'est construit : un détail peut encore bouger
d'ici la version, et cette page suivra. Là où la forme d'une clé n'est pas encore fixée, l'exemple le dit
au lieu de l'inventer. Chaque exemple deviendra un fichier de examples/, validé comme les autres, avec
sa version.
Déjà livré en
1.6.0, et donc décrit ailleurs : le proxy Kafka natif (Clients Kafka natifs : clusters virtuels par tenant), le chiffrement par champ (Chiffrement par champ), les charges Protobuf, Avro et CBOR sous schéma et le registre de schémas (Charges Avro sous schéma, registre de schémas), NATS et AMQP 1.0 (Exemples : Kafka, RabbitMQ, MQTT, Avro, NATS, AMQP 1.0).
Ce que fait la passerelle d'aujourd'hui de ces formes (vérifié avec flexway validate) :
| L'exemple déclare | Aujourd'hui |
|---|---|
Une clé nouvelle de flexway.yaml : listeners.mqtt, plans, webhooks, server.ip_deny, server.proxy_protocol, ai.budget.per_subject, ai.use_cases, ai.aliases, plan_claim, use_case_claim, trusted_clients, auth.mtls.clients[].plan |
Refusée au chargement (field ... not found) : toute la configuration est refusée |
Une liaison x-flexway-bind: "cert:<champ>", un schéma de webhook lu dans un autre en-tête qu'Authorization |
Refusé au chargement |
Un type de politique à venir : compress, ip-filter |
Accepté, jamais appliqué : flexway validate et le démarrage l'avertissent (a later version - NOT applied) |
Une extension de contrat à venir : x-flexway-device, x-flexway-sparkplug, x-flexway-use-case, x-flexway-webhook, x-flexway-coalesce |
Refusée au chargement (not served yet - planned for <version>) : tout le contrat est refusé, comme toute extension x-flexway-* inconnue (unknown extension, avec la plus proche si elle est évidente) ou écrite là où elle n'est pas lue (not read here) - Pas encore construit |
Prévu en 1.7
Appareils MQTT : X.509 ou jeton, topic lié au tenant et au certificat
Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui (
listeners.mqtt,x-flexway-bind: "cert:cn",x-flexway-device).
La passerelle est un proxy MQTT devant le courtier du client : une connexion amont par appareil, chaque paquet jugé par la même décision qu'une requête HTTP. Sessions, retenus, testaments et QoS 2 restent au courtier.
# flexway.yaml - l'écouteur ; aucun topic ici, ils viennent du contrat
version: 1
contracts:
asyncapi: api/asyncapi.yaml
listeners:
mqtt:
address: ":8883"
tls: { cert_file: /tls/tls.crt, key_file: /tls/tls.key, client_ca_file: /tls/ca.crt } # X.509 des appareils
websocket: { path: /mqtt }
max_packet_bytes: 262144
connect_timeout: 5s # un CONNECT lent est fermé
upstreams:
plant-broker:
protocol: mqtt
brokers: ["mqtts://broker.plant.internal:8883"]
protocol_version: 5
auth:
issuers: # pour un appareil sans certificat : un jeton en mot de passe
- name: devices
discovery: https://id.example.com/.well-known/openid-configuration
audience: plant-devices
tenant_claim: site# api/asyncapi.yaml - la convention des écouteurs, celle du proxy Kafka natif (1.6.0) :
# l'extension sur l'opération, send = l'appareil publie, receive = il s'abonne
channels:
telemetry:
address: "plant/{site}/line/{line}/{device}/telemetry"
parameters:
site: { x-flexway-bind: tenant } # le tenant vérifié, jamais celui du topic
device: { x-flexway-bind: "cert:cn" } # le nom commun du certificat de l'appareil
messages:
reading:
contentType: application/x-protobuf
payload: { schemaFormat: "application/vnd.google.protobuf;version=3", schema: { $ref: "./reading.binpb#Reading" } }
operations:
publishTelemetry:
action: send # l'appareil publie : écrit dans le canal
channel: { $ref: "#/channels/telemetry" }
x-flexway-upstream: plant-broker
x-flexway-device: { qos_max: 1, validate: full }
security: [{ deviceCert: [] }]| Paquet | Décision |
|---|---|
CONNECT |
Certificat vérifié, sinon mot de passe (jeton vérifié ou clé), sinon refus 0x87 ; client id réécrit, lié à l'identité - un appareil n'en évince jamais un autre ; testament jugé comme un PUBLISH |
PUBLISH |
Topic -> canal déclaré, paramètres liés égaux aux valeurs vérifiées (site = tenant, device = CN), portée, quota, schéma si validate: full ; refus PUBACK 0x87, 0x97 (quota), 0x99 (format) en MQTT 5, fermeture en 3.1.1 |
SUBSCRIBE |
Chaque filtre, jokers compris, doit tenir dans un canal déclaré une fois les paramètres liés posés ; sinon SUBACK 0x87 |
exp du jeton |
DISCONNECT 0xA0 (MQTT 5) ; l'appareil revient avec un jeton neuf |
Prévu en 1.7
Sparkplug B
Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui (
listeners.mqtt,x-flexway-bind: "cert:cn",x-flexway-sparkplug).
flexway.yaml est celui de 12.29.6. Le canal Sparkplug B 3.0 se déclare d'un mot : l'espace de noms
spBv1.0/... et le descripteur sparkplug_b.proto sont embarqués.
# api/asyncapi.yaml - extrait
channels:
sparkplug:
address: "spBv1.0/{group}/{type}/{edge_node}"
x-flexway-sparkplug: true
parameters:
group: { x-flexway-bind: tenant } # le groupe Sparkplug est le tenant
edge_node: { x-flexway-bind: "cert:cn" } # un nœud ne publie que sous son nom| Élément | Traitement |
|---|---|
| Naissance et mort | NBIRTH/DBIRTH -> en ligne, NDEATH (testament)/DDEATH -> hors ligne, par tenant ; bdSeq vérifié ; vus dans la console |
| Séquences | seq 0-255 vérifié par nœud, écart compté (flexway.sparkplug.sequence.gaps), jamais bloquant |
| Commandes | NCMD/DCMD publiés par une application (porte HTTP x-flexway-publish ou MQTT) sous une portée d'écriture déclarée |
Prévu en 1.7
OPC UA PubSub JSON sur MQTT
Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui (
listeners.mqtt,x-flexway-device).
Aucune extension propre : un éditeur OPC UA PubSub qui publie en JSON sur MQTT est un appareil de
Appareils MQTT, sous un contrat ordinaire - un canal, son schéma JSON, ses liaisons. Le préfixe
plant/{site}/ (configuré dans l'éditeur) lie le tenant ; la suite reprend la structure de topic
recommandée par OPC UA (partie 14).
# api/asyncapi.yaml - extrait
channels:
opcua-data:
address: "plant/{site}/opcua/json/data/{publisher}/{writer_group}"
parameters:
site: { x-flexway-bind: tenant }
publisher: { x-flexway-bind: "cert:cn" } # l'éditeur publie sous son propre nom
messages:
network-message:
contentType: application/json
payload: { $ref: "./opcua-network-message.json" } # le NetworkMessage JSON, validé
operations:
publishOpcuaData:
action: send
channel: { $ref: "#/channels/opcua-data" }
x-flexway-upstream: plant-broker
x-flexway-device: { qos_max: 1, validate: full }
security: [{ deviceCert: [] }]Prévu en 1.8
Plans lus d'une claim, plan par défaut
Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (
plans,plan_claim).
Vendre son API par plans : le plan d'un appelant vient d'une claim vérifiée de son jeton, ou de
l'attribut plan de sa clé d'API ou de son certificat ; ses limites sont déclarées ici. Un appelant
sans plan, ou dont le plan n'est pas déclaré, reçoit le plan par défaut - jamais « illimité ».
# flexway.yaml
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://orders.internal:8080
server:
counters: { store: "${COUNTERS_URL}" } # exigé : refus au chargement sans magasin partagé
auth:
issuers: # la clé existante est auth.issuers
- name: corporate
discovery: https://id.example.com/.well-known/openid-configuration
audience: orders-api
tenant_claim: org
plan_claim: subscription.plan # chemin pointé d'une claim du jeton vérifié
plans:
default: free # obligatoire dès que plans: existe
count_per: tenant # tenant (défaut) | subject
definitions:
free:
requests: { day: 1000, month: 20000 }
tokens: { day: 50000, month: 1000000 }
eur: { month: 5 }
pro:
requests: { day: 100000, month: 2000000 }
tokens: { month: 50000000 }
eur: { month: 500 }# api/openapi.yaml - extrait : rien de propre aux plans, le contrat dit seulement qui entre
paths:
/orders:
get:
operationId: listOrders
security: [ { corporate: [orders:read] } ]| Règle | Comportement |
|---|---|
| Plan inconnu | Le plan default, une ligne de journal plan_unknown et le compteur flexway.plans.unknown - l'appelant est authentifié, l'écart vient de son fournisseur |
| Avec les quotas du tenant (Poser des quotas) | Les deux s'appliquent, le premier atteint refuse : 429 quota_exceeded, membre limit: plan |
| Clé d'API, certificat | Attribut plan de l'entrée du magasin (écrit par flexway api-key) ou de auth.mtls.clients[] |
Prévu en 1.8
Budgets d'inférence par utilisateur et par cas d'usage
Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (
ai.budget.per_subject,ai.use_cases,ai.aliases,use_case_claim,x-flexway-use-case).
En plus du budget du tenant (Servir l'inférence), un budget par utilisateur (le sujet vérifié) et par cas
d'usage. Le cas d'usage vient du contrat (x-flexway-use-case), d'un alias de modèle de
/v1/*, ou de la claim d'un client de confiance - jamais d'un en-tête de l'appelant. Le budget le
plus restrictif gagne.
# flexway.yaml
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://tickets.internal:8080
auth:
issuers:
- name: corporate
discovery: https://id.example.com/.well-known/openid-configuration
audience: ai
tenant_claim: org
use_case_claim: ai_use_case # crue seulement pour les clients de trusted_clients
trusted_clients: [batch-reviewer]
ai:
security: [ { oidc: { scopes: [inference] } } ]
default_backend: hosted
backends:
hosted:
url: https://api.provider.example/v1
api_key: "file:/etc/flexway/secrets/provider-key"
default_model: gpt-4.1-mini
external: true
price: { input_per_mtok: 0.40, output_per_mtok: 1.60 }
budget:
store: "${COUNTERS_URL}"
monthly_eur: 2000 # tenant, inchangé
per_subject: { monthly_eur: 20, daily_tokens: 200000 } # chaque sujet vérifié
use_cases:
support-bot: { monthly_eur: 300 }
code-review: { monthly_eur: 800, daily_tokens: 5000000 }
aliases: # un nom de modèle de /v1/* qui porte un cas d'usage
support-assistant: { backend: hosted, model: gpt-4.1-mini, use_case: support-bot }# api/openapi.yaml - extrait : une opération (ou une méthode gRPC mesurée) porte son cas d'usage
paths:
/tickets/{id}/summary:
post:
operationId: summarizeTicket
x-flexway-use-case: support-bot # un cas d'usage absent de ai.use_cases : refus au chargement| Règle | Comportement |
|---|---|
| Appel sans sujet | Pas de budget utilisateur ; celui du tenant s'applique |
| Contrat et claim | Le contrat l'emporte : un client de confiance ne déplace pas une opération marquée vers un autre budget |
| Refus | 429 budget_exceeded, membre limit : tenant, plan, subject ou use_case (enveloppe OpenAI sur /v1/*) |
| Plafond souple | Celui du tenant seul ; un budget d'utilisateur ou de cas d'usage est un plafond dur |
| Consommation par utilisateur | GET /usage/subjects (administration), sujet haché ; jamais une dimension de métrique |
Prévu en 1.8
Webhook entrant Stripe et rotation du secret
Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (
webhooks,x-flexway-webhook, schéma lu dansStripe-Signature).
Recevoir les webhooks d'un fournisseur signés : la signature est l'identité de l'appelant, une
alternative de security au contrat. Un préréglage suffit pour Stripe, GitHub, Slack, Shopify et
SendGrid.
# flexway.yaml - les secrets par référence, jamais en clair
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://billing.internal:8080
webhooks:
secrets:
stripe-endpoint:
current: file:/secrets/stripe/current
previous: file:/secrets/stripe/previous # rotation : les deux vérifient
tenant: acme # lié comme une clé d'API
replay: { max_ids: 100000 } # identifiants vus, par réplique, bornés# api/openapi.yaml - extrait
components:
securitySchemes:
stripe:
type: apiKey
in: header
name: Stripe-Signature
x-flexway-webhook: { preset: stripe, secret: stripe-endpoint }
paths:
/hooks/stripe:
post:
operationId: stripeEvents
security: [{ stripe: [] }]Rotation du secret, sans perdre un évènement :
| Étape | Action |
|---|---|
| 1 | L'ancien secret passe dans previous, le nouveau dans current (deux clés du Secret monté) ; rechargement à chaud (Changer la configuration sans redémarrer) - les deux vérifient |
| 2 | Le fournisseur bascule sur le nouveau secret ; une signature contre l'un ou l'autre suffit |
| 3 | Une fois la bascule faite, previous est retiré |
| Refus | Réponse |
|---|---|
| En-tête absent ou illisible, signature fausse, horodatage hors tolérance, identifiant déjà vu | 401 webhook_signature_invalid, sans dire lequel ; la cause va au journal et à flexway.webhook.verifications{outcome} |
| Corps au-delà de la borne | 413, avant toute vérification |
Le corps est lu en entier et borné avant la vérification, puis relayé octet pour octet ; les en-têtes de signature restent (l'amont peut revérifier), le secret ne sort jamais.
Prévu en 1.8
Webhook générique HMAC
Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (
webhooks,x-flexway-webhook, schéma lu dansX-Signature).
Un partenaire sans préréglage : le schéma HMAC se déclare en entier, à gabarit fermé.
# flexway.yaml
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://partners.internal:8080
webhooks:
secrets:
partner-hooks:
current: file:/secrets/partner/current
tenant: partner-a
replay: { max_ids: 100000 }# api/openapi.yaml - extrait
components:
securitySchemes:
partner:
type: apiKey
in: header
name: X-Signature
x-flexway-webhook:
secret: partner-hooks
algorithm: sha256 # sha256 | sha512 ; sha1 refusé
signature: { header: X-Signature, prefix: "sha256=", encoding: hex }
timestamp: { header: X-Timestamp, format: unix }
id: { header: X-Delivery-Id }
signed: "{timestamp}.{body}" # gabarit fermé : {id}, {timestamp}, {body}
tolerance: 5m
paths:
/hooks/partner:
post:
operationId: partnerEvents
security: [{ partner: [] }]Prévu en 1.8
Standard Webhooks, mémoire de rejeu partagée
Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (
webhooks,x-flexway-webhook, schéma lu danswebhook-signature).
Un émetteur conforme à Standard Webhooks, le format des
webhooks sortants de Flexway (Publier et consommer sur un courtier) : HMAC-SHA256 (v1) ou Ed25519 (v1a), identifiant et horodatage
signés. Avec plusieurs répliques, la mémoire des identifiants vus peut être partagée (option
déclarée) : sans elle, un rejeu sur une autre réplique dans la fenêtre de tolérance passe.
# flexway.yaml
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://events.internal:8080
server:
counters: { store: "${COUNTERS_URL}" } # la mémoire partagée vit dans le magasin des compteurs
webhooks:
secrets:
platform-events:
current: file:/secrets/platform/current # whsec_<base64>
previous: file:/secrets/platform/previous
tenant: acme
replay: { max_ids: 100000, shared: true } # délai, et refus si le magasin ne répond pas# api/openapi.yaml - extrait
components:
securitySchemes:
platform:
type: apiKey
in: header
name: webhook-signature
x-flexway-webhook: { preset: standard-webhooks, secret: platform-events }
paths:
/hooks/platform:
post:
operationId: platformEvents
security: [{ platform: [] }]| Préréglage | Rejeu |
|---|---|
standard-webhooks, stripe, slack, sendgrid |
Horodatage signé : tolérance et identifiants vus |
github, shopify |
Identifiant non signé : protection partielle, dite par /config |
Prévu en 1.8
Listes d'adresses : liste noire globale et ip-filter
Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (
server.ip_deny,server.proxy_protocol). Seule, une politiqueip-filterest acceptée avec un avertissement et n'est pas appliquée.
Deux portes : server.ip_deny, liste noire globale jugée avant le routage (un flux de menaces ne
coûte ni routage ni cryptographie) ; la politique ip-filter, attachée à un hôte, une route ou une
opération, jugée après le routage et avant CORS et l'identité.
# flexway.yaml
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://shop.internal:8080
server:
trusted_proxies: [10.0.0.0/8] # X-Forwarded-For cru de ces pairs seulement (existe déjà)
proxy_protocol: { from: [10.0.5.0/24] } # v1 et v2, seulement de ces pairs ; lu au démarrage
ip_deny:
inline: [203.0.113.0/24]
files: [/lists/threat-feed.txt] # une plage par ligne, # commentaire ; relu au rechargement
policies:
definitions:
office-only: { type: ip-filter, config: { allow: [198.51.100.0/24, "2001:db8::/32"] } }
stripe-ranges: { type: ip-filter, config: { allow_files: [/lists/stripe-ips.txt] } }
chains:
hosts: { admin.example.com: [office-only] }# api/openapi.yaml - extrait : le niveau opération de la chaîne, au contrat comme toute politique
paths:
/hooks/stripe:
post:
operationId: stripeEvents
x-flexway-policies: [stripe-ranges] # les plages publiées par le fournisseur| Règle | Comportement |
|---|---|
| Adresse cliente | Le pair TCP ; derrière un pair de trusted_proxies, X-Forwarded-For lu de droite à gauche jusqu'au premier saut non fiable ; PROXY protocol d'un autre pair : connexion fermée |
ip-filter |
allow : l'adresse doit y être ; deny : refusée si elle y est ; deny gagne ; la plus précise des chaînes gagne, !office-only la retire |
| Route inconnue | La chaîne de l'hôte est jugée avant 404 ou 405 : un refusé reçoit 403 et ne cartographie pas les routes |
| Refus | 403 address_denied, avant toute cryptographie ; flexway.ip.refused{level} |
| Fichier illisible au rechargement | La configuration précédente reste - jamais une liste vide substituée |
Prévu en 1.8
Compression négociée
Cible de la version 1.8.0 - pas encore construit : aujourd'hui, la politique
compressest acceptée avec un avertissement (a later version - NOT applied) et n'est pas appliquée.
gzip, brotli et zstd, négociés par Accept-Encoding ; opt-in par une politique - sans elle, rien ne
change.
# flexway.yaml
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://catalog.internal:8080
policies:
definitions:
compress-json:
type: compress
config:
encodings: [zstd, br, gzip] # préférence du serveur à qualité égale
min_bytes: 1024 # défaut fixé au banc M1
types: [application/json, "application/*+json", "text/*"] # liste blanche
chains:
global: [compress-json]# api/openapi.yaml - extrait : rien à déclarer ; un flux n'est jamais compressé
paths:
/products:
get:
operationId: listProducts| Règle | Comportement |
|---|---|
| Éligible | Longueur connue au moins min_bytes, type de la liste blanche, pas de Content-Encoding de l'amont, méthode autre que HEAD ; un flux (SSE, longueur inconnue, 101) jamais |
| En-têtes | Content-Encoding, Content-Length recalculé, Vary: Accept-Encoding sur toute réponse éligible, ETag fort rendu faible |
| Niveaux | Fixés au banc, une constante par algorithme : aucune clé |
| Cache (Mettre une lecture en cache) | Une entrée par encodage choisi, jamais par valeur brute d'Accept-Encoding |
| BREACH | Une réponse qui mêle un secret et une entrée reflétée : l'exclure par une condition when |
Prévu en 1.8
Coalescence des lectures en vol
Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (
x-flexway-coalesce).
Une rafale de lectures identiques pendant qu'une première est en vol : un seul appel amont, la
réponse copiée à chaque attendant. Opt-in par opération, au contrat ; la clé est celle du cache -
tenant et sujet pour une opération qui exige une identité. Chaque attendant a été admis et
compté : la coalescence épargne l'amont, jamais le quota. flexway.yaml n'a rien de nouveau.
# flexway.yaml - rien de propre à la coalescence
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://catalog.internal:8080# api/openapi.yaml - extrait
paths:
/catalog/products:
get:
operationId: listProducts
x-flexway-cache: { ttl: 30s } # facultatif : le cache d'abord, la coalescence sur ses absences
x-flexway-coalesce: { max_waiters: 256, max_bytes: 1MiB }| Règle | Comportement |
|---|---|
| Refus au chargement | Méthode autre que GET ou HEAD, flux, WebSocket, méthode gRPC, inférence |
| Non partageable | Set-Cookie, longueur inconnue, au-delà de max_bytes : chaque attendant appelle l'amont |
Au-delà de max_waiters |
L'appel suit son chemin seul (flexway.coalesce.requests{role="overflow"}) |
| Erreur de l'amont | Partagée comme une réponse ; un délai dépassé du meneur ne l'est pas |