Secrets Management
Policy In Code, Niet In Hoop
In de cloud is consistentie cruciaal: policy in code, minimale rechten en zicht op drift.
Voor Secrets Management is automatisering leidend: guardrails in code, least privilege en continue driftcontrole.
Zo houd je snelheid in de cloud, zonder dat veiligheid afhankelijk wordt van handmatig geluk.
Directe maatregelen (15 minuten)
Waarom dit telt
De kern van Secrets Management is risicoreductie in de praktijk. Technische context ondersteunt de maatregelkeuze, maar implementatie en borging staan centraal.
Waarom Secrets Management
Hardcoded credentials zijn al jaren de nummer 1 oorzaak van datalekken. Uit onderzoek van GitGuardian blijkt dat er in 2024 meer dan 12 miljoen secrets in publieke GitHub-repositories zijn gevonden. Het probleem is niet dat ontwikkelaars niet weten dat het fout is – het probleem is dat het zo makkelijk is om het fout te doen.
Wat is een secret?
| Type | Voorbeelden | Risico bij lek |
|---|---|---|
| API keys | AWS access keys, Stripe keys, Google API keys | Volledige service-toegang, financieel misbruik |
| Database credentials | Connection strings, wachtwoorden | Data-exfiltratie, ransomware |
| Certificates & private keys | TLS certs, SSH private keys, signing keys | Man-in-the-middle, impersonatie |
| Tokens | OAuth tokens, JWT signing secrets, PATs | Account-overname, laterale beweging |
| Encryption keys | AES keys, KMS key material | Ontsleuteling van alle beschermde data |
| Service accounts | GCP service account JSON, Azure SP credentials | Volledige cloud-omgeving compromis |
Levenscyclus van een secret
Creatie → Opslag → Distributie → Gebruik → Rotatie → Revocatie
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Sterk Versleuteld Encrypted Minimale Automatisch Onmiddellijk
random at rest in transit scope & frequent bij compromis
Elke stap die je overslaat is een aanvalsvector. De meeste organisaties doen stap 1 en 2 redelijk en negeren de rest.
HashiCorp Vault
Vault is de de-facto standaard voor centraal secrets management. Kernconcepten: seal/unseal (master key via Shamir’s Secret Sharing, in productie auto-unseal via cloud KMS), auth methods (AppRole, Kubernetes, AWS IAM, OIDC), secret engines (KV, database, transit, PKI).
KV Secrets Engine (v2)
# Secret opslaan en ophalen
vault kv put secret/myapp/database username="dbadmin" password="s3cur3-p@ss"
vault kv get secret/myapp/database
vault kv get -version=2 secret/myapp/database # Specifieke versie
vault kv rollback -version=1 secret/myapp/database # RestoreDynamic Secrets: Database Credentials On-Demand
In plaats van langlevende credentials maakt Vault tijdelijke credentials aan per request:
# Database secret engine configureren
vault secrets enable database
vault write database/config/postgres \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/prod" \
allowed_roles="readonly" \
username="vault_admin" \
password="vault_admin_password"
# Role: credentials geldig voor 1 uur
vault write database/roles/readonly \
db_name=postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' \
VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# Dynamische credentials ophalen -- na 1 uur automatisch ingetrokken
vault read database/creds/readonlyTransit Engine & Policy
# Encryption as a Service
vault secrets enable transit
vault write -f transit/keys/payment-data
vault write transit/encrypt/payment-data plaintext=$(echo "NL91ABNA0417164300" | base64)# policy: app-readonly.hcl
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}
path "database/creds/readonly" {
capabilities = ["read"]
}
path "transit/encrypt/payment-data" {
capabilities = ["update"]
}
path "sys/*" {
capabilities = ["deny"]
}
Python hvac Library
import hvac
client = hvac.Client(url='https://vault.internal:8200')
client.auth.approle.login(
role_id='db02de05-c0f8-4d4b-a7c3-xxx',
secret_id='6a174c20-f6de-a53c-74d2-xxx'
)
# KV secret ophalen
secret = client.secrets.kv.v2.read_secret_version(
path='myapp/database', mount_point='secret'
)
db_password = secret['data']['data']['password']
# Dynamic database credentials
creds = client.secrets.database.generate_credentials(
name='readonly', mount_point='database'
)Cloud-Native Oplossingen
AWS Secrets Manager
import boto3, json
client = boto3.client('secretsmanager', region_name='eu-west-1')
response = client.get_secret_value(SecretId='prod/myapp/database')
secret = json.loads(response['SecretString'])# Automatische rotatie inschakelen (30 dagen)
aws secretsmanager rotate-secret \
--secret-id prod/myapp/database \
--rotation-lambda-arn arn:aws:lambda:eu-west-1:111111111111:function:SecretsRotation \
--rotation-rules '{"AutomaticallyAfterDays": 30}'
# SSM Parameter Store: goedkoper alternatief
aws ssm put-parameter --name "/prod/myapp/db-password" \
--value "s3cur3-p@ss" --type SecureString --key-id alias/myapp-keyAzure Key Vault
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential() # Managed identity in Azure
client = SecretClient(
vault_url="https://myapp-vault.vault.azure.net/",
credential=credential
)
secret = client.get_secret("database-password")
db_password = secret.valueGCP Secret Manager
from google.cloud import secretmanager
client = secretmanager.SecretManagerServiceClient()
name = "projects/my-project/secrets/database-password/versions/latest"
response = client.access_secret_version(request={"name": name})
db_password = response.payload.data.decode("UTF-8")Vergelijkingstabel
| Feature | AWS Secrets Manager | Azure Key Vault | GCP Secret Manager | HashiCorp Vault |
|---|---|---|---|---|
| Automatische rotatie | Ja (Lambda) | Ja (Event Grid) | Ja (Cloud Functions) | Ja (dynamic secrets) |
| Versioning | Ja | Ja | Ja | Ja (KV v2) |
| Audit logging | CloudTrail | Azure Monitor | Cloud Audit Logs | Audit device |
| Encryptie | KMS | HSM-backed | Cloud KMS | Transit / auto-unseal |
| Dynamic secrets | Nee | Nee | Nee | Ja |
| Multi-cloud | Nee | Nee | Nee | Ja |
| Managed identity | IAM roles | Managed Identity | Workload Identity | AppRole / K8s auth |
| Kosten | ~$0.40/secret/mnd | ~$0.03/operatie | ~$0.06/secret/mnd | Open source / Enterprise |
Secrets in CI/CD
GitHub Actions Secrets
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }} # Automatisch gemaskeerd in logs
run: ./deploy.shGitLab CI/CD Variables
Configureer secrets als masked (verborgen in logs), protected (alleen op protected branches), en met environment scope (alleen voor specifieke omgevingen).
OIDC Federation: Geen Static Credentials Meer
# GitHub Actions → AWS zonder langlevende keys
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # Nodig voor OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111111111111:role/GitHubActions-Deploy
aws-region: eu-west-1
# Geen AWS_ACCESS_KEY_ID of AWS_SECRET_ACCESS_KEY nodig
- name: Deploy
run: aws ecs update-service --cluster prod --service myapp --force-new-deploymentSecrets in Containers
Kubernetes Secrets: Base64 is Geen Encryptie
# Iedereen met kubectl get secret kan dit decoderen:
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -dSchakel encryption at rest in via
EncryptionConfiguration, of beter: gebruik External Secrets
Operator.
External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-creds
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: secret/data/myapp/database
property: passwordVault Agent Injector
apiVersion: v1
kind: Pod
metadata:
name: myapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/readonly"
vault.hashicorp.com/agent-inject-template-db-creds: |
{{- with secret "database/creds/readonly" -}}
postgresql://{{ .Data.username }}:{{ .Data.password }}@db:5432/prod
{{- end }}
spec:
containers:
- name: myapp
image: myapp:latest
# Credentials beschikbaar in /vault/secrets/db-credsSecrets Nooit in Docker Images
# FOUT: secret in image layer (zelfs als je het later verwijdert)
FROM python:3.12-slim
ENV DATABASE_URL=postgresql://admin:password123@db:5432/prod
# GOED: multi-stage build, secrets alleen in build-stage
FROM python:3.12-slim AS builder
RUN --mount=type=secret,id=pip_token \
PIP_INDEX_URL=https://$(cat /run/secrets/pip_token)@pypi.internal/simple/ \
pip install -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . .
# Geen secrets in de final imageRotatie
Rotatie beperkt de impact van een compromis. Als een gelekte key na 24 uur automatisch ongeldig wordt, heeft een aanvaller een beperkt window of opportunity.
| Strategie | Hoe het werkt | Geschikt voor |
|---|---|---|
| Dynamic secrets | Nieuwe credentials per request, korte TTL | Database credentials, cloud tokens |
| Scheduled rotation | Periodieke vervanging (30/60/90 dagen) | API keys, service accounts |
| Event-driven rotation | Roteren bij verdachte activiteit | Elk type secret |
| Dual-version graceful | Twee versies tijdelijk actief | Alles zonder downtime |
Graceful Rotatie met Twee Actieve Versies
import boto3
def lambda_handler(event, context):
"""AWS Secrets Manager rotation Lambda (4 stappen)."""
secret_id = event['SecretId']
step = event['Step']
client = boto3.client('secretsmanager')
if step == "createSecret":
new_password = client.get_random_password(
PasswordLength=32, ExcludeCharacters='/@"\\',
)['RandomPassword']
client.put_secret_value(
SecretId=secret_id,
ClientRequestToken=event['ClientRequestToken'],
SecretString=new_password,
VersionStages=['AWSPENDING']
)
elif step == "setSecret":
# Pas nieuw wachtwoord toe op de database
pass # ALTER USER ... PASSWORD ...
elif step == "testSecret":
# Verifieer dat nieuwe credentials werken
pass
elif step == "finishSecret":
# Promoveer AWSPENDING → AWSCURRENT
client.update_secret_version_stage(
SecretId=secret_id, VersionStage='AWSCURRENT',
MoveToVersionId=event['ClientRequestToken'],
)Detectie van Gelekte Secrets
Pre-commit Hooks
# gitleaks: scan op secrets
gitleaks detect --source . --verbose
# Pre-commit configuratie (.pre-commit-config.yaml)
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
# trufflehog: diepere scan inclusief git history
trufflehog git file://. --only-verifiedGitHub/GitLab Secret Scanning
Schakel secret scanning en push protection in via repository settings. GitHub blokkeert pushes die bekende secret-patronen bevatten (AWS keys, GCP service accounts, Stripe keys, etc.).
.gitignore Best Practices
# Secrets en credentials
.env
.env.*
*.pem
*.key
*.p12
credentials.json
service-account.json
secrets.yaml
vault-token
# Terraform state (bevat plaintext secrets!)
*.tfstate
*.tfstate.backup
.terraform/
Wat te Doen bij een Lek
# 1. ONMIDDELLIJK: roteer het gelekte secret
# Wacht niet. Doe het nu. Niet na de standup.
# 2. Controleer of het secret is misbruikt
# Check CloudTrail, Azure Activity Log, GCP Audit Logs
# 3. Verwijder uit git history met BFG Repo-Cleaner
bfg --replace-text passwords.txt repo.git
cd repo.git && git reflog expire --expire=now --all
git gc --prune=now --aggressive && git push --force
# 4. ALLE medewerkers: lokale clone verwijderen en opnieuw clonen
# 5. Documenteer het incidentVeelvoorkomende Fouten
| Fout | Waarom het misgaat | Oplossing |
|---|---|---|
| Secrets in broncode | “Het is maar even voor testen” | Vault of cloud secrets manager vanaf dag 1 |
.env in git |
.gitignore vergeten of te laat toegevoegd |
Pre-commit hooks, .env in template |
| Base64 als encryptie | Kubernetes docs suggereren het bijna | Encryption at rest, External Secrets Operator |
| Gedeelde service accounts | “Iedereen gebruikt dezelfde API key” | Per-service credentials, dynamic secrets |
| Geen rotatie | “Het werkt toch?” | Automatische rotatie, korte TTL |
| Secrets in CI/CD logs | echo $PASSWORD in debug mode |
Masked variables, nooit secrets printen |
| Secrets in Docker layers | COPY .env . in Dockerfile |
Multi-stage builds, runtime injection |
| Terraform state in git | State bevat plaintext secrets | Remote backend (S3, GCS) met encryptie |
| Langlevende PATs | Tokens die nooit verlopen | Korte expiry, OIDC federation |
| Secrets in Slack/Teams | “Kun je even het wachtwoord sturen?” | Vault URL delen, nooit het secret zelf |
| Dezelfde key overal | Dev, staging en prod delen een key | Aparte secrets per omgeving |
| Geen audit logging | Geen idee wie welk secret heeft gelezen | Vault audit logs, CloudTrail |
Checklist
| Prioriteit | Maatregel | Categorie |
|---|---|---|
| P0 - Nu | Scan repositories op bestaande secrets (gitleaks/trufflehog) | Detectie |
| P0 - Nu | Roteer alle gevonden gelekte secrets | Incident response |
| P0 - Nu | Schakel GitHub/GitLab secret scanning in | Detectie |
| P1 - Deze sprint | Implementeer pre-commit hooks voor secret detectie | Preventie |
| P1 - Deze sprint | Migreer hardcoded secrets naar Vault of cloud secrets manager | Opslag |
| P1 - Deze sprint | Verwijder static credentials uit CI/CD; gebruik OIDC | CI/CD |
| P1 - Deze sprint | Schakel encryption at rest in voor Kubernetes Secrets | Container |
| P2 - Dit kwartaal | Implementeer automatische rotatie voor database credentials | Rotatie |
| P2 - Dit kwartaal | Migreer naar dynamic secrets (Vault) waar mogelijk | Opslag |
| P2 - Dit kwartaal | Implementeer External Secrets Operator in Kubernetes | Container |
| P2 - Dit kwartaal | Configureer audit logging voor alle secret access | Monitoring |
| P3 - Roadmap | OIDC federation voor alle CI/CD pipelines | CI/CD |
| P3 - Roadmap | Transit engine voor applicatie-encryptie | Encryptie |
| P3 - Roadmap | Automatische incident response bij secret lek detectie | Automatisering |
| P3 - Roadmap | Centraal secrets management dashboard met compliance rapportage | Governance |
We bouwen de meest geavanceerde cloud-architecturen ter wereld.
Kubernetes clusters met service meshes. Zero-trust networking. Alles
Infrastructure as Code, alles geautomatiseerd. En dan commit iemand
AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE naar een
publieke GitHub-repository. Om 16:47 op een vrijdag. In een commit met
de message “quick fix.”
Het zou grappig zijn als het niet zo deprimerend was. We hebben
Vault. We hebben AWS Secrets Manager. We hebben OIDC federation. En toch
zijn .env-bestanden voor sommige teams nog steeds de
“secrets management oplossing.” Een .env-bestand is geen
secrets management. Het is een tekstbestand met wachtwoorden erin. Het
is het digitale equivalent van een Post-it op je monitor.
En dan is er de ontwikkelaar die zegt: “Het is een private repo, dus het is veilig.” Private. De repo waar drie ex-medewerkers, twee stagairs die vorig jaar zijn vertrokken, en die ene contractor uit 2021 nog steeds toegang toe hebben. Maar ja, private. Dus het is veilig. Slaap lekker.
Samenvatting
Secrets management is geen optionele toevoeging maar een fundamenteel onderdeel van elke beveiligingsarchitectuur. Gebruik een centrale secrets manager (HashiCorp Vault voor multi-cloud of de native oplossing van je cloud provider), implementeer automatische rotatie met korte TTLs, gebruik dynamic secrets waar mogelijk, elimineer langlevende credentials in CI/CD via OIDC federation, en bescherm containers met External Secrets Operator of Vault Agent Injector. Scan proactief op gelekte secrets met pre-commit hooks en platform-native scanning. De meeste datalekken beginnen niet met een geavanceerde aanval maar met een vergeten credential in een Git-repository. Dat voorkomen is effectiever dan welke detectiemaatregel dan ook.
Verder lezen in de kennisbank
Deze artikelen in het portaal geven je meer achtergrond en praktische context:
- De cloud — andermans computer, jouw verantwoordelijkheid
- Containers en Docker — wat het is en waarom je het moet beveiligen
- Encryptie — de kunst van het onleesbaar maken
- Least Privilege — geef mensen alleen wat ze nodig hebben
Je hebt een account nodig om de kennisbank te openen. Inloggen of registreren.
Gerelateerde securitymaatregelen
Deze artikelen bieden aanvullende context en verdieping: