Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ac60c54fa | ||
|
|
d2202414c0 | ||
|
|
f9adc2ec23 | ||
|
|
49763852d8 | ||
|
|
f53c3adb7d | ||
|
|
7d13a8d764 | ||
|
|
1dd905dc5f | ||
|
|
2cb19809c6 | ||
|
|
9de9a9663b | ||
|
|
2bdc486ae6 | ||
|
|
901aa9a9dc | ||
|
|
e8ab19962b | ||
|
|
5c7dce96e1 |
@@ -2,12 +2,15 @@
|
|||||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||||
name: Crowdsec
|
name: Crowdsec
|
||||||
|
|
||||||
on: #[push,pull_request]
|
# À LA DEMANDE, et seulement à la demande — comme minio.yaml : auth Vault par
|
||||||
|
# flux OIDC (un humain doit ouvrir un lien) et apply `auto_approve` contre la prod.
|
||||||
|
#
|
||||||
|
# Note : les triggers `push`/`pull_request` retirés ici étaient de toute façon
|
||||||
|
# INERTES — ils passaient par une ancre YAML (`&`/`*`), que le parseur
|
||||||
|
# d'événements de Gitea ne résout pas (vécu sur arcodange/kadans, issues 113
|
||||||
|
# → 117). Ce workflow ne partait déjà qu'à la main ; c'est maintenant écrit.
|
||||||
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
push: &crowdsecPaths
|
|
||||||
paths:
|
|
||||||
- 'crowdsec/**/*.tf'
|
|
||||||
pull_request: *crowdsecPaths
|
|
||||||
|
|
||||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -2,14 +2,32 @@
|
|||||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||||
name: Helm Charts
|
name: Helm Charts
|
||||||
|
|
||||||
on: [push,pull_request,workflow_dispatch]
|
# Celui-ci travaille SEUL (pas d'auth Vault, pas d'apply) : on le garde
|
||||||
# push: &helmPaths # turns out gitea don't handle well the paths filter
|
# automatique. Mais `push` sur TOUTES les branches + `pull_request` faisait
|
||||||
# paths:
|
# partir DEUX runs pour le même commit dès qu'une branche avait une PR.
|
||||||
# - '*/\.yaml'
|
#
|
||||||
# - '*/\.tpl'
|
# Même forme que la CI de kadans : la branche est couverte par `pull_request`,
|
||||||
# - '*/NOTES.txt'
|
# `main` par le `push` d'après-merge. Un run par événement, aucun angle mort.
|
||||||
# - '*/\.helmignore'
|
#
|
||||||
# pull_request: *helmPaths
|
# (Le filtre de chemins d'origine, resté en commentaire des années sous un
|
||||||
|
# « gitea don't handle well the paths filter », n'était probablement pas en
|
||||||
|
# cause : il passait par une ancre YAML, et le parseur d'événements de Gitea ne
|
||||||
|
# les résout pas — issues 113 → 117 de kadans. Le job `filter-chart` fait déjà
|
||||||
|
# ce tri au niveau job, donc on n'y retouche pas.)
|
||||||
|
#
|
||||||
|
# ⚠ Chaque clé porte un CORPS explicite : un `pull_request:` nu (valeur nulle)
|
||||||
|
# n'est pas une forme éprouvée sur ce Gitea, et son mode d'échec est le
|
||||||
|
# silencieux — aucun run, aucune erreur. On copie la forme qui tourne (kadans
|
||||||
|
# ci.yml), listes dupliquées à la main, sans ancre.
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
|
||||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
+11
-12
@@ -1,20 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: MinIO
|
name: MinIO
|
||||||
|
|
||||||
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`).
|
# À LA DEMANDE, et seulement à la demande. Deux raisons, chacune suffisante :
|
||||||
# Une ancre dans un trigger Gitea Actions fait taire push ET pull_request —
|
#
|
||||||
# en silence, aucun run, aucune erreur (vécu sur arcodange/kadans, issues 113
|
# 1. Ce workflow ne PEUT PAS aboutir sans un humain : l'auth Vault passe par
|
||||||
# → 117). Les autres workflows de ce repo utilisent encore des ancres : à
|
# un flux OIDC dont le lien doit être ouvert dans un navigateur connecté.
|
||||||
# vérifier séparément, c'est probablement pour ça qu'ils ne partent qu'à la
|
# Déclenché tout seul, il occupe un runner jusqu'à son timeout — et retarde
|
||||||
# main (workflow_dispatch).
|
# les runs que quelqu'un attend vraiment.
|
||||||
|
# 2. Il fait `terraform apply` en `auto_approve` CONTRE LA PROD. Se déclencher
|
||||||
|
# sur le push d'une branche, c'est appliquer du code que personne n'a relu.
|
||||||
|
#
|
||||||
|
# Au passage : `push` (toutes branches) + `pull_request` faisait partir DEUX runs
|
||||||
|
# par commit d'une branche en PR — le même SHA, deux fois.
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- 'minio/**/*.tf'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'minio/**/*.tf'
|
|
||||||
|
|
||||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||||
name: Plausible
|
name: Plausible
|
||||||
|
|
||||||
on: #[push,pull_request]
|
# À LA DEMANDE, et seulement à la demande — comme minio.yaml : auth Vault par
|
||||||
|
# flux OIDC (un humain doit ouvrir un lien) et apply `auto_approve` contre la prod.
|
||||||
|
#
|
||||||
|
# Note : les triggers `push`/`pull_request` retirés ici étaient de toute façon
|
||||||
|
# INERTES (ancre YAML non résolue par Gitea, issues 113 → 117 de kadans). Ce
|
||||||
|
# workflow ne partait déjà qu'à la main ; c'est maintenant écrit.
|
||||||
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
push: &plausiblePaths
|
|
||||||
paths:
|
|
||||||
- 'plausible/**/*.tf'
|
|
||||||
pull_request: *plausiblePaths
|
|
||||||
|
|
||||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
+10
-15
@@ -2,23 +2,18 @@
|
|||||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||||
name: Hashicorp Vault
|
name: Hashicorp Vault
|
||||||
|
|
||||||
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`) : une ancre
|
# À LA DEMANDE, et seulement à la demande — comme minio.yaml, et pour les mêmes
|
||||||
# dans un trigger Gitea Actions fait taire push ET pull_request, en silence
|
# deux raisons : l'auth Vault exige qu'un humain ouvre un lien OIDC (sans lui, le
|
||||||
# (vécu sur arcodange/kadans, issues 113 → 117) — c'est probablement pourquoi
|
# run squatte un runner jusqu'au timeout), et l'apply se fait en `auto_approve`
|
||||||
# ce workflow ne partait qu'à la main.
|
# contre la prod.
|
||||||
# Et `*.tfvars` compte AUTANT que `*.tf` : la liste des applications (donc les
|
#
|
||||||
# rôles gitea_cicd_<app>) vit dans terraform.tfvars — l'oublier, c'est ajouter
|
# ⚠ Ce qui change AUSSI de nature : `hashicorp-vault/**/*.tfvars` compte autant
|
||||||
# une app sans jamais créer son rôle.
|
# que `*.tf` — la liste des applications (donc les rôles gitea_cicd_<app>) vit
|
||||||
|
# dans terraform.tfvars. Ce n'est plus un filtre de chemins mais ça reste vrai
|
||||||
|
# du POURQUOI on relance : ajouter une app au tfvars sans relancer ce workflow,
|
||||||
|
# c'est une app sans rôle CI.
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- 'hashicorp-vault/**/*.tf'
|
|
||||||
- 'hashicorp-vault/**/*.tfvars'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'hashicorp-vault/**/*.tf'
|
|
||||||
- 'hashicorp-vault/**/*.tfvars'
|
|
||||||
|
|
||||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ pour chaque dossier de premier niveau contenant un fichier Chart.yaml (sauf les
|
|||||||
le pousser dans le registre helm de gitea
|
le pousser dans le registre helm de gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Réseau
|
||||||
|
|
||||||
|
- [Ce que Traefik voit du client](doc/ce-que-traefik-voit.md) — pourquoi
|
||||||
|
`ClientIP()` ne voit que le tunnel sur les hôtes `.fr`, pourquoi `localIp@file`
|
||||||
|
n'a rien à y faire, et comment obtenir « pas de mot de passe depuis la maison »
|
||||||
|
sans créer un contournement d'authentification.
|
||||||
|
|
||||||
## pgbouncer
|
## pgbouncer
|
||||||
|
|
||||||
## prometheus
|
## prometheus
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Ce que Traefik voit du client — et pourquoi « pas de mot de passe à la maison » ne se règle pas dans Traefik
|
||||||
|
|
||||||
|
Mesuré le 2026-07-28 sur le homelab (`kubectl --context=default`, k3s pi1/pi2/pi3).
|
||||||
|
Ce document existe parce que la question « peut-on sauter le basic auth quand on est
|
||||||
|
sur le Wi-Fi de la maison ? » a une réponse contre-intuitive, et que la réponse
|
||||||
|
intuitive est un **trou de sécurité**.
|
||||||
|
|
||||||
|
## 1. Le chemin réel d'une requête `.fr`
|
||||||
|
|
||||||
|
```
|
||||||
|
navigateur (maison)
|
||||||
|
└─ DNS : kadans.arcodange.fr → 172.67.130.8 / 104.21.3.16 (Cloudflare, PAS le LAN)
|
||||||
|
└─ edge Cloudflare (proxy orange)
|
||||||
|
└─ tunnel cloudflared (sortant, pod du cluster, CIDR 10.42.0.0/16)
|
||||||
|
└─ traefik.kube-system.svc:80 (entrypoint `web`, pas de TLS ici)
|
||||||
|
└─ crowdsec → basic auth → service
|
||||||
|
```
|
||||||
|
|
||||||
|
Le tunnel est **sortant** : il n'y a aucune redirection de port, aucun chemin
|
||||||
|
depuis Internet vers Traefik autre que Cloudflare. Le nom `.fr` est un CNAME
|
||||||
|
proxifié vers `<tunnel-id>.cfargotunnel.com` (`cms/cloudflare`, module
|
||||||
|
`cloudflared_tunnel`, mapping unique `*.arcodange.fr`).
|
||||||
|
|
||||||
|
**Conséquence n°1 : même depuis le salon, la requête sort de la maison, fait un
|
||||||
|
aller-retour par Cloudflare, et revient par le tunnel.** L'adresse de la socket
|
||||||
|
que Traefik accepte est toujours celle d'un pod `cloudflared` en `10.42.x.x`.
|
||||||
|
|
||||||
|
Le `.lab`, lui, va droit au but : `kadans.arcodange.lab → 192.168.1.201`.
|
||||||
|
|
||||||
|
## 2. Deux notions d'« IP client », et elles ne coïncident pas
|
||||||
|
|
||||||
|
L'entrypoint `web` fait confiance aux en-têtes transmis depuis le CIDR des pods
|
||||||
|
(`ports.web.forwardedHeaders.trustedIPs: ["10.42.0.0/16"]`, playbook
|
||||||
|
`factory/.../playbooks/system/k3s_config.yml`). Traefik **sait donc** qui est le
|
||||||
|
vrai client — le journal d'accès le prouve, sur une requête émise depuis la maison :
|
||||||
|
|
||||||
|
```
|
||||||
|
2a01:cb04:dff:cf00:f9d7:4026:9ffc:b042 - kadans [28/Jul/2026:14:22:49 +0200]
|
||||||
|
"GET /?sonde=… HTTP/1.1" 200 … "kadans-kadans-public-kadans-arcodange-fr@kubernetes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Deux choses à retenir de cette ligne :
|
||||||
|
|
||||||
|
- Traefik **résout** le vrai client depuis `X-Forwarded-For` ;
|
||||||
|
- la maison sort en **IPv6** (`2a01:cb04:dff:cf00::/64`), pas en IPv4. Le
|
||||||
|
`86.238.234.54/32` que le playbook injecte dans `localIp` via `ipify` n'est
|
||||||
|
qu'**une** des deux adresses publiques du foyer, et pas celle qu'un navigateur
|
||||||
|
moderne utilise en priorité.
|
||||||
|
|
||||||
|
Mais **le matcher de routeur `ClientIP()` n'utilise PAS cette résolution.** Il
|
||||||
|
juge l'adresse de la socket. Mesuré, pas supposé — trois routeurs temporaires
|
||||||
|
posés sur `kadans.arcodange.fr`, pointant vers un Service sans endpoint (donc
|
||||||
|
`503` si la règle matche, et repli sur le routeur normal — `401` — sinon) :
|
||||||
|
|
||||||
|
| sonde | règle ajoutée à `Host(kadans.arcodange.fr)` | attendu si… | mesuré |
|
||||||
|
| ----- | -------------------------------------------- | ------------------------------ | ------ |
|
||||||
|
| T | *(témoin, aucune contrainte d'IP)* | le dispositif fonctionne → 503 | **503** |
|
||||||
|
| A | `ClientIP("2a01:cb04:dff:cf00::/64")` | `ClientIP` lit X-Forwarded-For | **401** |
|
||||||
|
| B | `ClientIP("10.42.0.0/16")` | `ClientIP` lit la socket | **503** |
|
||||||
|
|
||||||
|
Le témoin valide le montage ; A échoue, B réussit. **`ClientIP()` voit le pod
|
||||||
|
`cloudflared`, jamais le vrai client.** (Sondes supprimées après mesure.)
|
||||||
|
|
||||||
|
## 3. Les deux pièges qui en découlent
|
||||||
|
|
||||||
|
### Piège A — `localIp@file` ne doit JAMAIS toucher un routeur `.fr`
|
||||||
|
|
||||||
|
`localIp` est un `ipAllowList` dont le `sourceRange` contient `10.42.0.0/16`
|
||||||
|
(le CIDR des pods). Comme `ipAllowList` juge lui aussi l'adresse de la socket
|
||||||
|
par défaut, **l'attacher à un hôte `.fr` laisserait passer Internet entier** :
|
||||||
|
toute requête arrivant par le tunnel se présente en `10.42.x.x`, donc « locale ».
|
||||||
|
|
||||||
|
État vérifié le 2026-07-28 : aucun routeur `.fr` ne porte `localIp@file`. Le
|
||||||
|
piège est **latent**, pas ouvert. Il le reste tant que personne ne recopie
|
||||||
|
`localIp@file` depuis un values `.lab` (grafana, vault, plausible) vers un
|
||||||
|
`ingress-public.yaml`. Les deux gabarits `.fr` de ce dépôt portent un
|
||||||
|
avertissement à cet endroit précis.
|
||||||
|
|
||||||
|
### Piège B — se fier à un en-tête est un contournement d'authentification
|
||||||
|
|
||||||
|
« Sauter le basic auth si `CF-Connecting-IP` est celle de la maison » (ou si un
|
||||||
|
en-tête maison est présent) fait reposer une **exemption d'authentification** sur
|
||||||
|
une valeur que le client écrit lui-même. Cloudflare ne retire pas les en-têtes
|
||||||
|
inconnus : n'importe qui sur Internet peut les envoyer. Et Traefik reste joignable
|
||||||
|
en direct sur le LAN (`192.168.1.201:80`). **On ne prend pas cette voie.**
|
||||||
|
|
||||||
|
## 4. Pourquoi ce n'est pas réparable dans Traefik
|
||||||
|
|
||||||
|
Traefik enchaîne ses middlewares **inconditionnellement** : il n'existe pas de
|
||||||
|
« basic auth sauf si … ». Le seul moyen de sauter un middleware est de faire
|
||||||
|
matcher un **autre routeur** — et le seul discriminant d'IP au niveau routeur est
|
||||||
|
`ClientIP()`, qui, on vient de le mesurer, ne voit que le tunnel. Un
|
||||||
|
`ipAllowList` avec `ipStrategy.depth` verrait bien la vraie IP, mais un
|
||||||
|
`ipAllowList` **bloque** ; il ne sait pas « laisser entrer sans mot de passe ».
|
||||||
|
|
||||||
|
Faire pointer le LAN vers Traefik (DNS à double horizon, ou enregistrement
|
||||||
|
Cloudflare « DNS only ») rendrait `ClientIP(192.168.1.0/24)` honnête — mais
|
||||||
|
coûterait, en plus du DNS : un **certificat publiquement valide** pour
|
||||||
|
`kadans.arcodange.fr` sur Traefik (aujourd'hui il sert la CA interne,
|
||||||
|
`CN=arcodange.lab` ; un `curl --resolve … 192.168.1.201` échoue au TLS, code
|
||||||
|
`000`), donc un nouveau `certResolver` DNS-01 Cloudflare et le jeton qui va avec,
|
||||||
|
plus un routeur `websecure` pour un hôte qui vit aujourd'hui en `web` clair.
|
||||||
|
Beaucoup, pour du confort. Et « DNS only » retirerait en prime le bouclier
|
||||||
|
Cloudflare de cet hôte.
|
||||||
|
|
||||||
|
**La décision doit donc être prise là où la vraie IP est native et non
|
||||||
|
falsifiable : au bord, chez Cloudflare (`ip.src`).**
|
||||||
|
|
||||||
|
## 5. La voie retenue — Cloudflare tape le mot de passe à ta place
|
||||||
|
|
||||||
|
Le basic auth de `kadans.arcodange.fr` est un **garde-fou assumé**, pas un
|
||||||
|
secret : `kadans:kkadans`, et son hash bcrypt est en clair dans
|
||||||
|
`chart/values.yaml` du dépôt `kadans`. C'est ce fait qui rend la solution
|
||||||
|
simple **et** sans coût de sécurité :
|
||||||
|
|
||||||
|
> Une règle Cloudflare pré-remplit l'en-tête `Authorization` pour les requêtes
|
||||||
|
> venant du réseau de la maison. Traefik ne change pas, le mot de passe reste
|
||||||
|
> exigé partout ailleurs. Cloudflare ne fait que le **taper à ta place**.
|
||||||
|
|
||||||
|
Surface d'attaque ajoutée : **aucune**. Un attaquant qui forge cet en-tête
|
||||||
|
obtient exactement ce que `curl -u kadans:kkadans` lui donne déjà aujourd'hui.
|
||||||
|
C'est la différence de fond avec le piège B : ici on ne crée pas d'exemption, on
|
||||||
|
automatise une saisie.
|
||||||
|
|
||||||
|
Et quand l'IP du foyer change (elle est dynamique), la règle cesse simplement de
|
||||||
|
matcher : **le navigateur redemande le mot de passe**. Dégradation douce, jamais
|
||||||
|
de panne.
|
||||||
|
|
||||||
|
### Action manuelle du fondateur (à faire une fois)
|
||||||
|
|
||||||
|
Cloudflare → zone `arcodange.fr` → **Rules** → **Transform Rules** → **Modify
|
||||||
|
Request Header** → *Create rule* :
|
||||||
|
|
||||||
|
- **Nom** : `kadans — confort LAN : Authorization pré-remplie depuis la maison`
|
||||||
|
- **When incoming requests match** (éditeur d'expression) :
|
||||||
|
|
||||||
|
```
|
||||||
|
(http.host eq "kadans.arcodange.fr" and ip.src in {86.238.234.54 2a01:cb04:dff:cf00::/64})
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Then** → *Set static* :
|
||||||
|
- Header name : `Authorization`
|
||||||
|
- Value : `Basic a2FkYW5zOmtrYWRhbnM=` *(base64 de `kadans:kkadans`)*
|
||||||
|
|
||||||
|
Équivalent Terraform, si on préfère le déclarer dans `cms/cloudflare` :
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
resource "cloudflare_ruleset" "confort_lan_kadans" {
|
||||||
|
zone_id = cloudflare_zone.arcodange_fr.id
|
||||||
|
name = "confort LAN"
|
||||||
|
kind = "zone"
|
||||||
|
phase = "http_request_late_transform"
|
||||||
|
|
||||||
|
rules = [{
|
||||||
|
expression = "(http.host eq \"kadans.arcodange.fr\" and ip.src in {86.238.234.54 2a01:cb04:dff:cf00::/64})"
|
||||||
|
description = "Depuis la maison, Cloudflare tape le garde-fou basic auth a notre place"
|
||||||
|
action = "rewrite"
|
||||||
|
action_parameters = {
|
||||||
|
headers = {
|
||||||
|
"Authorization" = { operation = "set", value = "Basic a2FkYW5zOmtrYWRhbnM=" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier que ça marche
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# depuis la maison — attendu : 200, SANS -u
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' https://kadans.arcodange.fr/
|
||||||
|
|
||||||
|
# depuis l'extérieur (partage de connexion du téléphone) — attendu : 401
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' https://kadans.arcodange.fr/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ce qu'il faudra surveiller
|
||||||
|
|
||||||
|
- **Les deux adresses du foyer sont dynamiques.** Le préfixe IPv6 relevé le
|
||||||
|
2026-07-28 est `2a01:cb04:dff:cf00::/64` et l'IPv4 `86.238.234.54` — la même
|
||||||
|
que celle qu'`ipify` injecte dans `localIp` à chaque passage du playbook
|
||||||
|
`k3s_config.yml`. S'il dérive, élargir en `/56` avant de soupçonner Cloudflare.
|
||||||
|
- ⚠ **Non vérifié en vrai** : que Cloudflare accepte `Authorization` comme
|
||||||
|
en-tête de requête modifiable (certains en-têtes sont réservés). Si l'éditeur
|
||||||
|
refuse la règle, le repli est **Cloudflare Access** avec une politique
|
||||||
|
*Bypass* sur les mêmes IP et une politique *Allow* par e-mail ailleurs — et le
|
||||||
|
basic auth de Traefik est alors retiré. Le repli n'est **pas** un en-tête
|
||||||
|
secret (piège B).
|
||||||
|
|
||||||
|
### La version encore plus simple, si le confort ne vaut pas la règle
|
||||||
|
|
||||||
|
Le mot de passe est public. Ce qu'il achète réellement, c'est « le produit en
|
||||||
|
chantier n'est pas indexé ni tombé dessus par hasard ». Un `robots.txt` +
|
||||||
|
`X-Robots-Tag: noindex` fait ce travail-là sans mot de passe du tout. Retirer le
|
||||||
|
garde-fou est une décision produit, pas une décision d'infra : elle n'est pas
|
||||||
|
prise ici, seulement nommée.
|
||||||
|
|
||||||
|
## 6. Ce qui a été vérifié et qui va bien
|
||||||
|
|
||||||
|
Le bouncer CrowdSec, lui, **juge la vraie IP** — contrairement à `ClientIP()`.
|
||||||
|
Journal Traefik sur une requête de la maison et sur des robots :
|
||||||
|
|
||||||
|
```
|
||||||
|
CrowdsecBouncerTraefikPlugin: ServeHTTP ip:2a01:cb04:dff:cf00:f9d7:4026:9ffc:b042 isTrusted:false
|
||||||
|
CrowdsecBouncerTraefikPlugin: ServeHTTP ip:144.24.58.222 isTrusted:false
|
||||||
|
```
|
||||||
|
|
||||||
|
Son `clientTrustedIPs` contient pourtant `10.42.0.0/16` : ce n'est **pas** un
|
||||||
|
trou, parce que le plugin ne compare pas cette liste à l'adresse de la socket
|
||||||
|
mais à l'IP résolue. La convention `.fr` (« crowdsec devant tout ») tient donc
|
||||||
|
ce qu'elle promet. Écrit ici pour que personne ne « corrige » cette ligne en
|
||||||
|
croyant reproduire le piège A.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Où vit quoi** (aucun de ces réglages n'est dans ce dépôt — vérifié) :
|
||||||
|
|
||||||
|
| réglage | dépôt / fichier |
|
||||||
|
| ------------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
|
| valeurs Helm Traefik, `dynamic.yaml`, `localIp` | `factory` — `ansible/arcodange/factory/playbooks/system/k3s_config.yml` |
|
||||||
|
| Middleware `crowdsec@kube-system` | `factory` — `.../playbooks/tools/roles/crowdsec/tasks/main.yml` |
|
||||||
|
| tunnel + DNS `*.arcodange.fr` | `cms` — `cloudflare/iac.tf`, `cloudflare/modules/cloudflared_tunnel/` |
|
||||||
|
| basic auth `.fr` de kadans | `kadans` — `chart/values.yaml` (`public.basicAuth`) |
|
||||||
|
| gabarits d'exposition `.fr` de ce dépôt | `grafana/templates/ingress-public.yaml`, `minio/templates/ingress-public.yaml` |
|
||||||
@@ -10,6 +10,12 @@
|
|||||||
# NB : ce chart grafana est en mode `tool.kind: SubChart`, donc les templates
|
# NB : ce chart grafana est en mode `tool.kind: SubChart`, donc les templates
|
||||||
# helm-chart*.yaml ne rendent rien ; ce fichier, lui, est rendu tel quel et
|
# helm-chart*.yaml ne rendent rien ; ce fichier, lui, est rendu tel quel et
|
||||||
# appliqué par ArgoCD (app `grafana`, destination namespace `tools`).
|
# appliqué par ArgoCD (app `grafana`, destination namespace `tools`).
|
||||||
|
#
|
||||||
|
# ⚠ NE JAMAIS ajouter `localIp@file` ici (ni sur aucun routeur `.fr`). Le trafic
|
||||||
|
# `.fr` arrive par le tunnel cloudflared, donc avec l'adresse d'un pod en
|
||||||
|
# 10.42.x.x — or `10.42.0.0/16` est dans le `sourceRange` de `localIp`, et
|
||||||
|
# `ipAllowList` juge l'adresse de la SOCKET. Le middleware « IP locale
|
||||||
|
# seulement » laisserait donc entrer Internet entier. Mesuré : doc/ce-que-traefik-voit.md §3.
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@@ -285,6 +285,8 @@ grafana: &grafana_config
|
|||||||
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
|
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
|
||||||
traefik.ingress.kubernetes.io/router.tls.domains.0.main: arcodange.lab
|
traefik.ingress.kubernetes.io/router.tls.domains.0.main: arcodange.lab
|
||||||
traefik.ingress.kubernetes.io/router.tls.domains.0.sans: grafana.arcodange.lab
|
traefik.ingress.kubernetes.io/router.tls.domains.0.sans: grafana.arcodange.lab
|
||||||
|
# ⚠ Valable parce que cet hôte est en `.lab` (le client arrive en direct sur
|
||||||
|
# 192.168.1.201). À NE PAS recopier sur un hôte `.fr` : cf. doc/ce-que-traefik-voit.md §3.
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: localIp@file
|
traefik.ingress.kubernetes.io/router.middlewares: localIp@file
|
||||||
hosts:
|
hosts:
|
||||||
- grafana.arcodange.lab
|
- grafana.arcodange.lab
|
||||||
|
|||||||
@@ -30,9 +30,36 @@ resource "vault_database_secret_backend_role" "role" {
|
|||||||
backend = local.vault_mount_postgres.path
|
backend = local.vault_mount_postgres.path
|
||||||
name = local.instance
|
name = local.instance
|
||||||
db_name = "postgres"
|
db_name = "postgres"
|
||||||
|
# ── Le rôle ÉPHÉMÈRE endosse le rôle STABLE, dès le login ───────────────────
|
||||||
|
#
|
||||||
|
# En PostgreSQL, un objet appartient au rôle qui l'a CRÉÉ. Comme chaque
|
||||||
|
# démarrage de pod obtient un rôle `v-kubernet-…` neuf, toute migration crée
|
||||||
|
# des objets que le pod SUIVANT ne peut plus lire. C'est ce qui a mis l'API
|
||||||
|
# kadans à terre une demi-journée le 2026-07-28 (« permission denied for table
|
||||||
|
# qualification_video »), et c'est ce que le CronJob `pg-fix-table-ownership`
|
||||||
|
# rattrape tous les jours à 03:00 — a posteriori, et pour les seules TABLES.
|
||||||
|
#
|
||||||
|
# `ALTER ROLE … SET ROLE` fait de l'endossement un DÉFAUT DE CONNEXION : plus
|
||||||
|
# rien à poser côté application, et ça vaut aussi pour les clients qui ne sont
|
||||||
|
# pas l'application (le `psql` d'un job, une console d'exploitation).
|
||||||
|
#
|
||||||
|
# AUCUN privilège nouveau : le `GRANT` de la ligne précédente rend déjà le
|
||||||
|
# rôle éphémère MEMBRE du rôle stable. Endosser une casquette qu'on porte
|
||||||
|
# déjà, ce n'est pas une élévation — et c'est pourquoi cette instruction ne
|
||||||
|
# peut pas échouer là où le `GRANT` réussit.
|
||||||
|
#
|
||||||
|
# MESURÉ (PostgreSQL 16, compte CREATEROLE non-superutilisateur, comme celui
|
||||||
|
# de Vault) : l'instruction passe, le login donne `session_user=v-test-1` /
|
||||||
|
# `current_role=proprio_v`, et un `ALTER ROLE … RESET role` la retire.
|
||||||
|
# Vérifié aussi qu'elle SURVIT à `RESET ALL` / `DISCARD ALL` — donc elle
|
||||||
|
# compose avec le `server_reset_query` de pgbouncer au lieu de s'y opposer.
|
||||||
|
#
|
||||||
|
# ⚠ Ne vaut que pour les identifiants créés APRÈS l'apply : les baux en cours
|
||||||
|
# gardent leur ancien comportement jusqu'à leur renouvellement.
|
||||||
creation_statements = [
|
creation_statements = [
|
||||||
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
|
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
|
||||||
"GRANT ${local.owner_role} TO \"{{name}}\";",
|
"GRANT ${local.owner_role} TO \"{{name}}\";",
|
||||||
|
"ALTER ROLE \"{{name}}\" SET ROLE ${local.owner_role};",
|
||||||
]
|
]
|
||||||
revocation_statements = [
|
revocation_statements = [
|
||||||
"REASSIGN OWNED BY \"{{name}}\" TO ${local.owner_role};", # reassign must be executed in the database where the reassgined objects are - TODO (one connection per database/app)
|
"REASSIGN OWNED BY \"{{name}}\" TO ${local.owner_role};", # reassign must be executed in the database where the reassgined objects are - TODO (one connection per database/app)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Le sous-chart vendored `vault` ne rend son PodDisruptionBudget qu'en mode HA
|
||||||
|
# (server.ha.disruptionBudget.enabled, gardé par `eq .mode "ha"` dans
|
||||||
|
# server-disruptionbudget.yaml). Ce Vault tourne en standalone (storage "file",
|
||||||
|
# un seul réplica, scellement Shamir sans auto-unseal) : le garde-fou du chart
|
||||||
|
# ne s'applique jamais ici, d'où ce PDB écrit à la main dans le chart parapluie.
|
||||||
|
#
|
||||||
|
# Il ne protège que des évictions VOLONTAIRES (kubectl drain, descheduler) — pas
|
||||||
|
# d'une panne de nœud ni d'une suppression manuelle. Avec un seul réplica et
|
||||||
|
# aucun auto-unseal, toute interruption exige un unseal humain ensuite ; ce PDB
|
||||||
|
# réduit seulement les interruptions évitables.
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: {{ .Release.Name }}-server
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: vault
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
spec:
|
||||||
|
maxUnavailable: 0
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: vault
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
component: server
|
||||||
@@ -9,6 +9,20 @@ vault: &vault_config
|
|||||||
auditStorage:
|
auditStorage:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
statefulSet:
|
||||||
|
securityContext:
|
||||||
|
pod:
|
||||||
|
# Le défaut du chart (runAsNonRoot/runAsGroup/runAsUser/fsGroup) est repris tel
|
||||||
|
# quel ici — seul fsGroupChangePolicy s'ajoute. Sans lui, chaque remount du
|
||||||
|
# volume (même sur le MÊME nœud) déclenche un chown -R complet ; mesuré à ~9 min
|
||||||
|
# sur ce PVC (kubelet event "VolumePermissionChangeInProgress"). OnRootMismatch
|
||||||
|
# saute le chown si le propriétaire racine est déjà bon.
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsGroup: 1000
|
||||||
|
runAsUser: 100
|
||||||
|
fsGroup: 1000
|
||||||
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
annotations:
|
annotations:
|
||||||
@@ -17,6 +31,8 @@ vault: &vault_config
|
|||||||
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
|
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
|
||||||
traefik.ingress.kubernetes.io/router.tls.domains.0.main: arcodange.lab
|
traefik.ingress.kubernetes.io/router.tls.domains.0.main: arcodange.lab
|
||||||
traefik.ingress.kubernetes.io/router.tls.domains.0.sans: vault.arcodange.lab
|
traefik.ingress.kubernetes.io/router.tls.domains.0.sans: vault.arcodange.lab
|
||||||
|
# ⚠ Valable parce que cet hôte est en `.lab` (le client arrive en direct sur
|
||||||
|
# 192.168.1.201). À NE PAS recopier sur un hôte `.fr` : cf. doc/ce-que-traefik-voit.md §3.
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: localIp@file
|
traefik.ingress.kubernetes.io/router.middlewares: localIp@file
|
||||||
hosts:
|
hosts:
|
||||||
- host: vault.arcodange.lab
|
- host: vault.arcodange.lab
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
# qu'elles déclarent leurs buckets sans qu'on leur confie le root.
|
# qu'elles déclarent leurs buckets sans qu'on leur confie le root.
|
||||||
# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md
|
# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md
|
||||||
#
|
#
|
||||||
# ⚠ Noms d'actions issus de la documentation MinIO, NON éprouvés contre le
|
# Noms d'actions confirmés par le premier apply réel (kadans, 2026-07-26) : le
|
||||||
# serveur : le premier apply les confirmera ou les corrigera.
|
# bloc admin passe tel quel ; côté s3 il manquait `s3:ListBucket`, que le
|
||||||
|
# provider appelle AVANT de créer un bucket pour savoir s'il existe déjà.
|
||||||
resource "minio_iam_policy" "provisioner" {
|
resource "minio_iam_policy" "provisioner" {
|
||||||
name = "provisioner"
|
name = "provisioner"
|
||||||
policy = jsonencode({
|
policy = jsonencode({
|
||||||
@@ -26,9 +27,15 @@ resource "minio_iam_policy" "provisioner" {
|
|||||||
Resource = ["arn:aws:s3:::*"]
|
Resource = ["arn:aws:s3:::*"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# s3:GetObject / s3:PutObject volontairement ABSENTS.
|
# s3:GetObject / s3:PutObject volontairement ABSENTS : le provisionneur
|
||||||
|
# ne LIT ni n'ÉCRIT aucun objet, c'est ce qui rend son partage entre
|
||||||
|
# rôles CI acceptable.
|
||||||
|
#
|
||||||
|
# `s3:ListBucket` est la seule concession : MinIO le demande pour un
|
||||||
|
# HeadBucket, et le provider teste l'existence du bucket avant de le
|
||||||
|
# créer. Il donne la vue des CLÉS d'un bucket, jamais leur contenu.
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
Action = ["s3:CreateBucket", "s3:DeleteBucket", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy"]
|
Action = ["s3:CreateBucket", "s3:DeleteBucket", "s3:ListBucket", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy"]
|
||||||
Resource = ["arn:aws:s3:::*"]
|
Resource = ["arn:aws:s3:::*"]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
# sa propre signature (SigV4), et un défi HTTP Basic casserait le PUT présigné
|
# sa propre signature (SigV4), et un défi HTTP Basic casserait le PUT présigné
|
||||||
# auquel le navigateur ne peut pas répondre.
|
# auquel le navigateur ne peut pas répondre.
|
||||||
#
|
#
|
||||||
|
# ⚠ Et PAS de `localIp@file` non plus : sur un routeur `.fr`, ce middleware
|
||||||
|
# laisserait entrer Internet entier (le tunnel se présente en 10.42.x.x, qui est
|
||||||
|
# dans son `sourceRange`). Mesuré : doc/ce-que-traefik-voit.md §3.
|
||||||
|
#
|
||||||
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
|
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
|
|||||||
+57
-1
@@ -14,7 +14,63 @@ pgbouncer: &pgbouncer_config
|
|||||||
auth_type: scram-sha-256
|
auth_type: scram-sha-256
|
||||||
auth_query: SELECT uname, phash FROM user_lookup($1)
|
auth_query: SELECT uname, phash FROM user_lookup($1)
|
||||||
ignore_startup_parameters: extra_float_digits # unsupported jdbc extra_float_digits=2 argument
|
ignore_startup_parameters: extra_float_digits # unsupported jdbc extra_float_digits=2 argument
|
||||||
server_reset_query: DEALLOCATE ALL # fix prepared statement already exist (crowdsec)
|
# Ce pgbouncer est PARTAGÉ (crowdsec, plausible, kadans, + le compte que
|
||||||
|
# Vault utilise sur la base `postgres`). Ce qu'un client laisse derrière
|
||||||
|
# lui sur une connexion serveur, le client SUIVANT en hérite : le reset
|
||||||
|
# est la SEULE barrière entre deux clients d'un même pool.
|
||||||
|
#
|
||||||
|
# `DEALLOCATE ALL` ne nettoie QUE les requêtes préparées — d'où sa mise en
|
||||||
|
# place (07e2c6d, « prepared statement already exists » de crowdsec).
|
||||||
|
# Tout le reste de l'état de session passait au suivant. MESURÉ contre un
|
||||||
|
# pgbouncer **1.23.1** — la version RÉELLEMENT déployée (image
|
||||||
|
# ghcr.io/icoretech/pgbouncer-docker:1.23.1-fixed, chart pgbouncer-2.3.1),
|
||||||
|
# montée avec CETTE configuration extraite du cluster (session,
|
||||||
|
# pool_size=1) : le client 2, qui n'avait rien demandé, héritait de
|
||||||
|
# `work_mem=17MB`, du `LISTEN canal_test` du client 1, de sa table TEMP —
|
||||||
|
# et de son `SET ROLE`, au point de créer des tables appartenant à un
|
||||||
|
# autre rôle que le sien.
|
||||||
|
#
|
||||||
|
# Portée de la fuite, mesurée et non supposée : les pools sont partitionnés
|
||||||
|
# par (base, utilisateur) — 210 pools, aucun ne mélange deux bases ni deux
|
||||||
|
# comptes. Le seul héritier possible d'un `SET ROLE` posé par kadans-api
|
||||||
|
# est un client de la base `kadans` avec le MÊME identifiant éphémère,
|
||||||
|
# c'est-à-dire kadans-api elle-même. Défaut d'hygiène, pas brèche
|
||||||
|
# inter-applications.
|
||||||
|
#
|
||||||
|
# `DISCARD ALL` est le défaut de pgbouncer, et c'est un SUR-ENSEMBLE strict
|
||||||
|
# des deux valeurs qui l'ont précédé ici : il contient `DEALLOCATE ALL`
|
||||||
|
# (donc le correctif crowdsec est conservé — vérifié : deux clients
|
||||||
|
# successifs préparent le même nom sans erreur) ET
|
||||||
|
# `SELECT pg_advisory_unlock_all()` (la valeur que pose le sous-chart).
|
||||||
|
#
|
||||||
|
# Il ne peut RIEN casser pour un client vivant : en `pool_mode = session`
|
||||||
|
# la connexion serveur n'est rendue qu'à la déconnexion du client, donc le
|
||||||
|
# reset ne court jamais entre deux requêtes d'une même session. Vérifié :
|
||||||
|
# table TEMP, `SET work_mem` et `LISTEN` d'un client VIVANT survivent.
|
||||||
|
#
|
||||||
|
# ⚠⚠ CE QUE CETTE LIGNE REND FRAGILE — et c'est l'inverse de ce qu'on croit.
|
||||||
|
#
|
||||||
|
# `pool_mode` n'est PAS déclaré ici, donc il vaut `session`, le défaut.
|
||||||
|
# C'est ce qui rend `DISCARD ALL` sans danger. **Le jour où quelqu'un
|
||||||
|
# écrira `pool_mode: transaction`, il cassera kadans-api en silence** :
|
||||||
|
# son `SET ROLE` est posé UNE FOIS à l'ouverture (`AfterConnect`, db.go),
|
||||||
|
# et en transaction pooling `DISCARD ALL` court ENTRE deux transactions —
|
||||||
|
# donc le rôle est effacé avant les migrations suivantes. MESURÉ, les
|
||||||
|
# quatre combinaisons, propriétaire de la table créée :
|
||||||
|
#
|
||||||
|
# session + DEALLOCATE ALL → rôle stable ✅ (mais la fuite reste)
|
||||||
|
# session + DISCARD ALL → rôle stable ✅ ← ce qu'on déploie
|
||||||
|
# transaction + DEALLOCATE ALL → rôle stable ✅ (par ACCIDENT : la fuite
|
||||||
|
# qu'on referme est ce qui le sauvait)
|
||||||
|
# transaction + DISCARD ALL → rôle ÉPHÉMÈRE ❌ le défaut du 28/07,
|
||||||
|
# qui a mis l'API à terre une demi-journée
|
||||||
|
#
|
||||||
|
# Et `poserRoleProprietaire` ne peut pas le voir : sa relecture de
|
||||||
|
# `current_role` a lieu à l'ouverture, où le rôle est encore correct.
|
||||||
|
# Ce qui couvre ce cas, c'est la ceinture Vault (`ALTER ROLE … SET ROLE`,
|
||||||
|
# module app_roles) : un défaut de rôle survit à `DISCARD ALL`. **Avant de
|
||||||
|
# passer en transaction pooling, vérifier que les baux Vault ont tourné.**
|
||||||
|
server_reset_query: DISCARD ALL # défaut pgbouncer — ⚠ ne pas réduire : voir ci-dessus
|
||||||
server_idle_timeout: 7200
|
server_idle_timeout: 7200
|
||||||
pgbouncerExporter:
|
pgbouncerExporter:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ ingressRoute:
|
|||||||
rule: Host(`analytics.arcodange.lab`)
|
rule: Host(`analytics.arcodange.lab`)
|
||||||
# -- List of [middleware objects](https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/#kind-middleware) for the ingress route.
|
# -- List of [middleware objects](https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/#kind-middleware) for the ingress route.
|
||||||
middlewares:
|
middlewares:
|
||||||
|
# ⚠ Valable parce que la règle ci-dessus est en `.lab` (le client arrive en direct
|
||||||
|
# sur 192.168.1.201). À NE PAS recopier sur un hôte `.fr` : cf. doc/ce-que-traefik-voit.md §3.
|
||||||
- name: localIp@file
|
- name: localIp@file
|
||||||
# -- Use an existing secret containing the TLS certificate.
|
# -- Use an existing secret containing the TLS certificate.
|
||||||
tlsSecretName: ''
|
tlsSecretName: ''
|
||||||
|
|||||||
+30
-7
@@ -140,13 +140,36 @@ redis: &redis_config
|
|||||||
runAsUser: 999
|
runAsUser: 999
|
||||||
|
|
||||||
# -- Compute resources used by the container. More info [here](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/).
|
# -- Compute resources used by the container. More info [here](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/).
|
||||||
resources: {}
|
#
|
||||||
# limits:
|
# ⚠ CE N'EST PAS DU CONFORT — c'est ce qui empêche Redis de mourir en boucle.
|
||||||
# cpu: 100m
|
#
|
||||||
# memory: 128Mi
|
# Le sous-chart FIGE ses sondes à `timeoutSeconds: 1` sur `redis-cli ping`, et
|
||||||
# requests:
|
# n'expose aucune valeur pour les surcharger. Sur ce matériel (Raspberry Pi),
|
||||||
# cpu: 100m
|
# un conteneur SANS réservation tombe dans la classe QoS `BestEffort` : il est
|
||||||
# memory: 128Mi
|
# le premier affamé quand le nœud est chargé, et le simple lancement de
|
||||||
|
# `redis-cli` y dépasse la seconde.
|
||||||
|
#
|
||||||
|
# Mesuré le 2026-07-29 sur `redis-0` : « Liveness probe failed: command timed
|
||||||
|
# out: "redis-cli ping" timed out after 1s » **836 fois en 23 jours**, 135
|
||||||
|
# redémarrages, le conteneur tué par SIGTERM après ~90 s de vie à chaque tour.
|
||||||
|
#
|
||||||
|
# ⚠ CE QUE ÇA CASSAIT, ET QUI N'AVAIT RIEN À VOIR AVEC REDIS EN APPARENCE : le
|
||||||
|
# plugin crowdsec de Traefik utilise ce Redis comme cache de décisions. Cache
|
||||||
|
# injoignable ⇒ `isCrowdsecStreamHealthy:false` ⇒ le plugin REFUSE PAR DÉFAUT
|
||||||
|
# tout client non listé dans `clientTrustedIPs` ⇒ **403, corps vide, sur
|
||||||
|
# `kadans.arcodange.fr` et `gitea.arcodange.fr`**, pendant que les hôtes qui ne
|
||||||
|
# portent pas ce middleware répondaient normalement. Le symptôme ne nomme ni
|
||||||
|
# Redis, ni crowdsec, ni la sonde — il ressemble à un bannissement d'IP, et
|
||||||
|
# c'est par là qu'on cherche d'abord.
|
||||||
|
#
|
||||||
|
# Une réservation fait passer le conteneur en `Burstable` et lui garantit sa
|
||||||
|
# part. Les limites restent larges : Redis n'est pas ce qui sature ce nœud.
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
|
||||||
# -- Pod-level affinity. More info [here](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling).
|
# -- Pod-level affinity. More info [here](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling).
|
||||||
affinity: {}
|
affinity: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user