# Poser des quotas

> Objectif : limiter par appelant, pas par adresse du pod qui relaie, et qu'un amont lent ou mort ne devienne pas une panne générale.

Guide - Depuis la version 0.1.0 - Flexway v1.6.3 - https://flexway.io/docs/operations-quotas/

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

**Objectif** : limiter par **appelant**, pas par adresse du pod qui relaie, et qu'un amont lent ou
mort ne devienne pas une panne générale.

**Commande** - une politique `rate-limit`, attachée globalement ou par hôte :

```yaml
policies:
  definitions:
    per-caller:
      type: rate-limit
      config:
        key: caller                  # le sujet du jeton, sinon l'adresse du client
        classes:
          read:  { rps: 50, burst: 100 }   # GET, HEAD, OPTIONS
          write: { rps: 10, burst: 20 }    # tout le reste
  chains:
    global: [per-caller]
server:
  trusted_proxies: [10.0.0.0/8]      # le bord : son X-Forwarded-For est cru, celui des autres non
```

> **Derrière un load balancer cloud, le bord peut ne pas voir le client non plus.** Un load balancer de
> couche 4 se connecte au bord depuis sa propre adresse privée : toutes les requêtes arrivent alors au
> bord - puis à la passerelle - d'une seule adresse, et tous les anonymes partagent un seau. Activez le
> proxy protocol aux deux bouts : le load balancer l'envoie (annotation du Service propre au
> fournisseur, par exemple `service.beta.kubernetes.io/scw-loadbalancer-proxy-protocol-v2: "*"` chez
> Scaleway ou `service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"` chez AWS) et les
> listeners HTTPS du bord l'exigent, ce qui empêche aussi un client de choisir son adresse. Avec Envoy
> Gateway :
>
> ```yaml
> apiVersion: gateway.envoyproxy.io/v1alpha1
> kind: ClientTrafficPolicy
> metadata: { name: public-client, namespace: envoy-gateway-system }
> spec:
>   targetRefs:
>     - { group: gateway.networking.k8s.io, kind: Gateway, name: public, sectionName: https }
>   proxyProtocol: {}                  # le load balancer l'envoie ; une connexion sans lui est refusée
> ```
>
> Les deux réglages vont ensemble : l'un sans l'autre coupe le trafic. Vérifiez le résultat dans le
> journal d'accès du bord - `x-forwarded-for` doit montrer des adresses publiques, pas celle du load
> balancer - et gardez `server.trusted_proxies` sur la seule plage du bord.

Une opération change de classe dans le contrat - la classe doit exister dans la politique, sinon
la passerelle refuse de démarrer :

```yaml
      x-flexway-rate-class: write
```

**Un seau par opération** (`1.0.0`) : par défaut (`scope: class`), un appelant a **un** seau
par classe, que toutes les opérations de la classe vident - un parcours qui enchaîne trois écritures
dans la seconde consomme trois jetons du même seau. `scope: operation` donne à chaque opération son
propre seau par appelant, la classe en fixant la taille - ce que comptent la plupart des
passerelles. Une classe peut surcharger la portée de la politique ; les compteurs partagés
(`server.counters`) suivent sans réglage de plus.

```yaml
      config:
        scope: operation                  # défaut : class
        classes:
          read:  { rps: 50, burst: 100 }
          write: { rps: 10, burst: 20 }
          sensitive: { rps: 1, burst: 3, scope: class }   # un seau pour toutes les opérations sensibles
```

**Derrière un client serveur** (`1.0.0`) : un BFF relaie des anonymes, tous depuis son adresse.
La clé `header` compte chacun à part, d'après un en-tête que le BFF pose - **cru seulement si le
pair a prouvé une identité de `trusted`**. Sinon l'en-tête est ignoré et l'adresse reste la clé :
une clé qu'un appelant choisit n'en est pas une. Un sujet authentifié reste toujours la clé.

```yaml
policies:
  definitions:
    per-user:
      type: rate-limit
      config:
        key: header
        header: X-Client-Key
        trusted: [web-bff.web.serviceaccount.identity.linkerd.cluster.local]
        classes: { read: { rps: 20, burst: 40 } }
server:
  trusted_proxies: [local]                  # le proxy du maillage : les adresses propres du pod
  peer_identity_header: l5d-client-id       # l'identité mTLS qu'il a vérifiée
```

> **Pas `127.0.0.1/32`** : le proxy du maillage se connecte depuis l'IP du pod, pas depuis la boucle
> locale (mesuré avec Linkerd). `local` désigne les adresses propres du pod, résolues au chargement -
> jamais un CIDR de pods, qui croirait chaque pod. Derrière un maillage, comptez les anonymes par
> identité de pair comme ci-dessus, jamais par adresse : le maillage ne nettoie pas `X-Forwarded-For`.

L'identité du pair vient du certificat vérifié par l'écouteur TLS de la passerelle, sinon - d'un pair
de `server.trusted_proxies` seulement - de `server.peer_identity_header` s'il est déclaré (et alors
de lui seul), sinon de `x-forwarded-client-cert`. **Précondition** de `peer_identity_header` : le proxy local **écrase**
cet en-tête quand un appelant l'envoie ; sans cette garantie, n'importe quel pod forge la clé.

**Validation et erreurs** (`1.0.0`) : la passerelle valide paramètres et corps (`full`, défaut).
Un service qui reste l'autorité de son corps demande `params` - le corps est relayé **sans être
lu**, ni validé ni borné par `max_body` - ou `off` - rien n'est validé. Aucun mode n'ouvre la liste
blanche : un paramètre non déclaré reste refusé. Le `type` des erreurs pointe vers votre portail :

```yaml
hosts:
  api.example.com: { default_upstream: orders, validation: params }   # tout l'hôte
errors:
  type_base: https://docs.example.com/errors/    # type = <base><code>
  field_codes: { minLength: too_short, minimum: out_of_range }   # errors[].code dans votre vocabulaire
```

Chaque entrée de `errors` porte `field`, `rule` (pour un humain) et `code`, ce sur quoi un
client aiguille : le mot-clé JSON Schema enfreint (`required`, `type`, `enum`, `minLength`,
`maximum`, `additionalProperties`...), ou `invalid` quand aucun schéma n'a pu être appliqué (corps
illisible). `errors.field_codes` renomme un mot-clé vers le jeton de **votre** contrat ; un mot-clé
absent de la table garde son nom. Une propriété manquante ou en trop est nommée comme champ
(`body.age`), et chaque règle enfreinte a son entrée.

```yaml
      x-flexway-validation: full     # dans le contrat : l'opération l'emporte sur l'hôte
```

Par amont, le cloisonnement et le coupe-circuit (actif par défaut, valeurs montrées = défauts) :

```yaml
upstreams:
  orders:
    url: http://orders:8080
    max_in_flight: 200              # sans plafond si absent
    circuit_breaker: { failure_ratio: 0.5, min_requests: 20, window: 10s, cooldown: 5s }
```

**Vérif** :

| Situation | Réponse |
| --- | --- |
| Seau vide | `429`, `Retry-After`, `RateLimit-Remaining: 0` - deux utilisateurs derrière un même client serveur ont des seaux **distincts** |
| Toute réponse limitée | `RateLimit-Limit`, `-Remaining`, `-Reset` |
| Corps ou paramètre hors du schéma | `422`, l'extension `errors` nomme chaque champ et son `code` - en `params`, le corps n'est plus examiné |
| Méthode non déclarée sur un chemin déclaré, `QUERY` ou jeton d'extension compris | `405` avec `Allow` |
| Corps au-delà de `server.limits.max_body` (1 Mio) | `413` |
| Amont en panne, circuit ouvert | `503` **immédiat**, `Retry-After`, `Flexway-Degraded: circuit-open` |
| Amont saturé | `503` immédiat, `Flexway-Degraded: upstream-saturated` |
| Quota du tenant dépensé | `429` `quota_exceeded`, `Retry-After` jusqu'à minuit ou la fin du mois (UTC) |
| Passerelle saturée | `503` `overloaded` sur le délestable d'abord, `Flexway-Degraded: shedding` ; `critical` jamais refusé |

**Partager entre répliques, vendre des forfaits, délester** (`1.0.0`) - sans `server.counters`, les
compteurs sont **locaux à chaque réplique** : trois répliques donnent jusqu'à trois fois le débit.
Avec, chaque réplique décide seule et se réconcilie avec le magasin toutes les `sync` - aucun
aller-retour sur le chemin, un plafond tenu à une fenêtre `sync` près :

```yaml
server:
  counters: { store: "${COUNTERS_URL}", sync: 1s }   # Redis, Dragonfly ou Valkey ; non rechargeable
  shedding: { target: 20ms }                         # facultatif ; non rechargeable
  in_flight: { max: 2048, queue: 0 }                 # facultatif - dérivé de la limite mémoire du conteneur s'il est absent ; non rechargeable
policies:
  definitions:
    plans:
      type: quota                                    # par tenant ; exige server.counters
      config: { limits: { day: 100000, month: 2000000 } }
  chains:
    global: [per-caller, plans]
```

```yaml
      x-flexway-priority: sheddable     # dans le contrat : critical | normal (défaut) | sheddable
```

Magasin perdu : la passerelle sert, en compteurs locaux, et le dit (`flexway_counters_store_up` à 0,
une ligne `WARN`). Le délestage suit la latence d'ordonnancement du runtime Go - la saturation de la
passerelle elle-même, pas la lenteur d'un amont.
