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:
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user