---
tags: [vps, opencode, securite, zeroclaw, Securite]
type: note
description: "Garde-fous ZeroClaw : deny dur sur 9 commandes irréversibles, 0 token, 0 friction au quotidien."
nature: documentation
projects:
  - [[03_PROJETS/VPS-IA]]
---

# VPS-IA_Garde-fous-OpenCode-ZeroClaw-20261102

**Date** : 2026-11-02 — **Agent** : vps-admin — **Serveur** : datagrandest.net (OVH / Yunohost)

Étape 1 des « bonnes pratiques ZeroClaw » transposables à OpenCode. Analyse complète puis **mise en œuvre du lot sans risque** (validation GRK : « les 5 items »). Étape 2 = garde-fous avancés, reportée.

## Principe retenu

ZeroClaw gate hors du modèle (risk profile, policies, sandbox) : on ne reprend que le **deny dur** sur opérations **irréversibles**, jamais d' `ask` (friction + risque en contexte non interactif), jamais de blocage de l'agent (confiance, autonomie). Résultat : **0 token, 0 prompt, 0 changement de comportement au quotidien**.

## Mises en œuvre (étape 1)

| # | Action | Fichier |
|---|---|---|
| 1 | **Policies deny** (9 `tool.use shell:*`) : `dd`, `mkfs`, `wipefs`, `reboot`, `shutdown`, `poweroff`, `swapoff`, `git push --force*`, `git push -f*` | `~/.config/opencode/opencode.json` → `policies` |
| 2 | **Audit sauvegardes** (voir ci-dessous) | lecture seule |
| 3 | **Doctor quotidien** (10 checks → ntfy si anomalie) | `~/.opencode/bin/cron-quotidien-opencode-doctor.sh` — cron **03:20 UTC** |
| 4 | **Alerte coût 24 h** (seuil **2,00 USD**, ~4× la moyenne de 0,45 USD/j) | `~/.opencode/bin/cron-quotidien-opencode-cout.sh` — cron **03:25 UTC** |
| 5 | **Bornage sorties d'outil** : `tool_output.max_lines = 1200`, `max_bytes = 32000` (défauts 2000 / 51200) | même fichier de config |

### Où vivent les policies et pourquoi

Pas dans `/etc/opencode/` (écriture root impossible depuis la session `opencode`), mais dans la **config globale** : pour les policies, l'ordre d'autorité est *managed > système > Console > **global** > projet > .opencode*. La config globale **écrase donc déjà** les projets et les agents — seule une config système en `/etc/opencode/` pourrait la renforcer (étape 2 possible, via root).

### Vérifications faites

- `opencode debug config` : `policies` + `tool_output` bien chargés ; aucun avertissement de validation dans `opencode.log`.
- **Test négatif réel** : `dd` (sans opérande, inoffensif) et `git push -f` dans un dépôt jetable **sans remote** → les deux renvoient `{"error":{"type":"permission.rejected","message":"Blocked by configuration policy"}}`.
- Doctor exécuté en **environnement cron** (`env -i PATH=... HOME=...`) → `OK`, rc=0 ; test négatif avec curl remplacé par `echo` → construction du message + récupération du token OK (**token non affiché**, longueur 32).
- `crontab -l` : 5 lignes, aucune variable d'environnement reprise ; `atq` vide. Les deux scripts sont en `755`, `bash -n` OK.
- Aucun fichier de log créé par le doctor → **pas de règle logrotate à ajouter**.

## Doctor — les 10 checks

service · API `/api/info` (uniquement si le service tourne, pour ne jamais relancer de serveur) · agent `vps-admin` chargé · config projet + `policies` chargées · 4 processus MCP (grist, silverbullet, excalidraw sentinel + maaker) · `opencode.db` présente + `PRAGMA integrity_check` · `opencode-costs` exécutable · erreurs `level=ERROR` > 10 sur 24 h · lignes crontab des crons QMD et consolidation · backlog git > 100 fichiers.

Complémentaire de `vps-check` (03:05) et `ntfy-alert` (03:10) qui couvrent services en échec génériques, disque ≥ 80 % et hygiène des one-shot.

## Audit des sauvegardes (étape 2 = restauration réelle)

