diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8f6ac55 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Normalise les fins de ligne : LF dans le dépôt, quel que soit l'OS. +* text=auto eol=lf + +# Scripts shell et fichiers compose : LF obligatoire (exécutés sous Linux). +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf + +# Binaires : pas de normalisation. +*.png binary +*.jpg binary +*.pdf binary diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c66ee0..c785025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi ## [Unreleased] +## [0.3.0] — 2026-06-23 +### Added +- Socle sécurité : `docs/security.md` (modèle de menace prompt-injection + + défenses par couche + invariants testés), `infra/networks.md` (réseaux interne + vs egress, ports, egress filtré `ollama.com`), `infra/socket-proxy/README.md` + (filtrage des endpoints Docker, lecture seule P1). +- `.gitattributes` : normalisation LF (scripts/compose en LF strict). + ## [0.2.0] — 2026-06-23 ### Added - Conventions versioning/doc : `docs/versioning.md` (SemVer, un-artefact-un-commit, diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..1e6cf47 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,68 @@ +# Sécurité CHLOVA + +> Non négociable. **Risque n°1 : la prompt injection.** L'agent a un accès +> quasi-root via Portainer — le modèle de menace part du principe qu'une entrée +> hostile peut détourner l'agent. + +## Modèle de menace +- **Prompt injection** : une donnée lue (message, contenu de workflow, log) tente + de faire exécuter à l'agent une action non voulue. +- **Exfiltration de secrets** : l'agent ne doit jamais voir de secret en clair. +- **Escalade** : reclasser un asset privilégié en réversible pour contourner la + review. Interdit par construction (voir `risk-tiers.md`). +- **Exposition réseau** : une brique interne deviendrait joignable de l'extérieur. + +## Défenses (par couche) + +### 1. Réduction de la surface +- **Seul le backend CHLOVA est exposé** (auth + TLS). Ollama, n8n, Portainer et + les serveurs MCP n'écoutent **que** sur `chlova-internal`. Voir `infra/networks.md`. +- Phase 1 : surface Telegram en **long-polling** → **aucun port publié**. + +### 2. Accès Docker via socket-proxy (obligatoire) +- Jamais de `/var/run/docker.sock` monté dans l'agent ou le MCP Portainer. +- `tecnativa/docker-socket-proxy` filtre les endpoints Docker autorisés. +- Phase 1 (lecture seule) : n'autoriser que les endpoints de **lecture** + (`CONTAINERS=1`, `IMAGES=1`, `NETWORKS=1`, `VOLUMES=1`, `INFO=1`, `VERSION=1`) + et **interdire** tout le reste (`POST=0`, `EXEC=0`, `CONTAINERS_CREATE` absent…). + Détail dans `infra/socket-proxy/README.md`. + +### 3. Scoping des tokens +- Token **n8n** : portée minimale (lecture en P1). +- Token **Portainer** : portée minimale + `PORTAINER_READ_ONLY=true` en P1 + (le serveur MCP annote `readOnlyHint` et **réécrit les secrets en `[REDACTED]`** + avant qu'ils n'atteignent le modèle). +- Un token = un usage. Rotation documentée. + +### 4. Egress filtré +- Sortie réseau **par défaut refusée** pour les conteneurs de l'agent. +- **Seule exception** : `ollama.com` (proxy cloud Ollama). À appliquer au niveau + pare-feu hôte / réseau Docker. Voir `infra/networks.md`. + +### 5. Secrets par référence +- Tous les secrets (dont `OLLAMA_API_KEY`) via variables d'env / coffre, **jamais + en dur**. `.env` n'est jamais commité (`.env.example` fait foi des clés requises). +- L'agent manipule des **références**, pas des valeurs. `config.ts` **plante au + boot** (fail-closed) si un secret requis manque. + +### 6. Lecture seule renforcée (Phase 1) +- Le **readonly-filter** de l'orchestrateur n'expose au LLM que les outils MCP + `readOnlyHint=true`. Les outils mutants ne sont pas branchés. Double barrière + avec le `PORTAINER_READ_ONLY=true` côté serveur MCP. + +### 7. Audit +- **Toute opération mutante est audit-loggée** (qui/quoi/quand/résultat). En P1, + 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). + +## 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`). +- `config.ts` refuse de démarrer si un secret requis manque. + +## Checklist de revue avant tout passage en Phase 2 (écriture) +- [ ] socket-proxy en place, endpoints d'écriture toujours refusés tant que non requis +- [ ] gatekeeper câblé (vérif statut avant exécution) +- [ ] egress toujours limité à `ollama.com` +- [ ] audit log couvre toute opération mutante +- [ ] tokens re-scopés au strict besoin de la capacité ajoutée diff --git a/infra/.gitkeep b/infra/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/infra/networks.md b/infra/networks.md new file mode 100644 index 0000000..61bf376 --- /dev/null +++ b/infra/networks.md @@ -0,0 +1,34 @@ +# Réseaux & exposition + +> Invariant : **une seule surface exposée**, le backend CHLOVA. Tout le reste +> reste interne. Voir `docs/security.md`. + +## Réseaux Docker + +| Réseau | Type | Membres | But | +|---|---|---|---| +| `chlova-internal` | `internal: true` | backend, ollama, mcp-n8n, mcp-portainer, socket-proxy, (n8n, portainer existants) | bus interne ; **aucun accès externe** | +| `chlova-egress` | bridge | ollama uniquement | sortie contrôlée vers `ollama.com` | + +- `chlova-internal` est déclaré `internal: true` : **pas de route vers l'extérieur**. +- Seul **Ollama** est aussi sur `chlova-egress` (il doit joindre `ollama.com`). +- Le backend n'a **pas** besoin d'egress en Phase 1 (Telegram en long-polling sort + vers `api.telegram.org` → à autoriser explicitement quand la surface est activée). + +## Ports publiés +- **Phase 1 : aucun.** La surface Telegram fonctionne en long-polling (le bot + appelle Telegram, rien n'écoute en entrée). +- Phases ultérieures (API/UI CHLOVA) : exposer **uniquement** le backend derrière + **Traefik + TLS**, et/ou via **VPN mesh** (Tailscale/Wireguard). Jamais Ollama, + n8n, Portainer ou un serveur MCP. + +## Egress (pare-feu) +- Politique par défaut : **deny**. +- Autoriser : `ollama.com` (proxy cloud), et `api.telegram.org` quand la surface + Telegram est active. +- À appliquer au niveau hôte (iptables/nftables ou réseau Docker dédié) ; documenté + ici, mis en œuvre côté infra serveur (hors dépôt si géré par l'hôte). + +## Rappels +- Ollama **n'a aucune auth native** → ne jamais lui publier de port. +- Accès Docker **uniquement** via `socket-proxy` (jamais le socket brut). diff --git a/infra/socket-proxy/README.md b/infra/socket-proxy/README.md new file mode 100644 index 0000000..5cdf903 --- /dev/null +++ b/infra/socket-proxy/README.md @@ -0,0 +1,39 @@ +# socket-proxy — accès Docker filtré + +`tecnativa/docker-socket-proxy` s'intercale entre le serveur MCP Portainer (et +toute brique ayant besoin de l'API Docker) et le socket Docker de l'hôte. Il +**filtre les endpoints** de l'API Docker via des variables d'env (0 = refusé, +1 = autorisé). C'est le seul à monter `/var/run/docker.sock` (en **lecture seule**). + +## Phase 1 — lecture seule +Autoriser uniquement les endpoints de lecture, refuser tout le reste. + +| Variable | Valeur P1 | Effet | +|---|---|---| +| `CONTAINERS` | `1` | lister/inspecter conteneurs | +| `IMAGES` | `1` | lister/inspecter images | +| `NETWORKS` | `1` | lister/inspecter réseaux | +| `VOLUMES` | `1` | lister/inspecter volumes | +| `SERVICES`/`TASKS`/`NODES` | `1` | lecture swarm (si applicable) | +| `INFO` | `1` | `docker info` | +| `VERSION` | `1` | `docker version` | +| `POST` | `0` | **refuse toute mutation** (create/start/stop/rm…) | +| `EXEC` | `0` | **refuse `exec`** dans un conteneur | +| `AUTH`/`SECRETS`/`CONFIGS` | `0` | refuse l'accès aux secrets/configs | +| `BUILD`/`COMMIT`/`IMAGES_CREATE` | `0` | refuse build/commit/pull | + +> `POST=0` est la barrière clé : la plupart des actions destructrices passent par +> des requêtes POST. En Phase 1 on ne l'active **jamais**. + +## Mise en œuvre +Le service est défini dans `infra/docker-compose.yml` (service `socket-proxy`), +branché **uniquement** sur `chlova-internal`, sans port publié. Le socket hôte est +monté en `:ro`. Le MCP Portainer pointe vers `tcp://socket-proxy:2375` au lieu du +socket brut. + +## Rollback / durcissement +- Pour revenir en arrière : remettre toutes les variables d'écriture à `0` et + redéployer. Le passage en Phase 2 (écriture) n'ouvrira que les endpoints + **strictement** nécessaires à la capacité ajoutée, après review (voir + `docs/security.md`, checklist Phase 2). +- Image **épinglée** (tag + digest) dans le compose, jamais `:latest`.