Zum Inhalt springen

Muster für das Secrets-Management in Cloud-Native-Anwendungen

Robustes Secrets-Management mit HashiCorp Vault, AWS Secrets Manager und Sealed Secrets: Rotation, Injection, Least Privilege, kein Secret Sprawl.

5 Min. Lesezeit
Diagramm des Secret-Lebenszyklus mit Erstellung, Speicherung in einem Vault, Injection in laufende Services, automatischer Rotation und Audit-Logging von Zugriffsereignissen

Secret Sprawl ist die stille Sicherheitskrise der meisten Organisationen. API-Schlüssel in Umgebungsvariablen, Datenbankpasswörter in Konfigurationsdateien, die ins Repository eingecheckt werden, Tokens, die in Slack-Nachrichten geteilt werden – jedes davon ist eine Angriffsfläche, die nur darauf wartet, ausgenutzt zu werden. Modernes Secrets-Management bedeutet nicht nur, Werte zu verschlüsseln. Es geht darum, den Zugriff zu kontrollieren, Rotation zu ermöglichen, Audit-Trails zu führen und Secrets von Orten fernzuhalten, an die sie nicht gehören.

Die hier vorgestellten Muster decken den gesamten Lebenszyklus ab: wie Secrets erstellt, gespeichert, an Anwendungen ausgeliefert, rotiert und widerrufen werden – ohne dabei so viel operative Komplexität einzuführen, dass Teams das System komplett umgehen.

Der Katalog der Secrets-Antipatterns

Bevor Sie Lösungen implementieren, sollten Sie zunächst die Muster erkennen, die diese Angriffsfläche überhaupt erst schaffen.

ymlyaml
# ❌ Secrets embedded in configuration
# docker-compose.yml
services:
  api:
    environment:
      DB_PASSWORD: "super_secret_password_123"
      API_KEY: "sk-live-abc123def456"
      JWT_SECRET: "my-jwt-signing-key"
# Visible in process listings, docker inspect,
# version control history, CI/CD logs
ymlyaml
# ✅ Secrets referenced, not embedded
# docker-compose.yml
services:
  api:
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_KEY_FILE: /run/secrets/api_key
    secrets:
      - db_password
      - api_key
 
secrets:
  db_password:
    external: true
  api_key:
    external: true
# Secrets injected at runtime, never in config files

Das dateibasierte Injection-Muster (Suffix _FILE) wird von vielen Docker-Images nativ unterstützt. Die Anwendung liest das Secret beim Start aus einer Datei statt aus einer Umgebungsvariablen, wodurch eine Offenlegung in Prozesslisten und Crash-Dumps vermieden wird.

Integration von HashiCorp Vault

Vault bietet dynamische Secrets, automatische Rotation und fein granulare Zugriffsrichtlinien. Das zentrale architektonische Muster besteht darin, kurzlebige Tokens mit eng begrenzten Berechtigungen zu verwenden.

tstypescript
// Vault client with token renewal and secret lease management
import Vault from "node-vault";
 
interface VaultConfig {
  endpoint: string;
  roleId: string;
  secretId: string;
}
 
class SecretManager {
  private client: ReturnType<typeof Vault>;
  private leases: Map<string, { leaseId: string; ttl: number }> =
    new Map();
 
  constructor(private config: VaultConfig) {
    this.client = Vault({
      apiVersion: "v1",
      endpoint: config.endpoint,
    });
  }
 
  async authenticate(): Promise<void> {
    const result = await this.client.approleLogin({
      role_id: this.config.roleId,
      secret_id: this.config.secretId,
    });
    this.client.token = result.auth.client_token;
 
    // Schedule token renewal before expiry
    const renewalInterval =
      (result.auth.lease_duration * 0.75) * 1000;
    setInterval(() => this.renewToken(), renewalInterval);
  }
 
  private async renewToken(): Promise<void> {
    try {
      await this.client.tokenRenewSelf();
    } catch {
      // Token renewal failed — re-authenticate
      await this.authenticate();
    }
  }
 
  async getDatabaseCredentials(
    role: string
  ): Promise<{ username: string; password: string }> {
    const result = await this.client.read(
      `database/creds/${role}`
    );
 
    // Track lease for renewal
    this.leases.set(role, {
      leaseId: result.lease_id,
      ttl: result.lease_duration,
    });
 
    return {
      username: result.data.username,
      password: result.data.password,
    };
  }
 
  async getSecret(path: string): Promise<Record<string, string>> {
    const result = await this.client.read(
      `secret/data/${path}`
    );
    return result.data.data;
  }
 
  async revokeAllLeases(): Promise<void> {
    for (const [, lease] of this.leases) {
      await this.client.write("sys/leases/revoke", {
        lease_id: lease.leaseId,
      });
    }
    this.leases.clear();
  }
}
 
// Usage with automatic credential rotation
const vault = new SecretManager({
  endpoint: process.env.VAULT_ADDR ?? "http://vault:8200",
  roleId: process.env.VAULT_ROLE_ID ?? "",
  secretId: process.env.VAULT_SECRET_ID ?? "",
});
 
await vault.authenticate();
 
// Dynamic database credentials — unique per instance,
// automatically expired
const dbCreds = await vault.getDatabaseCredentials(
  "api-readonly"
);

Dynamische Datenbank-Credentials sind das Killerfeature von Vault. Jede Anwendungsinstanz erhält ihren eigenen Benutzernamen und ihr eigenes Passwort mit einer konfigurierbaren TTL. Läuft der Lease ab, widerruft Vault die Credentials auf Datenbankebene. Ein kompromittiertes Credential ist dann nur noch für Stunden gültig, nicht für immer.

Sealed Secrets in Kubernetes

Für Teams, die Secrets in Git verwalten möchten, ohne den operativen Aufwand von Vault auf sich zu nehmen, verschlüsselt Sealed Secrets Werte, die nur der Cluster entschlüsseln kann.

ymlyaml
# ❌ Kubernetes Secret — base64 encoded, not encrypted
apiVersion: v1
kind: Secret
metadata:
  name: api-credentials
data:
  api-key: c2stbGl2ZS1hYmMxMjNkZWY0NTY=
# base64 is NOT encryption. Anyone with repo access
# can decode this.
ymlyaml
# ✅ Sealed Secret — encrypted, safe to commit
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: api-credentials
  namespace: production
spec:
  encryptedData:
    api-key: AgBy3i4OJSWK+PiTySYZZA9r...
    # Encrypted with the cluster's public key
    # Only the sealed-secrets controller can decrypt
  template:
    metadata:
      name: api-credentials
    type: Opaque
shbash
# Workflow: encrypt locally, commit safely
# Fetch the cluster's public cert
kubeseal --fetch-cert \
  --controller-namespace kube-system \
  > pub-cert.pem
 
# Encrypt a secret value
echo -n "sk-live-abc123" | kubeseal \
  --raw \
  --from-file=/dev/stdin \
  --namespace production \
  --name api-credentials \
  --cert pub-cert.pem
 
# The output is safe to commit to Git

Secret-Injection-Muster auf Anwendungsebene

Unabhängig davon, welches Backend Sie verwenden, sollte die Anwendung einem einheitlichen Muster folgen, um Secrets entgegenzunehmen.

tstypescript
// ❌ Scattered secret access throughout codebase
async function handleRequest(req: Request) {
  const apiKey = process.env.STRIPE_API_KEY; // Where does this come from?
  const dbUrl = process.env.DATABASE_URL;     // Who rotates this?
  // Secrets scattered across handler files
}
tstypescript
// ✅ Centralized secret provider with lazy loading
interface SecretProvider {
  get(key: string): Promise<string>;
  getAll(keys: string[]): Promise<Map<string, string>>;
}
 
class CompositeSecretProvider implements SecretProvider {
  constructor(
    private providers: SecretProvider[],
  ) {}
 
  async get(key: string): Promise<string> {
    for (const provider of this.providers) {
      try {
        return await provider.get(key);
      } catch {
        continue;
      }
    }
    throw new Error(`Secret not found: ${key}`);
  }
 
  async getAll(
    keys: string[]
  ): Promise<Map<string, string>> {
    const results = new Map<string, string>();
    for (const key of keys) {
      results.set(key, await this.get(key));
    }
    return results;
  }
}
 
// File-based provider for Docker/Kubernetes secrets
class FileSecretProvider implements SecretProvider {
  constructor(private basePath: string = "/run/secrets") {}
 
  async get(key: string): Promise<string> {
    const filePath = `${this.basePath}/${key}`;
    const content = await fs.readFile(filePath, "utf-8");
    return content.trim();
  }
 
  async getAll(
    keys: string[]
  ): Promise<Map<string, string>> {
    const results = new Map<string, string>();
    for (const key of keys) {
      results.set(key, await this.get(key));
    }
    return results;
  }
}
 
// Environment variable provider as fallback
class EnvSecretProvider implements SecretProvider {
  async get(key: string): Promise<string> {
    const value = process.env[key];
    if (!value) {
      throw new Error(`Environment variable ${key} not set`);
    }
    return value;
  }
 
  async getAll(
    keys: string[]
  ): Promise<Map<string, string>> {
    const results = new Map<string, string>();
    for (const key of keys) {
      results.set(key, await this.get(key));
    }
    return results;
  }
}
 
// Application bootstrap: resolve all secrets once at startup
const secrets = new CompositeSecretProvider([
  new FileSecretProvider(),
  new EnvSecretProvider(),
]);
 
const config = {
  stripeKey: await secrets.get("STRIPE_API_KEY"),
  databaseUrl: await secrets.get("DATABASE_URL"),
  jwtSecret: await secrets.get("JWT_SECRET"),
};
// Pass config to services — no secret lookups in handlers

Secret-Rotation ohne Downtime

Der schwierigste Teil des Secrets-Managements ist die Rotation – ein Secret zu ändern, ohne laufende Services zu stören.

tstypescript
// Dual-read pattern for zero-downtime rotation
interface RotatableSecret {
  current: string;
  previous: string | null;
  rotatedAt: Date;
}
 
class RotatingApiKeyValidator {
  private keys: RotatableSecret;
 
  constructor(keys: RotatableSecret) {
    this.keys = keys;
  }
 
  validate(providedKey: string): boolean {
    // Accept both current and previous during rotation window
    if (providedKey === this.keys.current) return true;
    if (
      this.keys.previous &&
      providedKey === this.keys.previous
    ) {
      // Log that a client is still using the old key
      console.warn(
        "Request using previous API key — client needs update"
      );
      return true;
    }
    return false;
  }
}
 
// Rotation procedure:
// 1. Generate new secret, store as "current"
// 2. Move old "current" to "previous"
// 3. Deploy — services accept both keys
// 4. Update all clients to use new key
// 5. After grace period, remove "previous"

Die wichtigsten Erkenntnisse

Secrets, die in Umgebungsvariablen, Konfigurationsdateien oder Quellcode eingebettet sind, schaffen Angriffsflächen über Prozesslisten, Container-Inspektion, Crash-Dumps und die Versionskontroll-Historie – verwenden Sie stattdessen dateibasierte Injection oder Vault-Referenzen. Dynamische Secrets von HashiCorp Vault erzeugen pro Anwendungsinstanz eindeutige Credentials mit automatischem Ablauf, wodurch sich der Schadensradius eines Kompromisses auf die TTL des Credentials begrenzt. Mit Sealed Secrets lassen sich verschlüsselte Secrets sicher in Git speichern – nur der sealed-secrets-Controller des Kubernetes-Clusters kann sie entschlüsseln, was GitOps-Workflows ermöglicht, ohne Werte offenzulegen. Eine zentrale SecretProvider-Abstraktion entkoppelt Ihre Anwendung vom Backend für die Secret-Speicherung und macht es möglich, in Produktion dateibasierte Secrets und in der Entwicklung Umgebungsvariablen zu verwenden, ohne den Code zu ändern. Die Rotation ohne Downtime nutzt ein Dual-Read-Muster: Sowohl der aktuelle als auch der vorherige Secret-Wert werden während einer Übergangsfrist akzeptiert, sodass Clients Zeit haben, sich zu aktualisieren, während die Servicekontinuität gewahrt bleibt.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX