feat: socle sécurité — socket-proxy, réseaux, egress (v0.3.0)
Modèle de menace centré prompt-injection + défenses par couche (surface réduite, socket-proxy obligatoire, scoping tokens, egress ollama.com only, secrets par référence, lecture seule renforcée, audit). Documente réseaux interne/egress et filtrage Docker en lecture seule P1. Normalise les fins de ligne (.gitattributes). Palier de risque : n/a (doc + conventions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user