Het risico van hardcoded secrets
Een ontwikkelaar zet een API-sleutel van Stripe in de code. De code gaat naar GitHub. Een bot scraapt GitHub. Anderhalf uur later wordt je account leeggeroofd.
Dit gebeurt duizenden keren per maand. En toch zien we het in productie-codebases nog steeds voorkomen.
Secrets management en environment configuratie zijn niet glamoureus. Ze voelen als administratie. Maar ze zijn essentieel voor de veiligheid van je SaaS, je klanten en je bedrijf.
In dit artikel laten we zien hoe je secrets veilig beheert, configuratie per omgeving organiseert, en hoe je compliance en rotation afhandelt. We gaan verder dan "zet het in .env" — we bouwen een aanpak die schaalbaar, veilig en onderhoudbaar is.
Het probleem met omgevingsvariabelen
Veel SaaS-toepassingen gebruiken .env bestanden. Dit werkt voor lokale development, maar breekt af zodra je naar production gaat:
# .env (NOOIT in git!)
DATABASE_URL=postgresql://user:password@localhost/mydb
STRIPE_API_KEY=sk_live_abcd1234...
SENDGRID_API_KEY=SG.xyz123...
JWT_SECRET=my-super-secret-key-that-i-used-locally
De problemen:
- Gedeelde secrets: dezelfde key in dev, test, staging, en production
- Geen rotatie: hoe vervang je een sleutel zonder downtime?
- Geen audit trail: wie had toegang tot welke secrets, en wanneer?
- Overmachtigde ontwikkelaars: junior developers hebben hetzelfde production-secret als architects
- Git leaks: ongeluk, er zit een secret in je history
- No expiration: secrets leven voor altijd
Dit is het verschil tussen een slordig systeem en een professional-grade setup.
De vier pijlers van secrets management
1. Centralisering
Al je secrets op één plaats, niet verspreid over bestanden, databases, en Slack-berichten.
Bad: Gedistribueerd
.env → ENV vars → Config files → Hardcoded strings → Slack messages → Developer notes
Good: Gecentraliseerd
Secrets Manager (AWS Secrets Manager, HashiCorp Vault, Doppler, etc)
↓
Applications (via API of environment injection)
2. Principle of Least Privilege (PoLP)
Een microservice hoeft niet de Stripe-sleutel van je partner-API te kennen.
// ❌ Slecht: alles bij elkaar
const stripe = require('stripe')(process.env.STRIPE_API_KEY);
const sendgrid = require('@sendgrid/mail')(process.env.SENDGRID_API_KEY);
const twilio = require('twilio')(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
async function processPayment() {
// Alledrie de integrations zijn hier beschikbaar, ook al hebben we er maar één nodig
await stripe.charges.create(...);
}
// ✅ Goed: service krijgt alleen wat het nodig heeft
class PaymentService {
constructor(stripeKey) {
this.stripe = require('stripe')(stripeKey);
}
async processPayment() {
// Sendgrid en Twilio zijn niet beschikbaar hier
return this.stripe.charges.create(...);
}
}
Op omgevingsniveau:
# kubernetes secrets per namespace
---
apiVersion: v1
kind: Secret
metadata:
namespace: payment-service
name: stripe-keys
data:
api_key: <base64>
---
apiVersion: v1
kind: Secret
metadata:
namespace: notification-service
name: sendgrid-keys
data:
api_key: <base64>
# payment-service ziet sendgrid-keys NIET
3. Rotatie (zonder downtime)
Je moet secrets kunnen vervangen zonder downtime.
Scenario:
- Eén van je developers verlaat het bedrijf → je moet alle secrets vervangen
- Een Stripe-sleutel is gelekt → je moet hem disablen en een nieuw exemplaar krijgen
- Security audit eist maandelijkse rotatie → automation is nodig
Strategie: Blue-green secrets
// Secrets manager ondersteunt meerdere versies
// "Staging" (blauw) → "Active" (groen)
interface SecretVersion {
name: 'stripe-api-key';
active: boolean; // Welke versie gebruiken we?
value: string;
createdAt: Date;
rotateAfter: Date; // Automatische rotatie?
}
// Je app controleert regelmatig op nieuwe versies
async function refreshSecrets() {
const secrets = await secretsManager.list();
const activeStripeKey = secrets.find(s =>
s.name === 'stripe-api-key' && s.active
);
if (activeStripeKey.version !== this.currentVersion) {
// Vernieuw in-memory secret, geen restart nodig
this.stripeClient = new Stripe(activeStripeKey.value);
this.currentVersion = activeStripeKey.version;
}
}
// Elke 6 uur controleren
setInterval(refreshSecrets, 6 * 60 * 60 * 1000);
4. Audit en monitoring
Je moet weten wie welk secret aanraakte en wanneer.
# CloudTrail audit log (AWS)
{
"eventTime": "2026-03-27T08:15:23Z",
"userIdentity": {
"principalId": "developer-alice"
},
"eventName": "GetSecretValue",
"requestParameters": {
"secretId": "stripe-api-key"
},
"sourceIPAddress": "203.0.113.42",
"userAgent": "aws-cli/2.x.x"
}
Mit alerts voor verdachte activiteiten:
// Alert als iemand van production secret leest buiten bedrijfstijden
secretsManager.on('secretAccess', (event) => {
const hour = new Date().getHours();
const isOutsideBusinessHours = hour < 6 || hour > 20;
if (event.environment === 'production' && isOutsideBusinessHours) {
await slack.notify({
text: `⚠️ Production secret accessed: ${event.secretName} by ${event.user}`,
channel: '#security-alerts'
});
}
});
Praktische implementatie per platform
AWS Secrets Manager
Best voor teams die al in AWS ecosysteem zitten.
// AWS SDK v3
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({ region: "eu-west-1" });
async function getSecret(secretName) {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
return JSON.parse(response.SecretString);
}
// In je applicatie
const dbConfig = await getSecret('prod/database');
const stripeKey = await getSecret('prod/stripe-api-key');
Cachen met TTL:
const secretCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minuten
async function getCachedSecret(name) {
const cached = secretCache.get(name);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.value;
}
const value = await getSecret(name);
secretCache.set(name, { value, timestamp: Date.now() });
return value;
}
Kubernetes Secrets + External Secrets Operator
Best voor containerized microservices.
# external-secrets operator syncen secrets van AWS naar K8s
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secret-store
spec:
provider:
aws:
service: SecretsManager
region: eu-west-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 15m
secretStoreRef:
name: aws-secret-store
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
data:
- secretKey: database-url
remoteRef:
key: prod/database-url
- secretKey: stripe-key
remoteRef:
key: prod/stripe-api-key
De K8s Secret wordt automatisch refreshed en beschikbaar als:
- Environment variables
- Files in /var/run/secrets
- Injection in applicatie via library
HashiCorp Vault
Best voor zeer gevoelige omgevingen (healthcare, finance, GDPR-streng).
import VaultClient from 'node-vault';
const vault = new VaultClient({
endpoint: 'https://vault.company.com',
token: process.env.VAULT_TOKEN // Zelf authenticated via K8s service account
});
async function getSecret(path) {
const secret = await vault.read(`secret/data/${path}`);
return secret.data.data;
}
// Audit trail ingebouwd
// Automatic key rotation via Vault policies
// Dynamic secrets (bv. database credentials die expire)
Dynamic secrets — erg nuttig voor databases:
# Vault policy: genereer automatisch DB credentials met TTL
path "database/creds/readonly" {
capabilities = ["read"]
}
# Vault genereert voor elke request een unieke user + password
# Die expiren na 1 uur
# Geen shared DB password meer!
Doppler (simplest option)
Best voor startups, snelle setup.
# CLI setup
doppler setup
doppler projects create my-saas
doppler secrets set STRIPE_API_KEY sk_live_...
doppler secrets set DATABASE_URL postgresql://...
# In je code
const config = require('@doppler/sdk');
const stripe_key = config.get('STRIPE_API_KEY');
# Local development
doppler run -- npm start
# Production (inject via environment)
# Doppler CLI in je docker container
RUN curl -Ls https://cli.doppler.com/install.sh | sh
ENTRYPOINT ["doppler", "run", "--"]
CMD ["node", "server.js"]
Compliance-specifieke vereisten
GDPR (EU)
- Encryption at rest: secrets moeten encrypted opgeslagen zijn (AWS/Vault doen dit standaard)
- Audit logging: wie accesst personal data integrations (Sendgrid voor emails, Stripe voor betalingen)
- Data residency: bewaar secrets in de juiste regio (eu-west-1 voor Europe)
NIS2 (EU kritieke infrastructuur)
- Mandatory logging van alle secret access
- Multi-factor authentication voor toegang
- Secret rotation minimaal elk kwartaal
- Incident response plan: wat als een secret geleakt is?
SOC 2 Type II
- Segregation of duties: niet iedereen ziet alle secrets
- Change management: secrets kunnen alleen gewijzigd worden door geautoriseerde rollen
- Availability: secrets manager moet 99.99% uptime hebben
- Monitoring: real-time detection van verdachte access patterns
Onboarder / Offboarding checklist
Employee leaves:
- Revoke toegang in Vault/Secrets Manager
- Automatisch gerotte alle secrets die zij kenden
- Audit log bekijken: wat hebben ze gelast geraadpleegd?
- Disable API keys/tokens die door hen waren gemaakt
async function offboardEmployee(employeeId) {
// Revoke alle access
await vault.revokeAllTokensForEmployee(employeeId);
// Rotate alle secrets die deze employee kon zien
const accessibleSecrets = await audit.getSecretsAccessedBy(employeeId);
for (const secret of accessibleSecrets) {
await secretsManager.rotateSecret(secret.id);
}
// Log the offboarding event
await audit.log({
event: 'EMPLOYEE_OFFBOARDED',
employeeId,
secretsRotated: accessibleSecrets.length,
timestamp: new Date()
});
}
Checklist: Build je secrets management system
- Centraliseer: alle secrets in één manager (AWS/Vault/Doppler), niet verspreid
- Environment segregation: prod secrets anders dan dev secrets
- Least privilege: services krijgen alleen de secrets die ze nodig hebben
- Rotatie: implementeer automatische rotatie (maandelijks minimum)
- Audit logging: elk secret access wordt gelogd
- Encryption: secrets zijn encrypted at rest en in transit
- Monitoring: alerts voor verdachte access patterns
- Onboarding/offboarding: automatisch manage secret access bij personeelswisselingen
- Backup & recovery: kun je secrets herstellen na incident?
- Testing: test je secret rotation zonder production impact
Conclusie
Secrets management voelt als een detail, maar het is een fundamenteel onderdeel van security engineering. Een slecht system kost je: breaches, compliance violations, downtime.
Een goed system geeft je rust: je weet dat credentials veilig zijn, automatisch gerotte, en volledig geaudit.
Begin klein — maak je .env files af, migreer naar een manager, automatiseer rotation. Het is één van de beste investments die je voor je SaaS kunt doen.