jan-karel.nl
Home / Securitymaatregelen / Cloudbeveiliging / Secrets Management

Secrets Management

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  # Restore

Dynamic 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/readonly

Transit 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-key

Azure 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.value

GCP 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.sh

GitLab 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-deployment

Secrets 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 -d

Schakel 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: password

Vault 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-creds

Secrets 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 image

Rotatie

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-verified

GitHub/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 incident

Veelvoorkomende 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.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Cloudbeveiliging ← Home