Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
61 lines
4.4 KiB
Plaintext
61 lines
4.4 KiB
Plaintext
---
|
||
title: Rotation des clés
|
||
icon: rotate
|
||
---
|
||
|
||
Twenty possède deux familles de clés indépendantes :
|
||
|
||
* **Clés de signature JWT** — paires de clés asymétriques ES256 (avec balise `kid`) stockées dans `core."signingKey"`, utilisées pour signer et vérifier les jetons d’accès / de rafraîchissement.
|
||
* **Clé de chiffrement au repos** — `ENCRYPTION_KEY`, utilisée pour chiffrer les jetons OAuth, les variables d’application, les clés privées de clés de signature, les valeurs de configuration sensibles et les secrets TOTP à l’intérieur d’une enveloppe `enc:v2:`.
|
||
|
||
`APP_SECRET` est un secret hérité conservé pour la rétrocompatibilité : lorsque `ENCRYPTION_KEY` n’est pas défini, il agit comme mécanisme de secours pour le chiffrement au repos / le cookie de session, et il continue de vérifier les jetons d’accès HS256 préexistants. Il sera déprécié.
|
||
|
||
## Clés de signature JWT
|
||
|
||
Chaque clé contient une `publicKey` (conservée indéfiniment afin de pouvoir vérifier les jetons émis précédemment), une `privateKey` chiffrée (utilisée uniquement tant que la clé est actuelle), un indicateur `isCurrent` (exactement une ligne à la fois) et un champ `revokedAt` facultatif.
|
||
|
||
### Faire pivoter la clé actuelle
|
||
|
||
Définissez `SIGNING_KEY_ROTATION_DAYS` pour l’activer : une tâche cron quotidienne émet alors une nouvelle clé actuelle dès que l’ancienne dépasse ce seuil. Les clés précédentes ne sont *pas* révoquées, donc les jetons signés avec celles-ci restent vérifiables. Laissez la variable non définie pour désactiver la rotation automatique.
|
||
|
||
<Note>La rotation automatique est disponible à partir de la v2.6+.</Note>
|
||
|
||
### Révoquer une clé (fuite / urgence uniquement)
|
||
|
||
**Settings → Admin Panel → Signing keys → Revoke** sur une ligne non actuelle. Efface le matériel privé chiffré, définit `revokedAt` et rejette chaque jeton existant signé avec ce `kid`.
|
||
|
||
## Faire pivoter `ENCRYPTION_KEY`
|
||
|
||
<Note>La commande `secret-encryption:rotate` décrite ci-dessous est fournie à partir de la v2.6+.</Note>
|
||
|
||
Chaque valeur chiffrée est encapsulée sous la forme `enc:v2:\<keyId>:\<payload>`, où `\<keyId>` est un préfixe hexadécimal sur 8 caractères dérivé de la clé brute. La rotation est en ligne et reprenable.
|
||
|
||
1. **Générez une nouvelle clé** : `openssl rand -base64 32`.
|
||
|
||
2. **Configurez les deux clés côte à côte** dans `.env`, puis redémarrez :
|
||
```ini
|
||
ENCRYPTION_KEY=NEW_VALUE
|
||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||
```
|
||
Les nouvelles écritures utilisent la nouvelle clé, les lignes existantes se déchiffrent toujours via la clé de secours.
|
||
|
||
3. **Rechiffrez les lignes existantes** :
|
||
|
||
```bash
|
||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||
```
|
||
|
||
La commande parcourt six sites (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Un filtre SQL ignore les lignes déjà sur le nouveau `\<keyId>`, de sorte que la commande est idempotente : interrompez-la et relancez-la si nécessaire. Quitte avec un code de sortie non nul si une ligne échoue — relancez pour réessayer.
|
||
|
||
| Option | Description |
|
||
| ---------------------------------------- | ----------------------------------------------------------------- |
|
||
| `-s, --site \<site>` | Limiter à un seul site. |
|
||
| `-b, --batch-size \<n>` | Lignes par lot (valeur par défaut `200`, maximum `5000`). |
|
||
| `-d, --dry-run` | Déchiffrer + rechiffrer en mémoire, ignorer la commande `UPDATE`. |
|
||
|
||
4. **Supprimez la clé de secours** une fois que `--dry-run` n’affiche plus aucune ligne restante : supprimez `FALLBACK_ENCRYPTION_KEY` et redémarrez.
|
||
|
||
## Prise en charge héritée de `APP_SECRET`
|
||
|
||
Les instances plus anciennes qui n’ont jamais défini `ENCRYPTION_KEY` utilisent `APP_SECRET` comme clé de chiffrement au repos (et comme secret de cookie de session, dérivé de celle-ci). Ce chemin est conservé pour la rétrocompatibilité mais est **déprécié** — définissez une `ENCRYPTION_KEY` dédiée et suivez la procédure de rotation ci-dessus pour migrer. `APP_SECRET` reste lui-même utilisé pour vérifier les anciens jetons d’accès HS256.
|