> ## Documentation Index
> Fetch the complete documentation index at: https://docs.snakysec.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets rotation

# Runbook — Auto-rotation des secrets

**Owner**: Plateforme · **Audience**: MSSP\_ADMIN avec accès Vault root
**Statut**: V1 (avril 2026)

## Résumé exécutif

| Type de secret             | Engine Vault                             | TTL          | Rotation                               | Effort opérateur                       |
| -------------------------- | ---------------------------------------- | ------------ | -------------------------------------- | -------------------------------------- |
| Clés Transit (Encryption)  | `transit/`                               | —            | Auto-rotate 90j                        | Aucun                                  |
| Postgres app creds         | `database/` (postgresql-database-plugin) | 7j (max 30j) | À chaque boot d'un conteneur           | Restart hebdo                          |
| Redis app creds            | `database/` (redis-database-plugin)      | 7j (max 30j) | À chaque boot d'un conteneur           | Restart hebdo                          |
| GitLab PAT                 | KV v2 + worker BullMQ                    | 90j          | Worker `worker-gitlab-rotator` mensuel | Aucun                                  |
| `AUTH_SECRET`, certs Entra | KV v2                                    | —            | Manuel                                 | Cf. [key-rotation.md](key-rotation.md) |

## 1 — Clés Transit (Encryption)

Vault Transit garde la clé maître en interne ; l'app ne fait que des appels HTTP `transit/encrypt|decrypt`. Auto-rotate 90j configuré au boot de Vault via [docker/vault/init.sh](../../platform/docker/vault/init.sh) :

```sh theme={null}
vault write transit/keys/mssp-platform/config auto_rotate_period=2160h
vault write transit/keys/mssp-clients/config  auto_rotate_period=2160h
```

Vault crée automatiquement une nouvelle version (`vault:v2:`, `vault:v3:` …). **Tous les ciphertexts existants restent déchiffrables** sans migration applicative ; seule la version active utilisée pour les nouvelles encryptions change.

Vérifier :

```sh theme={null}
docker exec mssp-vault vault read transit/keys/mssp-clients
# auto_rotate_period: 2160h
# latest_version: 1   ← s'incrémente après 90j
```

Pour forcer une rotation manuelle (urgence) :

```sh theme={null}
docker exec mssp-vault vault write -f transit/keys/mssp-clients/rotate
```

