Sichere Verwaltung von Geheimnissen in Next.js und Node.js mit HashiCorp Vault

Schritt für Schritt zeigen wir, wie man einen soliden Prozess zur Verwaltung von Geheimnissen in Next.js- und Node.js-Anwendungen mit HashiCorp Vault aufbaut.

Bezpieczne zarządzanie sekretami w Next.js i Node.js z HashiCorp Vault

Moderne Web‑Anwendungen, die auf Next.js und Node.js basieren, verarbeiten immer mehr sensible Daten – API‑Schlüssel, JWT‑Tokens, Datenbank‑Anmeldeinformationen. Eine unsachgemäße Speicherung dieser Geheimnisse führt zu Lecks, die eine Organisation Millionen von Euro kosten können. In diesem Artikel präsentieren wir ein vollständiges Bedrohungsmodell, beschreiben den Mechanismus von HashiCorp Vault und zeigen praxisnahe Code‑Beispiele – vom verwundbaren bis zum verbesserten – sowie eine Checkliste zur Minderung basierend auf OWASP, CWE und dem Prinzip des geringsten Privilegs.

Bedrohungsmodell – wo entsteht das Risiko?

Ein typischer Geheimnis‑Fluss in einem Next.js/Node.js‑Projekt sieht folgendermaßen aus: Der Entwickler speichert den Wert in einer .env-Datei, das CI/CD‑Repository holt die Datei und injiziert sie in einen Docker‑Container, und die Anwendung liest die Umgebungsvariable zur Laufzeit. In diesem Modell gibt es drei kritische Angriffsflächen:

  • Speicherung des Geheimnisses im Repository (selbst in einem privaten Repository kann es geklont oder geleakt werden);
  • Weitergabe des Geheimnisses als unverschlüsselter String in Umgebungsvariablen im Container;
  • Fehlende Zugriffsbeschränkungen der Anwendung auf ein konkretes Geheimnis (fehlendes Least‑Privilege‑Prinzip).

All diese Szenarien werden im OWASP Top 10 als Security Misconfiguration (A05) und in CWE‑522 – Insufficiently Protected Credentials beschrieben. Der beste Weg, das Risiko zu reduzieren, ist ein zentrales Geheimnis‑Management, das Audits, Rotation und minimale Berechtigungen ermöglicht.

HashiCorp Vault – wie funktioniert es?

Vault ist ein Geheimnis‑Management‑System, das verschlüsselte Werte in einem sicheren Backend (z. B. einem konsensbasierten Speicher) ablegt. Der Zugriff auf Geheimnisse erfolgt nach Authentifizierung über Methoden wie AppRole, Kubernetes‑Auth oder TLS‑Zertifikate. Nach erfolgreicher Authentifizierung gibt Vault ein kurzlebiges Token aus (TTL von wenigen Sekunden bis zu mehreren Stunden) und erlaubt das Lesen oder Generieren dynamischer Anmeldeinformationen (z. B. temporäre Datenbank‑Benutzer).

Schlüsselmerkmale, die OWASP‑ und CWE‑Anforderungen erfüllen:

  • Encryption‑at‑rest und Encryption‑in‑transit (TLS);
  • Strenge Richtlinien‑Kontrolle (Policy‑as‑Code), die definiert, welche Pfade und Operationen erlaubt sind;
  • Automatische Rotation und Widerruf von Tokens.

Integration von HashiCorp Vault in Node.js – Code‑Beispiel

Unten steht ein einfacher, aber verwundbarer Code‑Abschnitt, bei dem Geheimnisse aus einer .env-Datei geladen und direkt in API‑Aufrufen verwendet werden:

require('dotenv').config();
const axios = require('axios');

const apiKey = process.env.API_KEY; // verwundbar: Schlüssel in .env

async function callExternal() {
  const resp = await axios.get('https://api.example.com/data', {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });
  return resp.data;
}

Das Problem ist, dass API_KEY in CI‑Logs, im Docker‑Image oder im Speicher des Containers landen kann und es keine Kontrolle darüber gibt, welche Prozesse ihn lesen dürfen.

Die verbesserte Version nutzt das Vault‑SDK, authentifiziert sich über AppRole und holt das Geheimnis zur Laufzeit. Dadurch hat das Token eine kurze TTL und der Schlüssel wird nicht länger im Speicher gehalten als nötig:

const vault = require('node-vault')({
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_APPROLE_TOKEN // kurzes Token, nicht dauerhaft
});

async function getApiKey() {
  const result = await vault.read('secret/data/app/api-key');
  return result.data.data.key;
}

async function callExternal() {
  const apiKey = await getApiKey();
  const resp = await axios.get('https://api.example.com/data', {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });
  return resp.data;
}

Es ist zu beachten, dass das Vault‑SDK das Token‑Refresh automatisch handhabt und die Richtlinie app/api-key den Zugriff ausschließlich auf das Lesen dieses spezifischen Schlüssels beschränken kann.

Docker und Kubernetes – sicheres Speichern von JWT‑Tokens

