post

Secrets im Build sind Produktentscheidungen

Secrets im Build sind ein Vertrauensproblem: Wer darf was wann sehen, speichern und deployen?

≈ 9 Min. Lesezeit

Secrets im Build sind ein Vertrauensproblem: Wer darf was wann sehen, speichern und deployen? Die Frage klingt nach Betrieb, ist aber eine Produktentscheidung. Wo ein Token, ein Passwort oder ein Zertifikat entsteht, gespeichert und in ein Artefakt injiziert wird, legt fest, wer ohne Rücksprache liefern darf und wer warten muss. Genau das entscheidet über Tempo, Reibung und Fehlerrate eines Teams – lange bevor irgendein Tool ausgewählt ist.

Warum ich das aufschreibe

Parameter Store ist der konkrete Anlass. In einem Projekt lag die halbe Konfiguration in einer .env, die per COPY ins Image wanderte. Das lief monatelang gut, bis jemand das Image aus der Registry zog, docker history aufrief und die Datenbank-URL samt Passwort im Klartext sah. Kein Angriff, kein exotischer Bug – nur ein Artefakt, das mehr wusste, als es durfte.

Der Fehler war nicht die vergessene Verschlüsselung. Der Fehler war eine unausgesprochene Grenze: Das Team hatte nie entschieden, ob ein Build-Artefakt Secrets kennen darf. Es hatte nur ein Tutorial kopiert. Deshalb interessiert mich weniger, welches Secret-Werkzeug man nimmt, sondern welche Grenze durch die Wahl sichtbar wird – und welcher Fehler dadurch früher auffällt.

Die eigentliche Frage ist Vertrauen, nicht Verschlüsselung

Verschlüsselung löst ein enges Problem: Daten at rest und in transit schützen. Sie beantwortet nicht die Frage, die im Alltag teuer wird – wer im Prozess das Secret überhaupt in die Hand bekommt. Ein KMS-verschlüsselter Wert, den jeder CI-Job entschlüsseln darf, ist bequem und trotzdem eine offene Grenze.

Ich beschreibe das Thema deshalb entlang von drei Schichten, die ich bei fast jeder Architekturfrage anlege: fachliche Absicht, technische Grenze und betriebliche Konsequenz. Die Absicht erklärt, warum ein Secret existiert – ein Dienst braucht Zugriff auf einen anderen. Die Grenze entscheidet, welche Komponente das Secret sehen darf und welche nicht. Die Konsequenz zeigt sich erst bei Rotation, Leak, Rollback und Review. Wer nur die Absicht betrachtet, landet bei „irgendwie reinreichen". Wer die Grenze mitdenkt, trifft eine Entscheidung, die man morgen im Team verteidigen kann.

Praktisch heißt das: Bevor ich ein Werkzeug wähle, kläre ich vier Fragen. Welche Komponente ist führend für den Wert? Wer darf ihn lesen, und in welcher Umgebung? Wann wird er gelesen – zur Build-Zeit oder zur Laufzeit? Und was muss später nachvollziehbar sein, wenn er kompromittiert ist?

Build-Zeit und Laufzeit sind zwei verschiedene Vertrauenszonen

Die wichtigste Grenze verläuft zwischen Build und Runtime. Ein Build erzeugt ein Artefakt, das durch mehrere Umgebungen wandert und in einer Registry landet, auf die oft mehr Menschen Zugriff haben, als man denkt. Alles, was in dieses Artefakt eingebacken wird, ist damit so vertraulich wie die Registry – und nicht vertraulicher.

Der klassische Anti-Pattern sieht harmlos aus:

# ANTI-PATTERN: the secret becomes part of a shared, promotable artifact
COPY .env /app/.env
# ANTI-PATTERN: build args stay recoverable via `docker history`
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc \
  && npm ci \
  && rm .npmrc

Das rm täuscht Sicherheit vor. Der Token bleibt in den Layer-Metadaten sichtbar, und die .env reist als eigener Layer mit. Beides überlebt jedes spätere docker run in jeder Umgebung. Das eigentliche Problem ist nicht der Befehl, sondern die vermischte Vertrauenszone: Ein Artefakt, das in Staging und Production identisch sein soll, trägt plötzlich Production-Secrets.

Die architektonische Antwort ist eine saubere Trennung. Der Build produziert ein umgebungsneutrales Artefakt ohne Secrets. Die Konfiguration kommt erst zur Laufzeit dazu, injiziert aus einer Quelle, die pro Umgebung getrennt ist. Ein Multi-Stage-Build macht diese Absicht im Code sichtbar:

# build stage produces an artifact that knows nothing about any environment
FROM node:12 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# runtime stage receives config from the environment, never from the image
FROM node:12-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

Dieselbe Denkweise gilt für Build-Secrets, die der Build wirklich braucht – etwa ein privater Registry-Token. Die kommen über einen Secret-Mount, der im finalen Image nicht persistiert, statt über einen ARG, der in der History bleibt. Der Grundsatz bleibt: Ein Secret, das nur der Build braucht, darf das Runtime-Artefakt nicht erreichen.

Parameter Store als Interface, nicht als Datenbank

Sobald die Trennung steht, wird Parameter Store interessant – nicht als Ablage, sondern als Interface mit Struktur. Der hierarchische Pfad ist der eigentliche Hebel: Er kodiert die Umgebung, und die Umgebung ist die Grenze, an der Berechtigungen ansetzen.

# resolve runtime config just-in-time, scoped by environment path
aws ssm get-parameters-by-path \
  --path "/myapp/production/" \
  --with-decryption \
  --query "Parameters[].{Name:Name,Value:Value}"

Der Pfad /myapp/production/ ist mehr als Kosmetik. Er erlaubt es, Least-Privilege sauber auszudrücken: Ein Deploy-Job für Staging bekommt eine IAM-Policy, die genau auf staging/* zeigt und Production nicht einmal lesen kann.

{
  "Effect": "Allow",
  "Action": "ssm:GetParametersByPath",
  "Resource": "arn:aws:ssm:eu-central-1:123456789012:parameter/myapp/staging/*"
}

Damit ist die fachliche Absicht in der Infrastruktur verankert statt in einem Wiki-Satz. Der Wert wird zur Laufzeit aufgelöst und nie in Repo oder Image geschrieben:

#!/usr/bin/env bash
set -euo pipefail
# inject secrets at container start, keep them out of the image and the repo
export DATABASE_URL="$(aws ssm get-parameter \
  --name "/myapp/production/database-url" \
  --with-decryption --query "Parameter.Value" --output text)"
exec node dist/server.js

Ob dahinter Parameter Store, Secrets Manager oder ein Vault steht, ist die kleinere Frage. Die größere ist, dass der Zugriff pro Umgebung getrennt, auditierbar und rotierbar ist. Das Werkzeug drückt die Grenze aus – es erzeugt sie nicht.

Skizze

flowchart LR
  Repo[Code<br/>ohne Secrets] --> Build[Build<br/>reproduzierbares Artefakt]
  Build --> Registry[Artefakt<br/>je Umgebung gleich]
  Store[Parameter Store<br/>je Umgebung getrennt] --> Runtime
  Registry --> Runtime[Runtime<br/>injiziert Config just-in-time]

Die Skizze zeigt die eine Aussage, auf die es ankommt: Der linke Pfad ist überall identisch, der Secret-Pfad kommt getrennt und erst ganz rechts hinzu. Genau an dieser Kreuzung entscheidet sich, ob ein Team schnell und sicher liefern kann oder beides gegeneinander eintauscht.

Die Trade-offs, die man wirklich abwägt

Kein Ansatz ist umsonst. Ein umgebungsneutrales Artefakt plus Laufzeit-Injektion braucht eine funktionierende Secret-Quelle in jeder Umgebung und macht den Container-Start abhängig von einem weiteren Dienst. Fällt Parameter Store aus, startet der Service nicht – das ist ein bewusster Preis für die Grenze, kein Versehen. Man federt ihn mit Caching und klaren Fehlermeldungen ab, nicht damit, dass man Secrets doch wieder einbäckt.

Umgekehrt ist die Bequemlichkeit der .env-im-Image real: ein Artefakt, keine Abhängigkeit zur Startzeit, lokal leicht zu reproduzieren. Der Preis dafür fällt nur später an – bei Rotation, bei einem Leak, bei der Frage, wer das Staging-Image mit Production-Zugängen in der Hand hatte. Architektur heißt hier, den Preis an die Stelle zu verschieben, an der man ihn kontrollieren kann, statt ihn in einen späteren Incident zu verlagern.

Der zweite Trade-off betrifft Zentralisierung. Eine zentrale Secret-Quelle vereinfacht Rotation und Audit, wird aber selbst zum kritischen Pfad und zur begehrten Grenze. Deshalb ist die Pfad- und IAM-Struktur keine Formalie: Sie ist der Unterschied zwischen „ein Dienst kennt alles" und „jeder Dienst kennt genau seins". Zwischen beidem liegt die eigentliche Produktentscheidung.

Der Fehlerpfad entscheidet, nicht der Happy Path

Eine Secret-Architektur ist erst belastbar, wenn der Fehlerpfad beschrieben ist. Im Demo-Modus sieht jeder Ansatz sauber aus. Interessant wird es genau dort, wo Systeme normalerweise brechen.

Rotation ist der ehrlichste Test. Kann ich einen kompromittierten Wert ändern, ohne ein Image neu zu bauen und durch alle Umgebungen zu promoten? Bei Laufzeit-Injektion ist das ein Parameter-Update plus Neustart. Beim eingebackenen Secret ist es ein voller Release-Zyklus unter Zeitdruck – der schlechteste Moment für einen langen Weg.

Rollback ist der zweite Test. Ein zurückgerolltes Image bringt bei sauberer Trennung nur alten Code mit, nicht alte Secrets. Vermischt man beides, rollt man mit dem Code auch stillschweigend abgelaufene oder rotierte Zugangsdaten zurück. Und drittens der Audit: Wenn ein Wert leakt, will ich rekonstruieren, welcher Job ihn wann gelesen hat. Ein zentraler Store mit Zugriffs-Logs beantwortet das; eine Datei in einem Layer beantwortet es nicht. Diese Fragen wirken trocken, sind aber der Unterschied zwischen Demo und Betrieb.

Der Business-Outcome ist Tempo bei kontrolliertem Risiko

Der Grund, warum ich Secrets als Produktentscheidung behandle, ist messbar. Wenn ein Team Secrets nicht mehr manuell durchreichen muss, verschwindet eine ganze Klasse von Wartezeit. Ein Entwickler deployt nach Staging, ohne dass jemand aus dem Ops-Team ein Passwort per Chat kopiert – die IAM-Policy erlaubt genau diesen Pfad und nichts darüber hinaus. Das verkürzt den Zyklus von Idee zu laufender Änderung und senkt gleichzeitig das Risiko, weil der Zugriff eng geschnitten ist. Tempo und Sicherheit stehen hier nicht im Widerspruch; die richtige Grenze liefert beides.

Weniger Reibung entsteht auch dadurch, dass die Grenze im Code steht und nicht im Kopf einer Person. Neue Teammitglieder lesen aus Pfadstruktur und Policy ab, was erlaubt ist, statt es zu erfragen. Das gilt genauso für Nebenprojekte: Auch bei kleinen eigenen Systemen wie ContentKit halte ich mich an dieselbe Trennung, weil sich der Aufwand schon bei der ersten Rotation auszahlt und nicht erst bei Team-Größe. Und die Qualität steigt, weil der teuerste Fehler – ein Secret im geteilten Artefakt – strukturell nicht mehr passieren kann, statt nur durch Disziplin verhindert zu werden. Genau das meine ich mit Developer Productivity: nicht schneller tippen, sondern seltener warten und seltener aufräumen.

Der Effekt ist kein weiches Versprechen, sondern lässt sich beobachten. Ich messe ihn an drei Zahlen: an der Zeit von „Passwort muss getauscht werden" bis „getauscht und im Betrieb", an der Zahl der manuellen Übergaben pro Release und daran, wie viele Umgebungen ein einzelner Zugang lesen kann. Jede dieser Zahlen sinkt, wenn die Build-Runtime-Grenze steht – und jede ist im Zweifel überzeugender als die Behauptung, es sei jetzt sicherer.

Typische Fehler

Der erste Fehler ist, das Werkzeug mit der Lösung zu verwechseln. Ein Secret-Store ist keine Architektur, KMS ist keine Zugriffsgrenze, und ein --build-arg ist keine Vertraulichkeit. Diese Werkzeuge können gute Grenzen ausdrücken, aber sie erzeugen sie nicht automatisch. Wer den Store einführt, ohne die Build-Runtime-Grenze zu ziehen, hat die Datei nur an einen anderen Ort verschoben.

Der zweite Fehler ist, den Happy Path zu überschätzen. Viele Setups sehen im ersten Deploy sauber aus und zeigen ihre Schwäche erst bei der ersten dringenden Rotation. Der dritte ist, alles gleich vertraulich zu behandeln. Eine Feature-Flag-Konfiguration und ein Datenbank-Passwort brauchen unterschiedliche Grenzen; wer beides in denselben Topf wirft, macht den harmlosen Fall teuer und den kritischen Fall bequem.

Worauf ich prüfe

Drei Checks reichen mir meistens. Erstens: Ist ein Build-Artefakt umgebungsneutral, oder kennt es Secrets? Ein docker history und ein Blick in die Layer beantworten das in einer Minute. Zweitens: Ist der Zugriff pro Umgebung getrennt, sodass ein Staging-Job Production nicht lesen kann? Drittens: Kann ich ein Secret rotieren, ohne neu zu bauen, und den Zugriff im Nachhinein nachvollziehen?

Wenn eine dieser Fragen mit Nein endet, fehlt noch ein Teil der Architektur – kein weiteres Tool, sondern eine bewusst gezogene Grenze. Ich würde daraus keine große Initiative machen, sondern mit einem kleinen, überprüfbaren Ergebnis beginnen: ein Multi-Stage-Dockerfile, eine IAM-Policy mit Pfad-Scope, ein Startskript, das den Wert just-in-time auflöst. Konkret genug, dass ein anderer Entwickler sinnvoll widersprechen kann. Zustimmung ist gut, aber Widerspruch zeigt oft die versteckte Annahme.

Schluss

Der Beitrag ist dann stark, wenn er nicht nur zeigt, wie man ein Secret sicher ablegt, sondern warum die Grenze zwischen Build und Runtime so gezogen wird. Genau dort entsteht der Nutzen für Entwickler, Architekten und technische Entscheider: Entwickler sehen das Leak-Bild, Architekten die Vertrauenszone, Entscheider das Risiko und das Tempo. Der Wert steckt nicht im Namen des Secret-Stores, sondern in der wiederverwendbaren Entscheidung, wer was wann sehen, speichern und deployen darf.

Kommentare