---
tags: [vps, shogun, zeroclaw, yunohost]
type: note
---

# VPS-SHOGUN_ZEROCLAW

**Date** : 2026-11-01 — **Agent** : vps-admin — **Serveur** : `shogun.ovh` (2e VPS, Debian 12 / Yunohost 12.1.41.2)

Installation de **ZeroClaw 0.8.7** exposé sur `https://zc.shogun.ovh`.

## Architecture

| Élément | Valeur |
|---|---|
| Sous-domaine | `zc.shogun.ovh` (record A déjà présent chez OVH → IP `213.166.193.159`) |
| Certificat | **Let's Encrypt**, valide jusqu'au 30/01/2027 (auto-renouvelé, http-01 port 80) |
| App Yunohost | `redirect__4` « ZeroClaw (Redirect) », `redirect_type: reverseproxy`, `target: http://127.0.0.1:42617`, `allowed: [admins]` |
| Daemon | binaire `/opt/zeroclaw/bin/zeroclaw` (+ `zerocode` TUI, `zerorelay`) — **pas dans le PATH** |
| User | système `zeroclaw` (uid 967), HOME `/opt/zeroclaw`, shell nologin |
| Service | unit **user-scoped** `~/.config/systemd/user/zeroclaw.service` (`ExecStart=zeroclaw daemon`), **linger actif** |
| Dashboard | `web_dist_dir = /opt/zeroclaw/web/dist` (livré dans l'archive release) |
| Config | `/opt/zeroclaw/.zeroclaw/config.toml` (`schema_version = 3`) |
| Socket RPC | `/opt/zeroclaw/.zeroclaw/data/daemon.sock` (chemin fixe, indépendant de `XDG_RUNTIME_DIR`) |

## Sécurité — état final (2 couches)

1. **SSO Yunohost** (`allowed: [admins]`) — barrière de connexion à la web UI. **Basé cookie.**
2. **Pairing ZeroClaw** — `require_pairing = true` → bearer token 32 car. alphanum exigé sur chaque REST **et** WebSocket. Lockout 5 essais / 300 s, rate-limit 10 `/pair`/min. Registre d'appareils révocable : `get-paircode --rotate` (tous) ou `--rotate-device <id>`.

En contre-plaque : gateway `127.0.0.1:42617` **uniquement**, `allow_public_bind = false`, aucun port ouvert au firewall, TLS Let's Encrypt.

- `trust_forwarded_headers = true` (IP client réelle derrière nginx).
- `allow_remote_admin = true` → éditeur de config utilisable **en déplacement**.
- **TOTP** (`security.otp`) : `enabled = false` → à activer après enrôlement au dashboard.

## ⛔ Leçon : `auth_basic` nginx incompatible avec une SPA à Bearer

Couche créée puis **supprimée** le 01/11 (`zz-auth.conf`).

**Cause** : un en-tête `Authorization` ne porte qu'**une** valeur — `Basic` (nginx) **ou** `Bearer` (gateway).

```
POST /api/pair   200   ← navigateur envoie Basic → nginx OK, gateway OK → token émis
GET  /api/status 401   ← dashboard met Authorization: Bearer → plus de Basic → NGINX rejette
```

Le pairing **réussissait** (`devices.db` = 2 « Firefox on Windows ») ; c'était la couche nginx qui avalait le token juste après. Deux headers `Authorization` simultanés → gateway en 400.

**Pourquoi le SSO Yunohost fonctionne** : il authentifie via **cookie**, il ne consomme donc **pas** `Authorization` → il coexiste avec le Bearer. **Un reverse-proxy ne peut protéger une SPA à Bearer que par cookie, mTLS ou restriction IP.**

**Piège de diagnostic** : ne jamais identifier l'émetteur d'un 401 à la taille du corps. Ici : **172 B = page HTML nginx**, **95 B = JSON du gateway**, **574 B = page nginx sur `GET /`**. Comparer les corps réels.

## Commandes CLI — pourquoi si longues, et comment simplifier

`zeroclaw gateway get-paircode --new` ne marche pas **pour deux raisons indépendantes** :

1. **Pas dans le PATH** : le binaire vit `/opt/zeroclaw/bin/`, et aucun symlink n'a été mis dans `/usr/local/bin` (ni pour root, ni pour le user `zeroclaw`).
2. **HOME différent** : lancé en root sans `--config-dir`, il lirait `/root/.zeroclaw` (**n'existe pas**) au lieu de `/opt/zeroclaw/.zeroclaw`.

**`runuser -u zeroclaw -- env HOME=...` n'est PAS obligatoire** — vérifié : la sortie est **identique octet pour octet** (empreinte `585ce1ff7c9d6cc2`) que :

Suffit en root, sans `runuser` :

```bash
/opt/zeroclaw/bin/zeroclaw --config-dir /opt/zeroclaw/.zeroclaw gateway get-paircode --new
```

**Simplifications possibles** (à faire si besoin) :
```bash
ln -s /opt/zeroclaw/bin/zeroclaw /usr/local/bin/zeroclaw   # + zerocode, zerorelay
export ZEROCLAW_CONFIG_DIR=/opt/zeroclaw/.zeroclaw   # surcharge le --config-dir
zeroclaw gateway get-paircode --new
```
(`ZEROCLAW_CONFIG_DIR` a la priorité max sur la résolution du chemin de config.)

## Ajouter un appareil : SSH obligatoire ?

**Non.** Le dashboard expose ses propres endpoints de pairing — visibles dans `web/dist/assets/api-*.js` :
- `GET  /pair/code`  → affiche le code courant (le dashboard le fait déjà au chargement)
- `POST /paircode/new` → **minte un code frais** = équivalent CLI de `--new`

Le CLI n'est nécessaire que si l'on veut les actions **destructives** : `--rotate` (révoque tous les tokens), `--rotate-device <id>`.

⚠️ `openapi.json` ne documente que **14 paths** et **ne liste aucun** `/pair*` — la doc le reconnaît elle-même (route inventory construite séparément, issue #6175). **Le JS du dashboard fait foi, pas l'OpenAPI.**

## Accès TUI distant (`zerocode`)

- **Non configuré** : `config get wss` → `Unknown property 'wss'`, aucune section `[wss]`. Le RPC local passe par `daemon.sock` → **ne fonctionne que sur le serveur**.
- Le SSO ne bloque pas la TUI (elle parle au daemon, pas au dashboard), mais l'endpoint WSS n'est **pas exposé** de toute façon.
- **Recommandé : tunnel SSH** (aucune surface exposée, contourne nginx et le SSO) :
  ```bash
  ssh -p 2022 -N -L 42617:127.0.0.1:42617 shogun@shogun.ovh
  # puis zerocode sur 127.0.0.1:42617
  ```
- À étudier plus tard : activer `[wss]` et l'exposer via le vhost (le SSO s'appliquerait, à exclure pour ce chemin).

## Ménage effectué (01/11)

- `/etc/nginx/conf.d/zc.shogun.ovh.d/zz-auth.conf` — **supprimé**.
- `/etc/zeroclaw/htpasswd` + dossier `/etc/zeroclaw` — **supprimés**.
- Entrée coffre `opencode/zc-shogun-htpasswd` — **supprimée** (commit git `0582aff`).

## Pièges rencontrés

- **`/etc/nginx` est en `750 root:root`** (pas le 755 Debian) → htpasswd déposé dedans illisible par le worker → `open() ... Permission denied` → **500**.
- **`pass` n'existe pas sur shogun** (uniquement VPS principal) → `$(pass ...)` exécuté *sur shogun* échoue silencieusement avec un mdp vide. Tester depuis le VPS principal.
- **`[autonomy]` n'existe plus** en v0.8.7 (passé dans `risk_profiles`) ; l'écrire = config invalide.
- **`[gateway.pairing_dashboard]` n'a PAS de `code_length`** → c'est `[gateway.pairing_code]` (`charset`, `length`).
- **`yunohost domain cert install` échoue** sans diagnostic ; `diagnosis run dnsrecords web` ne produit jamais d'entrée pour `zc` (limite « trop de domaines », 64) → `--no-checks` justifié.
- **`zeroclaw status` ment sans `XDG_RUNTIME_DIR`** → faux « stopped ». Référence : `systemctl --user is-active zeroclaw`. (Le pair-code marche dans les deux cas : il passe par `daemon.sock`.)
- **Pas de journal user** : `journalctl --user` → « No journal files were found ». Logs = nginx + `state/runtime-trace.jsonl`.
- `htpasswd` exige `-c` ; `yunohost diagnosis run` n'a pas de flag `--checks` (catégories positionnelles).
- **L'app `redirect` n'est PAS cassée** (2.1~ynh1, CI verte) — fausse info d'un vieux fil de forum.
- **`settings.yml` peut dire `protected: false` alors que le SSO répond** : c'est `allowed: [admins]` qui protège. Se fier au comportement observé (302 vers `/yunohost/sso`).

## État vérifié (01/11, fin de session)

- `GET /` et `GET /api/status` sans cookie → **302** `https://shogun.ovh/yunohost/sso?r=…`
- Plus aucun `www-authenticate` / `auth_basic` actif.
- Service `active` + `enabled`, PID 577579, composant `gateway` = `ok`.
- Disque 41 %, aucun port supplémentaire ouvert.

## Reste à faire

- [ ] Vérifier en navigateur : login SSO (cookie) **puis** pairing (Bearer) doivent coexister.
- [ ] **Inscription TOTP** : `security.otp.enabled` → `true` après enrôlement au dashboard.
- [ ] **Provider + agent** : `ModelProvider: none`, `Agents: none` → `zeroclaw quickstart`, TUI `zerocode`, ou dashboard.
- [ ] Simplifier la CLI (symlink + `ZEROCLAW_CONFIG_DIR`) si besoin.
- [ ] Tunnel SSH pour la TUI distante, si besoin.

## Voir aussi

Skill [[80_CONFIG/IA_SKILLS/shogun-admin]] · note [[04_NOTES/VPS-SHOGUN_APT-DNS]]

## Matrix — ZeroClaw branché sur @zc:shogun.ovh (01/11, soir)

| Élément | Valeur |
|---|---|
| Compte bot | `@zc:shogun.ovh` — login **password + LDAP Yunohost** (`password` chiffré au repos dans ZeroClaw, jamais en clair) |
| Homeserver | `https://matrix.shogun.ovh` |
| Canal | alias `[channels.matrix.shogun_ovh]` : `enabled = true`, `mention_only = false` (répond sans être taggé), `reply_in_thread = true` |
| Room | alias `#kamio:shogun.ovh` → ID canonique `!oatEl-B2nlLHT2h15VoEf117E8AU0CnL65CblxwmjMI` — **non chiffrée**, membres `@zc:shogun.ovh` + `@shogun:shogun.ovh` (les deux `join`) |
| `allowed_rooms` | l'ID canonique ci-dessus (**les alias `#…` ne sont jamais résolus** pour cet allowlist) |
| Peer group | `peer_groups.matrix_local` : `channel = "matrix.shogun_ovh"`, `agents = ["kamio_zc"]`, `external_peers = ["@shogun:shogun.ovh"]` (deny-by-default) |
| Agent | `agents.kamio_zc.channels = ["matrix.shogun_ovh"]` (obligatoire : la validation exige que l'agent écoute sur le canal du peer group) |
| Pairing | **`gateway.require_pairing = false`** → le SSO Yunohost (`allowed: [admins]`, cookie) est la seule barrière ; la gateway reste en `127.0.0.1` |

### Pièges rencontrés

1. **Section « malformed »** : la cause réelle est `missing field homeserver in channels.matrix.default` (blocs laissés vides), **pas** les tables dupliquées `[channels]` / `[channels.matrix]` (python `tomllib` parse OK). Diagnostic : `zeroclaw config migrate --json` → `{"error": …}`. Fix : poser `homeserver` ou supprimer la section vide.
2. **CLI ZeroClaw = contexte utilisateur** : `config set` sur une clé d'**agent** doit se coordonner avec le daemon → lancée en root : `-32010 Credential rejected`. Il faut
   `runuser -u zeroclaw -- env HOME=/opt/zeroclaw XDG_RUNTIME_DIR=/run/user/967 zeroclaw --config-dir /opt/zeroclaw/.zeroclaw config set …`
   (même famille de piège que `zeroclaw status` qui dit « stopped » sans `XDG_RUNTIME_DIR` alors que `systemctl --user is-active` = active).
3. **Coffre/YNH désynchronisés** → 403 `M_FORBIDDEN` + `ldapwhoami` 49 alors que l'autre valeur marchait la veille. Cause : dernière écriture `pass` **antérieure** à `yunohost user update zc`. Diagnostic sans afficher de secret : `pass git log -- opencode/shogun-zeroclaw` (dates) vs `/var/log/yunohost/yunohost-api.log` (`user_update`), puis test `ldapwhoami -x -D uid=zc,ou=users,dc=yunohost,dc=org -y <fichier 600>` (jamais `cat`/`od`).
4. **Anomalie wallabag** : 21:12:46, un `sudo` lancé depuis `/var/www/wallabag2` a exécuté `fos:user:create zc zc@shogun.ovh <mdp en clair>` — **origine = notre VPS principal (217.182.65.158)**, pas un tiers. Un mot de passe reste lisible dans `journalctl` → à faire pivoter si le compte existe. **À identifier** (session qui aurait dû passer par `yunohost user create`).
5. **`enabled` repasse à `false`** quand la section est ré-écritre → revérifier après chaque `config set`.
6. Le `password` du canal doit être **re-set** après tout changement de mot de passe YNH (le champ chiffré ne suit pas).

### Reste à faire

- [ ] **Test de bout en bout** : Guillaume écrit dans `#kamio:shogun.ovh` → le bot doit répondre (fil de discussion).
- [ ] Activer `stream_mode = "partial"` si on veut le streaming des réponses.
- [ ] Faire pivoter le mot de passe exposé dans le journal (anomalie wallabag).

## Auto-join des invitations — cron (01/11)

**ZeroClaw 0.8.7 n'a aucun auto-join** : `MatrixConfig` (25 champs) ne contient ni `auto_join`, ni `accept_invite`, ni `invite_policy` — le binaire ne contient que la string du SDK (`Got MemberEvent::Sync in an invited room`). Le bot **voit** l'invitation (`rooms.invite` dans `/sync`) mais ne la prend pas. Contournement en place :

| Fichier | Rôle |
|---|---|
| `/usr/local/sbin/cron-2min-zeroclaw-autojoin.sh` (755) | lit le token dans `…/state/matrix/shogun_ovh/session.json` **à l'exécution**, `/sync` → `POST /rooms/{id}/join` pour chaque invitation |
| `/etc/cron.d/zeroclaw-autojoin` | `*/2 * * * * root … >/dev/null 2>&1` — **récurrent, jamais auto-détruit** |
| `/etc/logrotate.d/zeroclaw-autojoin` | daily, 7, compress, copytruncate |

- **Pas de processus résident** : 720 réveils/jour × ~0,2 s ≈ 2,4 s de CPU/jour.
- **Log silencieux** : une ligne **uniquement** quand une invitation est acceptée (`/var/log/zeroclaw-autojoin.log`) → pas de croissance.
- Token jamais en argument : écrit dans un fichier temp `umask 077` puis `curl -H @file` (sinon visible dans `ps`).
- Test cron-like validé : `env -i PATH=… HOME=/root <script>` → `join !E4CmTUW2… -> HTTP 200`, 2e passage `rc=0` sans log.

## État final validé (fin de session 01/11)

| Contrôle | Résultat |
|---|---|
| Réponse du bot **dans la timeline** (`reply_in_thread=false`) | ✅ testé par GRK |
| Salle **E2EE** (`#kamio-e2ee`, `m.megolm.v1.aes-sha2`) | ✅ déchiffrement OK, `recovery_key` = clé SSSS de `@zc` |
| `allowed_rooms = []` | ✅ toutes les salles rejointes écoutées |
| Auto-join | ✅ via cron (test `Test ZC E2EE` joint en ≤2 min) |
| Credentials IA | ✅ `providers.models.openrouter.default.api_key` (clé **dédiée**, saisie par GRK via le dashboard), `doctor: API key configured` |
| `doctor` | **29 ok / 2 warnings / 0 erreur** (2 warnings = lingering loginctl + ?) |

### Leçons de session (au-delà de celles de la section « Pièges »)

1. **Le trace vit dans `data/state/`** : `/opt/zeroclaw/.zeroclaw/data/state/runtime-trace.jsonl` — PAS `…/.zeroclaw/state/`. Mes premiers « 0 événement recovery » venaient d'un `grep` sur un fichier inexistant : **toujours vérifier l'existence du fichier avant de conclure d'une absence**.
2. **`recovery_key` ≠ backup megolm** : ce qui compte pour ZeroClaw, c'est le **SSSS** (`m.secret_storage.default_key` → 200) et le **cross-signing**. Fluffychat peut afficher une recovery key **sans** que `m.megolm.backup.version` soit créé (bug connu #963/#3047) → vérifier par API, pas par l'écran.
3. **Un message envoyé avant un redémarrage n'est pas rejoué** (doc : *old timeline history won't be replayed*) → toujours relancer un test **après** le restart.
4. **Provider sans credentials** : `providers list` vide + `auth list` vide + pas d'env ni de `.env` = la clé n'était nulle part ; l'erreur n'apparaît qu'à la 1re réponse (le canal marche, c'est l'appel LLM qui échoue).
5. **Deux processus, un même access_token** : notre cron fait un `/sync?timeout=0` sans `since` (full sync) pour ne jamais interférer avec la boucle `since` du daemon.

### `zeroclaw cron` vs cron OS — test comparatif (01/11, 22:30)

**Les tâches ZC s'exécutent bel et bien** (scheduler interne, résultat horodaté dans le trace : `last=2026-11-01T22:30:03 (ok)`), et **une commande shell ne consomme aucun token** : les 10 appels modèle de `costs.jsonl` sont tous des tours de conversation Matrix (`conv=matrix_shogun_ovh__…`, 21:59→22:17), **aucun à 22:30**. `echo probeA-ok` → ok, `ls` → ok.

**Mais notre script auto-join y est refusé à l'exécution :**
```
output: "blocked by security policy: forbidden path argument: /usr/local/sbin/cron-2min-zeroclaw-autojoin.sh"
```
Le profil `balanced` de `kamio_zc` porte `forbidden_paths = [/usr, /opt, /var, /tmp, /etc, /root, /home, …]` + `workspace_only = true` — alors que le script doit lire `/opt/…/session.json`, écrire `/var/log/…` et `mktemp` en `/tmp`. (À l'ajout, la politique a laissé passer ; c'est à l'exécution qu'elle bloque. Rejet *different* : une commande avec construct `if [ -r /opt/… ]` est refusée **dès l'ajout**.)

**Arbitrage retenu** : le **boucle mécanique** reste sur le cron OS (`/etc/cron.d/zeroclaw-autojoin`) — indépendant du daemon, pas de sandbox, déjà validé 2 fois ; `zeroclaw cron` est réservé aux tâches **agent** (`--prompt`) où le modèle est utile (résumés, rappels, annonces). Les 3 taches de test ont été supprimées (`cron list` → vide).

Pour forcer ZC il faudrait soit déplacer le script dans le workspace de l'agent (**le token deviendrait lisible par l'agent** → exposition au prompt), soit alléger `forbidden_paths` (perte du garde-fou) : les deux sont plus mauvais que le statu quo.

## Clôture de session — 02/11/2026

### Déployé et validé (feu vert GRK)

| Volet | Détail | Preuve |
|---|---|---|
| Pairing ZeroClaw | `gateway.require_pairing = false` (barrière = SSO YNH, gateway en 127.0.0.1) | `config get` = false |
| Canal Matrix | alias `shogun_ovh` : homeserver `https://matrix.shogun.ovh`, `user_id @zc:shogun.ovh`, password chiffré, `enabled`, `mention_only=false`, `reply_in_thread=false`, `allowed_rooms=[]`, `recovery_key` (SSSS de `@zc`) | `doctor` : `API key configured`, `channel:matrix fresh` |
| Agent | `agents.kamio_zc.channels = ["matrix.shogun_ovh"]` + peer group `matrix_local` (`external_peers = ["@shogun:shogun.ovh"]`) | réponses reçues par GRK |
| Provider IA | clé OpenRouter **dédiée** saisie par GRK dans le dashboard → `providers.models.openrouter.default.api_key` | doctor : 29 ok / 0 erreur |
| Salles | `#kamio:shogun.ovh` + `#kamio-e2ee` (chiffrée) ; les salles « bot seul » quittées (HTTP 200) | 2 salles restantes, 0 invite en attente |
| Auto-join | cron OS `*/2` + logrotate | 22:09 (manuel) et **22:20 (automatique)** → HTTP 200 |
| Tests GRK | 1. réponse **dans la timeline** ✅ 2. **E2EE déchiffré** ✅ 3. **auto-join** ✅ | — |

### Pièges retenus (résumé)

1. **CLI ZeroClaw = contexte utilisateur** (`runuser -u zeroclaw` + `HOME` + `XDG_RUNTIME_DIR`) sinon `-32010 Credential rejected` ; `zeroclaw status` ment sans `XDG_RUNTIME_DIR` (faire confiance à `systemctl --user is-active`).
2. **Trace = `data/state/runtime-trace.jsonl`** (PAS `state/`) — ne jamais conclure d'une absence sans vérifier que le fichier existe.
3. **`recovery_key` = SSSS**, pas le *backup megolm* : Fluffychat peut afficher une clé sans que `m.megolm.backup.version` existe (bugs #963/#3047) → vérifier par API.
4. **Un message envoyé avant restart n'est pas rejoué** → toujours re-tester après.
5. **Secrets** : afficher longueur/code retour seulement ; token Matrix lu à l'exécution depuis `session.json` (600), jamais en argv/env (`curl -H @file`).
6. **Désynchronisation coffre/YNH** : comparer `pass git log` avec `/var/log/yunohost/yunohost-api.log` (`user_update`).
7. **cron OS vs `zeroclaw cron`** : ce dernier exécute bien ses tâches (gratuites en tokens pour une commande shell) mais **son sandbox refuse nos chemins** → boucle mécanique = cron OS ; `zeroclaw cron --prompt` = tâches agent, fréquence raisonnable (1-2×/jour, ~18 k tokens d'entrée par tour, quota `max_actions_per_hour = 20`).

### Fichiers en place

```
/usr/local/sbin/cron-2min-zeroclaw-autojoin.sh   (755)
/etc/cron.d/zeroclaw-autojoin                    (*/2, root)
/etc/logrotate.d/zeroclaw-autojoin               (daily, 7, copytruncate)
/var/log/zeroclaw-autojoin.log                   (1 ligne/join accepté seulement)
/opt/zeroclaw/.zeroclaw/config.toml.bak-20261101 + .bak-pre-migrate   (sauvegardes)
```

### Reste à faire / points ouverts

- [ ] **Faire pivoter le mot de passe exposé dans `journalctl`** (commande `fos:user:create zc …` partie de notre VPS principale à 21:12 — origine **à identifier**, probablement une session qui aurait dû passer par `yunohost user create`).
- [ ] **Backup megolm absente** pour `@zc` (`m.megolm.backup.version` → 404) : relancer le Chat Backup dans Fluffychat si on veut que les clés de salle survivent à une perte du trousseau du bot.
- [ ] **Clé OpenRouter dédiée** : présente uniquement dans `config.toml` chiffré → la **dupliquer dans le coffre pass** (`opencode/zeroclaw-openrouter-key`) pour survivre à une réinstallation.
- [ ] Fédération Matrix désactivée (choix) + TOTP ZeroClaw désactivé — à activer si besoin.
- [ ] Port 8448 chez OVH à ouvrir si un jour la fédération est activée.

### Voir aussi

Note [[04_NOTES/VPS-SHOGUN_MATRIX-FLUFFYCHAT]] (domaine, fluffychat, tuwunel, compte `zc`) · skill [[80_CONFIG/IA_SKILLS/shogun-admin]]