| Donnée | Sauvegarde | État |
|---|---|---|
| Espace SilverBullet `shizendo` (180 Mo) | git → Forgejo `silverbullet-notes.git`, **0 commit non poussé** (02/11 05:40) | ✅ |
| + miroir | unison → Nextcloud `files/silverbullet` (service actif) | ✅ (miroir, non versionné) |
| + tarball manuelle | `~/backups/shizendo-pre-migration-univers_20261030.tar.gz` (154 Mo) | ✅ ponctuel |
| Config OpenCode + mémoire vive | git → Forgejo `vps-opencode.git`, 0 modif | ✅ |
| **`opencode.db`** (sessions, coûts, historique M) | **aucune** — `.local/` ignoré par git, pas de cron | 🔴 |
| **Grist** | aucun cron trouvé ; dossier en accès refusé pour `opencode` | ❓ root |
| **Apps YunoHost / Forgejo** | aucun `.ynhbak` dans `/var/backups` | 🔴 / ❓ root |
| Coffre `pass` | versionné git (`pass git log`) | ✅ |

**Risque structurel** : les sauvegardes git vivent sur Forgejo, **installé sur le même VPS** → perte = tout perdre. À traiter en étape 2 (copie externe), avec confirmation.

## ⚠️ Correction (02/11) — la web UI EST exposée

L'analyse initiale disait « aucun reverse-proxy » : ** faux**. `opencode.datagrandest.net` = app YunoHost **`redirect__2` « OpenCode (Redirect) »** (reverse-proxy vers 127.0.0.1:49374), derrière le **SSO**. Autres expositions détectées : `excalidraw-mcp.datagrandest.net` (`redirect__3`) et le WebDAV SilverBullet (rclone). La confusion vient de `/etc/nginx` en **750 root:root** — illisible pour `opencode` (le grep silencieux ≠ absence ; leçon déjà connue, ré-appliquée ici).

**Conséquence** : la surface externe = SSO + web UI. Un attaquant qui obtient un accès SSO peut **piloter l'agent** → les `policies` (deny dur) et le durcissement systemd gardent leur sens côté externe aussi. Les `ask` sont réduits au minimum (friction constatée : le pattern `rm -rf /*` matchait tout `rm -rf X` — corrigé en formes exactes ; `git clean/reset` et `external_directory` retirés ; testé : `rm -rf /tmp/...` s'exécute désormais sans prompt).

## Étape 2 — mise en œuvre (02/11/2026)

