SecretSpec : le fichier .env qui se prend pour un vrai gestionnaire de secrets
Déclare tes besoins en secrets dans git, stocke les valeurs où tu veux

J’ai un truc qui me fait hurler dans tous les projets où j’atterris : la gestion de secrets à l’arrache. Tu sais, le rituel immuable :
- Le .env commité dans git parce que “on fera un vault plus tard”
- Le vault jamais mis en place, du coup les secrets traînent dans un Google Doc
- Le nouveau dans l’équipe qui passe deux heures à chasser les bonnes valeurs
- Le .env.example qui est plus à jour depuis six mois
Et le pire, c’est que tout le monde sait que c’est de la merde, mais personne prend le temps de fixer ça.
J’ai trouvé un outil qui règle le problème. Il s’appelle SecretSpec, il est fait par Cachix (oui, les gens de Cachix pour Nix), et son concept est d’une simplicité déconcertante : tu commits dans git CE DONT t’as besoin, pas LES VALEURS. Chaque environnement — ton laptop, le CI, la prod — choisit où il va chercher les valeurs.
Lexique
C’est là mes définitions et si elles vous déplaisent, cassez-vous je suis pas là pour discuter lexique.
- SecretSpec: Outil CLI/SDK qui déclare les besoins en secrets dans un
secretspec.toml. Les valeurs sont stockées ailleurs. - Déclaration: Le fichier
secretspec.tomlcommité dans git. Contient les noms, descriptions, contraintes. JAMAIS les valeurs. - Provider: Le backend qui stocke les valeurs : keyring, 1Password, Vault, fichier .env, variables d’env, etc.
- Profile: Un ensemble de contraintes par environnement (default, production, ci). Tous héritent de
default.
Le problème : on mélange déclaration et stockage
Regarde comment on fait habituellement. Tu as un projet. Tu as besoin d’une URL de base de données, d’une clé API Stripe, d’un token GitHub. Tu créés un .env :
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
STRIPE_API_KEY=sk_live_123456
GITHUB_TOKEN=ghp_abcdef
Tu commités ça ? Non, t’es pas idiot. Tu créés un .env.example :
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
STRIPE_API_KEY=
GITHUB_TOKEN=
Et là, deux problèmes :
- Le
.env.exampleest toujours pas à jour. Tu ajoutesREDIS_URLdans le code mais tu oublies de le mettre dans l’exemple. Le nouveau passe une journée à debugger. - Chaque environnement stocke ses valeurs différemment. Le dev a son
.envlocal, le CI des variables d’env configurées dans GitHub Actions, la prod un vault. Et personne arrive à maintenir la synchro.
Le problème fondamental, c’est qu’on mélange deux choses : ce dont on a besoin (la déclaration) et où sont les valeurs (le stockage). SecretSpec sépare les deux.
Comment SecretSpec résout le bordel
Au lieu d’un .env, tu créés un secretspec.toml que tu commités dans git :
0[project]
1name = "mon-app"
2revision = "1.0"
3
4[profiles.default]
5DATABASE_URL = { description = "Connexion PostgreSQL", required = true }
6STRIPE_API_KEY = { description = "Clé API Stripe", required = true }
7GITHUB_TOKEN = { description = "Token GitHub", required = true }
8LOG_LEVEL = { description = "Niveau de log", required = false, default = "debug" }Ce fichier est la source de vérité de ce dont ton application a besoin. Chaque secret a une description, un statut required ou pas, une valeur par défaut éventuelle. Et rien d’autre.
Ensuite, chaque machine configure son provider. Le développeur utilise son keyring, le CI utilise les variables d’environnement, la prod utilise Vault.
0# Sur le laptop du dev
1$ secretspec config init
2? Select your preferred provider backend:
3 keyring # ← Il choisit le keychain macOS
4
5# En CI
6$ export SECRETSPEC_PROVIDER=env
7$ export SECRETSPEC_PROFILE=ci
8$ secretspec check --json # Vérifie que tout est dispoLe même fichier, partout. Chacun avec son backend.
Les providers disponibles
Y en a 14. Je vais pas tous les détailler, mais voilà les plus utiles :
| Provider | URI | Usage typique |
|---|---|---|
| Keyring | keyring:// |
Développement local (keychain OS) |
| .env | dotenv://[path] |
Legacy / migration |
| Env vars | env:// |
CI/CD (GitHub Actions, GitLab CI) |
| 1Password | onepassword://[account@vault] |
Équipe (cloud, end-to-end) |
| Proton Pass | protonpass://[vault] |
Équipe (cloud, chiffré) |
| Vault/OpenBao | vault://[ns@]host |
Production / self-hosted |
| AWS Secrets Manager | awssm://[profile@]REGION |
Production AWS |
| GCP Secret Manager | gcsm://PROJECT_ID |
Production GCP |
| Azure Key Vault | akv://VAULT_NAME |
Production Azure |
| Bitwarden BWS | bws://PROJECT_ID |
Équipe (cloud, open source) |
Tu peux aussi chaîner les providers par secret avec un fallback :
DATABASE_URL = {
description = "PostgreSQL connection string",
required = true,
providers = ["team-vault", "personal-keyring", "env"]
}
Le système essaye team-vault d’abord, puis personal-keyring, puis env. Si le vault d’équipe est down, le dev bosse avec son keyring local.
Les fonctionnalités qui tuent
Génération automatique de secrets
T’as besoin d’une clé JWT pour ton environnement de dev ? Tu déclares ça :
JWT_SIGNING_KEY = {
description = "Clé de signature JWT",
required = true,
type = "rsa_private_key",
generate = true
}
Si le secret existe pas, SecretSpec le génère et le stocke dans ton provider. Idempotent : la prochaine fois, il trouve la valeur et passe.
Secrets composés
Tu stockes les composants individuellement, tu reconstruis la chaîne de connexion à la volée :
DB_HOST = { description = "Hôte PostgreSQL", required = true }
DB_PORT = { default = "5432" }
DB_NAME = { description = "Nom de la base", required = true }
DB_USER = { description = "Utilisateur", required = true }
DB_PASSWORD = { description = "Mot de passe", required = true }
DATABASE_URL = {
description = "Connection string complète",
composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
}
Héritage entre projets
T’as une base commune partagée entre plusieurs microservices ?
[project]
name = "web-api"
revision = "1.0"
extends = ["../shared/base", "../shared/auth"]
Plus besoin de dupliquer les définitions de DATABASE_URL ou JWT_SECRET dans chaque projet.
Audit logging
Chaque accès à un secret est logué localement. Les valeurs des secrets ne sont jamais écrites dans le log — uniquement qui a accédé, quand, et si ça a réussi.
~/.local/share/secretspec/audit.jsonl
{"timestamp":"...","secret":"DATABASE_URL","profile":"default","provider":"keyring","outcome":"success"}
{"timestamp":"...","secret":"STRIPE_API_KEY","profile":"production","provider":"vault","outcome":"success"}
Protection anti-agent IA
Tu peux exiger qu’un agent IA fournisse une raison avant d’accéder aux secrets :
[project]
name = "web-app"
revision = "1.0"
require_reason = "agents"
Les humains en terminal interactif ne sont pas embêtés. Les agents (Claude Code, Cursor, Copilot, etc.) doivent passer --reason ou SECRETSPEC_REASON. Pratique pour savoir pourquoi un LLM a touché à tes credentials.
Mise en place dans un projet
Étape 1 : Installation
0$ brew install cachix/secretspec/secretspecÉtape 2 : Initialisation depuis un .env existant
0$ cd mon-projet
1$ secretspec init # Crée secretspec.toml à partir de .env
2$ secretspec config init # Choisir un providerÉtape 3 : Commit du fichier de déclaration
Étape 4 : Vérification et lancement
0$ secretspec check
1✓ DATABASE_URL → keyring (found)
2✓ STRIPE_API_KEY → keyring (found)
3✓ LOG_LEVEL → default (debug)
4
5$ secretspec run -- npm startÉtape 5 : En CI (GitHub Actions)
0$ SECRETSPEC_PROFILE=ci SECRETSPEC_PROVIDER=env secretspec check --jsonLe CI utilise les variables d’environnement configurées dans GitHub Secrets. Même fichier secretspec.toml, même validation, provider différent.
Et les SDKs ?
SecretSpec propose des SDKs pour Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP et C#. Tous utilisent le même résolveur Rust derrière — donc un provider ajouté au core marche partout immédiatement.
En Rust, tu génères des structs typés depuis ta déclaration :
#[derive(SecretSpec)]
#[secretspec(file = "secretspec.toml", profile = "production")]
struct AppSecrets {
database_url: String,
api_key: String,
log_level: Option<String>,
}
Plus de std::env::var("DATABASE_URL").expect("DATABASE_URL not set") à 15 endroits différents.
Pourquoi tu devrais passer à SecretSpec dès demain
Parce que la situation actuelle est intenable :
- Ton
.env.exampleest déjà obsolète - Les secrets du vault sont pas synchronisés avec les besoins réels de l’appli
- Le nouveau dans l’équipe passe deux heures à debugger pourquoi tel secret manque
- Et tu files des tokens d’accès à des agents IA qui peuvent fouiller dans tes secrets sans laisser de trace
SecretSpec règle tout ça avec un concept simple : déclaration dans git, stockage ailleurs. Le fichier TOML est la documentation vivante et vérifiée de ce dont ton appli a besoin. Plus de secrets dans git, plus de .env à synchroniser, plus de chase aux valeurs.
Et le mieux : t’as pas à choisir un seul provider. Chaque environnement utilise le sien. Le même projet. Le même fichier. Chacun son backend.
See ya space-cowboy!