Zum Inhalt springen

Das Zero-Trust-Sicherheitsmodell für Entwickler

Was Zero Trust jenseits des Buzzwords bedeutet: identitätsbasierter Zugriff, mutual TLS, Mikrosegmentierung und Umsetzung auf Anwendungsebene.

5 Min. Lesezeit
Netzwerkdiagramm einer Zero-Trust-Architektur mit Identitätsprüfung an jeder Dienstgrenze

Das klassische Sicherheitsmodell gleicht einer Burg mit Wassergraben: Ein starker Perimeter (Firewall, VPN) schützt ein als vertrauenswürdig geltendes internes Netzwerk. Einmal drinnen, kommt man an alles heran. Dieses Modell versagt katastrophal, sobald ein Angreifer den Perimeter durchbricht — was früher oder später über Phishing, kompromittierte Zugangsdaten oder Angriffe auf die Lieferkette geschieht.

Zero trust dreht das Prinzip um: niemals vertrauen, immer verifizieren. Jede Anfrage — ob sie von innerhalb oder außerhalb des Netzwerks kommt — muss authentifiziert, autorisiert und verschlüsselt werden. Ein vertrauenswürdiges internes Netzwerk gibt es nicht.

Die Kernprinzipien

Zero trust ist kein Produkt, das man kauft. Es ist eine Reihe architektonischer Prinzipien, die verändern, wie man über Zugriffskontrolle, Netzwerkkommunikation und Identitätsprüfung denkt.

tstypescript
// Traditional security: trust the network
interface CastleAndMoat {
  perimeter: 'firewall + VPN';
  internalTraffic: 'trusted by default';  // ← The vulnerability
  authCheck: 'at the perimeter only';
  // Once inside, an attacker has lateral movement to everything
}
 
// Zero trust: trust nothing, verify everything
interface ZeroTrustModel {
  perimeter: 'does not exist';
  allTraffic: 'encrypted and authenticated';
  authCheck: 'at every service, every request';
  principle: 'least privilege — minimum access for minimum time';
  assumption: 'the network is already compromised';
}
 
// Zero trust principles as code
const zeroTrustPrinciples = {
  verifyExplicitly: {
    rule: 'Authenticate and authorize every request',
    how: 'Use identity tokens (JWT, mTLS) — not network location',
  },
  leastPrivilege: {
    rule: 'Grant minimum required access for minimum time',
    how: 'Short-lived tokens, scoped permissions, JIT access',
  },
  assumeBreach: {
    rule: 'Design as if attackers are already inside',
    how: 'Encrypt internal traffic, segment networks, log everything',
  },
};

Identity-based access (nicht netzwerkbasiert)

In einem Zero-Trust-Modell basieren Zugriffsentscheidungen auf der Identität — wer du bist, welches Gerät du verwendest und in welchem Kontext du dich befindest — und nicht darauf, mit welchem Netzwerk du verbunden bist.

tstypescript
// ❌ Network-based access control
function handleRequest(req: Request): Response {
  // If the request comes from an internal IP, trust it
  const clientIp = req.headers.get('x-forwarded-for');
  if (isInternalIp(clientIp)) {
    // No authentication required — "it's internal"
    return processRequest(req);  // Attackers love this
  }
  return new Response('Forbidden', { status: 403 });
}
 
// ✅ Identity-based access control
async function handleRequest(req: Request): Promise<Response> {
  // Every request must present a valid identity token
  const token = req.headers.get('authorization')?.replace('Bearer ', '');
  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }
 
  // Verify the token regardless of network origin
  const identity = await verifyToken(token);
  if (!identity) {
    return new Response('Invalid token', { status: 401 });
  }
 
  // Check if this identity has access to this specific resource
  const hasAccess = await checkPermission(identity, req.url, req.method);
  if (!hasAccess) {
    return new Response('Forbidden', { status: 403 });
  }
 
  return processRequest(req, identity);
}

Gegenseitiges TLS zwischen Diensten

In traditionellen Architekturen kommunizieren interne Dienste über einfaches HTTP. In einer Zero-Trust-Architektur wird jede Verbindung zwischen Diensten mittels gegenseitigem TLS (mTLS) authentifiziert — beide Seiten legen Zertifikate vor, um ihre Identität zu belegen.

tstypescript
// Setting up mTLS in Node.js
import { createServer, request } from 'https';
import { readFileSync } from 'fs';
 
// Server: requires client certificates
const server = createServer({
  key: readFileSync('server-key.pem'),
  cert: readFileSync('server-cert.pem'),
  ca: readFileSync('ca-cert.pem'),    // Trust only certs signed by this CA
  requestCert: true,                   // Require client certificate
  rejectUnauthorized: true,            // Reject connections without valid certs
}, (req, res) => {
  // The client's identity is in the certificate
  const clientCert = (req as any).socket.getPeerCertificate();
  const clientService = clientCert.subject.CN;  // e.g., "billing-service"
 
  console.log(`Authenticated request from: ${clientService}`);
 
  // Authorize based on the service identity
  if (!isAllowedCaller(clientService, req.url)) {
    res.writeHead(403);
    res.end('Service not authorized for this endpoint');
    return;
  }
 
  handleRequest(req, res, clientService);
});
 
// Client: presents its own certificate
function callService(url: string): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const req = request(url, {
      key: readFileSync('client-key.pem'),
      cert: readFileSync('client-cert.pem'),
      ca: readFileSync('ca-cert.pem'),
    }, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    });
    req.on('error', reject);
    req.end();
  });
}

Autorisierung auf Dienstebene

