13 Commits

Author SHA1 Message Date
827af6b392 Merge pull request 'docs(runbooks): runbook « mettre en service une nouvelle application web »' (#9) from claude/new-web-app-runbook into main 2026-05-31 17:25:37 +02:00
8330d82225 docs(runbooks): add "new web app" setup runbook under doc/runbooks/
Document, as a tree-docs tree, the end-to-end procedure to stand up a new
web application on the Arcodange platform — a mechanic spread across the
factory, tools and app repos with non-trivial ordering dependencies.

Covers: Gitea repo creation (org-secret inheritance), Postgres DB + owner
role (factory/postgres/iac), platform Vault declaration (gitea_cicd_<app>
+ policies, tools/hashicorp-vault/iac), the app Helm chart (VSO dynamic
secrets via pgbouncer), the app Terraform (app_roles module), the CI
workflows (tofu apply + image build, incl. the copy-pasted role pitfall),
and ArgoCD registration (factory/argocd/values.yaml). Adds a naming-
conventions concept page and an ordered checklist.

Wires the legacy doc/adr "setup hello world web app" item and the factory
README to the runbook. New docs live under doc/ (singular) per the PR #8
convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:22:30 +02:00
54b3092305 Merge pull request 'fix(docs): place ADR under doc/adr (singular) per convention' (#8) from fix/adr-path-doc-singular into main
Reviewed-on: #8
2026-05-09 15:29:22 +02:00
e0fb337a5f docs: place new ADR under doc/adr (singular) per convention
The 20260509 ADR landed in docs/adr/ (plural) by mistake. Convention
is doc/adr/ (alongside the existing 00_*, 01_*, … docs and the
network-architecture/cicd-architecture ADRs that pre-existed there).

Note : 20260407-*.md files in the typo'd docs/adr/ are still untracked
(never committed) — separate cleanup task.
2026-05-09 14:25:37 +02:00
ea500abe62 Merge pull request 'docs(adr): telegram-gateway auth (Phase 1.5)' (#7) from docs/telegram-gateway-auth-adr into main
Reviewed-on: #7
2026-05-09 14:22:12 +02:00
62673a2d65 docs(adr): telegram-gateway auth (Phase 1.5)
Documents the authentication layer added to telegram-gateway in
Phase 1.5 :

- principal bot @arcodange_factory_bot (handler=auth) gère /auth, /whoami, /logout
- session Redis 24h keyed by Telegram from.id (TTL via AUTH_SESSION_TTL)
- allowlist optionnelle (ALLOWED_USERS) — silent drop avant la gate
- requireAuth secure-by-default (true), opt-out explicite par bot
- handler=auth force requireAuth=false (chicken-and-egg)

Cross-links bidirectionnels avec le code (Gitea URLs vers
arcodange/telegram-gateway), AUTH.md (user-facing) et HOWTO_ADD_BOT.md
(Cas 2 mis à jour). Diagrammes mermaid avec contrastes explicites.
2026-05-09 13:58:27 +02:00
4163b06659 Merge pull request 'argocd: add telegram-gateway application' (#6) from feat/homelab-gateway-app into main
Reviewed-on: #6
2026-05-09 12:41:49 +02:00
3fb7544351 argocd: rename homelab-gateway → telegram-gateway
Aligns with the upstream repo rename
(arcodange/homelab-gateway → arcodange/telegram-gateway) so the name
matches the public URL tg.arcodange.fr and Arcodange's naming
conventions.
2026-05-09 12:35:37 +02:00
5038956332 argocd: add homelab-gateway application
Adds the homelab-gateway Argo CD Application pointing at
arcodange/homelab-gateway (user space, like dance-lessons-coach).

Image Updater watches gitea.arcodange.lab/arcodange/homelab-gateway:latest
with digest strategy.

Phase 1 of the Telegram webhook gateway — a long-running pod that
receives webhooks (no more polling) and routes per-bot to handler
implementations. Initial bot: @arcodange_factory_bot, slug=factory,
echo handler.
2026-05-09 12:25:30 +02:00
6ede249da9 🔒 fix(ansible): gate vault auth disable behind vault_oidc_force_reset (default off) (#5)
Co-authored-by: Gabriel Radureau <arcodange@gmail.com>
Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
2026-05-06 15:03:33 +02:00
9e821e1626 ♻️ refactor(ansible): move gitea secret user-propagation list to inventory (#4)
Co-authored-by: Gabriel Radureau <arcodange@gmail.com>
Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
2026-05-06 14:48:05 +02:00
69b7e9ddcb Merge remote-tracking branch 'origin/main' 2026-05-06 14:38:01 +02:00
069edd72f1 chore(cicd): drop temporary commented-out tasks from 03_cicd.yml
Removes the commented PACKAGES_TOKEN/HOMELAB_CA_CERT blocks and the legacy
"Deploy Argo CD" play that were left behind during the migration to
Helm-based ArgoCD.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:37:48 +02:00
19 changed files with 1332 additions and 237 deletions

View File

@@ -80,4 +80,9 @@ classDef done fill:gold,stroke:indigo,stroke-width:4px,color:blue;
class prepare_hd,nodeId2 done;
```
## Documentation
- 📚 [`doc/`](doc/README.md) — ADR (décisions d'architecture) + runbooks.
- 🚀 [Runbook : mettre en service une nouvelle application web](doc/runbooks/new-web-app/README.md) — dépôt Gitea, base de données, Vault, chart Helm, Terraform, CI, ArgoCD.
🏹💻🪽

View File

@@ -0,0 +1,11 @@
---
# Gitea ownership configuration consumed by playbooks running on `localhost`
# (e.g. tools/hashicorp_vault.yml). Role-level defaults (gitea_username,
# gitea_organization) live in roles/gitea_secret/defaults/main.yml ; this file
# is for fact lists that the inventory should declare.
# Users (Gitea owner_type=user) to which org-level Gitea Action secrets must
# also be propagated. Repos owned by these users cannot read org-level secrets,
# so the secret propagation playbook iterates over this list.
gitea_secret_propagation_users:
- arcodange

View File

@@ -157,235 +157,3 @@
loop: ["absent", "present"]
loop_control:
loop_var: docker_compose_down_then_up
# - name: Set PACKAGES_TOKEN secret to upload packages from CI
# run_once: True
# block:
# - name: Generate cicd PACKAGES_TOKEN
# include_role:
# name: arcodange.factory.gitea_token
# vars:
# gitea_token_name: PACKAGES_TOKEN
# gitea_token_fact_name: cicd_PACKAGES_TOKEN
# gitea_token_scopes: write:package
# gitea_token_replace: true
# - name: Register cicd PACKAGES_TOKEN secrets
# include_role:
# name: arcodange.factory.gitea_secret
# vars:
# gitea_secret_name: PACKAGES_TOKEN
# gitea_secret_value: "{{ cicd_PACKAGES_TOKEN }}"
# loop: ["organization", "user"]
# loop_control:
# loop_var: gitea_owner_type # Peut être "user" ou "organization"
# - name: Set HOMELAB_CA_CERT secret to validate self signed ssl
# run_once: True
# block:
# - name: Download homelab CA certificate
# ansible.builtin.uri:
# url: "https://ssl-ca.arcodange.lab:8443/roots.pem"
# return_content: yes
# validate_certs: no
# register: homelab_ca_cert
# - name: Debug cert
# debug:
# msg: "{{ homelab_ca_cert.content }}..."
# - name: Register cicd HOMELAB_CA_CERT secrets
# include_role:
# name: arcodange.factory.gitea_secret
# vars:
# gitea_secret_name: HOMELAB_CA_CERT
# gitea_secret_value: "{{ homelab_ca_cert.content | b64encode }}"
# loop: ["organization", "user"]
# loop_control:
# loop_var: gitea_owner_type # Peut être "user" ou "organization"
# post_tasks:
# - include_role:
# name: arcodange.factory.gitea_token
# vars:
# gitea_token_delete: true
# - name: Deploy Argo CD
# hosts: localhost
# roles:
# - role: arcodange.factory.gitea_token # generate gitea_api_token used to replace generated token with set name if required
# tags:
# - gitea_sync
# tasks:
# - name: Set factory repo
# include_role:
# name: arcodange.factory.gitea_repo
# vars:
# gitea_repo_name: factory
# - name: Sync other repos
# tags: gitea_sync
# include_role:
# name: arcodange.factory.gitea_sync
# apply:
# tags: gitea_sync
# - name: Generate Argo CD token
# include_role:
# name: arcodange.factory.gitea_token
# vars:
# gitea_token_name: ARGOCD_TOKEN
# gitea_token_fact_name: argocd_token
# gitea_token_scopes: read:repository,read:package
# gitea_token_replace: true
# - name: Figure out k3s master node
# shell:
# kubectl get nodes -l node-role.kubernetes.io/control-plane=true -o name | sed s'#node/##'
# register: get_k3s_master_node
# changed_when: false
# - name: Get kubernetes server internal url
# command: >-
# echo https://kubernetes.default.svc
# # {%raw%}
# # kubectl get svc/kubernetes -o template="{{.spec.clusterIP}}:{{(index .spec.ports 0).port}}"
# # {%endraw%}
# register: get_k3s_internal_server_url
# changed_when: false
# - set_fact:
# k3s_master_node: "{{ get_k3s_master_node.stdout }}"
# k3s_internal_server_url: "{{ get_k3s_internal_server_url.stdout }}"
# - name: Read Step CA root certificate from k3s master
# become: true
# delegate_to: "{{ k3s_master_node }}"
# slurp:
# src: /home/step/.step/certs/root_ca.crt
# register: step_ca_root_cert
# - name: Decode Step CA root certificate
# set_fact:
# step_ca_root_cert_pem: "{{ step_ca_root_cert.content | b64decode }}"
# - name: Install Argo CD
# become: true
# delegate_to: "{{ k3s_master_node }}"
# vars:
# gitea_credentials:
# username: arcodange
# password: "{{ argocd_token }}"
# argocd_helm_values: # https://github.com/argoproj/argo-helm/blob/main/charts/argo-cd/values.yaml
# global:
# domain: argocd.arcodange.lab
# configs:
# cm:
# kustomize.buildOptions: "--enable-helm"
# helm.enablePostRenderer: "true"
# exec.enabled: "true"
# params:
# server.insecure: true # let k3s traefik do TLS termination
# ansible.builtin.copy:
# dest: /var/lib/rancher/k3s/server/manifests/argocd.yaml
# content: |-
# apiVersion: v1
# kind: Namespace
# metadata:
# name: argocd
# ---
# apiVersion: v1
# kind: ConfigMap
# metadata:
# name: argocd-tls-certs-cm
# namespace: argocd
# data:
# gitea.arcodange.lab: |
# {{ step_ca_root_cert_pem | indent(4) }}
# ---
# apiVersion: helm.cattle.io/v1
# kind: HelmChart
# metadata:
# name: argocd
# namespace: kube-system
# spec:
# repo: https://argoproj.github.io/argo-helm
# chart: argo-cd
# targetNamespace: argocd
# valuesContent: |-
# {{ argocd_helm_values | to_nice_yaml | indent( width=4 ) }}
# ---
# apiVersion: networking.k8s.io/v1
# kind: Ingress
# metadata:
# name: argocd-server-ingress
# namespace: argocd
# annotations:
# # For Traefik v2.x
# traefik.ingress.kubernetes.io/router.entrypoints: websecure
# traefik.ingress.kubernetes.io/router.tls: "true"
# 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.sans: argocd.arcodange.lab
# traefik.ingress.kubernetes.io/router.middlewares: localIp@file
# spec:
# rules:
# - host: argocd.arcodange.lab
# http:
# paths:
# - path: /
# pathType: Prefix
# backend:
# service:
# name: argocd-server
# port:
# number: 80 #TLS is terminated at Traefik
# ---
# apiVersion: v1
# kind: Secret
# metadata:
# name: gitea-arcodangeorg-factory-repo
# namespace: argocd
# labels:
# argocd.argoproj.io/secret-type: repository
# stringData:
# type: git
# url: https://gitea.arcodange.lab/arcodange-org/factory
# ---
# apiVersion: v1
# kind: Secret
# metadata:
# name: gitea-arcodangeorg-repo-creds
# namespace: argocd
# labels:
# argocd.argoproj.io/secret-type: repo-creds
# stringData:
# type: git
# url: https://gitea.arcodange.lab/arcodange-org
# password: {{ gitea_credentials.password }}
# username: {{ gitea_credentials.username }}
# ---
# apiVersion: argoproj.io/v1alpha1
# kind: Application
# metadata:
# name: factory
# namespace: argocd
# spec:
# project: default
# source:
# repoURL: https://gitea.arcodange.lab/arcodange-org/factory
# targetRevision: HEAD
# path: argocd
# destination:
# server: {{ k3s_internal_server_url }}
# namespace: argocd
# syncPolicy:
# automated:
# prune: true
# selfHeal: true
# - name: touch manifests/argocd.yaml to trigger update
# delegate_to: "{{ k3s_master_node }}"
# ansible.builtin.file:
# path: /var/lib/rancher/k3s/server/manifests/argocd.yaml
# state: touch
# become: true
# post_tasks:
# - include_role:
# name: arcodange.factory.gitea_token
# apply:
# tags: gitea_sync
# tags:
# - gitea_sync
# vars:
# gitea_token_delete: true

View File

@@ -36,6 +36,11 @@
# WARNING : this disables AND wipes ALL gitea_cicd_* per-app JWT roles
# (created by tools/hashicorp-vault/iac/) every time it runs. Default is OFF
# to preserve those roles across normal ansible runs ; opt-in only when you
# really want to rebuild the OIDC backend from scratch (e.g. config drift on
# bound_issuer or similar).
- name: Delete existing Gitea OIDC backends if they exist
include_tasks: vault_cmd.yml
vars:
@@ -48,6 +53,7 @@
- gitea_jwt
loop_control:
loop_var: backend_name
when: vault_oidc_force_reset | default(false) | bool
- name: use tofu to provision vault
block:
@@ -123,7 +129,6 @@
}) | b64encode }}
gitea_owner_type: 'user'
gitea_owner_name: '{{ item }}'
loop:
- arcodange
loop: '{{ gitea_secret_propagation_users }}'
loop_control:
label: '{{ item }}'

View File

@@ -14,6 +14,11 @@ gitea_applications:
annotations:
argocd-image-updater.argoproj.io/image-list: webapp=gitea.arcodange.lab/arcodange-org/webapp:latest
argocd-image-updater.argoproj.io/webapp.update-strategy: digest
telegram-gateway:
org: arcodange
annotations:
argocd-image-updater.argoproj.io/image-list: telegram-gateway=gitea.arcodange.lab/arcodange/telegram-gateway:latest
argocd-image-updater.argoproj.io/telegram-gateway.update-strategy: digest
erp:
annotations: {}
cms:

35
doc/README.md Normal file
View File

@@ -0,0 +1,35 @@
[Factory](../README.md) > **Doc**
# Documentation Factory
> **Last Updated:** 2026-05-31
> **Status:** Production · maintenue activement
> **Related:** [README racine du dépôt](../README.md) · [Collection Ansible `arcodange.factory`](../ansible/arcodange/factory/README.md)
## C'est quoi ?
Le dossier `doc/` rassemble la documentation de la plateforme Arcodange (k3s + Gitea + ArgoCD + OpenTofu + Vault + Postgres, auto-hébergée sur 3 Raspberry Pi). On y trouve deux familles :
- les **ADR** (records de décisions d'architecture), qui expliquent *pourquoi* la plateforme est faite ainsi ;
- les **runbooks**, qui expliquent *comment* exécuter une procédure opérationnelle de bout en bout.
> [!NOTE]
> Convention du dépôt (cf. PR #8) : la documentation vit sous `doc/` (**singulier**). Un ancien dossier `docs/` (pluriel) a pu traîner non-suivi par git — ne pas y ajouter de contenu.
## Sections
| Section | Ce qu'on y trouve | Statut |
|---|---|---|
| [Runbooks](runbooks/README.md) | Procédures opérationnelles pas-à-pas (créer une app, etc.) | ✅ |
| [ADR](adr/README.md) | Décisions d'architecture + checklist de mise en place de la plateforme | ✅ |
## Légende de statut
✅ actif · 🟡 dégradé/beta · 🔴 critique/EOL · ⚠️ problème connu · ❌ désactivé
## Comment éditer cette documentation
1. **Ajouter une page** → la créer depuis le template adéquat **et** ajouter sa ligne dans la table d'index du `README.md` parent.
2. **Supprimer une page** → la marquer *Décommissionnée (date)* d'abord ; supprimer le fichier et sa ligne d'index ensemble une fois qu'elle est vraiment partie.
3. **Garder les liens croisés bidirectionnels** → quand on lie A→B, ajouter B→A.
4. **Mettre à jour `Last Updated:`** en tête de la racine concernée après tout changement de structure.

View File

@@ -0,0 +1,261 @@
[← ADRs](.) · [factory](../..) · **20260509 — telegram-gateway auth**
> **Cross-references** (bidirectionnel : chaque fichier listé doit citer cette ADR en tête)
>
> - **Code** (repo `arcodange/telegram-gateway`) :
> [`auth.go`](https://gitea.arcodange.lab/arcodange/telegram-gateway/src/branch/main/auth.go) ·
> [`handler_auth.go`](https://gitea.arcodange.lab/arcodange/telegram-gateway/src/branch/main/handler_auth.go) ·
> [`allowlist.go`](https://gitea.arcodange.lab/arcodange/telegram-gateway/src/branch/main/allowlist.go) ·
> [`server.go`](https://gitea.arcodange.lab/arcodange/telegram-gateway/src/branch/main/server.go) ·
> [`chart/values.yaml`](https://gitea.arcodange.lab/arcodange/telegram-gateway/src/branch/main/chart/values.yaml)
> - **User docs** :
> [`AUTH.md`](https://gitea.arcodange.lab/arcodange/telegram-gateway/src/branch/main/AUTH.md) ·
> [`HOWTO_ADD_BOT.md`](https://gitea.arcodange.lab/arcodange/telegram-gateway/src/branch/main/HOWTO_ADD_BOT.md)
> - **Related ADR** :
> [`20260407-network-architecture.md`](20260407-network-architecture.md) (Cloudflare / Traefik / CrowdSec stack)
> - **Implementation plan** : `~/.claude/plans/pour-les-notifications-on-inherited-seal.md` § Phase 1.5
# ADR 20260509: Telegram Gateway — Authentication Layer
## Status
Proposed
## Context
Le service `telegram-gateway` (Phase 1, livré le 2026-05-09) expose des bots Telegram via webhooks publics sur `tg.arcodange.fr/bot/<slug>`. À ce stade :
- Tout utilisateur Telegram qui connaît le handle d'un bot peut le DM et déclencher son handler.
- Le gateway valide le `secret_token` Telegram (qui prouve que **Telegram** envoie le webhook), pas l'identité du **user** derrière le message.
- Avant d'ouvrir le gateway à d'autres bots utiles (commandes `/build`, scripts Ollama, etc.), il faut un protocole d'authentification.
Le besoin métier :
- Un **bot principal** (`@arcodange_factory_bot`, slug interne `factory`) sert de point d'auth.
- Une commande **`/auth <code>`** valide une session pour l'utilisateur Telegram qui l'envoie.
- Les autres bots du gateway ne répondent qu'aux **utilisateurs déjà authentifiés**, **par défaut** (secure-by-default).
- En garde-fou supplémentaire, une **allowlist d'IDs Telegram** peut filtrer les utilisateurs autorisés à parler aux bots, indépendamment de l'auth (silent-drop avant tout traitement).
## Decision
### 1. Identité utilisateur
Telegram n'expose **pas l'IP** de l'utilisateur côté bot. La clé stable est **`from.id`** (Telegram user ID, `int64`, identique pour un même compte sur tous les devices). On l'utilise comme identifiant de session.
> Hors scope : auth liée au device/IP — nécessiterait un canal d'auth séparé (web UI sur LAN, etc.).
### 2. Stockage de session
- **Redis** (`redis.tools.svc.cluster.local:6379`, déjà déployé dans le namespace `tools`).
- Clé : `tg-gw:auth:<from.id>` → valeur `1` (ou JSON metadata si on enrichit plus tard).
- TTL : **24 h par défaut**, configurable via env `AUTH_SESSION_TTL` (Go duration : `12h`, `7d`, etc.).
- Refresh : chaque `/auth` réussi remet le TTL à zéro.
### 3. Bot principal & commandes
Le bot `factory` passe du handler `echo` au handler `auth`. Le handler `auth` reconnaît :
| Commande | Effet |
|---|---|
| `/start` | Message d'accueil + liste des commandes disponibles |
| `/auth <code>` | Compare `<code>` à `AUTH_SECRET` en constant-time ; si OK → SET Redis, deleteMessage du message original (replay defense), reply "✅ Authentifié pour 24 h" |
| `/whoami` | Affiche le user_id et le TTL restant (ou "non authentifié") |
| `/logout` | DEL Redis, reply "Déconnecté" |
| _autre_ | Rappel des commandes |
### 4. Garde-fou allowlist
Env `ALLOWED_USERS` : CSV de `from.id` Telegram (`12345,67890`). Comportement :
- Vide ou absent → ouvert à tous (rétro-compat Phase 1).
- Set → tout `from.id` hors-liste fait l'objet d'un **silent-drop** (HTTP 200 vide vers Telegram, log INFO côté gateway, **pas de réponse au user**).
- Le silent-drop intervient **avant** la gate auth. Permet de masquer l'existence des bots à des inconnus.
### 5. Gate `requireAuth` par bot — secure-by-default
Champ booléen dans `chart/values.yaml`, par bot. Sémantique :
- **Default = `true`** (secure-by-default). Tout bot omet ce champ → gated.
- Pour rendre un bot public, ajout explicite `requireAuth: false`.
- Pour `handler: auth` (le bot principal), `requireAuth` est **forcé à `false`** automatiquement (chicken-and-egg : si l'auth elle-même est gated, personne ne peut s'authentifier).
```yaml
bots:
factory:
handler: auth # requireAuth auto-forcé à false
pingbot:
handler: echo # requireAuth: true (implicite, défaut)
statusbot:
handler: echo
requireAuth: false # opt-out explicite, bot public
```
Lorsque `requireAuth: true` et que le user n'est pas authentifié :
> 🔒 Authentifie-toi d'abord avec `/auth <code>` chez @arcodange_factory_bot
… puis ack 200 à Telegram. Le handler du bot n'est **pas** appelé.
### 6. Fail-at-startup
Si `AUTH_SECRET` est vide ET au moins un bot a `handler=auth` ou `requireAuth: true` (y compris par défaut) → le pod **échoue au boot** avec un message clair. Évite le scénario "auth silencieusement off, bots accessibles à tous sans le savoir". Avec un défaut `requireAuth: true`, en pratique tout déploiement exige `AUTH_SECRET` (sauf si tous les bots font opt-out explicite).
## Architecture Diagrams
### 1. Flow `/auth` (login)
```mermaid
%%{init: {'theme':'neutral'}}%%
sequenceDiagram
participant U as Utilisateur
participant TG as Telegram
participant GW as telegram-gateway
participant R as Redis (tools)
U->>TG: /auth s3cr3t (DM @arcodange_factory_bot)
TG->>GW: POST /bot/factory<br/>X-Telegram-Bot-Api-Secret-Token: …
GW->>GW: verify secret_token (Telegram→GW)
GW->>GW: check ALLOWED_USERS (si configuré)
GW->>GW: factory.handler = auth, parse "/auth s3cr3t"
GW->>GW: subtle.ConstantTimeCompare(s3cr3t, AUTH_SECRET)
alt Code valide
GW->>R: SET tg-gw:auth:<from.id> EX 24h
GW->>TG: deleteMessage (replay defense)
GW->>TG: sendMessage "✅ Authentifié pour 24h"
GW->>TG: 200 OK (ack webhook)
TG->>U: "✅ Authentifié pour 24h"
else Code invalide
GW->>TG: sendMessage "❌ Mauvais code"
GW->>TG: 200 OK
TG->>U: "❌ Mauvais code"
end
```
### 2. Accès à un bot gated (`requireAuth: true`, défaut)
```mermaid
%%{init: {'theme':'neutral'}}%%
sequenceDiagram
participant U as Utilisateur
participant TG as Telegram
participant GW as telegram-gateway
participant R as Redis
participant H as Bot handler (echo / http / shell…)
U->>TG: ping (DM @autre_bot)
TG->>GW: POST /bot/autre_bot
GW->>GW: verify secret_token + parse Update
GW->>GW: ALLOWED_USERS check
GW->>R: EXISTS tg-gw:auth:<from.id>
alt Authentifié
R-->>GW: 1
GW->>H: Handler.Handle(update, bot)
H->>TG: sendMessage (réponse métier)
GW->>TG: 200 OK
else Non authentifié
R-->>GW: 0
GW->>TG: sendMessage "🔒 /auth chez @arcodange_factory_bot"
GW->>TG: 200 OK
end
```
### 3. Décision globale à l'arrivée d'un webhook
```mermaid
%%{init: {'theme':'neutral'}}%%
graph TD
%% classDef avec contraste explicite : fond clair → texte sombre
classDef ok fill:#d4edda,stroke:#28a745,color:#155724;
classDef block fill:#f8d7da,stroke:#dc3545,color:#721c24;
classDef neutral fill:#e2e3e5,stroke:#6c757d,color:#383d41;
Start[Webhook POST /bot/&lt;slug&gt;]:::neutral
SecretCheck{secret_token<br/>match ?}:::neutral
AllowlistCheck{from.id ∈<br/>ALLOWED_USERS ?}:::neutral
HandlerKind{handler == auth ?}:::neutral
AuthGate{requireAuth ?<br/>+ session valide ?}:::neutral
Reject401[401 Unauthorized]:::block
SilentDrop[200 vide<br/>silent drop]:::block
Forbidden[reply &quot;🔒 /auth …&quot;<br/>200 OK]:::block
AuthHandler[handler auth<br/>/auth /whoami /logout]:::ok
BotHandler[Bot handler<br/>echo / http / shell]:::ok
Start --> SecretCheck
SecretCheck -- non --> Reject401
SecretCheck -- oui --> AllowlistCheck
AllowlistCheck -- non --> SilentDrop
AllowlistCheck -- oui --> HandlerKind
HandlerKind -- oui --> AuthHandler
HandlerKind -- non --> AuthGate
AuthGate -- pas autorisé --> Forbidden
AuthGate -- OK --> BotHandler
```
## Consequences
### Positive
- **Confidentialité** : les bots métier ne répondent qu'aux comptes Telegram authentifiés, **par défaut**.
- **Défense en profondeur** : `ALLOWED_USERS` (allowlist), `secret_token` (Telegram→GW), `AUTH_SECRET` (user→bot), TTL session.
- **UX simple** : un `/auth <code>` ponctuel, valide 24 h.
- **Pas de migration** côté Phase 2/3 : la gate s'insère cleanly avant l'enqueue ou le forward.
- **Replay defense** : le message contenant le code est supprimé du chat après login réussi.
- **Secure-by-default** : un nouveau bot ajouté au gateway exige une session sans rien à configurer.
### Negative
- **Code partagé** : `AUTH_SECRET` global (pas TOTP/per-user). Si compromis → rotation manuelle (changer Secret + redeploy).
- **Pas de rate-limit** sur `/auth` : un utilisateur dans `ALLOWED_USERS` peut bruteforce le code en pratique. Mitigation : `ALLOWED_USERS` agit en floor, et 128+ bits de code rendent le bruteforce inutile dans la fenêtre de TTL.
- **Dépendance Redis** : si Redis tombe, plus aucun user n'est considéré authentifié → tous les bots gated répondent "🔒". Acceptable (fail-closed) ; Phase 1 a déjà restauré Redis cleanly.
- **Pas de session multi-device explicite** : `from.id` est le même sur tous les devices d'un compte → l'auth couvre déjà tous les devices, ce qui est le comportement attendu.
## Alternatives Considered
### Alternative 1 : auth par IP
**Rejetée**. Telegram n'expose pas l'IP du user au bot. Aurait nécessité un canal d'auth secondaire (web UI sur LAN, page d'accueil arcodange.fr) et un binding device. Coût significatif pour un bénéfice ambigu.
### Alternative 2 : TOTP / OTP rotatif
**Rejetée à ce stade**. Plus sécurisé que le code partagé mais ajoute :
- Une étape d'enrôlement (afficher un QR code, scanner avec une app).
- Une horloge synchronisée côté gateway et côté user.
- De la complexité utilisateur (sortir l'app à chaque /auth).
À reconsidérer si le code partagé fuit régulièrement ou si on ouvre à plus d'utilisateurs.
### Alternative 3 : Postgres au lieu de Redis pour les sessions
**Rejetée**. Postgres serait nécessaire pour Phase 2 (queue durable), mais pour des sessions à TTL court, Redis est l'outil idiomatique :
- Latence sub-ms.
- TTL natif (`SET … EX 86400`).
- Déjà déployé et utilisé (CrowdSec bouncer).
### Alternative 4 : pas de session, vérification du code à chaque message
**Rejetée**. UX terrible (devoir re-taper le code à chaque DM) et n'apporte rien (le code en clair traîne plus longtemps en chat).
### Alternative 5 : `requireAuth: false` par défaut (insecure-by-default)
**Rejetée** (initialement retenue, puis renversée). Avoir `requireAuth: false` par défaut signifie qu'un bot ajouté sans précaution est accessible à tous. Avec un gateway pensé "private by design", le défaut sécurisé `true` cadre bien mieux.
## Plan d'implémentation
Voir `~/.claude/plans/pour-les-notifications-on-inherited-seal.md` § Phase 1.5.
Résumé des fichiers touchés :
- **Nouveaux** (repo `arcodange/telegram-gateway`) : `auth.go`, `handler_auth.go`, `allowlist.go`, `AUTH.md`
- **Modifiés** : `telegram_types.go`, `telegram.go`, `handlers.go`, `config.go`, `server.go`, `main.go`, `go.mod`, `chart/values.yaml`, `chart/templates/deployment.yaml`, `HOWTO_ADD_BOT.md`
- **Cluster** : `kubectl patch secret telegram-gateway-bots` pour ajouter `AUTH_SECRET` et (optionnel) `ALLOWED_USERS`
## Success Metrics
- `/auth <wrong>` → 100 % refus, 0 SET Redis.
- `/auth <right>` → 100 % succès, deleteMessage best-effort exécuté.
- Bot avec `requireAuth: true` (défaut) répond le message "🔒 …" à 100 % des users non authentifiés.
- Session expire effectivement après TTL (vérif via `kubectl exec redis-0 -- redis-cli TTL …`).
- Aucun secret (code, token bot) dans les logs.
- Latence ajoutée par la gate < 5 ms (Redis EXISTS local).

View File

@@ -18,9 +18,9 @@
- [ ] terrakube
- [ ] prometheus/grafana
- [ ] ansible AWX
- [ ] setup hello world web app
- [ ] manage postgres credentials
- [ ] protect public endpoint (crowdsec)
- [ ] setup hello world web app — 📖 procédure complète : [runbook « Nouvelle application web »](../runbooks/new-web-app/README.md)
- [ ] manage postgres credentials → [base de données](../runbooks/new-web-app/02-database.md) + [Vault plateforme](../runbooks/new-web-app/03-vault-platform.md)
- [ ] protect public endpoint (crowdsec) → [chart : ingress public](../runbooks/new-web-app/04-helm-chart.md)
> [!NOTE]
> Reference: [Arcodange _**Factory**_ Ansible Collection](/ansible/arcodange/factory/README.md)

21
doc/runbooks/README.md Normal file
View File

@@ -0,0 +1,21 @@
[Factory](../../README.md) > [Doc](../README.md) > **Runbooks**
# Runbooks
> **Scope :** procédures opérationnelles pas-à-pas de la plateforme Arcodange. Chaque runbook se lit du début à la fin et mène à un résultat vérifiable. Pour le *pourquoi* (décisions d'architecture), voir les [ADR](../adr/README.md).
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart LR
classDef node fill:#059669,stroke:#047857,color:#fff
OP["Opérateur"]:::node --> RB["Runbook<br>(procédure ordonnée)"]:::node --> RES["Résultat vérifiable<br>(app en ligne, etc.)"]:::node
```
## Index
| Runbook | Résumé | Statut |
|---|---|---|
| [Nouvelle application web](new-web-app/README.md) | Créer une app web de zéro dans un nouveau dépôt Gitea : dépôt, base de données, Vault, chart Helm, Terraform, CI, ArgoCD | ✅ |
> [!TIP]
> Pour ajouter un runbook : créer un dossier `kebab-case/` avec son `README.md` (front door : intro + diagramme + index), puis ajouter sa ligne ci-dessus.

View File

@@ -0,0 +1,88 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **1. Dépôt Gitea**
# 1. Créer le dépôt Gitea
> **Status:** ✅ Active
> **Downstream:** [2. Base de données](02-database.md), [3. Vault plateforme](03-vault-platform.md)
> **Related:** [Conventions de nommage](conventions.md) · [6. Workflows CI](06-ci-workflows.md)
---
## Summary
Tout part d'un dépôt Gitea nommé exactement `<app>` (voir [conventions](conventions.md)). Le créer **sous l'organisation `arcodange-org`** n'est pas un détail : c'est ce qui lui fait **hériter automatiquement des secrets Actions d'organisation** dont tous les workflows dépendent. Le dépôt contient un squelette fixe que les étapes suivantes viennent remplir.
## Pourquoi sous `arcodange-org`
Les workflows `.gitea/workflows/*` (voir [étape 6](06-ci-workflows.md)) référencent des secrets qui ne sont **pas** définis dans le dépôt mais au niveau de l'organisation et hérités par tous ses dépôts :
| Secret d'organisation | Usage |
|---|---|
| `HOMELAB_CA_CERT` | CA interne (base64) pour parler en TLS à `vault.arcodange.lab` |
| `vault_oauth__sh_b64` | Script (base64) qui réalise l'échange OIDC Gitea → JWT Vault |
| `PACKAGES_TOKEN` | Token de push vers le registre d'images `gitea.arcodange.lab` |
Ces secrets sont propagés par le rôle Ansible [`gitea_secret`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/roles/gitea_secret/defaults/main.yml) (`gitea_owner_type: organization`).
> [!IMPORTANT]
> Un dépôt créé **hors** `arcodange-org` (par ex. sous l'org `arcodange`) n'hérite pas forcément de ces secrets. Si tu surcharges l'org (cf. `org:` à l'[étape 7](07-argocd-register.md)), assure-toi que les mêmes secrets y existent.
## Options pour créer le dépôt
| Méthode | Quand | Comment |
|---|---|---|
| UI Gitea | one-shot manuel | `https://gitea.arcodange.lab` → New Repository sous `arcodange-org` |
| MCP Gitea | depuis un agent | outil `mcp__gitea__create_repo` (cf. règle « Gitea = MCP, pas `gh` » du guide global) |
| Rôle Ansible `gitea_repo` | reproductible/inventaire | [`roles/gitea_repo`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/roles/gitea_repo/defaults/main.yml) |
| Ressource Terraform `gitea_repository` | tout-en-IaC | dans [`factory/iac`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac) (provider `go-gitea/gitea` déjà configuré) |
## Squelette minimal du dépôt
```
<app>/
├── chart/ # chart Helm — ArgoCD déploie CE dossier (path: chart) → étape 4
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
├── iac/ # Terraform/OpenTofu de l'app → étape 5
│ ├── providers.tf
│ ├── backend.tf
│ └── main.tf
├── .gitea/workflows/ # CI (tofu apply + build image) → étape 6
│ ├── vault.yaml
│ └── dockerimage.yaml # uniquement si l'app build sa propre image
├── Dockerfile # uniquement si l'app build sa propre image
├── README.md
└── .gitignore
```
> [!IMPORTANT]
> Le dossier du chart **doit** s'appeler `chart/` et être à la racine : le template ArgoCD impose `path: chart` (cf. [étape 7](07-argocd-register.md)). Pas de `helm/`, pas de sous-dossier.
## `.gitignore` recommandé
Aligné sur les dépôts existants (exclut tout secret local) :
```gitignore
.terraform
.terraform.*
.env
*.key
secrets/
.DS_Store
```
## Le bot `tofu_module_reader`
La CI de l'app clone le module Terraform partagé `tools` **en SSH** (cf. [étape 5](05-app-terraform.md)). C'est l'utilisateur restreint `tofu_module_reader` (créé dans [`factory/iac/gitea_tofu_ci_user.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac/gitea_tofu_ci_user.tf), clé privée dans Vault `kvv1/gitea/tofu_module_reader`) qui sert d'identité de lecture. Rien à faire de spécial, mais le dépôt `tools` doit rester lisible par ce bot.
## Notes / contraintes
- Le nom du dépôt = `<app>` **exactement** (kebab-case minuscule). Voir [conventions](conventions.md) — ce nom se propage partout.
- Pas besoin de protéger `main` autrement que par la convention worktree/PR de l'équipe ; ArgoCD suit `HEAD`.
## Related
- [2. Base de données](02-database.md) — provisionner la base, en parallèle de l'étape 3.
- [3. Vault plateforme](03-vault-platform.md) — déclarer l'app côté Vault, en parallèle de l'étape 2.
- [6. Workflows CI](06-ci-workflows.md) — consomme les secrets d'org hérités ici.

View File

@@ -0,0 +1,93 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **2. Base de données**
# 2. Provisionner la base de données
> **Status:** ✅ Active
> **Upstream:** [1. Dépôt Gitea](01-gitea-repo.md)
> **Downstream:** [5. Terraform de l'app](05-app-terraform.md)
> **Related:** [3. Vault plateforme](03-vault-platform.md) · [4. Chart Helm](04-helm-chart.md) · [Conventions de nommage](conventions.md)
---
## Summary
La base de l'app et son **rôle propriétaire** `<app>_role` sont créés par un Terraform **côté plateforme** (`factory/postgres/iac`), pas dans le dépôt de l'app. On ajoute simplement le nom de l'app à une liste, et la CI de `factory` applique. L'app, elle, ne se connectera **jamais** avec un mot de passe statique : elle obtiendra des identifiants éphémères de Vault (cf. [étape 4](04-helm-chart.md)) qui héritent de `<app>_role`.
## Action
Ajouter `"<app>"` au set `applications` de [`factory/postgres/iac/terraform.tfvars`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/terraform.tfvars) :
```hcl
applications = [
"webapp",
"erp",
"crowdsec",
"plausible",
"dance-lessons-coach",
"<app>", # ← ajouter
]
```
Puis pousser : la CI applique automatiquement (voir plus bas).
## Ce que ça crée
[`postgres/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/main.tf) itère `for_each` sur le set et crée, **par app** :
| Ressource | Nom | Rôle |
|---|---|---|
| `postgresql_role` | `<app>_role` | Rôle **non-login**, propriétaire de la base |
| `postgresql_grant_role` | `<app>_role``credentials_editor` (WITH ADMIN OPTION) | Laisse Vault rattacher les users dynamiques à ce rôle |
| `postgresql_database` | `<app>` | La base (owner `<app>_role`, `template0`, `alter_object_ownership`) |
| `postgresql_function` | `user_lookup()` (dans la base `<app>`) | Authentification pgbouncer (lit `pg_shadow`) |
| `postgresql_grant` | EXECUTE sur `user_lookup``pgbouncer_auth` | Autorise pgbouncer à résoudre les users |
Extrait clé :
```hcl
resource "postgresql_role" "app_role" {
for_each = var.applications
name = "${each.value}_role"
login = false # non-login : ne sert que de "porteur de droits"
}
resource "postgresql_database" "app_db" {
for_each = var.applications
name = each.value
owner = postgresql_role.app_role[each.value].name
template = "template0"
alter_object_ownership = true
}
```
## Comment c'est appliqué
Le workflow [`factory/.gitea/workflows/postgres.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/.gitea/workflows/postgres.yaml) se déclenche sur tout changement de `postgres/**/*.tf` ou `*.tfvars` :
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart LR
classDef ci fill:#059669,stroke:#047857,color:#fff
classDef db fill:#2563eb,stroke:#1e40af,color:#fff
PUSH["push tfvars"]:::ci --> JWT["OIDC Gitea → JWT Vault<br>(role gitea_cicd)"]:::ci
JWT --> READ["lit kvv1/postgres/credentials<br>→ TF_VAR_postgres_*"]:::ci
READ --> APPLY["tofu apply postgres/iac"]:::ci
APPLY --> DB["base app + app_role"]:::db
```
Le provider PostgreSQL pointe l'hôte `192.168.1.202` (`sslmode=disable`, `superuser=true`) et s'authentifie avec le compte `credentials_editor`, dont les identifiants sont dans Vault à `kvv1/postgres/credentials_editor/credentials`.
## Le modèle de connexion (à retenir)
> [!IMPORTANT]
> L'application **ne se connecte pas** directement à Postgres avec un user fixe. Elle vise **`pgbouncer.tools:5432`** et utilise des **users dynamiques courts** émis par Vault, qui héritent de `<app>_role` (donc des droits sur la base `<app>`). C'est l'étape 4 (chart + VSO) et l'étape 5 (rôle Vault `creds/<app>`) qui câblent ça. Ici, on ne fait qu'établir *la base et le rôle propriétaire*.
## Notes / contraintes
- `credentials_editor` est un compte unique partagé par toutes les apps, à fort privilège (il peut créer des rôles). Il sert aussi de compte de connexion au moteur Postgres de Vault (cf. [étape 3](03-vault-platform.md)).
- La fonction `user_lookup()` est indispensable au mode `auth_query` de pgbouncer ; elle est `security_definer` et n'est exécutable que par `pgbouncer_auth`.
## Related
- [3. Vault plateforme](03-vault-platform.md) — la connexion Vault→Postgres réutilise `credentials_editor` ; à faire en parallèle.
- [5. Terraform de l'app](05-app-terraform.md) — le module `app_roles` fait `GRANT <app>_role TO …` : il **exige** que `<app>_role` existe déjà (créé ici).
- [4. Chart Helm](04-helm-chart.md) — où la connexion `pgbouncer.tools` + creds dynamiques est configurée.

View File

@@ -0,0 +1,89 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **3. Vault plateforme**
# 3. Déclarer l'app côté Vault plateforme
> **Status:** ✅ Active
> **Upstream:** [1. Dépôt Gitea](01-gitea-repo.md)
> **Downstream:** [5. Terraform de l'app](05-app-terraform.md), [6. Workflows CI](06-ci-workflows.md)
> **Related:** [2. Base de données](02-database.md) · [4. Chart Helm](04-helm-chart.md) · [Conventions de nommage](conventions.md)
---
## Summary
Avant que la CI de l'app puisse gérer ses propres secrets, Vault doit connaître l'app : il lui faut un **rôle JWT de CI** (`gitea_cicd_<app>`) pour que la pipeline s'authentifie, une **policy CI** (`<app>-ops`) qui l'autorise à créer ses rôles Postgres/K8s, et une **policy runtime** (`<app>`) que le pod utilisera. Tout ça est généré par un module, depuis une seule ligne ajoutée côté plateforme dans le dépôt `tools`.
## Action
Ajouter une entrée pour l'app au set `applications` de [`tools/hashicorp-vault/iac/terraform.tfvars`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/terraform.tfvars) :
```hcl
applications = [
{ name = "webapp" },
{ name = "erp" },
{ name = "<app>" }, # ← ajouter
# options possibles :
# {
# name = "<app>"
# ops_policies = ["factory__cf_r2_arcodange_tf"] # policies ops supplémentaires (ex. token Cloudflare)
# service_account_names = ["cloudflared"] # SA additionnels autorisés à prendre la policy runtime
# service_account_namespaces = ["tools"] # namespaces additionnels
# },
]
```
Puis pousser : la CI [`tools/.gitea/workflows/vault.yaml`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/.gitea/workflows/vault.yaml) applique le Terraform.
## Ce que ça crée
Le module [`app_policy`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf) (appelé en `for_each` depuis [`main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf)) crée, **par app** :
| Ressource Vault | Nom | À quoi ça sert |
|---|---|---|
| `vault_jwt_auth_backend_role` | `gitea_cicd_<app>` | **Identité de la CI** : la pipeline de l'app s'y authentifie (mount `gitea_jwt`, `user_claim=email`, `bound_audiences=[gitea_app_id]`) |
| `vault_policy` (ops) | `<app>-ops` | Droits CI : créer `postgres/roles/<app>*`, `auth/kubernetes/role/<app>*`, éditer `kvv2/<app>/*`, lire les secrets de bootstrap google/gitea |
| `vault_identity_group` | `<app>-ops` | Groupe Vault rattachant les comptes Gitea à la policy ops |
| `vault_policy` (runtime) | `<app>` | Droits **du pod** : lire `kvv2/data/<app>/*` et `postgres/creds/<app>*` |
Extraits clés :
```hcl
resource "vault_jwt_auth_backend_role" "gitea_jwt_cicd" {
backend = data.vault_auth_backend.gitea_jwt.path # "gitea_jwt"
role_name = "gitea_cicd_${local.name}"
token_policies = concat(["default"], var.ops_policies)
bound_audiences = [var.gitea_app_id]
user_claim = "email"
role_type = "jwt"
}
resource "vault_policy" "app" { # policy runtime du pod
name = local.name # = "<app>"
policy = data.vault_policy_document.app.hcl # read kvv2/data/<app>/* + postgres/creds/<app>*
}
```
## Prérequis plateforme (déjà là)
Ces fondations vivent dans [`tools/hashicorp-vault/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf) et n'ont **pas** à être recréées :
- mounts `kvv2` (KV v2), `postgres` (moteur de bases de données), `transit` (cache VSO), auth `kubernetes` ;
- la **connexion Vault→Postgres** (`vault_database_secret_backend_connection`) qui se connecte à `pgbouncer.tools:5432/postgres` avec le compte `credentials_editor` (issu de l'[étape 2](02-database.md)) ;
- `var.gitea_app_id` = l'id de l'application OAuth2 Gitea, réglé une fois au setup ([`gitea_oidc_auth.yml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml)).
## Pourquoi cette étape vient avant la CI de l'app
> [!IMPORTANT]
> C'est ici que `gitea_cicd_<app>` **naît**. Le `iac/providers.tf` de l'app ([étape 5](05-app-terraform.md)) et le step `vault-action` du workflow ([étape 6](06-ci-workflows.md)) s'authentifient avec ce rôle. S'il n'existe pas encore, la toute première exécution de la CI de l'app échoue à l'authentification Vault. **Appliquer cette étape (et l'[étape 2](02-database.md)) avant de pousser le `iac/` de l'app.**
## Notes / contraintes
- Découpage des privilèges : la policy **`<app>-ops`** (CI, large) est distincte de la policy **`<app>`** (runtime, en lecture seule sur ses propres secrets). Le pod ne peut jamais créer de rôles.
- `ops_policies` permet d'octroyer à la CI des droits transverses (ex. `cms` lit un token Cloudflare R2 via `factory__cf_r2_arcodange_tf`).
## Related
- [2. Base de données](02-database.md) — fournit `credentials_editor`, réutilisé par la connexion Vault→Postgres.
- [5. Terraform de l'app](05-app-terraform.md) — s'authentifie avec `gitea_cicd_<app>` et crée `creds/<app>` + le rôle K8s `<app>`.
- [6. Workflows CI](06-ci-workflows.md) — le step `vault-action` et `tofu apply` utilisent `gitea_cicd_<app>`.
- [4. Chart Helm](04-helm-chart.md) — le pod utilise la policy runtime `<app>` via son ServiceAccount.

View File

@@ -0,0 +1,183 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **4. Chart Helm**
# 4. Le chart Helm de l'application
> **Status:** ✅ Active
> **Upstream:** [5. Terraform de l'app](05-app-terraform.md) (crée les rôles Vault que le chart consomme)
> **Downstream:** [7. Enregistrement ArgoCD](07-argocd-register.md) (déploie ce chart)
> **Related:** [2. Base de données](02-database.md) · [3. Vault plateforme](03-vault-platform.md) · [6. Workflows CI](06-ci-workflows.md) · [Conventions de nommage](conventions.md)
---
## Summary
Le dossier `chart/` est l'**unité de déploiement** : c'est lui qu'ArgoCD applique (`path: chart`). Il décrit le Deployment, le Service, l'Ingress, et surtout le câblage des secrets via le **Vault Secrets Operator (VSO)** : pas un mot de passe en clair, des identifiants Postgres **dynamiques** injectés à l'exécution. On part d'un `helm create <app>` puis on ajoute les CRD Vault, la ConfigMap, et on ajuste l'ingress.
## Structure du chart
```
chart/
├── Chart.yaml # name: <app>, version, appVersion (= tag image par défaut)
├── values.yaml # image, service, ingress, serviceAccount, autoscaling…
└── templates/
├── deployment.yaml # consomme vso-db-credentials + secretkv
├── service.yaml # ClusterIP
├── ingress.yaml # Traefik (voir patterns ci-dessous)
├── serviceaccount.yaml # SA <app> (serviceAccount.create: true)
├── config.yaml # ConfigMap : env non-secrets (host DB = pgbouncer)
├── vaultauth.yaml # VaultAuth : SA <app> ↔ rôle Vault <app>
├── vaultdynamicsecret.yaml # creds Postgres dynamiques (postgres/creds/<app>)
├── vaultsecret.yaml # config statique (kvv2/<app>/config)
├── hpa.yaml # désactivé par défaut
├── _helpers.tpl # name/fullname/labels
└── NOTES.txt
```
> [!TIP]
> Bootstrap : `helm create <app>` génère deployment/service/ingress/serviceaccount/hpa/_helpers/NOTES. Il reste à **ajouter** `config.yaml` (ConfigMap), les 3 CRD Vault (`vaultauth`, `vaultdynamicsecret`, `vaultsecret`), et à **ajuster** `values.yaml` (image, ingress) + `deployment.yaml` (envFrom). Copier ceux d'[`erp/chart`](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/chart) ou [`webapp/chart`](https://gitea.arcodange.lab/arcodange-org/webapp/src/branch/main/chart) est le plus rapide.
## Connexion à la base : via pgbouncer, jamais en direct
La ConfigMap pointe **`pgbouncer.tools`** (le pooler dans le namespace `tools`), port 5432, base `<app>`. Pas de host Postgres en dur, pas d'utilisateur statique.
```yaml
# chart/templates/config.yaml — exemple erp
data:
DOLI_DB_TYPE: pgsql
DOLI_DB_HOST: pgbouncer.tools # ← le pooler, pas postgres directement
DOLI_DB_HOST_PORT: !!str 5432
DOLI_DB_NAME: erp # = <app>
```
```yaml
# chart/templates/config.yaml — exemple webapp (chaîne de connexion)
data:
DATABASE_URL: postgres://pgbouncer_auth:pgbouncer_auth@pgbouncer.tools/postgres?sslmode=disable
```
Le **vrai** user/mot de passe vient du Secret K8s `vso-db-credentials` (voir ci-dessous), pas de la ConfigMap.
## Les secrets via VSO (le cœur)
Trois CRD du Vault Secrets Operator (extraits du chart `erp`) :
```yaml
# vaultauth.yaml — authentifie le pod auprès de Vault
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: erp # = rôle K8s Vault <app> (créé à l'étape 5)
serviceAccount: {{ include "erp.serviceAccountName" . }}
audiences: [vault]
```
```yaml
# vaultdynamicsecret.yaml — identifiants Postgres dynamiques
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
spec:
mount: postgres
path: creds/erp # = postgres/creds/<app>
destination:
create: true
name: vso-db-credentials # Secret K8s consommé par le Deployment
rolloutRestartTargets:
- kind: Deployment
name: {{ include "erp.fullname" . }} # redémarre le pod à chaque rotation
vaultAuthRef: auth
```
```yaml
# vaultsecret.yaml — config statique (mots de passe admin initiaux, etc.)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
spec:
type: kv-v2
mount: kvv2
path: erp/config # = kvv2/<app>/config (rempli à l'étape 5)
destination:
name: secretkv
create: true
refreshAfter: 24h
vaultAuthRef: auth
```
Le Deployment consomme ensuite `vso-db-credentials` (clés `username`/`password`) et `secretkv` (via `envFrom`/`secretKeyRef`).
### Flux runtime
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart LR
classDef pod fill:#b45309,stroke:#92400e,color:#fff
classDef vault fill:#7c3aed,stroke:#6d28d9,color:#fff
classDef db fill:#2563eb,stroke:#1e40af,color:#fff
SA["Pod · SA app"]:::pod
VA["VaultAuth<br>role app (k8s)"]:::vault
VDS["VaultDynamicSecret<br>postgres/creds/app"]:::vault
VSS["VaultStaticSecret<br>kvv2/app/config"]:::vault
SEC["Secret K8s<br>vso-db-credentials + secretkv"]:::pod
PGB["pgbouncer.tools:5432"]:::db
DB["base app<br>(user dynamique → app_role)"]:::db
SA --> VA
VA --> VDS
VA --> VSS
VDS --> SEC
VSS --> SEC
SEC --> SA
SA --> PGB --> DB
```
> [!IMPORTANT]
> `serviceAccount.create` doit valoir `true` dans `values.yaml` : c'est ce SA `<app>` que `VaultAuth` lie au rôle Vault `<app>`. Sans lui, pas d'authentification, donc pas de creds DB.
## Ingress : interne `.lab` vs public `.fr`
Deux patterns selon l'exposition voulue (annotations Traefik dans `values.yaml`) :
| | Interne (`.lab`) — ex. `erp` | Public (`.fr`) — ex. `webapp` |
|---|---|---|
| `entrypoints` | `websecure` | `web` |
| TLS | `router.tls: "true"` + `certresolver: letsencrypt` | (terminé en amont) |
| Middleware | `localIp@file` (restreint au LAN) | `kube-system-crowdsec@kubernetescrd` (WAF) |
| `nodeSelector` | — | `kubernetes.io/hostname: pi1` (garde l'IP source, point d'entrée réseau) |
| Hôte | `<app>.arcodange.lab` | `<app>.arcodange.fr` |
```yaml
# values.yaml — ingress interne .lab (extrait erp)
ingress:
enabled: true
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
traefik.ingress.kubernetes.io/router.middlewares: localIp@file
hosts:
- host: erp.arcodange.lab
paths: [{ path: /, pathType: Prefix }]
```
## Image
| Cas | `image.repository` | Exemple |
|---|---|---|
| Image **maison** (build dans le dépôt) | `gitea.arcodange.lab/arcodange-org/<app>` | `webapp` (cf. [étape 6](06-ci-workflows.md)) |
| Image **publique** | l'image upstream | `erp``dolibarr/dolibarr` |
## Notes / contraintes
- Si l'app stocke des fichiers, ajouter un `pvc.yaml` (`storageClassName: longhorn`, `accessModes: [ReadWriteMany]`, annotation `helm.sh/resource-policy: keep` pour survivre à un `helm uninstall`) — cf. `erp/chart/templates/pvc.yaml`.
- Les CRD VSO supposent que le VSO tourne dans le cluster (déployé par `tools`) et que le rôle K8s `<app>` + le rôle `creds/<app>` existent (étape 5).
## Related
- [5. Terraform de l'app](05-app-terraform.md) — crée `postgres/creds/<app>`, le rôle K8s `<app>` et remplit `kvv2/<app>/config` que ces CRD consomment.
- [2. Base de données](02-database.md) — la base `<app>` et `pgbouncer.tools` ciblés ici.
- [3. Vault plateforme](03-vault-platform.md) — la policy runtime `<app>` qu'utilise `VaultAuth`.
- [6. Workflows CI](06-ci-workflows.md) — construit l'image référencée par `image.repository`.
- [Référence VSO/secrets faisant autorité](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/README.md) — explication détaillée VaultConnection/VaultAuth/VaultDynamicSecret côté `tools` (à ne pas dupliquer ici).

View File

@@ -0,0 +1,96 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **5. Terraform de l'app**
# 5. Le Terraform de l'application
> **Status:** ✅ Active
> **Upstream:** [2. Base de données](02-database.md), [3. Vault plateforme](03-vault-platform.md)
> **Downstream:** [4. Chart Helm](04-helm-chart.md), [6. Workflows CI](06-ci-workflows.md)
> **Related:** [Conventions de nommage](conventions.md)
---
## Summary
Le `iac/` du dépôt déclare les ressources Vault **propres à l'app** : un rôle Postgres dynamique (`postgres/creds/<app>`), un rôle d'authentification Kubernetes (`<app>`), et les secrets de config KV. Le gros du travail est fait par un **module partagé** (`app_roles`, dans `tools`) ; le dépôt se contente de l'appeler avec son nom et d'ajouter ses secrets spécifiques.
## Les trois fichiers
### `providers.tf` — s'authentifier à Vault avec le rôle CI de l'app
```hcl
terraform {
required_providers {
vault = { source = "vault", version = "4.4.0" }
}
}
provider "vault" {
address = "https://vault.arcodange.lab"
auth_login_jwt { # JWT fourni par la CI via TERRAFORM_VAULT_AUTH_JWT
mount = "gitea_jwt"
role = "gitea_cicd_<app>" # ← créé à l'étape 3 ; DOIT exister avant le 1er apply
}
}
```
### `backend.tf` — état distant sur GCS, préfixe par app
```hcl
terraform {
backend "gcs" {
bucket = "arcodange-tf"
prefix = "<app>/main" # ← un préfixe d'état dédié par app
}
}
```
### `main.tf` — appeler le module partagé + secrets de l'app
```hcl
module "app_roles" {
source = "git::ssh://git@192.168.1.202:2222/arcodange-org/tools.git//hashicorp-vault/iac/modules/app_roles?depth=1&ref=main"
name = "<app>"
# database = "<autre>" # optionnel ; par défaut = name
}
# Exemple : secrets de config statiques de l'app, écrits dans kvv2/<app>/config
resource "vault_kv_secret_v2" "config" {
mount = module.app_roles.mount_paths.kvv2 # "kvv2"
name = format("%sconfig", module.app_roles.kvv2_path_prefix) # "<app>/config"
data_json = jsonencode({
# … clés propres à l'app (ex. erp : DOLI_ADMIN_LOGIN/PASSWORD, DOLI_INSTANCE_UNIQUE_ID) …
})
}
```
## Ce que le module `app_roles` crée
[`modules/app_roles/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_roles/main.tf) :
| Ressource | Effet |
|---|---|
| `vault_database_secret_backend_role``postgres/creds/<app>` | À chaque demande : `CREATE ROLE "…" LOGIN PASSWORD … VALID UNTIL … ; GRANT <app>_role TO "…"`. Le user éphémère **hérite** de `<app>_role` (donc des droits sur la base). À la révocation : `REASSIGN OWNED … TO <app>_role` + `REVOKE`. |
| `vault_kubernetes_auth_backend_role``<app>` | Lie le SA `<app>` du namespace `<app>` aux policies `default` + `<app>` (TTL 3600 s). C'est ce que `VaultAuth` cible (étape 4). |
Sorties utiles : `mount_paths` (`{k8s, pg, kvv2}`), `kvv2_path_prefix` (`<app>/`), `name`, `database`.
## Dépendances (à respecter)
> [!IMPORTANT]
> Ce Terraform **suppose** que deux choses existent déjà :
> - le rôle Postgres `<app>_role` (créé à l'[étape 2](02-database.md)) — sinon le `GRANT <app>_role TO …` du rôle dynamique est invalide ;
> - le rôle JWT `gitea_cicd_<app>` et la policy `<app>` (créés à l'[étape 3](03-vault-platform.md)) — sinon l'authentification du provider échoue / le rôle K8s ne peut référencer la policy.
>
> Ce `iac/` est appliqué par la CI de l'app, voir [étape 6](06-ci-workflows.md). Ne pas pousser ce dossier avant d'avoir appliqué les étapes 2 et 3.
## Notes / contraintes
- Le module est récupéré **en SSH** via le bot `tofu_module_reader` (cf. [étape 1](01-gitea-repo.md)) ; `?ref=main&depth=1` épingle la branche et limite le clone.
- L'état est isolé par `prefix = "<app>/main"` : pas de collision entre apps dans le bucket `arcodange-tf`.
- `erp` et `webapp` montrent deux variantes : `erp` passe par `module "app_roles"` ; `webapp` inline encore les ressources (`vault_database_secret_backend_role` + `vault_kubernetes_auth_backend_role`) — préférer le module pour une nouvelle app.
## Related
- [2. Base de données](02-database.md) — fournit `<app>_role`.
- [3. Vault plateforme](03-vault-platform.md) — fournit `gitea_cicd_<app>` et la policy `<app>`.
- [4. Chart Helm](04-helm-chart.md) — consomme `postgres/creds/<app>`, le rôle K8s `<app>` et `kvv2/<app>/config`.
- [6. Workflows CI](06-ci-workflows.md) — applique ce `iac/`.

View File

@@ -0,0 +1,108 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **6. Workflows CI**
# 6. Les workflows CI (`.gitea/workflows/`)
> **Status:** ✅ Active
> **Upstream:** [1. Dépôt Gitea](01-gitea-repo.md) (secrets d'org), [3. Vault plateforme](03-vault-platform.md) (`gitea_cicd_<app>`)
> **Related:** [4. Chart Helm](04-helm-chart.md) · [5. Terraform de l'app](05-app-terraform.md) · [7. Enregistrement ArgoCD](07-argocd-register.md) · [Conventions de nommage](conventions.md)
---
## Summary
Deux workflows Gitea Actions vivent dans le dépôt : **`vault.yaml`** applique le Terraform de l'app (`iac/`) en s'authentifiant à Vault via OIDC, et **`dockerimage.yaml`** (optionnel) construit l'image et la pousse au registre Gitea. Le déploiement lui-même n'est pas dans la CI : c'est ArgoCD qui s'en charge ([étape 7](07-argocd-register.md)).
## `vault.yaml` — appliquer le `iac/` de l'app
Déclenché sur tout changement de `iac/*.tf`. Deux jobs : obtenir un JWT depuis Gitea, puis `tofu apply`.
```yaml
on:
workflow_dispatch: {}
push: { paths: ['iac/*.tf'] }
pull_request: { paths: ['iac/*.tf'] }
# job 1 : échange OIDC Gitea → JWT (script base64 fourni en secret d'org)
# run: echo -n "${{ secrets.vault_oauth__sh_b64 }}" | base64 -d | bash
# job 2 : lire les secrets de bootstrap puis appliquer
- name: read vault secret
uses: https://gitea.arcodange.lab/arcodange-org/vault-action.git@main
with:
url: https://vault.arcodange.lab
caCertificate: ${{ secrets.HOMELAB_CA_CERT }}
jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
role: gitea_cicd_<app> # ← le rôle JWT de l'app (étape 3)
method: jwt
path: gitea_jwt
secrets: |
kvv1/google/credentials credentials | GOOGLE_BACKEND_CREDENTIALS ;
kvv1/gitea/tofu_module_reader ssh_private_key | TERRAFORM_SSH_KEY ;
- uses: actions/checkout@v4
- name: terraform apply
uses: dflook/terraform-apply@v1
with: { path: iac, auto_approve: true }
```
Les deux secrets lus servent au backend (clé GCS `GOOGLE_BACKEND_CREDENTIALS`) et au clone du module partagé en SSH (`TERRAFORM_SSH_KEY`, cf. [étape 5](05-app-terraform.md)).
> [!WARNING]
> **Piège du `role:` copié-collé.** Le `role:` du step `vault-action` **et** le `role` de `iac/providers.tf` doivent tous deux être `gitea_cicd_<app>`. L'exemple `erp` porte encore `role: gitea_cicd_webapp` dans son [`vault.yaml`](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/.gitea/workflows/vault.yaml) (reliquat de copier-coller) alors que son `providers.tf` utilise bien `gitea_cicd_erp`. Vérifie et aligne sur le nom de **ton** app, sinon la CI lit/écrit avec la mauvaise identité.
## `dockerimage.yaml` — construire l'image (si image maison)
À n'ajouter **que** si l'app build sa propre image (pas pour une image publique comme `erp`/Dolibarr). Déclenché au push sur `main`, en ignorant `README.md` et `chart/**` (changer le chart ne reconstruit pas l'image).
```yaml
on:
push: { branches: [main], paths-ignore: ['README.md', 'chart/**'] }
jobs:
build-and-push-image:
steps:
- uses: docker/login-action@v3
with:
registry: gitea.arcodange.lab
username: ${{ github.actor }}
password: ${{ secrets.PACKAGES_TOKEN }} # secret d'org (étape 1)
- uses: actions/checkout@v4
- run: |
TAGS="latest ${{ github.ref_name }}"
docker build -t app .
for TAG in $TAGS; do
docker tag app gitea.arcodange.lab/${{ github.repository }}:$TAG
docker push gitea.arcodange.lab/${{ github.repository }}:$TAG
done
```
L'image atterrit donc en `gitea.arcodange.lab/arcodange-org/<app>:latest` — exactement ce que `image.repository` du chart référence ([étape 4](04-helm-chart.md)). Un `Dockerfile` multi-stage à la racine convient (cf. [`webapp/Dockerfile`](https://gitea.arcodange.lab/arcodange-org/webapp/src/branch/main/Dockerfile)).
## Vue d'ensemble
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart TB
classDef ci fill:#059669,stroke:#047857,color:#fff
classDef out fill:#7c3aed,stroke:#6d28d9,color:#fff
PUSH["push sur main"]:::ci
PUSH -->|"iac/*.tf modifié"| TF["vault.yaml<br>OIDC → JWT → tofu apply iac/"]:::ci
PUSH -->|"code modifié"| IMG["dockerimage.yaml<br>build + push image"]:::ci
TF --> VR["creds/app + rôle K8s app + KV"]:::out
IMG --> REG["registre gitea.arcodange.lab/arcodange-org/app"]:::out
```
## Déploiement automatique sur nouvelle image
Pour qu'ArgoCD redéploie quand une nouvelle image est poussée, on n'ajoute **rien** dans la CI : ce sont les annotations `argocd-image-updater` posées à l'[étape 7](07-argocd-register.md) (stratégie `digest`) qui surveillent le tag `latest`.
## Notes / contraintes
- `concurrency: cancel-in-progress` est activé sur les deux workflows : un nouveau push annule le run précédent sur la même ref.
- Le `vault-action` est lui-même un dépôt Gitea (`arcodange-org/vault-action`) épinglé `@main`.
## Related
- [3. Vault plateforme](03-vault-platform.md) — d'où vient `gitea_cicd_<app>`.
- [5. Terraform de l'app](05-app-terraform.md) — ce que `vault.yaml` applique.
- [4. Chart Helm](04-helm-chart.md) — `image.repository` = l'image poussée ici.
- [7. Enregistrement ArgoCD](07-argocd-register.md) — déploie, et porte les annotations d'auto-update.

View File

@@ -0,0 +1,91 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **7. Enregistrement ArgoCD**
# 7. Enregistrer l'app dans ArgoCD
> **Status:** ✅ Active
> **Upstream:** [4. Chart Helm](04-helm-chart.md), [6. Workflows CI](06-ci-workflows.md)
> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md)
---
## Summary
ArgoCD fonctionne en **app-of-apps** : le chart `factory/argocd` lit une liste d'applications dans son `values.yaml` et génère une ressource `Application` par entrée. Enregistrer l'app = ajouter son nom à cette liste. ArgoCD se charge ensuite de cloner le dépôt, déployer `chart/` dans le namespace `<app>`, et resynchroniser à chaque push.
## Action
Ajouter `<app>` sous `gitea_applications` dans [`factory/argocd/values.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/values.yaml) :
```yaml
gitea_applications:
webapp: { … }
erp:
annotations: {}
<app>: # ← cas simple
annotations: {}
```
Variante avec **auto-déploiement** sur nouvelle image (recommandé pour une image maison) :
```yaml
<app>:
annotations:
argocd-image-updater.argoproj.io/image-list: <app>=gitea.arcodange.lab/arcodange-org/<app>:latest
argocd-image-updater.argoproj.io/<app>.update-strategy: digest
```
Options supplémentaires :
| Champ | Quand l'utiliser | Effet |
|---|---|---|
| `org: arcodange` | dépôt hors `arcodange-org` | change le `repoURL` (défaut `arcodange-org`) |
| `syncPolicy: …` | contrôle manuel | surcharge la policy (défaut : `automated {prune, selfHeal}`) |
## Ce que ça génère
Le template [`argocd/templates/apps.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/templates/apps.yaml) rend, pour chaque entrée :
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: <app>
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.arcodange.lab/arcodange-org/<app> # <org> = arcodange-org par défaut
targetRevision: HEAD
path: chart # ← d'où l'exigence du dossier chart/
destination:
server: https://kubernetes.default.svc
namespace: <app> # = nom de l'app
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ CreateNamespace=true ] # le namespace est créé tout seul
```
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart LR
classDef gitops fill:#7c3aed,stroke:#6d28d9,color:#fff
classDef k8s fill:#2563eb,stroke:#1e40af,color:#fff
VAL["values.yaml<br>gitea_applications.app"]:::gitops --> APP["Application ArgoCD<br>app"]:::gitops
APP -->|"path: chart, HEAD"| SYNC["sync du dépôt<br>arcodange-org/app"]:::gitops
SYNC --> NS["namespace app<br>(CreateNamespace=true)"]:::k8s
NS --> DEP["Deployment + Service + Ingress + CRD Vault"]:::k8s
```
## 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)).
- 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`.
## Related
- [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.
- [8. Checklist](08-checklist.md) — vérifier que l'`Application` passe `Healthy`/`Synced`.

View File

@@ -0,0 +1,77 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **8. Checklist**
# 8. Checklist & Definition of Done
> **Status:** ✅ Active
> **Upstream:** [7. Enregistrement ArgoCD](07-argocd-register.md)
> **Related:** [README du runbook](README.md) · [Conventions de nommage](conventions.md)
---
## Summary
Récapitulatif imprimable de toute la procédure, dans l'ordre des dépendances. À cocher au fur et à mesure ; chaque ligne renvoie à sa page détaillée.
## Ordre des dépendances
```
[01] Dépôt Gitea ──┬──> [02] base + <app>_role ──┐
└──> [03] gitea_cicd_<app> ────┤ (02 et 03 avant 05)
[04+05+06] chart/ + iac/ + .gitea/ ── push → CI
[07] ArgoCD ── déploie ── Runtime
```
## Checklist
**Préparation**
- [ ] Nom `<app>` choisi (kebab-case minuscule), cohérent partout — [conventions](conventions.md)
**1 · Dépôt** — [détails](01-gitea-repo.md)
- [ ] Dépôt `arcodange-org/<app>` créé (hérite `HOMELAB_CA_CERT`, `vault_oauth__sh_b64`, `PACKAGES_TOKEN`)
- [ ] Squelette en place : `chart/`, `iac/`, `.gitea/workflows/`, `.gitignore` (+ `Dockerfile` si image maison)
**2 · Base de données** — [détails](02-database.md)
- [ ] `"<app>"` ajouté à `factory/postgres/iac/terraform.tfvars`
- [ ] CI `factory` « Postgres » verte → base `<app>` + rôle `<app>_role` créés
**3 · Vault plateforme** — [détails](03-vault-platform.md)
- [ ] `{ name = "<app>" }` ajouté à `tools/hashicorp-vault/iac/terraform.tfvars`
- [ ] CI `tools` « Vault » verte → `gitea_cicd_<app>`, policies `<app>` / `<app>-ops` créées
> [!IMPORTANT]
> Ne pas pousser le `iac/` de l'app (étape 5/6) tant que **2 et 3** ne sont pas appliquées : la CI Terraform de l'app en dépend.
**4 · Chart Helm** — [détails](04-helm-chart.md)
- [ ] `Chart.yaml` (`name: <app>`), `values.yaml` (image, ingress, `serviceAccount.create: true`)
- [ ] `config.yaml` pointe `pgbouncer.tools` / base `<app>`
- [ ] `vaultauth.yaml` (role `<app>`), `vaultdynamicsecret.yaml` (`creds/<app>`), `vaultsecret.yaml` (`kvv2/<app>/config`)
- [ ] Ingress choisi : interne `.lab` (websecure + letsencrypt + `localIp@file`) ou public `.fr` (web + crowdsec + `nodeSelector pi1`)
**5 · Terraform de l'app** — [détails](05-app-terraform.md)
- [ ] `providers.tf` : role `gitea_cicd_<app>`
- [ ] `backend.tf` : prefix `<app>/main`
- [ ] `main.tf` : `module "app_roles"` (`name = "<app>"`) + secrets `kvv2/<app>/config`
**6 · Workflows CI** — [détails](06-ci-workflows.md)
- [ ] `vault.yaml` : `role: gitea_cicd_<app>` **aligné** (pas un reliquat copié-collé) ⚠️
- [ ] `dockerimage.yaml` + `Dockerfile` (si image maison) → push `gitea.arcodange.lab/arcodange-org/<app>`
- [ ] Push → CI « vault » verte (`creds/<app>` + rôle K8s `<app>` + KV créés), CI « image » verte
**7 · ArgoCD** — [détails](07-argocd-register.md)
- [ ] `<app>` ajouté sous `gitea_applications` dans `factory/argocd/values.yaml` (+ annotations image-updater si voulu)
- [ ] Commit sur `main``Application` `<app>` apparaît dans ArgoCD
## Definition of Done
- [ ] `Application` ArgoCD `<app>` = **Synced** + **Healthy**
- [ ] Pod `Running` dans le namespace `<app>` (SA `<app>`)
- [ ] Secret K8s `vso-db-credentials` présent et **roté** par VSO (TTL ~1 h) ; le pod redémarre à la rotation
- [ ] L'app répond sur son ingress (`<app>.arcodange.lab` ou `<app>.arcodange.fr`)
- [ ] Connexion DB OK via `pgbouncer.tools` avec un user dynamique héritant de `<app>_role`
## Related
- [README du runbook](README.md) — vue d'ensemble + carte de bout en bout.
- [Conventions de nommage](conventions.md) — la cohérence du nom `<app>`, source de la plupart des ratés.

View File

@@ -0,0 +1,104 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > **Nouvelle application web**
# Mettre en service une nouvelle application web
> **Last Updated:** 2026-05-31
> **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)
## C'est quoi ?
Ce runbook décrit, de zéro, comment faire vivre une nouvelle application web sur la plateforme Arcodange. Le pattern est **GitOps** : l'app habite son propre dépôt Gitea, sa base de données et ses accès Vault sont provisionnés par Terraform/OpenTofu, et **ArgoCD** déploie son chart Helm dans un namespace dédié. Les identifiants Postgres ne sont jamais écrits en clair : ils sont générés à la volée par Vault et injectés dans le pod par le **Vault Secrets Operator (VSO)**.
La mécanique est répartie sur **trois dépôts** — le dépôt plateforme [`factory`](https://gitea.arcodange.lab/arcodange-org/factory), le dépôt des services partagés [`tools`](https://gitea.arcodange.lab/arcodange-org/tools), et le **nouveau dépôt de l'app** — avec des **dépendances d'ordre** strictes (voir plus bas). Les exemples de référence sont [`erp`](https://gitea.arcodange.lab/arcodange-org/erp) (image publique + DB) et [`webapp`](https://gitea.arcodange.lab/arcodange-org/webapp) (image maison + DB).
**Sert à :**
1. Créer un dépôt Gitea et son squelette (`chart/`, `iac/`, `.gitea/workflows/`).
2. Provisionner la base de données, son rôle propriétaire, et les accès Vault (statiques + dynamiques).
3. Déployer l'app via ArgoCD et l'exposer derrière Traefik (interne `.lab` ou public `.fr` + CrowdSec).
## Carte de bout en bout
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart TB
classDef step fill:#2563eb,stroke:#1e40af,color:#fff
classDef plat fill:#059669,stroke:#047857,color:#fff
classDef gitops fill:#7c3aed,stroke:#6d28d9,color:#fff
classDef run fill:#b45309,stroke:#92400e,color:#fff
REPO["1 · Dépôt Gitea<br>arcodange-org/app"]:::step
DB["2 · factory/postgres/iac<br>base app + rôle app_role"]:::plat
VAULT["3 · tools/hashicorp-vault/iac<br>gitea_cicd_app + policies app / app-ops"]:::plat
CONTENT["4·5·6 · chart/ + iac/ + .gitea/<br>push → CI build image &amp; tofu apply"]:::step
ARGO["7 · factory/argocd/values.yaml<br>→ Application ArgoCD (ns app)"]:::gitops
POD["Runtime · Pod(SA app) → VSO → Vault<br>creds/app → pgbouncer.tools → base app"]:::run
REPO --> DB
REPO --> VAULT
DB --> CONTENT
VAULT --> CONTENT
CONTENT --> ARGO
ARGO --> POD
```
## Ordre des opérations (le point le plus important)
> [!IMPORTANT]
> Les étapes ne sont pas interchangeables. Le rôle JWT de CI `gitea_cicd_<app>` (étape 3) et le rôle Postgres `<app>_role` (étape 2) doivent **exister avant** que la CI Terraform de l'app (étape 6 appliquant l'étape 5) ne s'exécute — sinon l'authentification Vault de la CI échoue, ou le module `app_roles` n'a pas de `<app>_role` à qui rattacher les credentials dynamiques.
```
[01] Dépôt Gitea sous arcodange-org (hérite les secrets CI d'org)
├──> [02] factory/postgres/iac → base <app> + <app>_role + user_lookup()
└──> [03] tools/hashicorp-vault/iac → gitea_cicd_<app> (JWT CI) + policies <app> / <app>-ops
│ (02 et 03 indépendants entre eux, mais TOUS DEUX avant 05)
[04+05+06] Contenu du dépôt : chart/ + iac/ + .gitea/workflows/ (+ Dockerfile)
│ push → CI « dockerimage » build l'image · CI « vault » applique iac/
│ → creds/<app> (rôle DB dynamique) + rôle K8s <app> + secrets KV
[07] factory/argocd/values.yaml → ArgoCD crée l'Application → déploie le chart dans le namespace <app>
Runtime : Pod(SA <app>) → VSO → VaultAuth(role <app>) → creds/<app>
→ user PG dynamique héritant de <app>_role → pgbouncer.tools → base <app>
```
## Prérequis plateforme (déjà en place)
Ces fondations existent et ne sont **pas** à refaire pour chaque app :
| Brique | Où | Rôle |
|---|---|---|
| Mounts Vault `kvv2`, `postgres`, `transit`, auth `kubernetes` | [`tools/hashicorp-vault/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf) | Moteurs de secrets + auth K8s |
| Connexion Vault→Postgres (via `pgbouncer.tools`, user `credentials_editor`) | idem | Permet à Vault d'émettre des users PG dynamiques |
| Rôle JWT de bootstrap `gitea_cicd` + app OAuth2 Gitea (`gitea_app_id`) | [`gitea_oidc_auth.yml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml) | Échange OIDC Gitea → JWT Vault dans la CI |
| Bot `tofu_module_reader` (clé SSH dans `kvv1/gitea/tofu_module_reader`) | [`factory/iac/gitea_tofu_ci_user.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac/gitea_tofu_ci_user.tf) | Laisse la CI cloner le module partagé `tools` en SSH |
| Secrets Actions d'organisation (`HOMELAB_CA_CERT`, `vault_oauth__sh_b64`, `PACKAGES_TOKEN`) | org Gitea `arcodange-org` | Hérités par tout dépôt de l'org |
## Index des étapes
| # | Page | Ce qu'on y fait | Statut |
|---|---|---|---|
| — | [Conventions de nommage](conventions.md) | Le nom `<app>` réutilisé à l'identique partout (à lire en premier) | ✅ |
| 01 | [Dépôt Gitea](01-gitea-repo.md) | Créer le dépôt sous `arcodange-org` + squelette | ✅ |
| 02 | [Base de données](02-database.md) | `factory/postgres/iac` → base `<app>` + rôle `<app>_role` | ✅ |
| 03 | [Vault plateforme](03-vault-platform.md) | `tools/hashicorp-vault/iac``gitea_cicd_<app>` + policies | ✅ |
| 04 | [Chart Helm](04-helm-chart.md) | Le chart de l'app (DB via pgbouncer, secrets VSO, ingress) | ✅ |
| 05 | [Terraform de l'app](05-app-terraform.md) | `iac/` → module `app_roles` (creds dynamiques + rôle K8s) | ✅ |
| 06 | [Workflows CI](06-ci-workflows.md) | `.gitea/workflows/` : `tofu apply` + build image | ✅ |
| 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 | ✅ |
## Légende de statut
✅ actif · 🟡 dégradé/beta · 🔴 critique/EOL · ⚠️ problème connu · ❌ désactivé
## Comment éditer ce runbook
1. **Ajouter une page** → la créer depuis le template tree-docs adéquat **et** ajouter sa ligne dans l'index ci-dessus.
2. **Garder les liens croisés bidirectionnels** → toute dépendance citée dans une page (`Upstream`/`Downstream`) doit avoir sa réciproque sur l'autre page.
3. **Mettre à jour `Last Updated:`** ci-dessus après tout changement de structure.
4. Les exemples cités (`erp`, `webapp`) sont vivants : revérifier les snippets contre le code réel avant de s'y fier aveuglément.

View File

@@ -0,0 +1,55 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **Conventions de nommage**
# Conventions de nommage
> **Pourquoi cette page.** Un seul nom — `<app>` — est réutilisé à l'identique dans une dizaine de systèmes (Gitea, Postgres, Vault, Kubernetes, ArgoCD, GCS, DNS). Toutes les étapes du runbook en dépendent ; on le centralise ici pour ne pas le ré-expliquer partout.
> **Audience.** Quiconque crée ou audite une app sur la plateforme.
> **Status.** Actif · revu le 2026-05-31.
---
## TL;DR
Choisis **un** identifiant `<app>` en **kebab-case minuscule** (`webapp`, `erp`, `dance-lessons-coach`, `url-shortener`). Ce nom devient, **sans variation**, la clé de toutes les ressources. Une seule faute de frappe quelque part casse la chaîne (auth Vault, rattachement DB, sync ArgoCD).
## Le nom `<app>` dans chaque système
| Système | Identifiant dérivé de `<app>` | Exemple (`erp`) | Source de vérité |
|---|---|---|---|
| Dépôt Gitea | `arcodange-org/<app>` (ou `arcodange/<app>` si `org` surchargé) | `arcodange-org/erp` | [argocd/templates/apps.yaml](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/templates/apps.yaml) |
| Base PostgreSQL | `<app>` | `erp` | [postgres/iac/main.tf](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/main.tf) |
| Rôle propriétaire PG (non-login) | `<app>_role` | `erp_role` | postgres/iac/main.tf |
| Rôle DB dynamique Vault | `postgres/creds/<app>` | `postgres/creds/erp` | [modules/app_roles/main.tf](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_roles/main.tf) |
| Rôle d'auth Kubernetes (Vault) | `<app>` | `erp` | modules/app_roles/main.tf |
| Policy Vault **runtime** (pod) | `<app>` | `erp` | [modules/app_policy/main.tf](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf) |
| Policy Vault **CI** (ops) | `<app>-ops` | `erp-ops` | modules/app_policy/main.tf |
| Rôle JWT de CI (Vault) | `gitea_cicd_<app>` | `gitea_cicd_erp` | modules/app_policy/main.tf |
| Groupe d'identité Vault | `<app>-ops` | `erp-ops` | modules/app_policy/main.tf |
| Secret KV de config | `kvv2/<app>/config` | `kvv2/erp/config` | modules/app_roles (sortie `kvv2_path_prefix`) |
| Namespace Kubernetes | `<app>` | `erp` | apps.yaml (`CreateNamespace=true`) |
| ServiceAccount Kubernetes | `<app>` | `erp` | [chart/templates/serviceaccount.yaml](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/chart/templates/serviceaccount.yaml) |
| Application ArgoCD | `<app>` | `erp` | apps.yaml |
| Préfixe d'état OpenTofu (GCS) | `<app>/main` | `erp/main` | [iac/backend.tf](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/iac/backend.tf) |
| Domaine interne | `<app>.arcodange.lab` | `erp.arcodange.lab` | chart/values.yaml (ingress) |
| Domaine public | `<app>.arcodange.fr` | `webapp.arcodange.fr` | chart/values.yaml (ingress) |
## Pourquoi cette uniformité est structurante
Les briques se « branchent » entre elles **par convention de nom**, pas par configuration explicite :
- Le module `app_roles` génère un user PG dynamique avec `GRANT <app>_role TO …` → il **suppose** que `<app>_role` (créé à l'[étape 2](02-database.md)) porte exactement ce nom.
- Le `VaultDynamicSecret` du chart lit `postgres/creds/<app>` → il **suppose** que le rôle Vault (créé à l'[étape 5](05-app-terraform.md)) porte exactement `<app>`.
- L'`Application` ArgoCD déduit `repoURL=.../<app>`, `path=chart`, `namespace=<app>` du seul nom → le dépôt ([étape 1](01-gitea-repo.md)) et le namespace doivent matcher.
**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.
## Références croisées
- [01 · Dépôt Gitea](01-gitea-repo.md) — fixe `<app>` comme nom de dépôt sous `arcodange-org`.
- [02 · Base de données](02-database.md) — crée `<app>` et `<app>_role`.
- [03 · Vault plateforme](03-vault-platform.md) — crée `gitea_cicd_<app>`, policies `<app>` / `<app>-ops`.
- [04 · Chart Helm](04-helm-chart.md) — référence `<app>`, `creds/<app>`, `kvv2/<app>/config`.
- [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`.