feat: exposition Traefik + provisioning auth (v0.21.0)

Backend exposé via Traefik+TLS (réseau traefik-public externe, labels,
CHLOVA_DOMAIN) — surface unique. Script provision-auth (hash scrypt +
TOTP otpauth + JWT). .env.example section API/UI. security.md : surface
exposée Phase 4. Compose revalidé.

Palier de risque : privilégié (exposition réseau) — non déployé ; auth
requise pour activer l'API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kantin-Petit
2026-06-23 02:15:17 +02:00
parent 26debf2fe0
commit e97c885ebf
6 changed files with 80 additions and 4 deletions
+12
View File
@@ -42,5 +42,17 @@ CHLOVA_DB_PATH=./data/chlova.db # SQLite : table assets (need-review, Phase
# workflows-n8n/chlova-alerts.v1.0.0.json). Vide = alertes log-only (fail-safe). # workflows-n8n/chlova-alerts.v1.0.0.json). Vide = alertes log-only (fail-safe).
# Peut contenir un token de chemin → secret, jamais commité. # Peut contenir un token de chemin → secret, jamais commité.
ALERT_WEBHOOK_URL= # ex. http://n8n:5678/webhook/chlova-alert ALERT_WEBHOOK_URL= # ex. http://n8n:5678/webhook/chlova-alert
# ── API/UI (Phase 4) — surface exposée, login fort ─────────────────────
# L'API/UI n'est ACTIVE que si les 4 valeurs ci-dessous sont présentes.
# Générer hash + secrets : `npm run provision-auth -- <user> <password>`.
CHLOVA_ADMIN_USER=
CHLOVA_ADMIN_PASSWORD_HASH= # SECRET — hash scrypt
CHLOVA_TOTP_SECRET= # SECRET — secret TOTP (2FA)
CHLOVA_JWT_SECRET= # SECRET — clé de signature JWT
# Origine CORS autorisée en DEV (Vite). En prod le SPA est servi same-origin.
CHLOVA_WEB_ORIGIN= # ex. http://localhost:5173 (dev uniquement)
# Domaine public derrière Traefik (label compose).
CHLOVA_DOMAIN=chlova.example.com
# Phase 1 : aucun port publié (Telegram en long-polling). Renseigné en P3+ si API/UI. # Phase 1 : aucun port publié (Telegram en long-polling). Renseigné en P3+ si API/UI.
# CHLOVA_HTTP_PORT=8080 # CHLOVA_HTTP_PORT=8080
+10
View File
@@ -6,6 +6,16 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi
## [Unreleased] ## [Unreleased]
## [0.21.0] — 2026-06-23
### Added
- `orchestrator/scripts/provision-auth.ts` (+ `npm run provision-auth`) : génère
hash mdp scrypt + secret TOTP + otpauth:// + JWT secret pour `.env`.
- Compose : backend exposé via **Traefik + TLS** (réseau `traefik-public` externe,
labels router/tls), `CHLOVA_DOMAIN`. Surface unique exposée.
- `.env.example` : section API/UI (admin/hash/TOTP/JWT/web-origin/domaine).
- `docs/security.md` : section surface exposée Phase 4 (login fort, rate-limit,
CORS, fail-safe). Compose revalidé.
## [0.20.0] — 2026-06-23 ## [0.20.0] — 2026-06-23
### Added ### Added
- `src/api/routes.ts` : API HTTP (`registerApi`) — `POST /api/auth/login` - `src/api/routes.ts` : API HTTP (`registerApi`) — `POST /api/auth/login`
+15
View File
@@ -55,6 +55,21 @@
toute exécution d'outil est tracée même si read-only. toute exécution d'outil est tracée même si read-only.
- Déploiements de stacks en **GitOps** dès que possible (rollback + audit gratuits). - Déploiements de stacks en **GitOps** dès que possible (rollback + audit gratuits).
## Surface exposée API/UI (Phase 4)
- **Seul le backend** est exposé, via **Traefik + TLS** (label compose, réseau
`traefik-public`). Ollama, MCP, n8n, Portainer restent internes.
- **Login fort obligatoire** : mot de passe (scrypt) **+ TOTP 2FA** → **JWT** court
(HS256). `/api/auth/login` est **rate-limité** (anti brute-force) ; tout le reste
exige un Bearer valide.
- L'API/UI ne s'**active que si l'auth est configurée** (`apiAuth()` : 4 secrets
présents). Sinon, surface Telegram seule (fail-safe).
- Secrets d'auth via env (`provision-auth` les génère) ; **jamais commités**,
masqués dans les logs (`redactedConfig`).
- CORS restreint à `CHLOVA_WEB_ORIGIN` (dev) ; en prod le SPA est servi en
same-origin par le backend (Phase 4 frontend).
- La capacité d'écriture reste derrière le gatekeeper (Phase 2) quelle que soit
la surface : l'API n'élève aucun privilège.
## Invariants vérifiés par test ## Invariants vérifiés par test
- Aucun outil non-read-only n'est exposé en Phase 1 (`readonly-filter.test.ts`). - Aucun outil non-read-only n'est exposé en Phase 1 (`readonly-filter.test.ts`).
- Un asset `privileged` ne peut pas être reclassé `reversible` (`gatekeeper.test.ts`). - Un asset `privileged` ne peut pas être reclassé `reversible` (`gatekeeper.test.ts`).
+13 -3
View File
@@ -96,15 +96,25 @@ services:
- mcp-portainer - mcp-portainer
networks: networks:
- chlova-internal - chlova-internal
# Phase 1 : surface Telegram en long-polling → AUCUN port publié. - traefik-public # API/UI exposée via Traefik (Phase 4)
# Phases ultérieures (API/UI) : exposer UNIQUEMENT ce service derrière # Phase 4 : SEUL service exposé, via Traefik + TLS (jamais de port publié en
# Traefik + TLS et/ou VPN mesh. Voir infra/networks.md. # direct). L'API/UI ne s'active que si l'auth est configurée (.env).
labels:
traefik.enable: "true"
traefik.docker.network: traefik-public
traefik.http.routers.chlova.rule: Host(`${CHLOVA_DOMAIN:-chlova.example.com}`)
traefik.http.routers.chlova.entrypoints: websecure
traefik.http.routers.chlova.tls: "true"
traefik.http.routers.chlova.tls.certresolver: le
traefik.http.services.chlova.loadbalancer.server.port: "8080"
networks: networks:
chlova-internal: chlova-internal:
internal: true # aucune route vers l'extérieur internal: true # aucune route vers l'extérieur
chlova-egress: chlova-egress:
driver: bridge # sortie contrôlée (Ollama → ollama.com), egress filtré côté hôte driver: bridge # sortie contrôlée (Ollama → ollama.com), egress filtré côté hôte
traefik-public:
external: true # réseau Traefik existant du homelab
volumes: volumes:
ollama-data: ollama-data:
+2 -1
View File
@@ -13,7 +13,8 @@
"start": "node dist/index.js", "start": "node dist/index.js",
"typecheck": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest" "test:watch": "vitest",
"provision-auth": "tsx scripts/provision-auth.ts"
}, },
"dependencies": { "dependencies": {
"@fastify/cors": "11.2.0", "@fastify/cors": "11.2.0",
+28
View File
@@ -0,0 +1,28 @@
import { randomBytes } from "node:crypto";
import { generateSecret } from "otplib";
import { hashPassword } from "../src/api/auth.js";
/**
* Génère les secrets d'auth de la surface exposée (Phase 4) à coller dans .env.
* Usage : npm run provision-auth -- <user> <password>
*
* N'imprime que des valeurs à mettre dans le coffre/.env (jamais commitées).
* Le secret TOTP est aussi affiché en otpauth:// pour l'ajouter à une app 2FA.
*/
const [, , user, password] = process.argv;
if (!user || !password) {
console.error("Usage : npm run provision-auth -- <user> <password>");
process.exit(1);
}
const totpSecret = generateSecret();
const jwtSecret = randomBytes(48).toString("base64url");
const otpauth = `otpauth://totp/CHLOVA:${encodeURIComponent(user)}?secret=${totpSecret}&issuer=CHLOVA`;
console.log("# --- À coller dans .env (NE JAMAIS committer) ---");
console.log(`CHLOVA_ADMIN_USER=${user}`);
console.log(`CHLOVA_ADMIN_PASSWORD_HASH=${hashPassword(password)}`);
console.log(`CHLOVA_TOTP_SECRET=${totpSecret}`);
console.log(`CHLOVA_JWT_SECRET=${jwtSecret}`);
console.log("\n# Ajoute ce TOTP à ton app 2FA (Aegis, Google Authenticator…) :");
console.log(otpauth);