Pour rewrap **tous** les ciphertexts vers la dernière version (optionnel, n'affecte pas le déchiffrement) :

```sh theme={null}
node platform/scripts/rewrap-legacy-secrets.mjs --dry-run
node platform/scripts/rewrap-legacy-secrets.mjs
```

## 2 — Postgres dynamic credentials

### Architecture

```
Boot                       Runtime (every lease/4 sec)         Max TTL (30j)
────                       ──────────────────────────         ──────────────
GET database/creds/    →   PUT sys/leases/renew          →   Vault refuses renewal
   mssp-app                 (lease prolongé +7j)              → Sentry alert
{ username: v-token-...                                       → restart container
  password: ...                                               → fresh creds
  lease_duration: 168h }
```

### Pré-requis (one-time setup)

1. **Postgres** : créer un user `vault` avec `CREATEROLE` + grant du rôle `mssp_app_role`.
2. **Vault** : `vault_pg_password` dans `mssp/data/platform`, engine `database/` configuré.

Bootstrap (run après `make preprod` initial OU après reset de la KV Vault) :

```sh theme={null}
# 1. Génère un password dédié vault → Postgres
VAULT_PG_PASS=$(openssl rand -hex 32)

# 2. Stocke dans Vault
docker exec mssp-vault vault kv patch mssp/platform vault_pg_password="$VAULT_PG_PASS"

# 3. Crée user Postgres (idempotent)
docker exec mssp-postgres psql -U mssp -d mssp_platform <<SQL
DO \$\$
BEGIN
  IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'vault') THEN
    CREATE ROLE vault WITH LOGIN CREATEROLE PASSWORD '$VAULT_PG_PASS';
  ELSE
    ALTER ROLE vault WITH PASSWORD '$VAULT_PG_PASS';
  END IF;
  IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'mssp_app_role') THEN
    CREATE ROLE mssp_app_role INHERIT;
  END IF;
END
\$\$;

GRANT CONNECT ON DATABASE mssp_platform TO mssp_app_role;
GRANT USAGE ON SCHEMA public TO mssp_app_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO mssp_app_role;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO mssp_app_role;
ALTER DEFAULT PRIVILEGES FOR ROLE mssp IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO mssp_app_role;
ALTER DEFAULT PRIVILEGES FOR ROLE mssp IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO mssp_app_role;
GRANT mssp_app_role TO vault WITH ADMIN TRUE, INHERIT TRUE, SET TRUE;
SQL

# 4. Re-run init.sh pour configurer le database engine
docker exec mssp-vault sh /vault/init.sh
```

### Activer le mode dynamique côté app

```sh theme={null}
# .env (dev) ou docker-compose env (prod)
VAULT_DYNAMIC_DB=true
```

Recreate les conteneurs (`make preprod` / `make app-rebuild`). Au boot :

```
[INFO] Vault dynamic Postgres creds issued { leaseId, role, leaseDurationSec: 604800 }
[INFO] Vault secrets injected { injected: 2, dynamicDb: true, dynamicRedis: false }
```

### Vérifier la rotation

```sh theme={null}
# Lister les leases actifs
docker exec mssp-vault vault list sys/leases/lookup/database/creds/mssp-app

# Inspecter un lease
docker exec mssp-vault vault read sys/leases/lookup lease_id=<id>
```

Toutes les 168/4h ≈ 42h, l'app appelle `sys/leases/renew` automatiquement (`platform/src/lib/vault-lease-renewer.ts`). Logs :

```
[INFO] Vault lease renewed { leaseId, label: "database/mssp-app", newTtlSec: 604800 }
```

### Restart hebdo

Vault refuse de renouveler après `max_ttl=720h` (30j). À ce moment, l'app doit avoir été redémarrée pour fetch des creds frais. Cadence recommandée : **deploy/restart hebdomadaire**, soit via :

* pipeline GitLab CI nightly/weekly redeploy
* cron host : `0 4 * * 0 docker compose restart` (dimanche 04:00)

Si le restart est manqué, Sentry alerte via la signature `vault-lease-renewer` quand TTL \< 24h.

## 3 — Redis dynamic credentials

Identique à Postgres mais via le plugin `redis-database-plugin` du même engine `database/`. Les rôles sont nommés avec suffixe `-redis` pour ne pas collisionner :

* `database/roles/mssp-app-redis`
* `database/roles/mssp-worker-redis`

### Activer

```sh theme={null}
# .env
VAULT_DYNAMIC_REDIS=true
```

Le password Redis admin (`mssp/platform.redis_password`) reste utilisé par Vault pour créer/révoquer les users dynamiques. Il n'est plus utilisé par l'app au runtime quand `VAULT_DYNAMIC_REDIS=true`.

### ACL Redis

V1 = `[+@all, ~*]` (toutes commandes, toutes clés). À tightener Q3 vers `[+@read, +@write, +@stream, ~*]` (BullMQ utilise les streams).

## 4 — GitLab PAT — worker BullMQ mensuel

GitLab n'a pas d'engine Vault dynamique. Le worker `gitlab-token-rotator-worker.ts` rotates le PAT le **15 de chaque mois à 03:00 UTC** :

```ts theme={null}
// Pseudo-code
const self = await GET /personal_access_tokens/self
const rotated = await POST /personal_access_tokens/{self.id}/rotate
                       { expires_at: now + 90j }
await putSecret("mssp/platform", { ...secrets, gitlab_token: rotated.token })
```

`POST /rotate` est atomique : nouveau token créé + ancien révoqué dans la même requête.

### Audit log

Chaque rotation émet un événement `secret.rotate` dans `platform_audit_log` (chaîne intègre Ed25519). Filtrer par :

```sql theme={null}
SELECT created_at, after FROM platform_audit_log
WHERE action = 'secret.rotate'
  AND resource_label = 'mssp/platform.gitlab_token'
ORDER BY created_at DESC
LIMIT 10;
```

### Alertes Sentry

| Signal                                             | Niveau                   |
| -------------------------------------------------- | ------------------------ |
| Token expire dans \<14j (cycle de rotation manqué) | warning                  |
| `POST /rotate` échoue                              | error + captureException |
| Token introuvable via `/self` (déjà révoqué ?)     | error                    |

### Forcer une rotation manuelle

Cas : fuite de token, lockdown urgence.

```sh theme={null}
docker exec mssp-app node /app/dist/workers/gitlab-token-rotator-worker.js
# Ou via BullMQ board : POST job "rotate" sur queue "gitlab-token-rotator"
```

Note : le worker logue dans `platform_audit_log` même en run manuel.

## 5 — Garde-fous communs

### Test post-rotation

Après chaque rotation (auto ou manuelle), vérifier :

```sh theme={null}
# 1. Health endpoint
curl -fs http://localhost:3000/api/health
# attendu: {"db":"ok","redis":"ok"}

# 2. Lease actif et frais
docker exec mssp-vault vault list sys/leases/lookup/database/creds/mssp-app

# 3. Pas d'erreurs Sentry sur composant vault-*
```

### Rollback Vault Transit

Vault Transit conserve toutes les versions (`min_decryption_version=1`). Pas de risque de perte de données — un rewrap raté laisse les anciens ciphertexts utilisables.

### Rollback dynamic DB / Redis

Désactiver le flag :

```sh theme={null}
# .env
VAULT_DYNAMIC_DB=false
VAULT_DYNAMIC_REDIS=false
```

Recreate les conteneurs → fallback automatique sur les passwords statiques de `mssp/data/platform`.

### Rollback GitLab PAT

```sh theme={null}
# Lire la version précédente dans Vault KV v2 (10 versions retenues par défaut)
docker exec mssp-vault vault kv get -version=N mssp/platform | grep gitlab_token

# Réécrire en version courante
docker exec mssp-vault vault kv patch mssp/platform gitlab_token="<old_token>"
```

## 6 — Calendrier de bascule

| Date       | Action                                                        | Owner |
| ---------- | ------------------------------------------------------------- | ----- |
| 2026-04-30 | Phase 0/1/2/3 mergée sur `main`, code activable via flags     | Dev   |
| 2026-05-15 | Phase 1 (Postgres) bascule sur preprod, monitoring 2 semaines | Ops   |
| 2026-06-01 | Phase 1 + 2 + 3 bascule en prod, alertes Sentry actives       | Ops   |
| 2026-09-01 | Premier auto-rotate Transit observé (90j depuis init)         | —     |
| 2026-Q3    | Tightening ACL Redis (`@read+@write+@stream`)                 | Dev   |