| # | Action | Où | Vérification |
|---|---|---|---|
| 1 | 5 règles **`ask`** : `rm -rf /*`, `git clean -fdx*`, `git reset --hard*`, `edit */.ssh/*`, `edit */.password-store/*` | `permissions` de la config **globale** | ✅ test `opencode run` (non interactif) sur `rm -rf /tmp/...` → **auto-reject, aucun blocage** ; en TUI/web → demande de confirmation |
| 2 | `pkm-manager` : `deny shell:sudo *` + `ask external_directory *` (il n'a aucun besoin système) | SilverBullet `IA_AGENTS` → `sync-silverbullet.sh` | ✅ frontmatter local à jour |
| 3 | Bloc **« Voix & personnalité »** (ton, format, honnêteté, posture, choix, registre) + règle mémoire **« Tags & liens »** (`#decision`/`#leçon`/`#fait` + liens `[[...]]`, ni secret ni PII) | SilverBullet `IA_CONTEXT/AGENTS` → sync | ✅ AGENTS.md régénéré (source de vérité = SB) |
| 4 | **Filtre secret** dans la consolidation (sbt_ / sk-or-v1- / ghp_ / gho_ / Bearer / `clé=valeur`) appliqué **uniquement à la copie promue vers L** — le fichier local garde l'entrée d'origine | `cron-memory-consolidation.sh` | ✅ `--dry-run` : 4 secrets factices → `[SECRET:filtré]` ×4, rien d'écrit |
| 5 | **`opencode-estop.sh`** : `GET /api/session/active` → `POST …/interrupt` ; `--dry-run` liste sans couper | `~/.opencode/bin/` | ✅ dry-run : 2 sessions actives détectées (pas de test réel : il couperait la session en cours) |
| 6 | **Backup `opencode.db`** quotidien : `.backup` SQLite cohérent + gzip → `~/.local/state/backups/` (ignoré par git), rétention **5** = ~60 Mo | cron **03:30 UTC** | ✅ rc=0, 1 copie de 12 Mo |
| 7 | **Matrix maintenu** comme solution de repli (décision GRK) : `opencode-matrix.service` reste actif (~311 Mo) | — | — |

**Test décisif (1)** : en non-interactif, OpenCode répond *« This non-interactive run cannot ask the user for permission, so the request was rejected »* → les `ask` sont **fail-closed et ne figent jamais** un cron, un bridge ou un appel Node-RED. Un `rm -rf` en commande composée (`ls; rm -rf …; ls`) déclenche aussi l'ask sur toute l'opération.

### Anomalies repérées en chemin

- `memory-common.md` ≈ **88 %** de son budget (une ligne dupliquée d'AGENTS.md a été retirée) → consolidation manuelle à prévoir (décision GRK : quoi purger).
- `memory-common.md` et `memory-vps-admin.md` **ne se terminaient pas par `\n`** → risque de fusion de deux entrées à la prochaine écriture (corrigé ; bug confirmé pendant les tests).
- **Le tarball SilverBullet de 154 Mo est suivi par git** → `.git` = **221 Mo**, chaque push traîne ce binaire ; `backups/` n'est pas dans `.gitignore`.

## Piste root — réalisée par GRK (02/11, fin de journée)

| Action | État | Détail |
|---|---|---|
| `/etc/opencode/opencode.json` (9 policies) | ✅ | chargé en priorité 2 (vu dans `opencode debug config`) |
| Durcissement systemd | ✅ **après 3 bisects** | versions finales : **`ProtectSystem=full` + `ProtectControlGroups`** |
| Vérifs YNH | ✅ | 18 archives **pre-upgrade uniquement** (= aucune sauvegarde planifiée) · Grist = `/home/yunohost.app/grist/home.db` (5 Mo) · Forgejo = 117 Mo · Node-RED pilote OpenCode via l'API sessions (36 mentions) |

### Leçon : le seccomp de systemd pose `no_new_privs`

Chronologie des régressions (chaque fois : **sudo cassé** + **crontab setgid illisible** → faux positif du doctor, corrigé pour annoncer la vraie cause) :

1. `NoNewPrivileges=yes` → retiré, **le flag restait à 1**.
2. `RestrictSUIDSGID=yes` → *implique* `NoNewPrivileges` quand le service tourne avec `User=` (man systemd.exec) — et `systemctl show` affiche **la valeur d'origine**, pas l'appliquée.
3. `LockPersonality` + `RestrictRealtime` → retirés, **le flag restait à 1**.
4. **Bisect sans redémarrer** via `systemd-run --property=… --uid=opencode python3 prctl(39)` → coupables finaux : **`ProtectKernelTunables`** et **`ProtectKernelModules`** (les deux posent du seccomp → NNP requis par le noyau). `ProtectSystem=full` et `ProtectControlGroups` = **NNP 0** ✅.

Diagnostic : la ligne s'appelle **`NoNewPrivs`** dans `/proc/self/status` (pas `NoNewPrivileges`), ou `prctl(39, …)`. Valeur conservée : les 2 options retirées protègent contre des actions **réservées à root** de toute façon → **zéro perte de sécurité réelle**.

**État final vérifié** : `NNP=0` · `sudo -n ss -tulnp` répond (allowlist intacte) · `crontab -l` = 6 lignes · doctor = **OK** (fin des alertes ntfy).

## Sauvegardes externes → pCloud (rclone natif `pcloud_oc`)

| Fichier | Fréquence | Ce qu'il fait |
|---|---|---|
| `cron-quotidien-pcloud-backup.sh` (04:35 UTC) | quotidien | 6 sources : shizendo · config+mémoire OC · scripts `.opencode` · db-backups · coffre `pass` · staging · **snapshot Grist** (`sqlite3 .backup` cohérent) → `pcloud_oc:VPS-Backup/` ; exclut le binaire opencode (242 Mo) |
| `cron-hebdo-yunohost-backup.sh` (dim. 05:10 UTC) | hebdo | `yunohost backup create -n hebdo-… -o /var/backups/pcloud-staging/yunohost --apps grist forgejo` → upload → **purge immédiate du staging** (0 croissance disque) → rotation 35 j côté pCloud |

**Garanties** : le dossier `/home/yunohost.backup/archives` (pre-upgrade) n'est **jamais** lu ni modifié ; seuls les fichiers préfixés `hebdo-` sont supprimés ; échec = alerte ntfy + archive conservée 7 jours.

## Étape 3 (ouverte)

Consolidation manuelle de `memory-common.md` · réduction des secrets en environnement du service · ménage éventuel des archives pre-upgrade (mesure `du` demandée, **validation GRK obligatoire**) · éventuel ajout d'apps YNH à la liste hebdo.

6. Reste aussi : `git rm --cached` du tarball 154 Mo + règle `.gitignore` (le fichier reste sur disque ; l'historique git ne rétrécit pas sans réécriture) — **validation GRK**.


## Voir aussi

Note [[04_NOTES/VPS-IA_Audit-Securite-VPS-2026-10-16]] · [[04_NOTES/VPS-IA_Permissions-MCP-Par-Agent]] · skill [[80_CONFIG/IA_SKILLS/planification-taches]]
