diff --git a/.env.example b/.env.example index 0c27052..e4b0bc6 100644 --- a/.env.example +++ b/.env.example @@ -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). # Peut contenir un token de chemin → secret, jamais commité. 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 -- `. +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. # CHLOVA_HTTP_PORT=8080 diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d85df..d77fd37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi ## [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 ### Added - `src/api/routes.ts` : API HTTP (`registerApi`) — `POST /api/auth/login` diff --git a/docs/security.md b/docs/security.md index 1e6cf47..0ab5dd3 100644 --- a/docs/security.md +++ b/docs/security.md @@ -55,6 +55,21 @@ 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). +## 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 - 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`). diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 5079fa1..ddab1f1 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -96,15 +96,25 @@ services: - mcp-portainer networks: - chlova-internal - # Phase 1 : surface Telegram en long-polling → AUCUN port publié. - # Phases ultérieures (API/UI) : exposer UNIQUEMENT ce service derrière - # Traefik + TLS et/ou VPN mesh. Voir infra/networks.md. + - traefik-public # API/UI exposée via Traefik (Phase 4) + # Phase 4 : SEUL service exposé, via Traefik + TLS (jamais de port publié en + # 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: chlova-internal: internal: true # aucune route vers l'extérieur chlova-egress: 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: ollama-data: diff --git a/orchestrator/package.json b/orchestrator/package.json index a924ef1..b46f118 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -13,7 +13,8 @@ "start": "node dist/index.js", "typecheck": "tsc -p tsconfig.json", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "provision-auth": "tsx scripts/provision-auth.ts" }, "dependencies": { "@fastify/cors": "11.2.0", diff --git a/orchestrator/scripts/provision-auth.ts b/orchestrator/scripts/provision-auth.ts new file mode 100644 index 0000000..f874945 --- /dev/null +++ b/orchestrator/scripts/provision-auth.ts @@ -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 -- + * + * 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 -- "); + 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);