Compare commits
13 Commits
a644436746
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 827af6b392 | |||
| 8330d82225 | |||
| 54b3092305 | |||
| e0fb337a5f | |||
| ea500abe62 | |||
| 62673a2d65 | |||
| 4163b06659 | |||
| 3fb7544351 | |||
| 5038956332 | |||
| 6ede249da9 | |||
| 9e821e1626 | |||
| 69b7e9ddcb | |||
| 069edd72f1 |
@@ -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.
|
||||
|
||||
🏹💻🪽
|
||||
|
||||
11
ansible/arcodange/factory/inventory/group_vars/all/gitea.yml
Normal file
11
ansible/arcodange/factory/inventory/group_vars/all/gitea.yml
Normal 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
|
||||
@@ -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
|
||||
@@ -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 }}'
|
||||
@@ -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
35
doc/README.md
Normal 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.
|
||||
261
doc/adr/20260509-telegram-gateway-auth.md
Normal file
261
doc/adr/20260509-telegram-gateway-auth.md
Normal 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/<slug>]:::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 "🔒 /auth …"<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).
|
||||
@@ -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
21
doc/runbooks/README.md
Normal 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.
|
||||
88
doc/runbooks/new-web-app/01-gitea-repo.md
Normal file
88
doc/runbooks/new-web-app/01-gitea-repo.md
Normal 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.
|
||||
93
doc/runbooks/new-web-app/02-database.md
Normal file
93
doc/runbooks/new-web-app/02-database.md
Normal 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.
|
||||
89
doc/runbooks/new-web-app/03-vault-platform.md
Normal file
89
doc/runbooks/new-web-app/03-vault-platform.md
Normal 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.
|
||||
183
doc/runbooks/new-web-app/04-helm-chart.md
Normal file
183
doc/runbooks/new-web-app/04-helm-chart.md
Normal 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).
|
||||
96
doc/runbooks/new-web-app/05-app-terraform.md
Normal file
96
doc/runbooks/new-web-app/05-app-terraform.md
Normal 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/`.
|
||||
108
doc/runbooks/new-web-app/06-ci-workflows.md
Normal file
108
doc/runbooks/new-web-app/06-ci-workflows.md
Normal 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.
|
||||
91
doc/runbooks/new-web-app/07-argocd-register.md
Normal file
91
doc/runbooks/new-web-app/07-argocd-register.md
Normal 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`.
|
||||
77
doc/runbooks/new-web-app/08-checklist.md
Normal file
77
doc/runbooks/new-web-app/08-checklist.md
Normal 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.
|
||||
104
doc/runbooks/new-web-app/README.md
Normal file
104
doc/runbooks/new-web-app/README.md
Normal 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 & 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.
|
||||
55
doc/runbooks/new-web-app/conventions.md
Normal file
55
doc/runbooks/new-web-app/conventions.md
Normal 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`.
|
||||
Reference in New Issue
Block a user