In Container‑Umgebungen werden Tokens häufig über Umgebungsvariablen übergeben. Stattdessen kann man Folgendes einsetzen:

  • Vault Agent Injector – injiziert das Geheimnis automatisch als Datei unter /vault/secrets im Container;
  • Sidecar‑Container mit Vault Agent, der das Token im Hintergrund aktualisiert.

Beispiel für ein Kubernetes‑Manifest mit Sidecar:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-app
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: app
          image: myregistry/nextjs:latest
          env:
            - name: VAULT_ADDR
              value: "https://vault.mycompany.com"
          volumeMounts:
            - name: vault-secrets
              mountPath: /vault/secrets
        - name: vault-agent
          image: hashicorp/vault-agent:latest
          args:
            - "agent"
            - "-config=/etc/vault/agent-config.hcl"
          volumeMounts:
            - name: vault-config
              mountPath: /etc/vault
            - name: vault-secrets
              mountPath: /vault/secrets
      volumes:
        - name: vault-config
          configMap:
            name: vault-agent-config
        - name: vault-secrets
          emptyDir: {}

Der Sidecar speichert das aktualisierte Token in /vault/secrets/jwt, und die Anwendung liest es beim Start aus. Dadurch erscheint das Token nicht in Umgebungsvariablen und wird nach Ablauf automatisch widerrufen.

Checkliste zur Minderung – von OWASP bis Least‑Privilege

  • Verwende einen zentralen Speicher (Vault) anstelle von .env-Dateien im Repository.
  • Setze kurze TTLs für Tokens und automatisiere die Rotation.
  • Definiere Vault‑Richtlinien im Stil von path "secret/data/app/*" { capabilities = ["read"] } – beschränke den Zugriff auf einen konkreten Namespace.
  • In Docker und Kubernetes Secrets als Dateien oder Volumes injizieren, nicht als env‑Variablen.
  • Auditiere den Zugriff – aktiviere das Audit‑Device in Vault und integriere die Logs in ein SIEM.
  • Überprüfe die Berechtigungen der Anwendung (Least‑Privilege) – die Anwendung sollte nur Zugriff auf die unbedingt benötigten Secrets haben.

Typische Fehler und Kompromisse

Even experienced teams machen einige Fallen:

  • Hard‑coding von Tokens – feste Tokens im Quellcode unter dem Vorwand eines schnellen Prototyps ablegen; Lösung: immer AppRole oder Kubernetes Auth verwenden.
  • Zu weit gefasste Richtlinien setzen – z. B. path "secret/*" { capabilities = ["read"] } gewährt Zugriff auf alle Secrets; Lösung: Secrets nach Domänen aufteilen und minimale Berechtigungen zuweisen.
  • Keine Rotation – unveränderte API‑Schlüssel über Jahre hinweg behalten; Lösung: max_ttl und rotation_period in Vault konfigurieren.
  • Logging von Secrets – unbeabsichtigtes Ausgeben von Werten in den Anwendungs‑Logs; Lösung: einen Logger verwenden, der Felder mit Namen wie *secret*, *key*, *token* maskiert.
Sicherheit ist keine einmalige Konfiguration, sondern ein kontinuierlicher Prozess aus Kontrolle, Audits und Privilegien‑Reduktion.

Praktisches Beispiel – kompletter Workflow

1. Vault‑Konfiguration: Erstelle den Namespace app, aktiviere das Audit‑Device, definiere die Richtlinie app-read mit Zugriff auf secret/data/app/*.

2. AppRole erstellen: role_id und secret_id werden in einem Kubernetes Secret gespeichert (nicht im Repository).

3. Anwendung deployen: Manifest mit Vault‑Agent‑Sidecar, wie im obigen Beispiel.

4. In der Next.js‑Anwendung (pages/api/data.ts):

import { createClient } from '@next-auth/vault';

export default async function handler(req, res) {
  const client = createClient({
    endpoint: process.env.VAULT_ADDR,
    roleId: process.env.VAULT_ROLE_ID,
    secretId: process.env.VAULT_SECRET_ID
  });
  const { data } = await client.read('secret/data/app/jwt');
  const jwt = data.data.token;
  // Verwendung des Tokens zur Autorisierung eines externen Dienstes
  res.status(200).json({ status: 'ok' });
}

5. Sicherheitstests: Führe einen OWASP ZAP‑Scan durch und prüfe, ob Umgebungsvariablen nicht in Container‑Logs lecken.

Nach der Implementierung dieses Workflows erhält das Engineering‑Team:

  • einen zentralen Ort zur Verwaltung aller Secrets;
  • die Möglichkeit zur sofortigen Rotation im Falle eines Vorfalls;
  • vollständige Audit‑Kontrolle, konform mit OWASP und CWE.

Wenn du möchtest, dass deine Next.js‑ und Node.js‑Projekte gegen Datenlecks geschützt sind, kontaktiere Coderia.it – wir helfen bei der Implementierung von Vault, der Anpassung von Richtlinien und der Durchführung von Sicherheit‑Audits.

Loslegen

Ein Projekt im Kopf?

Beschreiben Sie es in wenigen Sätzen. Ich antworte innerhalb von 24 Stunden mit Angebot und Stack-Vorschlag.