Authentifizierung sagt dir, wer die Anfrage stellt. Autorisierung sagt dir, ob diese Person tun darf, was sie verlangt. Bei zero trust setzt jeder Dienst seine eigene Autorisierung durch — man verlässt sich nicht darauf, dass vorgelagerte Dienste die Berechtigungen bereits geprüft haben.

tstypescript
// Policy-based authorization at every service
interface AccessPolicy {
  service: string;          // Which service can access
  resource: string;         // Which endpoint/resource
  actions: string[];        // Which HTTP methods
  conditions?: Condition[]; // Additional constraints
}
 
const policies: AccessPolicy[] = [
  {
    service: 'api-gateway',
    resource: '/api/users/*',
    actions: ['GET'],
    conditions: [{ type: 'time', allowedHours: { start: 6, end: 22 } }],
  },
  {
    service: 'billing-service',
    resource: '/api/users/*/payment-methods',
    actions: ['GET'],
    // Billing can read payment methods but not user profiles
  },
  {
    service: 'admin-service',
    resource: '/api/users/*',
    actions: ['GET', 'PUT', 'DELETE'],
    conditions: [{ type: 'approval', requiredApprovers: 1 }],
  },
];
 
function isAllowedCaller(
  callerService: string,
  requestPath: string,
  method: string = 'GET'
): boolean {
  return policies.some(policy =>
    policy.service === callerService &&
    matchesPath(requestPath, policy.resource) &&
    policy.actions.includes(method) &&
    (policy.conditions?.every(c => evaluateCondition(c)) ?? true)
  );
}
tstypescript
// ❌ Trusting upstream services to enforce authorization
function userService(req: Request): Response {
  // "The API gateway already checked permissions"
  // But what if the request bypasses the gateway?
  // What if the gateway has a bug in its auth logic?
  return getUser(req.params.id);  // No authorization check here
}
 
// ✅ Every service enforces its own authorization
async function userService(req: AuthenticatedRequest): Promise<Response> {
  // Verify the caller's identity (from mTLS or token)
  const caller = req.identity;
 
  // Check if THIS specific service allows THIS action
  if (!isAllowedCaller(caller.service, req.url, req.method)) {
    auditLog.warn('Unauthorized access attempt', {
      caller: caller.service,
      resource: req.url,
      method: req.method,
    });
    return new Response('Forbidden', { status: 403 });
  }
 
  return getUser(req.params.id);
}

Kurzlebige Zugangsdaten

Langlebige API-Schlüssel und Passwörter für Service-Konten sind ein erhebliches Risiko: Werden sie kompromittiert, ermöglichen sie unbegrenzten Zugriff. Zero trust setzt auf kurzlebige, automatisch rotierende Zugangsdaten.

tstypescript
// Token lifecycle management
interface TokenPolicy {
  maxLifetime: number;      // 15 minutes for access tokens
  refreshWindow: number;    // Can refresh within 5 minutes of expiry
  rotationSchedule: string; // Service credentials rotate daily
}
 
// Short-lived service-to-service tokens
async function getServiceToken(
  targetService: string
): Promise<string> {
  const token = await tokenService.issue({
    issuer: 'user-service',
    audience: targetService,
    expiresIn: '15m',           // 15 minutes — not days or months
    scopes: ['read:users'],      // Minimum required scope
  });
 
  return token;
}
 
// The token service issues short-lived JWTs
interface ServiceToken {
  iss: string;    // Issuing service
  aud: string;    // Target service
  exp: number;    // Expires in 15 minutes
  iat: number;    // Issued at
  scopes: string[]; // Minimum required permissions
  jti: string;    // Unique ID for revocation tracking
}

Protokollierung und Überwachung

Zero trust erfordert lückenlose Protokollierung, denn man kann nicht mehr davon ausgehen, dass interner Datenverkehr sicher ist. Jede Authentifizierungsentscheidung, jede Autorisierungsprüfung und jeder Zugriffsversuch müssen für Audits und die Anomalieerkennung protokolliert werden.

tstypescript
// Audit logging for zero trust
interface AuditLogEntry {
  timestamp: string;
  requestId: string;
  caller: {
    service: string;
    identity: string;
    ipAddress: string;
  };
  target: {
    service: string;
    resource: string;
    method: string;
  };
  decision: 'allowed' | 'denied';
  reason: string;
  policyMatched?: string;
}
 
function logAccessDecision(entry: AuditLogEntry): void {
  // All access decisions are logged — both allowed and denied
  // Denied requests are especially important for detecting attacks
  console.log(JSON.stringify(entry));
  
  // Alert on anomalies
  if (entry.decision === 'denied') {
    anomalyDetector.track(entry);
  }
}

Die wichtigsten Erkenntnisse

  1. Niemals vertrauen, immer verifizieren — jede Anfrage unabhängig vom Netzwerkursprung authentifizieren und autorisieren
  2. Identität statt IP-Adressen verwenden — Zugriffsentscheidungen sollten darauf beruhen, wer die Anfrage stellt, verifiziert durch Tokens oder Zertifikate
  3. Autorisierung auf jedem Dienst durchsetzen — sich nicht darauf verlassen, dass vorgelagerte Dienste die Berechtigungen bereits geprüft haben
  4. Gegenseitiges TLS zwischen Diensten verwenden — Client und Server authentifizieren sich gegenseitig mit Zertifikaten
  5. Kurzlebige Zugangsdaten ausstellen — Zugriffstoken mit 15 Minuten Gültigkeit begrenzen den möglichen Schaden bei kompromittierten Zugangsdaten
  6. Jede Zugriffsentscheidung protokollieren — lückenlose Audit-Protokolle ermöglichen Anomalieerkennung und forensische Untersuchungen
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX