Verwaltung von Geheimnissen in Kubernetes kann einfach klingen – bis man es tatsächlich in großem Maßstab umsetzt. Zu den gängigen Ansätzen gehören das Einbinden von Kubernetes-Geheimnissen als Umgebungsvariablen, die Verwendung eines externen Secrets-Operators oder der direkte Aufruf von Cloud-SDKs aus dem Anwendungscode heraus. Jede Option bringt Kompromisse in Bezug auf Caching, Auditing, Sicherheitsgrenzen und Anwendungskopplung mit sich.

In diesem Beitrag stellen wir ein alternatives Muster vor: den Betrieb des AWS Secrets Manager-Agenten als Sidecar-Container. Bei diesem Ansatz ruft Ihre Anwendung Geheimnisse über einen lokalen HTTP-Aufruf ab – ein AWS SDK ist nicht erforderlich – und profitiert gleichzeitig von integrierter Zwischenspeicherung, SSRF-Schutz und einer klaren Trennung der Zuständigkeiten.

Wir werden die Einrichtung anhand eines kleinen Go-Dienstes veranschaulichen, aber dieses Sidecar-Muster funktioniert mit jeder Sprache und jeder Laufzeitumgebung.

Der Secrets Manager Agent läuft als separater Container im selben Pod. Ihre Anwendung kommuniziert mit ihm über localhost:2773, was mehrere Vorteile bietet:

Die Nachteile sind ein zusätzlicher Container pro Pod und ein lokaler HTTP-Aufruf. In der Praxis verursachen beide nur einen vernachlässigbaren Mehraufwand.

AWS Secrets Manager-Agent

Beim Start des Pods generiert der Agent ein zufälliges SSRF-Token, schreibt es in ein gemeinsames In-Memory-Volume und exportiert es als Umgebungsvariable.
Der Anwendungscontainer liest dasselbe Token aus dem gemeinsamen Volume und fügt es in jede Anfrage an den Agenten ein.

AWS stellt den Agenten als Rust-Projekt auf GitHub zur Verfügung. Wir erstellen ihn aus dem Quellcode und fixieren ihn zur Gewährleistung der Reproduzierbarkeit auf ein Release-Tag.

# Build stage - compile the Rust binary
FROM rust:1.82-bookworm AS builder
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*

WORKDIR /build
# Clone the agent repo (pin to a release tag for reproducibility)
RUN git clone --branch v2.0.0 --depth 1 https://github.com/aws/aws-secretsmanager-agent.git .
RUN cargo build --release

# Runtime stage - minimal image with just the binary
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /build/target/release/aws_secretsmanager_agent ./secrets-manager-agent
COPY startup.sh .
COPY config.toml .
RUN chmod +x startup.sh secrets-manager-agent
ENTRYPOINT ["./startup.sh"]

Agent-Konfiguration (config.toml)

log_level = "INFO"
log_to_file = false
http_port = 2773
ttl_seconds = 300
cache_size = 1000

Die Einstellung `ttl_seconds = 300` bedeutet, dass Geheimnisse fünf Minuten lang zwischengespeichert werden. Danach ruft der Agent automatisch neue Werte ab – ein Neustart des Pods ist nicht erforderlich.

Startskript und Umgang mit SSRF-Token

#!/bin/bash
set -e

# Generate a random SSRF token, write to shared volume and export as env var
TOKEN=$(head -c 32 /dev/urandom | base64 | tr -d '=+/')
echo -n "$TOKEN" > /shared/awssmatoken
chmod 444 /shared/awssmatoken
export AWS_TOKEN="$TOKEN"

# Start the agent with logging to stdout
exec ./secrets-manager-agent --config config.toml 2>&1

Das Token wird bei jedem Start des Pods temporär generiert, ausschließlich im Arbeitsspeicher gespeichert und zusammen mit dem Pod gelöscht. Es gibt nichts, was rotiert oder dauerhaft gespeichert werden müsste.

Erstellen und an ECR übertragen (in diesem Beispiel arm64 für Graviton-Knoten):

docker buildx build --platform linux/arm64 \ -t <account>.dkr.ecr.us-east-1.amazonaws.com/aws-secrets-manager-agent:lates\--push ./secrets-manager-agent

Die Anwendung nutzt nicht das AWS SDK. Stattdessen führt sie einen HTTP-GET-Aufruf an den lokalen Agenten aus und übergibt das SSRF-Token in einem Request-Header.

func sidecarHandler(w http.ResponseWriter, r *http.Request) {
   secretName := os.Getenv("secret")
   if secretName == "" {
       writeJSON(w, http.StatusOK, map[string]string{"message": "no secret env var set"})
       return
   }

   agentURL := os.Getenv("SECRETS_MANAGER_AGENT_URL")
   if agentURL == "" {
       agentURL = "http://localhost:2773"
   }

   tokenBytes, err := os.ReadFile("/shared/awssmatoken")
   if err != nil {
       writeJSON(w, http.StatusInternalServerError,
           map[string]string{"error": "failed to read SSRF token: " + err.Error()})
       return
   }

   req, err := http.NewRequestWithContext(
       r.Context(),
       http.MethodGet,
       agentURL+"/secretsmanager/get?secretId="+secretName,
       nil,
   )
   if err != nil {
       writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
       return
   }

   req.Header.Set("X-Aws-Parameters-Secrets-Token", string(tokenBytes))

   resp, err := http.DefaultClient.Do(req)
   if err != nil {
       writeJSON(w, http.StatusBadGateway, map[string]string{
           "error":  "failed to reach secrets manager agent sidecar",
           "detail": err.Error(),
       })
       return
   }
   defer resp.Body.Close()

   var secretResp struct {
       SecretString string `json:"SecretString"`
   }

   if err := json.NewDecoder(resp.Body).Decode(&secretResp); err != nil {
       writeJSON(w, http.StatusInternalServerError,
           map[string]string{"error": "failed to decode secret response"})
       return
   }

   var secretData map[string]string
   if err := json.Unmarshal([]byte(secretResp.SecretString), &secretData); err != nil {
       writeJSON(w, http.StatusOK, map[string]string{"secret_value": secretResp.SecretString})
       return
   }

   writeJSON(w, http.StatusOK, secretData)
}

Wichtige Details zur Umsetzung:

Bei dieser Bereitstellung werden zwei Container in einem einzigen Pod ausgeführt, die sich ein In-Memory-Volume für das SSRF-Token teilen.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-demo
  template:
    metadata:
      labels:
        app: go-demo
    spec:
      serviceAccountName: go-demo
      containers:
        - name: go-demo
          image: <account>.dkr.ecr.<region>.amazonaws.com/go-demo:latest
          ports:
            - containerPort: 8080
          env:
            - name: secret
              value: "demo-secret"
          volumeMounts:
            - name: ssrf-token
              mountPath: /shared
              readOnly: true
        - name: secrets-manager-agent
          image: <account>.dkr.ecr.<region>.amazonaws.com/aws-secrets-manager-agent:latest
          ports:
            - containerPort: 2773
          volumeMounts:
            - name: ssrf-token
              mountPath: /shared
      volumes:
        - name: ssrf-token
          emptyDir:
            medium: Memory

Wichtige Hinweise:

Vermeiden Sie es, das freigegebene Volume unter /var/run einzuhängen. Kubernetes benötigt diesen Pfad für das Token des Dienstkontos, und eine Überschreibung führt zu Fehlern bei den Pods – insbesondere bei distroless-Images.

Der Agent gleicht das SSRF-Token mit einer dieser Umgebungsvariablen ab:

Wenn keine festgelegt sind, kann der Agent nicht gestartet werden.

Deshalb schreibt das Startskript das Token sowohl auf die Festplatte als auch exportiert es als AWS_TOKEN.

  1. Pod wird gestartet und der Sidecar führt die Datei „startup.sh“ aus
  2. Es wird ein zufälliges 32-Byte-Token generiert
  3. Das Token wird in /shared/awssmatoken geschrieben und als AWS_TOKEN exportiert
  4. Der Agent gleicht eingehende Anfragen mit dem Token ab
  5. Die Anwendung liest das Token aus und sendet es als HTTP-Header
  6. Der Agent überprüft das Token und gibt das Geheimnis zurück

Das Token ist kurzlebig, auf die Lebensdauer des Pods beschränkt und wird niemals extern gespeichert.

Durch den Einsatz des AWS Secrets Manager-Agenten als Kubernetes-Sidecar bleibt der Anwendungscode übersichtlich und auf das Wesentliche konzentriert. Anstatt Cloud-SDKs einzubinden, Anmeldedaten zu verwalten und Caching-Logik zu implementieren, führt die Anwendung lediglich einen einfachen lokalen HTTP-Aufruf durch.

Der Makler kümmert sich um:

Wichtigste Erkenntnisse:

Dieser auf Sidecars basierende Ansatz bietet eine sichere, wartungsfreundliche und portable Möglichkeit, Geheimnisse in Kubernetes-Umgebungen zu verwalten.

Weitere Blogbeiträge findest duhier.