Compare commits

..
Author SHA1 Message Date
arcodangeandClaude Opus 5 b92da95701 docs(adr) — stockage objet MinIO : qui déclare quoi, et qui détient quoi
Trois questions indépendantes, tranchées lors du branchement de Kadans sur
MinIO (2026-07-26) : qui déclare les buckets d'une app, qui détient les
identifiants capables de les créer, et comment l'app lit les siens.

La décision de fond est du fondateur : CHACUN SON PÉRIMÈTRE. Une application
déclare ses buckets depuis son propre dépôt ; `tools` fournit le serveur, un
module de standardisation et un compte de provisionnement — pas la liste. Une
première version faisait tout porter par l'infra partagée : à ce rythme, chaque
bucket de chaque app devenait une PR sur le dépôt commun.

L'ADR consigne aussi les trois identités et leurs portées (root / provisionneur
/ compte de service), pourquoi la lecture des identifiants est une propriété
inconditionnelle de la plateforme plutôt qu'une déclaration par app, et pourquoi
les octets ne transitent pas par l'API — avec les conséquences que ça impose
(endpoint public, CORS aux origines exactes, pas de basic-auth sur l'ingress S3).

Les alternatives écartées sont listées avec leur motif, dont deux que j'avais
moi-même proposées et qui étaient plus faibles.

Deux limites assumées y figurent : le provisionneur est un secret PARTAGÉ entre
rôles CI (sa compromission permet de créer des buckets, pas de lire des objets),
et les noms d'actions d'administration MinIO n'ont pas été éprouvés contre le
serveur au moment d'écrire.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 10:19:48 +02:00
arcodange b06b7e79ac Merge pull request 'fix(argocd): url-shortener enfin syncable — ignorer le volumeName épinglé de son PVC' (#44) from arcodange/url-shortener-pvc-sync into main 2026-07-25 10:49:41 +02:00
arcodangeandClaude Fable 5 73bf7d1170 fix(argocd): url-shortener enfin syncable — ignorer le volumeName épinglé de son PVC
L'app url-shortener était en SyncError permanent : son PVC live porte un
spec.volumeName épinglé (rebind du volume Longhorn après le drill coupure de
courant) absent du chart ; chaque sync tentait donc de le vider, refus API
(spec immuable après création), échec en boucle malgré automated+selfHeal.

- apps.yaml : passthrough générique ignoreDifferences + syncOptions par app.
- values.yaml : url-shortener ignore /spec/volumeName du PVC, avec
  RespectIgnoreDifferences=true pour que l'apply réinjecte la valeur live au
  lieu de la vider (le cas d'usage documenté d'ArgoCD pour les champs
  immuables).

Rendu helm vérifié : seule l'Application url-shortener change.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-25 10:47:34 +02:00
arcodange 1ed3154668 Merge pull request 'fix(dns): coredns-custom — importer les blocs *.server à la racine du Corefile (sinon CoreDNS crash)' (#40) from arcodange/coredns-custom-import into main
Reviewed-on: #40
2026-07-24 12:44:33 +02:00
arcodange a59049d436 Merge pull request 'docs(runbook) — corrige 09 : ne pas écrire vaultConnectionRef hors du ns tools' (#43) from arcodange/runbook-fix-connref into main 2026-07-24 11:29:41 +02:00
arcodangeandClaude Opus 4.8 51d01f47c2 docs(runbook) — corrige 09 : NE PAS écrire vaultConnectionRef dans un ns applicatif
Le chapitre « service compagnon » montrait `vaultConnectionRef: default` dans
l'exemple VaultAuth — c'est faux hors du namespace `tools` et ça a réellement bloqué
le déploiement de kadans-api (pods en CreateContainerConfigError, VaultDynamicSecret
sur « VaultConnection default not found »).

VSO résout vaultConnectionRef dans le namespace DU CR ; la VaultConnection `default`
ne vit que dans `tools`. Les apps hors `tools` (erp, webapp) OMETTENT le champ et
laissent VSO retomber sur sa defaultVaultConnection. On retire donc la ligne de
l'exemple + on ajoute un encart WARNING dédié au piège.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ws8L74dVZmp97Wu36fm8j
2026-07-24 11:29:17 +02:00
arcodange 97b2f49d49 Merge pull request 'docs(runbook) — chapitre « service compagnon » (namespace + stack partagés)' (#42) from arcodange/runbook-service-compagnon into main 2026-07-24 10:40:13 +02:00
arcodange cb83c03d15 Merge pull request 'feat(argocd) — enregistre kadans-api (app-of-apps, namespace kadans)' (#41) from arcodange/register-kadans-api into main 2026-07-24 10:39:54 +02:00
arcodangeandClaude Opus 4.8 e1167eec27 docs(runbook) — chapitre « service compagnon » (namespace + stack partagés)
Le runbook new-web-app couvre l'app autonome (dépôt/base/Vault/namespace propres,
tout nommé <app>). Il manquait le cas du SERVICE COMPAGNON : un second service qui
partage le namespace — et parfois le stack Vault/DB — d'une app existante (API cœur
à côté de son front, façade d'analyse). Deux précédents vivants non documentés :
kadans-jobs (namespace seul) et kadans-api (namespace + base + Vault).

- Nouvelle page 09-service-compagnon.md : compagnon vs app autonome ; les deux
  formes (sans état / partage Vault+DB) ; le PIÈGE du VaultAuth manquant quand
  l'app primaire ne consomme pas Vault (front statique) → le compagnon pose son
  propre VaultAuth mais avec le rôle+SA du PRIMAIRE ; carte, précédents, delta de
  checklist.
- 07-argocd-register.md : ajoute la ligne `namespace:` aux options (elle existait
  dans values.yaml — kadans-jobs — mais n'était pas documentée) ; corrige le
  callout qui affirmait le namespace « non configurable ».
- conventions.md : note l'exception compagnon à la règle « tout est <app> ».
- README.md : entrée 09 dans l'index + Last Updated.

Vérifié : VaultAuth erp nommé `auth` ; connexion via pgbouncer.tools ; liens
internes tous résolus.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ws8L74dVZmp97Wu36fm8j
2026-07-24 10:38:27 +02:00
arcodangeandClaude Opus 4.8 1365c95c2f feat(argocd) — enregistre kadans-api (app-of-apps, namespace kadans)
Ajoute l'API cœur au registre gitea_applications. ArgoCD crée une Application
`kadans-api` (source arcodange/kadans-api, path chart, targetRevision HEAD), sync
automatique prune+selfHeal, image-updater par digest sur :latest — même moule que
les autres apps.

Namespace `kadans` (comme kadans-jobs) : kadans-api partage le stack Vault/DB déjà
en place pour l'app front (VaultAuth `kadans`, rôle Postgres dynamique
postgres/creds/kadans, ServiceAccount `kadans`, policy KV `kadans`). Aucun nouvel
iac/DB/Vault à provisionner.

À merger APRÈS le fix chart kadans-api (VaultAuth + hôte DB pgbouncer.tools) pour
que la première synchro ArgoCD parte d'un chart correct.

helm template rend l'Application kadans-api → repoURL arcodange/kadans-api,
namespace kadans, CreateNamespace, digest.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ws8L74dVZmp97Wu36fm8j
2026-07-24 10:28:28 +02:00
arcodangeandClaude Fable 5 ec49706952 fix(dns): import coredns custom *.server blocks at Corefile root — inside .:53 it crashes CoreDNS
The never-yet-applied k3s_dns.yml placed 'import /etc/coredns/custom/*.server'
INSIDE the .:53 server block. *.server files hold full server blocks
(arcodange.lab:53 {…}), which only parse at Corefile root — inside a block
CoreDNS dies at startup with "Unknown directive 'arcodange.lab:53'"
(CrashLoopBackOff, cluster DNS fully down; lived it on 2026-07-24 while
restoring the expired *.arcodange.lab certificate).

Also restores the stock 'loadbalance' plugin dropped by the playbook.

Context: cluster CoreDNS forwarded to the node's resolv.conf, which lists the
ISP box's IPv6 RDNSS next to the Pi-holes — NXDOMAIN roulette for *.lab names.
That's what left step-issuer unable to reach ssl-ca.arcodange.lab:8443 and let
the 24h wildcard cert expire this morning. The (fixed) playbook pins .lab
resolution to the Pi-holes via the coredns-custom ConfigMap; applied live on
2026-07-24, wildcard renewed, strict TLS verified on gitea/argocd/grafana.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 10:13:41 +02:00
arcodange 0612da184c Merge pull request 'fix(cicd): cap act_runner jobs (3g/2cpu) and capacity 2→1 — a build can no longer take down pi1' (#39) from arcodange/runner-limits into main
Reviewed-on: #39
2026-07-24 09:51:08 +02:00
20 changed files with 1513 additions and 3 deletions
@@ -54,7 +54,11 @@
cache 30
loop
reload
loadbalance
import /etc/coredns/custom/*.override
import /etc/coredns/custom/*.server
forward . {{ pihole_ips | map('regex_replace', '^(.*)$', '\1:53') | join(' ') }}
}
# Les fichiers *.server contiennent des BLOCS SERVEUR complets (ex: `arcodange.lab:53 {…}`) :
# leur import doit vivre au niveau racine du Corefile. À l'intérieur de `.:53 {}`,
# CoreDNS crashe au parse (« Unknown directive 'arcodange.lab:53' ») — vécu le 2026-07-24.
import /etc/coredns/custom/*.server
@@ -0,0 +1,20 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/vault" {
version = "4.4.0"
constraints = "4.4.0"
hashes = [
"h1:IhKDv0pTgpy89K3QYmDX872H75Wl7kZKR2scUQynuiA=",
"zh:0309ea8f81386e17ab13c06c5991ca959708c55c815b0cfba2bbcd865e0d606e",
"zh:40e56199ccd266bffa216e8ebbcdc2e29b6ef5145b39377be766e763cac759c8",
"zh:6fad1f073bd2e53e34736e000f98db581137e153ac80bbb5c4f1a1e38b46a1d2",
"zh:74564fd4759decccf7f3c952aa2feba1012f103a66ec354aa3b3292a2f1b2412",
"zh:7aae012c1a43e6e5dae6f608ec0f08cdb3f95fa121a32e413fe7ee37cb99947f",
"zh:7c83f508e164844b1dd9bafe9de0fe60c7be7b55a02e704a6e2f50cff38b7d96",
"zh:873a42322b68d9fba4a38217b97ee04a1eb617e811d7f9954016f5c3eb6cb0bc",
"zh:9db2b13472cf91a5f18f0a7c6ae532277c05b0980d87f492341426b981679f7b",
"zh:ac1cbd2926265db80efe3f1814bed82901f7d8a7d4e5b1e22592e1eef234b1c7",
"zh:f465a955cc96f640e7426a648ba672c169a4a2959bad6146fe61583d67642561",
]
}
+66
View File
@@ -0,0 +1,66 @@
# minio-values.yaml - Configuration MinIO pour k3s
# À placer dans factory/argocd/apps/minio/
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: minio
namespace: argocd
spec:
project: default
source:
repoURL: https://charts.bitnami.com/bitnami
chart: minio
targetRevision: 12.13.0
helm:
values: |
global:
storageClass: longhorn
auth:
rootUser: kadans-admin
rootPassword: "${MINIO_PASSWORD}" # À définir dans Vault
mode: standalone
persistence:
enabled: true
size: 50Gi
storageClass: longhorn
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 1Gi
cpu: 500m
service:
type: ClusterIP
port: 9000
ingress:
enabled: true
hostname: minio.arcodange.lab
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
tls: true
buckets:
- name: kadans-videos
policy: none
purge: false
- name: kadans-thumbnails
policy: none
purge: false
destination:
server: https://kubernetes.default.svc
namespace: storage
syncPolicy:
automated:
prune: true
selfHeal: true
+11
View File
@@ -24,6 +24,14 @@ spec:
destination:
server: https://kubernetes.default.svc
namespace: {{ $ns }}
{{- /* Champs à exclure du diff (ex: /spec/volumeName d'un PVC rebindé à la
main après le drill coupure de courant — immuable côté API). À coupler
avec la syncOption RespectIgnoreDifferences=true pour que l'apply
réinjecte la valeur live au lieu de tenter de la vider. */}}
{{- with $app_attr.ignoreDifferences }}
ignoreDifferences:
{{- toYaml . | nindent 4 }}
{{- end }}
syncPolicy:
{{- if $app_attr.syncPolicy }}
{{- toYaml $app_attr.syncPolicy | nindent 4 }}
@@ -34,6 +42,9 @@ spec:
{{- end }}
syncOptions:
- CreateNamespace=true
{{- range $app_attr.syncOptions }}
- {{ . }}
{{- end }}
{{- /*
Non-prod environments (ADR-0002 elision rule): one extra Application per env
under `<app_attr>.envs`. Each renders the SAME repo + chart, overlaid with
+17
View File
@@ -4,6 +4,15 @@
gitea_applications:
url-shortener:
annotations: {}
# Le PVC live a un spec.volumeName épinglé (rebind du volume Longhorn) que
# le chart ne déclare pas : sans ceci, chaque sync tente de le vider et
# l'API le refuse (spec immuable) → SyncError permanent.
ignoreDifferences:
- kind: PersistentVolumeClaim
jsonPointers:
- /spec/volumeName
syncOptions:
- RespectIgnoreDifferences=true
tools:
annotations: {}
syncPolicy:
@@ -51,6 +60,14 @@ gitea_applications:
annotations:
argocd-image-updater.argoproj.io/image-list: kadans-jobs=gitea.arcodange.lab/arcodange/kadans-jobs:latest
argocd-image-updater.argoproj.io/kadans-jobs.update-strategy: digest
kadans-api:
org: arcodange
# L'API cœur partage le stack Vault/DB « kadans » (VaultAuth, creds Postgres,
# policy KV) : elle vit donc dans le namespace de l'app front qu'elle sert.
namespace: kadans
annotations:
argocd-image-updater.argoproj.io/image-list: kadans-api=gitea.arcodange.lab/arcodange/kadans-api:latest
argocd-image-updater.argoproj.io/kadans-api.update-strategy: digest
argocd_image_updater_chart_values:
config:
+111
View File
@@ -0,0 +1,111 @@
[← ADRs](.) · [factory](../..) · **20260726 — stockage objet (MinIO) : qui déclare quoi**
> **Cross-references** (bidirectionnel : chaque fichier listé doit citer cette ADR en tête)
>
> - **Infra partagée** (repo `arcodange-org/tools`) :
> [`minio/iac/modules/minio_app/`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/minio/iac/modules/minio_app) ·
> [`minio/iac/provisioner.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/minio/iac/provisioner.tf) ·
> [`minio/values.yaml`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/minio/values.yaml) ·
> [`hashicorp-vault/iac/modules/app_policy/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf)
> - **Premier consommateur** (repo `arcodange/kadans`) :
> [`iac/main.tf`](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/iac/main.tf)
> - **API consommatrice** (repo `arcodange/kadans-api`) :
> [`stockage.go`](https://gitea.arcodange.lab/arcodange/kadans-api/src/branch/main/stockage.go) ·
> [`chart/values.yaml`](https://gitea.arcodange.lab/arcodange/kadans-api/src/branch/main/chart/values.yaml)
> - **Related ADR** :
> [`04_tool_hashicorp_vault.md`](04_tool_hashicorp_vault.md) (rôles et politiques Vault) ·
> [`20260407-network-architecture.md`](20260407-network-architecture.md) (Cloudflare / Traefik / CrowdSec)
# ADR 20260726 : stockage objet (MinIO) — qui déclare quoi, et qui détient quoi
## Status
Proposed
## Context
MinIO est déployé dans le namespace `tools` (chart officiel, standalone, volume Longhorn). Le premier consommateur est Kadans, qui doit téléverser des rendus vidéo depuis le navigateur pour qu'ils suivent l'utilisateur d'un appareil à l'autre.
Trois questions se posaient, et elles sont indépendantes :
1. **Qui déclare les buckets** d'une application ?
2. **Qui détient les identifiants** capables de les créer ?
3. **Comment l'application lit** les siens à l'exécution ?
Une première version faisait tout porter par `tools` : une liste de consommateurs dans son Terraform, les buckets dans son chart. Elle a été rejetée — à ce rythme, chaque bucket de chaque application devient une PR sur l'infra partagée, et le dépôt commun devient le goulot de tout le monde.
## Decision
### 1. Chacun son périmètre
**Une application déclare ses buckets depuis son propre dépôt.** `tools` fournit le serveur, un module de standardisation, et un compte de provisionnement — **pas la liste**.
```hcl
# iac/main.tf de l'application
module "stockage" {
source = "git::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
app = "kadans"
buckets = ["kadans-videos"]
providers = { minio = minio }
}
```
Le module crée les buckets (privés), une politique bornée à ces buckets, un compte de service, et écrit ses clés dans `kvv2/minio/<app>`.
### 2. Trois identités, trois portées
| Identité | Peut | Ne peut pas | Qui la lit |
|---|---|---|---|
| **root** MinIO | tout | — | le seul pipeline `minio` (`kvv2/minio/config`) |
| **provisionneur** | créer bucket, politique, compte de service | lire ou écrire un objet | le rôle **CI** de chaque app (`kvv2/minio/provisioner`) |
| **compte de service** d'une app | lire/écrire dans **ses** buckets | tout le reste | le **pod** de l'app (`kvv2/minio/<app>`) |
C'est la pièce qui rend le point 1 possible. Provisionner demande des droits d'administration ; confier le **root** aurait donné à chaque application la lecture des objets de **toutes** les autres. Le provisionneur, lui, peut créer des buckets — une nuisance si une app est compromise — mais **pas lire les vidéos d'une autre**.
### 3. La lecture est une propriété de la plateforme
Le module Vault central `app_policy` accorde à **toute** application la lecture de `kvv2/data/minio/<son nom>`, **inconditionnellement**.
Pas de drapeau, pas de déclaration par app : le chemin porte le nom de l'application, donc la règle **ne peut jamais exposer que ses propres clés**. Une app qui ne stocke rien y lit un chemin qui n'existe pas — une règle inerte, pas un privilège.
Conséquence pratique : déclarer un consommateur se fait à **un seul endroit**, son propre `iac/`. Rien à synchroniser, donc rien à oublier.
### 4. Les octets ne passent pas par l'API
L'application signe des **URL présignées** ; le navigateur téléverse **directement** vers MinIO. Faire transiter 50 à 200 Mo par un pod applicatif doublerait le transit et exposerait l'API à un seul gros fichier.
Corollaires :
- l'endpoint signé doit être **joignable par le navigateur**, donc **public** (`s3.arcodange.fr`) — une page servie en HTTPS ne peut pas téléverser vers `http://` (contenu mixte), et un TLD interne ne se résout pas hors du LAN ;
- **CORS** liste les origines **exactes** de l'application, jamais `*` : une URL présignée qui fuiterait serait sinon rejouable depuis n'importe quel site ;
- l'ingress public ne porte **pas** de basic-auth, contrairement aux autres : une requête S3 porte sa propre signature, et un défi HTTP Basic casserait un PUT présigné auquel le navigateur ne peut pas répondre.
### 5. Un bucket par cycle de vie, pas par application
Une application peut avoir plusieurs buckets. Deux contenus aux durées de vie différentes méritent deux politiques de purge — Kadans en aura deux (un rendu de travail à garder, un aperçu régénérable).
Le compte de service est **par application** : ajouter un bucket ne crée aucune clé, le compte existant gagne l'accès.
## Consequences
- **Le dépôt `tools` n'est plus modifié** quand une application change ses buckets. C'était l'objet de la décision.
- **Ordre de déploiement contraint** : le module doit exister sur `main` de `tools` avant qu'une application l'appelle (`?ref=main`), et le provisionneur doit exister avant le premier plan d'application.
- **Le provisionneur est un secret partagé** entre les rôles CI. Sa compromission permet de créer des buckets et des comptes, pas de lire des objets. Si ce risque devient inacceptable, la suite est une identité de provisionnement **par application**, bornée par préfixe de bucket — MinIO ne le permet pas simplement aujourd'hui.
- **Non vérifié à la rédaction** : les noms d'actions d'administration MinIO de la politique du provisionneur viennent de la documentation, pas d'un essai. Le premier `apply` les confirmera ou les corrigera.
## Alternatives Considered
| Option | Pourquoi non |
|---|---|
| `tools` détient la liste des consommateurs | Chaque bucket de chaque app devient une PR sur l'infra partagée — rejeté par le fondateur, et c'est le cœur de cette ADR |
| Les buckets déclarés dans le chart de MinIO (`values.yaml`) | Même défaut : la déclaration vit chez l'infra, pas chez l'application |
| Chaque app crée son compte de service avec le **root** | Le root lit et écrit tous les objets de toutes les apps : le distribuer à chaque rôle CI revient à ne plus avoir de cloisonnement |
| Déclarer la lecture Vault par app (`kv_read_paths`) | Mécanisme réel, mais c'est la trappe pour lire un secret appartenant à une **autre** app (creds GCS de Longhorn pour l'ERP). Y ranger un motif standard le rend invisible et oblige à le redéclarer partout |
| Un drapeau `object_storage = true` par app | Une déclaration de plus à tenir synchronisée avec le `iac/` de l'app — donc une à oublier. La règle inerte ne coûte rien |
| Une identité de provisionnement par app | Souhaitable, mais MinIO ne borne pas simplement les actions d'administration par préfixe. À reconsidérer si le modèle de menace change |
## Success Metrics
- Ajouter une application consommatrice ne touche **aucun** fichier de `tools`.
- Un compte de service compromis ne donne accès qu'aux objets qu'il gérait déjà.
- Le root de MinIO n'apparaît dans aucune politique Vault en dehors du pipeline `minio`.
+1
View File
@@ -15,6 +15,7 @@
- [x] gitea packages
- [ ] devsecops tools
- [x] [hashicorp vault](./04_tool_hashicorp_vault.md)
- [x] [stockage objet MinIO — qui déclare quoi](./20260726-stockage-objet-minio.md)
- [ ] terrakube
- [ ] prometheus/grafana
- [ ] ansible AWX
@@ -39,6 +39,7 @@ Options supplémentaires :
| Champ | Quand l'utiliser | Effet |
|---|---|---|
| `org: arcodange` | dépôt hors `arcodange-org` | change le `repoURL` (défaut `arcodange-org`) |
| `namespace: <autre>` | **service compagnon** partageant le namespace d'une app existante | déploie hors du namespace `<app>` (défaut = nom de l'app) — voir [9. Service compagnon](09-service-compagnon.md) |
| `syncPolicy: …` | contrôle manuel | surcharge la policy (défaut : `automated {prune, selfHeal}`) |
## Ce que ça génère
@@ -79,7 +80,7 @@ flowchart LR
## Notes / contraintes
> [!IMPORTANT]
> `path: chart` et `namespace: <app>` sont **déduits du nom**, pas configurables par entrée. C'est pourquoi le dossier doit s'appeler `chart/` ([étape 1](01-gitea-repo.md)) et le nom doit être cohérent partout ([conventions](conventions.md)).
> `path: chart` est **fixe** (jamais configurable) : c'est pourquoi le dossier doit s'appeler `chart/` ([étape 1](01-gitea-repo.md)) et le nom doit être cohérent partout ([conventions](conventions.md)). Le `namespace` vaut **le nom de l'app par défaut**, mais se surcharge via `namespace:` — utilisé par les [services compagnons](09-service-compagnon.md) qui partagent le namespace d'une app existante.
- Le chart `factory/argocd` est lui-même réconcilié par ArgoCD (app-of-apps racine) : committer `values.yaml` sur `main` suffit à faire apparaître/synchroniser la nouvelle `Application`. Pas de `kubectl apply` manuel.
- `prune: true` + `selfHeal: true` : ArgoCD supprime ce qui n'est plus dans le chart et réécrase les dérives manuelles. En tenir compte avant tout `kubectl edit`.
@@ -88,4 +89,5 @@ flowchart LR
- [4. Chart Helm](04-helm-chart.md) — le contenu déployé (le dossier `chart/`).
- [6. Workflows CI](06-ci-workflows.md) — les annotations `argocd-image-updater` collaborent avec l'image poussée.
- [9. Service compagnon](09-service-compagnon.md) — le champ `namespace:` pour déployer dans le namespace d'une app existante.
- [8. Checklist](08-checklist.md) — vérifier que l'`Application` passe `Healthy`/`Synced`.
@@ -0,0 +1,146 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **9. Service compagnon**
# 9. Service compagnon (namespace partagé)
> **Status:** ✅ Active
> **Upstream:** [7. Enregistrement ArgoCD](07-argocd-register.md) (le champ `namespace:` utilisé ici)
> **Related:** [Conventions de nommage](conventions.md) · [4. Chart Helm](04-helm-chart.md) · [2. Base de données](02-database.md) · [Checklist](08-checklist.md)
---
## Summary
Tous les autres chapitres décrivent une app **autonome** : son dépôt, sa base, son stack Vault, son namespace, son ServiceAccount — tout porte le même nom `<app>`. Mais certains services ne sont pas une app à part entière : ce sont des **compagnons** d'une app existante. Une **API cœur** à côté de son front, une **façade d'analyse** qui sert une app — ils vivent dans le **même namespace** que l'app qu'ils servent et **réutilisent son identité** (Vault, base, ServiceAccount) plutôt que d'en provisionner une nouvelle.
Ce chapitre décrit ce raccourci et son **piège principal** : la convention « tout est nommé `<app>` » ([conventions](conventions.md)) **ne tient plus** pour un compagnon — ses identités Vault/DB/SA restent celles de l'app **primaire**, pas les siennes.
## Compagnon ou app autonome ?
Fais un **compagnon** quand le service partage réellement l'identité et les données de l'app primaire. Fais une **app autonome** (chapitres 1→8) dès qu'il lui faut sa propre base ou ses propres accès.
| Prends un compagnon si… | Prends une app autonome si… |
|---|---|
| Il lit/écrit **la base de l'app primaire** (même données) | Il lui faut **sa propre base** |
| Il partage le cycle de vie de l'app (déployé avec, pour elle) | Il a un cycle de vie indépendant |
| Une seule origine CORS / un seul domaine logique | Domaine et exposition propres |
Un compagnon garde **son propre dépôt Gitea et son propre chart** (donc sa propre image, sa CI de build, son ingress). Ce qu'il **ne** refait pas : base, rôles Vault, ServiceAccount, namespace.
## Les deux formes de compagnon
### A. Compagnon sans état — juste le namespace partagé
Le service n'a **ni base ni secret Vault** (ex. façade d'analyse `kadans-jobs`). Il suffit de le déployer dans le namespace de l'app primaire. Une seule chose le distingue d'une app normale à l'[étape 7](07-argocd-register.md) : la clé **`namespace:`**.
```yaml
# factory/argocd/values.yaml
kadans-jobs:
org: arcodange
namespace: kadans # ← sinon ArgoCD déduirait « kadans-jobs »
annotations:
argocd-image-updater.argoproj.io/image-list: kadans-jobs=…/kadans-jobs:latest
argocd-image-updater.argoproj.io/kadans-jobs.update-strategy: digest
```
Son chart ne contient que `deployment` / `service` / `ingress`. **Pas d'`iac/`, rien dans `postgres/iac/terraform.tfvars`, pas de CRD Vault.**
### B. Compagnon partageant le stack Vault/DB de l'app primaire
Le service lit la **base de l'app primaire** avec **ses** creds dynamiques (ex. API cœur `kadans-api` sur la base `kadans`). Il réutilise, **à l'identique**, tout ce qui a été provisionné pour le primaire :
| Ressource | Elle porte le nom du **primaire**, jamais du compagnon |
|---|---|
| Base PostgreSQL | `kadans` (pas `kadans-api`) |
| Rôle DB dynamique Vault | `postgres/creds/kadans` |
| Rôle d'auth K8s Vault | `kadans` (bound au SA `kadans` / ns `kadans`) |
| Policy KV runtime | `kadans` (accès `kvv2/kadans/*`) |
| ServiceAccount K8s | `kadans` (créé par le chart du **primaire**) |
Donc le compagnon **ne fait PAS** l'[étape 2](02-database.md) (pas de nouvelle base), **PAS** l'[étape 5](05-app-terraform.md) (pas de nouvel `app_roles`, pas d'`iac/`), et **ne s'ajoute PAS** à la liste `applications` de `postgres` / `tools`. Son `VaultDynamicSecret` pointe simplement le mount/chemin du primaire :
```yaml
# chart du compagnon — vaultdynamicsecret.yaml
spec:
mount: postgres
path: creds/kadans # = le rôle DB du PRIMAIRE
vaultAuthRef: kadans # cf. le VaultAuth ci-dessous
```
Le host DB reste **`pgbouncer.tools`**, base = celle du primaire ([étape 4](04-helm-chart.md) « via pgbouncer, jamais en direct »).
> [!IMPORTANT]
> **Le piège du VaultAuth manquant.** Le `VaultDynamicSecret` a besoin d'un CR **`VaultAuth`** dans le namespace (VSO le résout par nom, dans le même namespace). Deux cas :
>
> - **Le primaire consomme déjà Vault** → son chart a déjà posé un `VaultAuth` (nommé `auth` par convention, [étape 4](04-helm-chart.md)). Le compagnon **le référence** (`vaultAuthRef: auth`) et ne crée rien.
> - **Le primaire ne consomme PAS Vault** (front statique, aucune base — cas de `kadans`) → **personne** n'a créé de `VaultAuth` dans le namespace. Le compagnon doit alors **poser le sien**, mais pointant le rôle et le SA du **primaire** :
>
> ```yaml
> # chart du compagnon — vaultauth.yaml (cas « primaire sans Vault »)
> apiVersion: secrets.hashicorp.com/v1beta1
> kind: VaultAuth
> metadata:
> name: kadans # ou « auth » ; l'important est spec.kubernetes.*
> namespace: {{ .Release.Namespace }}
> spec:
> # PAS de vaultConnectionRef → VSO retombe sur sa connexion globale (comme erp/webapp).
> method: kubernetes
> mount: kubernetes
> kubernetes:
> role: kadans # ← rôle K8s Vault du PRIMAIRE
> serviceAccount: kadans # ← SA du PRIMAIRE (créé par SON chart)
> audiences: [vault]
> ```
>
> C'est la seule raison pour laquelle le SA et le rôle du VaultAuth ne portent **pas** le nom du service qui le déploie. Ne crée **pas** un second SA `kadans-api` : le rôle K8s Vault `kadans` n'accepte que le SA `kadans`.
> [!WARNING]
> **N'écris PAS `vaultConnectionRef: default` dans un namespace applicatif.** VSO résout `vaultConnectionRef` **dans le namespace du CR** — or la VaultConnection `default` n'existe que dans le namespace **`tools`**. La nommer explicitement ailleurs fait chercher `<ns>/default` (inexistant) : le `VaultDynamicSecret` reste bloqué sur `VaultConnection "default" not found`, le Secret n'est jamais matérialisé, et le pod tourne en `CreateContainerConfigError`. Les apps hors `tools` (erp, webapp) **omettent** ce champ et laissent VSO utiliser sa `defaultVaultConnection`. (crowdsec/plausible peuvent l'écrire car ils vivent **dans** `tools`.)
## Carte
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart TB
classDef prim fill:#059669,stroke:#047857,color:#fff
classDef comp fill:#b45309,stroke:#92400e,color:#fff
classDef sh fill:#7c3aed,stroke:#6d28d9,color:#fff
subgraph NS["namespace « kadans »"]
SA["ServiceAccount kadans<br>(chart du PRIMAIRE)"]:::sh
VA["VaultAuth<br>role kadans · SA kadans"]:::sh
PRIM["Deployment kadans<br>(front, sans Vault)"]:::prim
COMP["Deployment kadans-api<br>(compagnon, lit la base)"]:::comp
end
VA -->|"vaultAuthRef"| VDS["VaultDynamicSecret<br>postgres/creds/kadans"]:::sh
COMP --> VA
VDS --> COMP
COMP --> PGB["pgbouncer.tools → base kadans"]:::sh
SA -.->|"identité empruntée"| VA
```
## Précédents vivants
| Compagnon | Primaire | Partage | CRD Vault dans son chart |
|---|---|---|---|
| [`kadans-jobs`](https://gitea.arcodange.lab/arcodange/kadans-jobs) | `kadans` | namespace seul (sans état) | aucun |
| [`kadans-api`](https://gitea.arcodange.lab/arcodange/kadans-api) | `kadans` | namespace + base + Vault | `vaultauth` (le primaire n'a pas de Vault) + `vaultdynamicsecret` |
## Delta de checklist
Par rapport à la [checklist standard](08-checklist.md), un compagnon **saute** :
- ❌ [Étape 2](02-database.md) — pas de nouvelle base ni de rôle propriétaire.
- ❌ [Étape 5](05-app-terraform.md) — pas d'`iac/`, pas d'`app_roles`, rien à ajouter aux listes `applications`.
…et **ajuste** :
- ✅ [Étape 4](04-helm-chart.md) — `VaultDynamicSecret` pointe `creds/<primaire>` ; poser un `vaultauth.yaml` **seulement** si le primaire ne consomme pas déjà Vault (rôle + SA = ceux du primaire).
- ✅ [Étape 7](07-argocd-register.md) — ajouter `namespace: <primaire>` à l'entrée `gitea_applications`.
- ✅ Ordre de merge : le fix/chart du compagnon **avant** son enregistrement ArgoCD, pour que la 1ʳᵉ synchro parte d'un chart correct.
## Related
- [7. Enregistrement ArgoCD](07-argocd-register.md) — le champ `namespace:` qui place le compagnon dans le namespace du primaire.
- [4. Chart Helm](04-helm-chart.md) — la forme des CRD VSO et la connexion via `pgbouncer.tools`.
- [Conventions de nommage](conventions.md) — la règle « tout est `<app>` » que ce chapitre nuance pour un compagnon.
- [Référence VSO faisant autorité](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/README.md) — VaultConnection/VaultAuth/VaultDynamicSecret côté `tools`.
+2 -1
View File
@@ -2,7 +2,7 @@
# Mettre en service une nouvelle application web
> **Last Updated:** 2026-07-12
> **Last Updated:** 2026-07-24
> **Status:** ✅ Procédure courante
> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md) · [ADR CI/CD](../../adr/03_cicd_gitea_action_argocd.md) · [ADR Vault](../../adr/04_tool_hashicorp_vault.md)
@@ -92,6 +92,7 @@ Ces fondations existent et ne sont **pas** à refaire pour chaque app :
| 06b | [CI des apps Bun/Nuxt](06b-bun-nuxt-ci.md) | `.gitea/workflows/ci.yml` : gates lint/test/`nuxt build` + piège Node 18→20 | ✅ |
| 07 | [Enregistrement ArgoCD](07-argocd-register.md) | `factory/argocd/values.yaml` → Application + déploiement | ✅ |
| 08 | [Checklist](08-checklist.md) | Récapitulatif ordonné + definition of done | ✅ |
| 09 | [Service compagnon](09-service-compagnon.md) | Un service qui partage le namespace + stack Vault/DB d'une app existante (ex. API cœur, façade) | ✅ |
## Légende de statut
+4
View File
@@ -44,6 +44,9 @@ Les briques se « branchent » entre elles **par convention de nom**, pas par co
**Utilise un nom court, stable, kebab-case** dès le départ.
**N'introduis pas** de variantes (`my_app` vs `my-app`, `MyApp`, pluriels) : rien ne te préviendra, l'app échouera silencieusement à se connecter ou à se déployer.
> [!NOTE]
> **Exception : les services compagnons.** Un service qui partage le namespace et le stack d'une app existante (ex. une API cœur à côté de son front) **emprunte l'identité du primaire** — sa base, son rôle Vault et son ServiceAccount portent le nom du **primaire**, pas le sien. La règle « tout est `<app>` » ne vaut alors que pour son dépôt, son chart et son image. Voir [9. Service compagnon](09-service-compagnon.md).
## Plusieurs environnements pour une même app
Une application peut être déployée plusieurs fois (prod, sandbox, …) **sans devenir une app distincte** : même dépôt, même chart, même version. On ajoute une seconde coordonnée `<env>` au nom, régie par une **règle d'élision** ([ADR-0002](../../../vibe/ADR/0002-per-application-environments.md)) :
@@ -76,3 +79,4 @@ Déclaration : `postgres/iac/terraform.tfvars` et la liste `applications` côté
- [05 · Terraform de l'app](05-app-terraform.md) — appelle `app_roles` avec `name=<app>`.
- [06 · Workflows CI](06-ci-workflows.md) — s'authentifie avec `gitea_cicd_<app>`.
- [07 · Enregistrement ArgoCD](07-argocd-register.md) — déclare `<app>` dans `gitea_applications`.
- [09 · Service compagnon](09-service-compagnon.md) — l'exception : un compagnon emprunte l'identité de l'app primaire.
+160
View File
@@ -0,0 +1,160 @@
# ADR 20260407: CI/CD Architecture with ArgoCD, Gitea, and Vault
## Status
Proposed
## Context
The home lab requires a secure and automated CI/CD pipeline to deploy applications to the k3s cluster. The pipeline must integrate with:
- **Gitea**: For Git repository management and CI runners.
- **ArgoCD**: For GitOps-based continuous deployment.
- **Vault**: For secrets management and OIDC authentication.
- **Gitea Act Runner**: For executing CI jobs.
## Decision
We will implement a **GitOps-driven CI/CD pipeline** with the following components:
### 1. Gitea OIDC Authentication with Vault
- Gitea is registered as an OIDC application in Vault.
- Vault issues short-lived tokens for Gitea users.
- The `gitea_oidc_auth.yml` playbook automates this setup using Playwright and OpenTofu.
- **OIDC Workflow**:
1. The `oidc_jwt_token.sh` script (base64-encoded in `secrets.vault_oauth__sh_b64`) handles the OIDC flow.
2. Gitea Act Runner executes the script to obtain an ID token from Gitea.
3. The ID token is used to authenticate with Vault and retrieve secrets.
### 2. Gitea Act Runner
- Deployed on `pi1` and `pi3` (not on the Gitea host, which is `pi2`).
- Uses Docker-in-Docker for job execution.
- **Custom Runner Image (`ubuntu-latest-ca`)**: Required due to the self-signed `.lab` domain. The custom image includes the local CA certificate to trust the Gitea instance (`gitea.arcodange.lab`).
- Managed via Docker Compose (`03_cicd.yml`).
### 3. ArgoCD
- Deployed on the k3s cluster (via HelmChart in `/var/lib/rancher/k3s/server/manifests/argocd.yaml`).
- Uses Gitea as the source of truth for GitOps.
- Synchronizes the `factory` repository to deploy applications.
- Configured with Traefik for TLS termination.
### 4. Vault Secrets Operator
- Deployed in the `tools` namespace.
- Manages secrets for applications deployed via ArgoCD.
- Integrates with Gitea OIDC for authentication.
- **Helm Chart Integration**:
- `VaultAuth`: Authenticates with Vault using Kubernetes service accounts.
- `VaultStaticSecret`: Retrieves static secrets (e.g., `kvv2/webapp/config`).
- `VaultDynamicSecret`: Generates dynamic secrets (e.g., PostgreSQL credentials).
### 5. Security
- **TLS**: Traefik terminates TLS using Let's Encrypt.
- **OIDC**: Gitea authentication via Vault.
- **Secrets**: Stored in Vault, injected via the Vault Secrets Operator.
## Architecture Diagram
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#333333', 'edgeLabelBackground':'#f0f0f0', 'tertiaryColor': '#e67e22'}}}%%
graph TD
%% Styles
classDef gitea fill:#ffcc99,stroke:#cc9966,color:#333;
classDef argocd fill:#99ffcc,stroke:#66cc99,color:#333;
classDef vault fill:#ccccff,stroke:#6666cc,color:#333;
classDef k3s fill:#ff9999,stroke:#cc0000,color:#333;
classDef runner fill:#ffff99,stroke:#cccc00,color:#333;
%% Components
Gitea["Gitea (pi2)"]:::gitea
ArgoCD["ArgoCD (k3s)"]:::argocd
Vault["Vault (k3s/tools)"]:::vault
Runner1["Gitea Act Runner (pi1)"]:::runner
Runner2["Gitea Act Runner (pi3)"]:::runner
VaultOperator["Vault Secrets Operator (k3s/tools)"]:::vault
k3s["k3s Cluster"]:::k3s
%% Workflow
Gitea -->|OIDC Auth| Vault
Gitea -->|Trigger CI| Runner1
Gitea -->|Trigger CI| Runner2
Runner1 -->|Deploy to| k3s
Runner2 -->|Deploy to| k3s
ArgoCD -->|GitOps Sync| Gitea
ArgoCD -->|Deploy Apps| k3s
VaultOperator -->|Inject Secrets| k3s
Vault -->|Secrets| VaultOperator
%% Annotations
linkStyle 0,1,2,3,4,5,6,7 stroke:#999,stroke-width:1px;
```
## Consequences
### Positive
- **Automated Deployments**: ArgoCD ensures the cluster state matches Git.
- **Secure Secrets**: Vault centralizes secret management.
- **Scalable CI**: Gitea Act Runners can be added to any host.
- **OIDC Integration**: Secure authentication via Vault.
### Negative
- **Complexity**: Multiple moving parts (Gitea, ArgoCD, Vault).
- **Dependency on Vault**: If Vault fails, CI/CD may be disrupted.
- **Learning Curve**: Requires familiarity with GitOps and Vault.
## Alternatives Considered
### Alternative 1: GitHub Actions
- **Rejected**: Self-hosted Gitea aligns better with the home lab's privacy goals.
### Alternative 2: Jenkins
- **Rejected**: ArgoCD + Gitea Act Runner is lighter and more GitOps-native.
### Alternative 3: No CI/CD
- **Rejected**: Manual deployments are error-prone and unscalable.
## Sequence Diagrams
### 1. CI/CD Workflow for OpenTofu/Terraform
```mermaid
sequenceDiagram
participant Gitea
participant Runner as Gitea Act Runner (pi1/pi3)
participant Vault
participant WebApp as WebApp (k3s)
Gitea->>Runner: Trigger vault.yaml workflow
Runner->>Gitea: Execute vault_oauth__sh_b64 (OIDC)
Gitea-->>Runner: Return ID Token
Runner->>Vault: Authenticate with ID Token
Vault-->>Runner: Return Vault Token
Runner->>Runner: Run OpenTofu/Terraform
Runner->>Vault: Fetch Secrets (via Vault Action)
Vault-->>Runner: Return Secrets
Runner->>WebApp: Deploy Changes
```
### 2. Vault Secrets Operator Workflow
```mermaid
sequenceDiagram
participant ArgoCD
participant WebApp as WebApp (k3s)
participant VaultOperator as Vault Secrets Operator
participant Vault
ArgoCD->>WebApp: Deploy Helm Chart
WebApp->>VaultOperator: Create VaultAuth (K8s Auth)
VaultOperator->>Vault: Authenticate (K8s Service Account)
Vault-->>VaultOperator: Return Vault Token
WebApp->>VaultOperator: Create VaultStaticSecret (kvv2/webapp/config)
VaultOperator->>Vault: Fetch Static Secret
Vault-->>VaultOperator: Return Secret
VaultOperator->>WebApp: Inject Secret (secretkv)
WebApp->>VaultOperator: Create VaultDynamicSecret (postgres/creds/webapp)
VaultOperator->>Vault: Generate Dynamic Secret
Vault-->>VaultOperator: Return Credentials
VaultOperator->>WebApp: Inject Credentials (vso-db-credentials)
WebApp->>WebApp: Restart Pods (Rollout)
```
## Success Metrics
- Gitea Act Runners successfully execute CI jobs.
- ArgoCD synchronizes the `factory` repository without errors.
- Vault Secrets Operator injects secrets into deployed applications.
+334
View File
@@ -0,0 +1,334 @@
# ADR 20260407: Network Architecture
## Status
Proposed
## Context
The home lab requires a secure and resilient network architecture to support:
- Internal services (`.lab` domain).
- External services (`.arcodange.fr` domain).
- DNS resolution and ad-blocking (Pi-hole).
- TLS certificate management (Step CA).
- Ingress routing (Traefik).
- CDN and DDoS protection (Cloudflare).
## Decision
We will implement a **multi-layered network architecture** with the following components:
### 1. External Layer (Internet)
- **Cloudflare**: CDN, DDoS protection, and DNS for `.arcodange.fr`.
- **DuckDNS**: Dynamic DNS for external access.
- **Livebox**: ISP-provided gateway (NAT, DHCP, firewall).
### 2. Internal Layer (Home Lab)
- **Pi-hole (pi1, pi3)**: DNS sinkhole for ad-blocking and internal DNS resolution.
- **Step CA (pi1)**: Internal certificate authority for `.lab` domain.
- **Traefik (k3s)**: Ingress controller with TLS termination.
- **k3s Cluster**: Hosts internal services with Longhorn storage.
### 3. DNS Architecture
- **Pi-hole**: Primary DNS for internal clients.
- Forwards `.lab` queries to Step CA.
- Forwards external queries to Cloudflare (1.1.1.1).
- **Step CA**: Issues certificates for `.lab` services.
- **Cloudflare**: Manages `.arcodange.fr` DNS records.
### 4. Ingress and TLS
- **Traefik**: Terminates TLS for both `.lab` and `.arcodange.fr` domains.
- Uses Let's Encrypt for `.arcodange.fr`.
- Uses Step CA for `.lab`.
- **Helm Chart Annotations**:
- `traefik.ingress.kubernetes.io/router.entrypoints: websecure`
- `traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt`
- `traefik.ingress.kubernetes.io/router.middlewares: localIp@file`
### 5. Security
- **Cloudflare Tunnel**: Securely exposes internal services without port forwarding.
- **CrowdSec**: Intrusion detection and banning.
- **Traefik Middlewares**: IP filtering, rate limiting, and authentication.
- **Cloudflare Turnstile**: CAPTCHA protection for public-facing services.
## Architecture Diagrams
### 0. High-Level Network Architecture (Architecture Beta)
```mermaid
%%{init: {'theme': 'neutral', 'themeVariables': {
'primaryColor': '#f0f0f0',
'primaryBorderColor': '#333333',
'primaryTextColor': '#333333',
'lineColor': '#333333',
'tertiaryColor': '#e67e22'
}}}%%
architectureBeta
%% External Layer
box "Internet" #f9f9f9
component Cloudflare["Cloudflare\n(CDN/DNS)"] #f9f9f9
component DuckDNS["DuckDNS\n(DDNS)"] #f9f9f9
end
%% External Gateway
box "External Gateway" #e6e6e6
component Livebox["Livebox\n(NAT/Firewall)"] #e6e6e6
end
%% Internal Layer
box "Internal Network\n(192.168.1.0/24)" #d4d4d4
%% DNS Layer
box "DNS" #ffff99
component PiHole1["Pi-hole\n(pi1)"] #ffff99
component PiHole3["Pi-hole\n(pi3)"] #ffff99
component StepCA["Step CA\n(pi1)"] #ccccff
end
%% k3s Layer
box "k3s Cluster" #ff9999
component Traefik["Traefik\n(Ingress)"] #ff9999
component CrowdSec["CrowdSec\n(Security)"] #ff9999
component Gitea["Gitea\n(pi2)"] #ffcc99
component Vault["Vault\n(Secrets)"] #ccccff
end
end
%% Connections
Cloudflare --> Livebox : "DNS"
DuckDNS --> Livebox : "DDNS"
Livebox --> PiHole1 : "NAT"
Livebox --> PiHole3 : "NAT"
Livebox --> Traefik : "NAT"
PiHole1 --> StepCA : "Forward .lab"
PiHole1 --> Cloudflare : "Forward External"
PiHole3 --> StepCA : "Forward .lab"
PiHole3 --> Cloudflare : "Forward External"
Traefik --> Cloudflare : "TLS (Let's Encrypt)"
Traefik --> StepCA : "TLS (Step CA)"
CrowdSec --> Traefik : "Ban IPs"
Traefik --> Gitea : "Route"
Traefik --> Vault : "Route"
```
### 1. High-Level Network Architecture
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#333333', 'edgeLabelBackground':'#f0f0f0', 'tertiaryColor': '#f89136'}}}%%
graph TD
%% Styles
classDef internet fill:#f9f9f9,stroke:#999,color:#333;
classDef external fill:#e6e6e6,stroke:#555,color:#333;
classDef internal fill:#d4d4d4,stroke:#777,color:#333;
classDef security fill:#ff9999,stroke:#cc0000,color:#333;
classDef dns fill:#ffff99,stroke:#cccc00,color:#333;
classDef ca fill:#ccccff,stroke:#6666cc,color:#333;
%% Internet
subgraph "Internet"
Cloudflare["Cloudflare (CDN/DNS)"]:::internet
DuckDNS["DuckDNS (DDNS)"]:::internet
end
%% External Gateway
subgraph "External Gateway"
Livebox["Livebox (NAT/Firewall)"]:::external
end
%% Internal Network
subgraph "Internal Network (192.168.1.0/24)"
%% Pi-hole DNS
PiHole1["Pi-hole (pi1)"]:::dns
PiHole3["Pi-hole (pi3)"]:::dns
%% Step CA
StepCA["Step CA (pi1)"]:::ca
%% k3s Cluster
k3s["k3s Cluster"]:::internal
Traefik["Traefik (k3s)"]:::internal
CrowdSec["CrowdSec (k3s)"]:::security
%% Services
Gitea["Gitea (pi2)"]:::internal
Vault["Vault (k3s)"]:::internal
end
%% Connections
Cloudflare -->|DNS| Livebox
DuckDNS -->|DDNS| Livebox
Livebox -->|NAT| PiHole1
Livebox -->|NAT| PiHole3
Livebox -->|NAT| k3s
%% Internal DNS
PiHole1 -->|Forward .lab| StepCA
PiHole1 -->|Forward External| Cloudflare
PiHole3 -->|Forward .lab| StepCA
PiHole3 -->|Forward External| Cloudflare
%% Ingress
Traefik -->|"TLS (Let's Encrypt)"| Cloudflare
Traefik -->|"TLS (Step CA)"| StepCA
CrowdSec -->|Ban IPs| Traefik
%% Service Access
Traefik -->|Route| Gitea
Traefik -->|Route| Vault
```
### 2. DNS Resolution Flow
```mermaid
sequenceDiagram
participant Client
participant PiHole
participant StepCA
participant Cloudflare
participant ExternalDNS
Client->>PiHole: Query example.lab
PiHole->>StepCA: Forward .lab query
StepCA-->>PiHole: Return A record
PiHole-->>Client: Return response
Client->>PiHole: Query example.com
PiHole->>Cloudflare: Forward to 1.1.1.1
Cloudflare->>ExternalDNS: Resolve externally
ExternalDNS-->>Cloudflare: Return response
Cloudflare-->>PiHole: Return response
PiHole-->>Client: Return response
```
### 3. Ingress and TLS Flow
```mermaid
sequenceDiagram
participant User
participant Cloudflare
participant Traefik
participant StepCA
participant Service
User->>Cloudflare: HTTPS Request (webapp.arcodange.fr)
Cloudflare->>Traefik: Forward to internal IP
Traefik->>Let's Encrypt: Request Certificate
Let's Encrypt-->>Traefik: Issue Certificate
Traefik->>Service: Route request
Service-->>Traefik: Return response
Traefik-->>Cloudflare: Return HTTPS response
Cloudflare-->>User: Return response
User->>Traefik: HTTPS Request (webapp.arcodange.lab)
Traefik->>StepCA: Request Certificate
StepCA-->>Traefik: Issue Certificate
Traefik->>Service: Route request
Service-->>Traefik: Return response
Traefik-->>User: Return HTTPS response
```
### 4. Security Flow (CrowdSec + Traefik)
```mermaid
sequenceDiagram
participant Attacker
participant Traefik
participant CrowdSec
participant BannedIPs
Attacker->>Traefik: Malicious Request
Traefik->>CrowdSec: Log suspicious activity
CrowdSec->>BannedIPs: Add IP to ban list
BannedIPs-->>Traefik: Update middleware
Traefik-->>Attacker: Block request (403)
```
## Playbook and Role Analysis
### 1. Pi-hole Deployment
- **Playbook**: `playbooks/system/pihole.yml`
- **Role**: `arcodange.factory.pihole`
- **Configuration**:
- Upstream DNS: Cloudflare (1.1.1.1) and Step CA for `.lab`.
- Blocklists: Ad-blocking and malware domains.
### 2. Step CA Deployment
- **Playbook**: `playbooks/ssl/ssl.yml`
- **Role**: `step_ca`
- **Configuration**:
- Internal CA for `.lab` domain.
- Short-lived certificates (default: 24h).
### 3. Traefik Deployment
- **Playbook**: `playbooks/system/system_k3s.yml` (via k3s)
- **Helm Chart**: `traefik` (installed via k3s)
- **Key Annotations**:
```yaml
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
traefik.ingress.kubernetes.io/router.middlewares: localIp@file
```
### 4. CrowdSec Deployment
- **Playbook**: `playbooks/tools/crowdsec.yml`
- **Role**: `arcodange.factory.crowdsec`
- **Configuration**:
- Bouncer integration with Traefik.
- Custom scenarios for brute-force and bot detection.
## Consequences
### Positive
- **Resilient DNS**: Pi-hole provides ad-blocking and internal DNS resolution.
- **Secure TLS**: Step CA for internal services, Let's Encrypt for external.
- **DDoS Protection**: Cloudflare absorbs external attacks.
- **Intrusion Detection**: CrowdSec bans malicious IPs automatically.
### Negative
- **Complexity**: Multiple layers require careful configuration.
- **Single Point of Failure**: Pi-hole is critical for internal DNS.
- **Certificate Management**: Step CA requires maintenance for `.lab` domain.
## Alternatives Considered
### Alternative 1: Public DNS for `.lab`
- **Rejected**: Exposing internal domains is a security risk.
### Alternative 2: No Ad-Blocking
- **Rejected**: Pi-hole provides essential security and privacy.
### Alternative 3: Self-Signed Certificates
- **Rejected**: Step CA provides better usability with short-lived certs.
### 5. Cloudflare Turnstile + CrowdSec Flow
```mermaid
sequenceDiagram
participant User
participant Cloudflare
participant Turnstile
participant Traefik
participant CrowdSec
participant BannedIPs
User->>Cloudflare: Request protected endpoint
Cloudflare->>Turnstile: Challenge (CAPTCHA)
Turnstile-->>Cloudflare: Return token
Cloudflare->>Traefik: Forward request with token
alt Valid Token
Traefik->>Service: Route request
Service-->>Traefik: Return response
Traefik-->>Cloudflare: Return response
Cloudflare-->>User: Return success
else Invalid Token
Traefik->>CrowdSec: Log suspicious activity
CrowdSec->>BannedIPs: Add IP to ban list
BannedIPs-->>Traefik: Update middleware
Traefik-->>Cloudflare: Block request (403)
Cloudflare-->>User: Return "Access Denied"
end
```
## Success Metrics
- Pi-hole blocks >50% of ads and trackers.
- Step CA issues certificates without downtime.
- Traefik routes 100% of external traffic via Cloudflare.
- CrowdSec bans >10 malicious IPs per day.
- Cloudflare Turnstile blocks >90% of bot traffic.
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# Script pour générer un token Gitea et configurer Hermes
# Usage: ./get_gitea_token.sh
set -e
echo "🔑 Génération d'un token Gitea pour Hermes..."
# Créer l'inventaire
cat > ~/Work/Arcodange/factory/inventory.ini << 'EOF'
[gitea]
gitea.arcodange.lab ansible_host=192.168.1.202 ansible_user=git
[gitea:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa_arcodange
EOF
# Créer le playbook
cat > ~/Work/Arcodange/factory/get_token.yml << 'EOF'
- name: Générer un token Gitea pour Hermes
hosts: localhost
connection: local
gather_facts: true
vars:
gitea_user_name: arcodange
gitea_token_name: hermes-{{ ansible_date_time.iso8601 }}
gitea_token_scopes: "write:repository,write:user"
gitea_token_fact_name: gitea_api_token
gitea_base_url: "https://gitea.arcodange.lab"
tasks:
- name: Inclure le role gitea_token
include_role:
name: .claude/worktrees/focused-dirac-151213/ansible/arcodange/factory/roles/gitea_token
- name: Afficher le token généré
debug:
var: gitea_api_token
when: gitea_api_token is defined and gitea_api_token != 'deleted'
- name: Sauvegarder le token dans un fichier temporaire
copy:
content: "{{ gitea_api_token }}"
dest: /tmp/gitea_token.txt
when: gitea_api_token is defined and gitea_api_token != 'deleted'
EOF
# Exécuter le playbook
echo "📜 Exécution du playbook Ansible..."
cd ~/Work/Arcodange/factory
ansible-playbook -i inventory.ini get_token.yml
# Récupérer le token depuis le fichier temporaire
if [ -f /tmp/gitea_token.txt ]; then
TOKEN=$(cat /tmp/gitea_token.txt)
echo "✅ Token généré avec succès !"
# Configurer Hermes
echo "🛠 Configuration de Hermes..."
hermes config set gitea.token "$TOKEN"
hermes config set gitea.base_url "https://gitea.arcodange.lab"
hermes config set gitea.username "arcodange"
# Nettoyer
rm -f /tmp/gitea_token.txt
rm -f inventory.ini
rm -f get_token.yml
echo "✨ Configuration terminée !"
echo "Token stocké dans la configuration Hermes."
else
echo "⚠️ Impossible de récupérer le token. Vérifie que :"
echo " 1. Le serveur Gitea est accessible"
echo " 2. La clé SSH est configurée (~/.ssh/id_rsa_arcodange)"
echo " 3. L'utilisateur 'git' a les permissions nécessaires"
fi
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Script pour générer un token Gitea via API REST et configurer Hermes
# Usage: ./get_gitea_token_rest.sh
set -e
echo "🔑 Génération d'un token Gitea via API REST..."
# Demander les informations nécessaires
read -p "Nom d'utilisateur Gitea (arcodange) : " GITEA_USER
GITEA_USER=${GITEA_USER:-arcodange}
read -s -p "Mot de passe Gitea : " GITEA_PASSWORD
echo ""
read -p "Nom du token (hermes-access) : " TOKEN_NAME
TOKEN_NAME=${TOKEN_NAME:-hermes-access}
read -p "Portées (write:repository,write:user) : " SCOPES
SCOPES=${SCOPES:-write:repository,write:user}
# Appeler l'API Gitea pour créer un token
echo "📡 Appel à l'API Gitea..."
RESPONSE=$(curl -s -X POST \
"https://gitea.arcodange.lab/api/v1/users/${GITEA_USER}/tokens" \
-H "Content-Type: application/json" \
-u "${GITEA_USER}:${GITEA_PASSWORD}" \
-d "{\"name\":\"${TOKEN_NAME}\",\"scopes\":[\"${SCOPES//,/\",\"}\"]}")
# Extraire le token de la réponse
TOKEN=$(echo "$RESPONSE" | jq -r '.sha1' 2>/dev/null)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "❌ Échec de la création du token"
echo "Réponse de l'API :"
echo "$RESPONSE" | jq .
exit 1
fi
echo "✅ Token généré avec succès !"
echo "Token: $TOKEN"
# Configurer Hermes
echo "🛠 Configuration de Hermes..."
hermes config set gitea.token "$TOKEN"
hermes config set gitea.base_url "https://gitea.arcodange.lab"
hermes config set gitea.username "$GITEA_USER"
echo "✨ Configuration terminée !"
echo "Token stocké dans la configuration Hermes."
echo ""
echo "Pour vérifier la configuration :"
echo " hermes config get gitea"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Script pour générer un token Gitea via API REST avec fichier temporaire
# Usage: ./get_gitea_token_tmp.sh
set -e
echo "🔑 Génération d'un token Gitea via API REST..."
# Utiliser le fichier temporaire existant
PASS_FILE="/tmp/gitea_pass.txt"
if [ ! -f "$PASS_FILE" ]; then
echo "❌ Fichier de mot de passe introuvable : $PASS_FILE"
echo "Créez-le avec : echo 'votre_mot_de_passe' > $PASS_FILE"
exit 1
fi
# Variables
GITEA_USER="arcodange"
TOKEN_NAME="hermes-$(date +%Y%m%d-%H%M%S)"
SCOPES="write:repository,write:user"
# Appeler l'API Gitea pour créer un token
echo "📡 Appel à l'API Gitea..."
RESPONSE=$(curl -s -X POST \
"https://gitea.arcodange.lab/api/v1/users/${GITEA_USER}/tokens" \
-H "Content-Type: application/json" \
-u "${GITEA_USER}:$(cat $PASS_FILE)" \
-d "{\"name\":\"${TOKEN_NAME}\",\"scopes\":[\"${SCOPES//,/\",\"}\"]}")
# Nettoyer le fichier temporaire
rm -f "$PASS_FILE"
# Extraire le token de la réponse
TOKEN=$(echo "$RESPONSE" | jq -r '.sha1' 2>/dev/null)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "❌ Échec de la création du token"
echo "Réponse de l'API :"
echo "$RESPONSE" | jq .
exit 1
fi
echo "✅ Token généré avec succès !"
echo "Token: $TOKEN"
# Configurer Hermes
echo "🛠 Configuration de Hermes..."
hermes config set gitea.token "$TOKEN"
hermes config set gitea.base_url "https://gitea.arcodange.lab"
hermes config set gitea.username "$GITEA_USER"
echo "✨ Configuration terminée !"
echo "Token stocké dans la configuration Hermes."
echo ""
echo "Pour vérifier la configuration :"
hermes config get gitea
+27
View File
@@ -0,0 +1,27 @@
- name: Générer un token Gitea pour Hermes
hosts: localhost
connection: local
gather_facts: true
vars:
gitea_user_name: arcodange
gitea_token_name: hermes-{{ ansible_date_time.iso8601 }}
gitea_token_scopes: "write:repository,write:user"
gitea_token_fact_name: gitea_api_token
gitea_base_url: "https://gitea.arcodange.lab"
tasks:
- name: Inclure le role gitea_token
include_role:
name: .claude/worktrees/focused-dirac-151213/ansible/arcodange/factory/roles/gitea_token
- name: Afficher le token généré
debug:
var: gitea_api_token
when: gitea_api_token is defined and gitea_api_token != 'deleted'
- name: Sauvegarder le token dans un fichier temporaire
copy:
content: "{{ gitea_api_token }}"
dest: /tmp/gitea_token.txt
when: gitea_api_token is defined and gitea_api_token != 'deleted'
+5
View File
@@ -0,0 +1,5 @@
[gitea]
gitea.arcodange.lab ansible_host=192.168.1.202 ansible_user=git
[gitea:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa_arcodange
+59
View File
@@ -0,0 +1,59 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cyrilgdn/postgresql" {
version = "1.24.0"
constraints = "1.24.0"
hashes = [
"h1:GDmie2p7Ii7BOPHeMUtM6bwSvr3YvsIWUQ30TsSc+5I=",
"zh:023153dbb689296f3edbc34cb6d57355bbfa8af6faa3e20ab71a71a909c8ff56",
"zh:0d222293c1c30b0af5b9ac9a662f7fd6bbbdca0859b02046527ad133838fafd8",
"zh:1234c6ec6bd49d8a2afc80fecc748ebdd01555ac4f1b4618dc7fce670bf18858",
"zh:18d047ffa30156da8281206b977d23dc9690e87137b0a9dd7049d2776e603ce6",
"zh:2f009822960405a20f2d304f70187f77338bf3e633f522da35021f4b83bc6a8f",
"zh:39e540af29165e9dc28712651d308fdf0fcac018850a0841f7c5380f58aafb64",
"zh:4930ac482b2b0f06681228932d0ed91ac4f34555b15285787f048e33ffa72d39",
"zh:4d33f4a9f449166a5cb495e764c8a09bf9710927162d30f5db46283a88023cad",
"zh:55d6584517ebaf7b4ecefedbf600af0d12077a00834b777f884b4631f77b7e13",
"zh:5f6ab45cbb3f2b62f82f0377c7f10d4aa0f35db7f426af7529915401097c252f",
"zh:5fac6d4d676425d9491a42b992b166bdbae5ecd1fe6815bc75435f7c3024015b",
"zh:a13f1a7cc763053da2317178c7ba1d57f5fddf14f9be282881da63fc28fd0e57",
"zh:d566eaae8ba33542f7b290e8fa0f2eb7f2cfb51d5ae7cafaae261f76f3f3e422",
"zh:ea4d50591767696b6f27d977c44b17390415d21b761e00f591d67c6248c18949",
]
}
provider "registry.opentofu.org/hashicorp/random" {
version = "3.7.2"
hashes = [
"h1:cFGCdxTlsrteTiaOV/iOQdql7eJkD3F/vtJxenkj9IE=",
"zh:2ffeb1058bd7b21a9e15a5301abb863053a2d42dffa3f6cf654a1667e10f4727",
"zh:519319ed8f4312ed76519652ad6cd9f98bc75cf4ec7990a5684c072cf5dd0a5d",
"zh:7371c2cc28c94deb9dba62fbac2685f7dde47f93019273a758dd5a2794f72919",
"zh:9b0ac4c1d8e36a86b59ced94fa517ae9b015b1d044b3455465cc6f0eab70915d",
"zh:c6336d7196f1318e1cbb120b3de8426ce43d4cacd2c75f45dba2dbdba666ce00",
"zh:c71f18b0cb5d55a103ea81e346fb56db15b144459123f1be1b0209cffc1deb4e",
"zh:d2dc49a6cac2d156e91b0506d6d756809e36bf390844a187f305094336d3e8d8",
"zh:d5b5fc881ccc41b268f952dae303501d6ec9f9d24ee11fe2fa56eed7478e15d0",
"zh:db9723eaca26d58c930e13fde221d93501529a5cd036b1f167ef8cff6f1a03cc",
"zh:fe3359f733f3ab518c6f85f3a9cd89322a7143463263f30321de0973a52d4ad8",
]
}
provider "registry.opentofu.org/hashicorp/vault" {
version = "4.4.0"
constraints = "4.4.0"
hashes = [
"h1:IhKDv0pTgpy89K3QYmDX872H75Wl7kZKR2scUQynuiA=",
"zh:0309ea8f81386e17ab13c06c5991ca959708c55c815b0cfba2bbcd865e0d606e",
"zh:40e56199ccd266bffa216e8ebbcdc2e29b6ef5145b39377be766e763cac759c8",
"zh:6fad1f073bd2e53e34736e000f98db581137e153ac80bbb5c4f1a1e38b46a1d2",
"zh:74564fd4759decccf7f3c952aa2feba1012f103a66ec354aa3b3292a2f1b2412",
"zh:7aae012c1a43e6e5dae6f608ec0f08cdb3f95fa121a32e413fe7ee37cb99947f",
"zh:7c83f508e164844b1dd9bafe9de0fe60c7be7b55a02e704a6e2f50cff38b7d96",
"zh:873a42322b68d9fba4a38217b97ee04a1eb617e811d7f9954016f5c3eb6cb0bc",
"zh:9db2b13472cf91a5f18f0a7c6ae532277c05b0980d87f492341426b981679f7b",
"zh:ac1cbd2926265db80efe3f1814bed82901f7d8a7d4e5b1e22592e1eef234b1c7",
"zh:f465a955cc96f640e7426a648ba672c169a4a2959bad6146fe61583d67642561",
]
}
+355
View File
@@ -0,0 +1,355 @@
# Home lab
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#333333', 'edgeLabelBackground':'#f0f0f0', 'tertiaryColor': '#e67e22'}}}%%
graph LR
%% Styles
classDef internet fill:#f9f9f9,stroke:#999,color:#333,font-size:12px;
classDef arcodange fill:#e6e6e6,stroke:#555,color:#333,font-size:12px;
classDef cluster fill:#d4d4d4,stroke:#777,color:#333,font-size:12px;
%% Internet
subgraph "Internet / Le monde"
direction TB
DuckDNS["🌐 DuckDNS<br>(duckdns.org)"]:::internet
GoogleCloud["☁️ Google Cloud<br>(cloud.google.com)"]:::internet
Cloudflare["🛡️ Cloudflare<br>(cloudflare.com)"]:::internet
OVHcloud["🖥️ OVHcloud<br>(ovhcloud.com)"]:::internet
Zoho["📧 Zoho<br>(zoho.eu)"]:::internet
CrowdSec["🔒 CrowdSec<br>(crowdsec.net)"]:::internet
end
%% Bureaux Arcodange
subgraph "Bureaux Arcodange"
direction TB
Livebox["📡 Livebox<br>(gateway)"]:::arcodange
MacBook["💻 MacBook Pro M4"]:::arcodange
subgraph "Cluster Raspberry Pi"
direction LR
Pi1["🐧 pi1"]:::cluster
Pi2["🐧 pi2"]:::cluster
Pi3["🐧 pi3"]:::cluster
end
end
```
## Network
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#333333', 'edgeLabelBackground':'#f0f0f0', 'tertiaryColor': '#e67e22'}}}%%
graph LR
%% Styles
classDef internet fill:#f9f9f9,stroke:#999,color:#333,font-size:12px;
classDef arcodange fill:#e6e6e6,stroke:#555,color:#333,font-size:12px;
classDef k3sServer fill:#ff9999,stroke:#cc0000,color:#333,font-size:12px;
classDef k3sAgent fill:#99ff99,stroke:#00cc00,color:#333,font-size:12px;
classDef traefik fill:#9999ff,stroke:#0000cc,color:#333,font-size:12px;
classDef dns fill:#ffff99,stroke:#cccc00,color:#333,font-size:12px;
classDef cloudflared fill:#ccccff,stroke:#6666cc,color:#333,font-size:12px;
%% Internet
subgraph "Internet / Le monde"
direction TB
DuckDNS["🌐 DuckDNS<br>(duckdns.org)"]:::internet
subgraph "Cloudflare"
CloudflareService["🛡️ Cloudflare<br>(cloudflare.com)"]:::internet
DNSNote@{ shape: text, label: "redirige *.arcodange.fr<br>→ Cloudflared Tunnel" }
end
end
%% Bureaux Arcodange
subgraph "Bureaux Arcodange"
direction TB
Livebox["📡 Livebox<br>(passerelle)"]:::arcodange
MacBook["💻 MacBook Pro M4"]:::arcodange
%% Cluster Raspberry Pi
subgraph "Cluster Raspberry Pi"
direction LR
%% Noeud pi1
subgraph "pi1"
Pi1["🐧 pi1"]:::k3sServer
subgraph "K3s (Server)"
Traefik["🚀 Traefik"]:::traefik
Cloudflared["☁️ Cloudflared Tunnel"]:::cloudflared
end
PiHole1["🧹 Pi-hole"]:::dns
NS1@{ shape: text, label: "nameserver : pi3" }
DNSArcodange@{ shape: text, label: ".arcodange.lab<br>→ Traefik", color:#cccc00 }
end
%% Noeud pi2
subgraph "pi2"
Pi2["🐧 pi2"]:::k3sAgent
end
%% Noeud pi3
subgraph "pi3"
Pi3["🐧 pi3"]:::k3sAgent
PiHole3["🧹 Pi-hole"]:::dns
NS3@{ shape: text, label: "nameserver : pi1" }
end
end
end
%% Connexions
%% Pi-hole → Livebox (DNS)
PiHole1 -.->|DNS| Livebox
PiHole3 -.->|DNS| Livebox
%% Cloudflare → Cloudflared Tunnel → Traefik
CloudflareService -.-> Cloudflared
Cloudflared --> Traefik
%% Livebox → Traefik (HTTPS)
Livebox -->|redirige HTTPS| Traefik
```
## with Gitea
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#333333', 'edgeLabelBackground':'#f0f0f0', 'tertiaryColor': '#e67e22'}}}%%
graph LR
%% Styles
classDef internet fill:#f9f9f9,stroke:#999,color:#333,font-size:12px;
classDef arcodange fill:#e6e6e6,stroke:#555,color:#333,font-size:12px;
classDef k3sServer fill:#ff9999,stroke:#cc0000,color:#333,font-size:12px;
classDef k3sAgent fill:#99ff99,stroke:#00cc00,color:#333,font-size:12px;
classDef traefik fill:#9999ff,stroke:#0000cc,color:#333,font-size:12px;
classDef dns fill:#ffff99,stroke:#cccc00,color:#333,font-size:12px;
classDef cloudflared fill:#ccccff,stroke:#6666cc,color:#333,font-size:12px;
classDef gitea fill:#ffcc99,stroke:#cc9966,color:#333,font-size:12px;
classDef cms fill:#99ffcc,stroke:#66cc99,color:#333,font-size:12px;
classDef domainFR fill:#99ccff,stroke:#0066cc,color:#333,font-size:12px;
classDef domainLAB fill:#ccff99,stroke:#66cc66,color:#333,font-size:12px;
%% Internet
subgraph "Internet / Le monde"
direction TB
DuckDNS["🌐 DuckDNS<br>(duckdns.org)"]:::internet
subgraph "Cloudflare"
CloudflareService["🛡️ Cloudflare<br>(cloudflare.com)"]:::internet
DNSNote@{ shape: text, label: "redirige *.arcodange.fr<br>→ Cloudflared Tunnel" }
end
end
%% Bureaux Arcodange
subgraph "Bureaux Arcodange"
direction TB
Livebox["📡 Livebox<br>(passerelle)"]:::arcodange
MacBook["💻 MacBook Pro M4"]:::arcodange
%% Cluster Raspberry Pi
subgraph "Cluster Raspberry Pi"
direction LR
%% Noeud pi1
subgraph "pi1"
Pi1["🐧 pi1"]:::k3sServer
subgraph "K3s (Server)"
Traefik["🚀 Traefik"]:::traefik
Cloudflared["☁️ Cloudflared Tunnel"]:::cloudflared
end
PiHole1["🧹 Pi-hole"]:::dns
GiteaRunner1["🏃 Gitea Act Runner"]:::gitea
NS1@{ shape: text, label: "nameserver : pi3" }
DNSArcodange@{ shape: text, label: ".arcodange.lab<br>→ Traefik", color:#cccc00 }
end
%% Noeud pi2
subgraph "pi2"
Pi2["🐧 pi2"]:::k3sAgent
Gitea["🦊 Gitea"]:::gitea
subgraph "K3s (Agent)"
CMS["📝 CMS"]:::cms
end
end
%% Noeud pi3
subgraph "pi3"
Pi3["🐧 pi3"]:::k3sAgent
PiHole3["🧹 Pi-hole"]:::dns
GiteaRunner3["🏃 Gitea Act Runner"]:::gitea
NS3@{ shape: text, label: "nameserver : pi1" }
end
end
end
%% Connexions Pi-hole → Livebox (DNS)
PiHole1 -.->|DNS| Livebox
PiHole3 -.->|DNS| Livebox
%% Cloudflare → Cloudflared Tunnel → Traefik
CloudflareService -.-> Cloudflared
Cloudflared --> Traefik
%% Livebox → Traefik (HTTPS)
Livebox -->|redirige HTTPS| Traefik
%% Gitea Act Runner → Gitea
GiteaRunner1 -.->|communication| Gitea
GiteaRunner3 -.->|communication| Gitea
%% Domains → CMS via Traefik
Traefik -->|cms-rec.arcodange.fr| CMS:::domainFR
Traefik -->|www.arcodange.lab| CMS:::domainLAB
```
### with security
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#333333', 'edgeLabelBackground':'#f0f0f0', 'tertiaryColor': '#05e2ff1c'}}}%%
graph LR
%% Styles
classDef internet fill:#f9f9f9,stroke:#999,color:#333,font-size:12px;
classDef arcodange fill:#e6e6e6,stroke:#555,color:#333,font-size:12px;
classDef k3sServer fill:#ff9999,stroke:#cc0000,color:#333,font-size:12px;
classDef k3sAgent fill:#99ff99,stroke:#00cc00,color:#333,font-size:12px;
classDef traefik fill:#9999ff,stroke:#0000cc,color:#333,font-size:12px;
classDef dns fill:#ffff99,stroke:#cccc00,color:#333,font-size:12px;
classDef cloudflared fill:#ccccff,stroke:#6666cc,color:#333,font-size:12px;
classDef gitea fill:#ffcc99,stroke:#cc9966,color:#333,font-size:12px;
classDef cms fill:#99ffcc,stroke:#66cc99,color:#333,font-size:12px;
classDef domainFR fill:#99ccff,stroke:#0066cc,color:#333,font-size:12px;
classDef domainLAB fill:#ccff99,stroke:#66cc66,color:#333,font-size:12px;
classDef crowdsec fill:#ff99cc,stroke:#cc6699,color:#333,font-size:12px;
classDef security fill:#ffcccc,stroke:#cc9999,color:#333,font-size:12px;
%% Internet
subgraph "Internet / Le monde"
direction TB
subgraph "Cloudflare"
CloudflareService["🛡️ Cloudflare<br>(cloudflare.com)"]:::internet
DNSNote@{ shape: text, label: "redirige *.arcodange.fr<br>→ Cloudflared Tunnel" }
end
end
%% Bureaux Arcodange
subgraph "Bureaux Arcodange"
direction TB
Livebox["📡 Livebox<br>(passerelle)"]:::arcodange
DNSArcodange@{ shape: text, label: ".arcodange.lab<br>→ Traefik" }
%% Cluster Raspberry Pi
subgraph "Cluster Raspberry Pi"
direction LR
%% Noeud pi1
subgraph "pi1"
Pi1["🐧 pi1"]:::k3sServer
subgraph "K3s (Server)"
Traefik["🚀 Traefik"]:::traefik
Cloudflared["☁️ Cloudflared Tunnel"]:::cloudflared
end
PiHole1["🧹 Pi-hole"]:::dns
GiteaRunner1["🏃 Gitea Act Runner"]:::gitea
NS1@{ shape: text, label: "nameserver : pi3" }
end
%% Noeud pi2
subgraph "pi2"
Pi2["🐧 pi2"]:::k3sAgent
Gitea["🦊 Gitea"]:::gitea
subgraph "K3s (Agent)"
CMS["📝 CMS"]:::cms
end
end
%% Noeud pi3
subgraph "pi3"
Pi3["🐧 pi3"]:::k3sAgent
PiHole3["🧹 Pi-hole"]:::dns
GiteaRunner3["🏃 Gitea Act Runner"]:::gitea
subgraph "K3s (Agent)"
CrowdSec["🔒 CrowdSec"]:::crowdsec
end
NS3@{ shape: text, label: "nameserver : pi1" }
end
end
%% Sécurité combinée
subgraph "Sécurité"
Turnstile["🛡️ Cloudflared Turnstile<br>(CAPTCHA)"]:::security
BannedIPs["🚫 CrowdSec<br>(IP bannies)"]:::security
end
end
%% Connexions Pi-hole → Livebox (DNS)
PiHole1 -.->|DNS| Livebox
PiHole3 -.->|DNS| Livebox
%% Internet → Livebox
CloudflareService --> Livebox
%% Gitea Act Runner → Gitea
GiteaRunner1 -.->|communication| Gitea
GiteaRunner3 -.->|communication| Gitea
%% Cloudflare → Cloudflared Tunnel → Sécurité → Traefik
CloudflareService -.-> Cloudflared
Cloudflared --> Turnstile
Turnstile --> BannedIPs
BannedIPs --> Traefik
%% Livebox → Sécurité → Traefik
Livebox --> Turnstile
%% Domaines → Gitea/CMS via Traefik
Traefik -->|gitea.arcodange.fr| Gitea:::domainFR
Traefik -->|gitea.arcodange.lab| Gitea:::domainLAB
Traefik -->|cms-rec.arcodange.fr| CMS:::domainFR
Traefik -->|www.arcodange.lab| CMS:::domainLAB
```
## CI/CD Architecture
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#333333', 'edgeLabelBackground':'#f0f0f0', 'tertiaryColor': '#e67e22'}}}%%
graph TD
%% Styles
classDef gitea fill:#ffcc99,stroke:#cc9966,color:#333;
classDef argocd fill:#99ffcc,stroke:#66cc99,color:#333;
classDef vault fill:#ccccff,stroke:#6666cc,color:#333;
classDef k3s fill:#ff9999,stroke:#cc0000,color:#333;
classDef runner fill:#ffff99,stroke:#cccc00,color:#333;
%% Components
Gitea["Gitea (pi2)"]:::gitea
ArgoCD["ArgoCD (k3s)"]:::argocd
Vault["Vault (k3s/tools)"]:::vault
Runner1["Gitea Act Runner (pi1)"]:::runner
Runner2["Gitea Act Runner (pi3)"]:::runner
VaultOperator["Vault Secrets Operator (k3s/tools)"]:::vault
k3s["k3s Cluster"]:::k3s
WebApp["WebApp (k3s)"]:::k3s
%% Workflow
Gitea -->|OIDC Auth| Vault
Gitea -->|Trigger CI| Runner1
Gitea -->|Trigger CI| Runner2
Runner1 -->|Deploy to| k3s
Runner2 -->|Deploy to| k3s
ArgoCD -->|GitOps Sync| Gitea
ArgoCD -->|Deploy Apps| k3s
VaultOperator -->|Inject Secrets| WebApp
Vault -->|Secrets| VaultOperator
WebApp -->|Uses| VaultOperator
%% Annotations
linkStyle 0,1,2,3,4,5,6,7 stroke:#999,stroke-width:1px;
%% Notes
note1["Custom runner image includes<br>.lab CA cert"]
Runner1 --> note1
Runner2 --> note1
%% OIDC Workflow
note2["OIDC Flow:<br>1. Runner executes vault_oauth__sh_b64<br>2. Obtains ID token from Gitea<br>3. Authenticates with Vault"]
Runner1 --> note2
Runner2 --> note2
```