Homepage
All Cases
Zuletzt aktualisiert:
Autor: Max Spanier

Secure by Design

Uhren Symbol8 min.

Fünf Secure-by-Design-Praktiken, die du heute umsetzen kannst

Die meisten Entwickler schreiben keinen unsicheren Code mit Absicht – die Leitplanken fehlten einfach. Dieser Beitrag zeigt fünf Secure-by-Design-Praktiken, die du noch im laufenden Sprint umsetzen kannst. Ohne Reorganisation, ohne großes Security-Programm.

A group of astronaut IT developers sit in a room working on their computers. Their current sprint board in the background reads security implementations.

Die Entwickler*Innen schreiben unsicheren Code nicht mit Absicht. Unsicherer Code ist meistens eine Folge von unsicheren Defaults, Error Handling als nachträglicher Gedanke oder veralteten, ungeprüften Templates für Service Account Berechtigungen. In den meisten Fällen, die uns begegnen, ist das Problem nicht, dass jemand unachtsam war. Es fehlten schlicht die richitgen Leitplanken. 

In diesem Blogpost stellen wir dir fünf Praktiken vor, die du in deinem aktuellen Sprint anwenden kannst. Keine Reorganisation nötig, keine Abhängigkeit von einer Initiative des Security-Teams. Jede Praktik adressiert eine wiederkehrende Ursache für Vulnerabilities, die wir projektübergreifend beobachten. 

Praktik 1: Service Accounts auf das Minimum eingrenzen 

Service Accounts starten oft mit weitreichenden Berechtigungen und verlieren diese nicht wieder. Jemand richtet eine Deployment-Pipeline ein, kopiert eine IAM Policy aus einem funktionierenden Beispiel und macht weiter. Sechs Monate später hat dieser Account immer noch Schreibzugriff auf Produktionsdatenbanken, die er nie anfasst. Default Policies in Cloud-Plattformen sind weiter gefasst als nötig, und Shared Accounts über Teams hinweg machen es unmöglich nachzuvollziehen, wer was getan hat. 

  • Ein Account, ein Job. Wenn ein Service Account mehrere Pipelines bedient, teile ihn auf. Wird einer kompromittiert, bleibt der Blast Radius klein.
  • Statische Credentials durch kurzlebige Tokens ersetzen. Hardcodierte API Keys in Umgebungsvariablen sind schnell eingerichtet und schnell vergessen. Wechsle zu Workload Identity Federation oder vergleichbaren Mechanismen, die temporäre Credentials bei Bedarf ausstellen.
  • Just-in-Time-Elevation statt permanenter Admin-Rollen. Wenn ein CI/CD-Schritt für eine bestimmte Aufgabe erhöhte Privilegien benötigt, gewähre sie für genau diese Aufgabe und entziehe sie danach automatisch.
  • Prüfen, was tatsächlich genutzt wird. Die meisten Cloud Provider bieten Access-Analysis-Tooling, das zeigt, welche Berechtigungen ein Account tatsächlich nutzt im Vergleich zu denen, die ihm gewährt wurden. Die Differenz ist deine Angriffsfläche.

Die IAM Policy ist die Stelle, an der das Copy-Paste-Problem am deutlichsten sichtbar wird. Hier derselbe Deployment Service Account in zwei Varianten: die Version, die meistens ausgeliefert wird, und die Version, die ausgeliefert werden sollte. Beide Policies decken dieselbe Aufgabe ab, nämlich das Aktualisieren einer bestimmten Lambda-Function-Familie und das Lesen von Deployment-Artefakten aus S3. Der Unterschied liegt darin, wie viel Vertrauen jede davon dafür einräumt. 

Vorher: Offene Version

{ 
"Version": "2012-10-17", 
"Statement": [ 
{ 
  "Effect": "Allow", 
  "Action": [ 
    "s3:*", 
    "lambda:*", 
    "iam:*" 
  ], 
  "Resource": "*" 
} 
] 
}

Nachher: Sauber Eingegrenzt 

{ 
"Version": "2012-10-17", 
"Statement": [ 
{ 
  "Sid": "DeployPaymentServiceLambdas", 
  "Effect": "Allow", 
  "Action": [ 
    "lambda:UpdateFunctionCode", 
    "lambda:UpdateFunctionConfiguration", 
    "lambda:GetFunction", 
    "lambda:PublishVersion" 
  ], 
  "Resource": "arn:aws:lambda:eu-central-1:123456789012:function:payment-service-*" 
}, 
{ 
  "Sid": "ReadDeploymentArtifacts", 
  "Effect": "Allow", 
  "Action": ["s3:GetObject"], 
  "Resource": "arn:aws:s3:::deploy-artifacts-prod/payment-service/*", 
  "Condition": { 
    "StringEquals": { 
      "aws:SourceVpc": "vpc-0abc1234def567890" 
    } 
  } 
} 
] 
}

Die eingegrenzte Version ersetzt Wildcard-Actions durch explizite Verben – lambda:UpdateFunctionCode, s3:GetObject – sodass der Account nur exakt das tun kann, was die Pipeline tatsächlich erfordert. Resource ARNs sind auf eine bestimmte Service-Familie beschränkt, statt accountweit zu gelten. Ein Condition Block fügt eine zweite Einschränkung hinzu: Der Call muss aus einer bekannten VPC stammen, sodass selbst gestohlene Credentials außerhalb dieser Grenze nicht funktionieren. Die Statements sind mit Sid-Identifiern aufgeteilt, was CloudTrail Logs aus einer Wand von Allow-Einträgen in etwas verwandelt, das Engineers tatsächlich einem konkreten Deployment-Schritt zuordnen können. 

Praktik 2: Fail-Secure statt Fail-Open beim Error Handling 

Ein Service fällt aus, und der Load Balancer leitet Traffic auf einen Fallback um, der die Authentication überspringt. Eine unbehandelte Exception schreibt einen vollständigen Stack Trace in die API Response – inklusive Datenbank-Connection-Strings. Das gemeinsame Muster: Das System hat einen unerwarteten Zustand erreicht, und der Failure Path war nicht durchdacht. Firewalls, die bei einem Crash der Rule Engine standardmäßig auf „allow” schalten, Auth-Services, die „authorized” zurückgeben, wenn sie den Identity Provider nicht erreichen können. Jeder dieser Fälle verwandelt einen temporären Ausfall in einen Security Incident. 

  • Default auf Deny, nicht auf Allow. Jede Access-Entscheidung sollte ein explizites „Ja” aus deiner Auth-Logik erfordern. Schlägt die Prüfung fehl, läuft sie in einen Timeout oder wirft eine Exception, lautet die Antwort „Nein”.
  • Exceptions abfangen, bevor sie Informationen leaken. Verpacke nach außen sichtbare Fehlermeldungen in generische Meldungen und leite die Details in deine Logging-Pipeline. Stack Traces, interne Pfade und Tokens in API Responses oder Fehlermeldungen sind kostenlose Reconnaissance für Angreifer.
  • Fallback-Konfigurationen restriktiv halten. Wenn deine Anwendung ihre Config nicht lesen kann, sollte sie in einem minimalen, gesperrten Zustand starten, statt auf permissive Defaults zurückzufallen. Fehlende Werte sollten Funktionalität deaktivieren, nicht aktivieren.
  • Failure Paths testen. Lege im Staging eine Dependency lahm und beobachte, wie dein Service reagiert. Wenn du das noch nie gemacht hast, lautet die Antwort vermutlich „nicht gut”.

So sieht Fail-Secure-Error-Handling in einer Express.js-Anwendung aus. Erst der Default, der ausgeliefert wird, dann eine für die Produktion gedachte Variante. 

Vorher: Default, der interne Details leakt

app.use((err, req, res, next) => { 
  res.status(500).send(err.stack); 
});

Nachher: Generische Response an den Client mit vollständigen Details in den Logs 

const logger = require('./logger');
const { randomUUID } = require('crypto'); 

app.use((err, req, res, next) => { 
  const correlationId = req.correlationId || randomUUID(); 
  const status = err.statusCode || 500; 

  // Everything sensitive goes to the log pipeline, never to the client 
  logger.error({ 
    correlationId, 
    method: req.method, 
    path: req.path, 
    userId: req.user?.id, 
    ip: req.headers['x-forwarded-for'] 
    err: { name: err.name, message: err.message, stack: err.stack } 
  }, 'Unhandled exception'); 

  // 5xx: hide details. 4xx: only expose a message the error class marked safe. 
  res.status(status).json({ 
    error: status >= 500 ? 'Internal Server Error' : ('Bad Request'), 
    correlationId 
  }); 
});

Der überarbeitete Handler sendet niemals sensible Details an den Client. Stack Traces, interne Pfade und Request-Metadaten gehen stattdessen in die strukturierte Logging-Pipeline, wo Entwickler*Innen sie finden können und Angreifer*Innen nicht. Der Client erhält eine generische Meldung und eine Correlation ID: genug für ein Support-Gespräch, nutzlos für Reconnaissance. Der 5xx-Pfad gibt nur ‘Internal Server Error’ zurück; 4xx-Responses zeigen nur das, was die Error Class explizit als sicher markiert. Die correlationID wird über crypto.randomUUID() erzeugt und nicht aus einem vom User gelieferten Wert abgeleitet, sodass sie nicht manipuliert werden kann, um Rückschlüsse auf den internen Zustand zu ziehen. 

Praktik 3: Jeden Request validieren, nicht nur den ersten 

Ein*e User*In loggt sich ein, bekommt ein Session Token, und ab da läuft jeder API Call durch, weil das System einmal geprüft und sich entschieden hat zu vertrauen. Dreißig Minuten später hat sich ihre Rolle geändert, das Token ist noch gültig, und die Requests laufen weiter mit veralteten Berechtigungen. Das ist eine der leiseren Vulnerabilities, weil sie nicht wie ein Bug aussieht. Das Auth-System funktioniert. Das Problem ist, dass Authorization als einmaliges Event behandelt wurde. Gecachte Berechtigungen, die nie aktualisiert werden, langlebige Tokens, interne Endpoints ohne Auth-Logik, weil „die rufen ja nur unsere eigenen Services auf” – in einer Microservices-Architektur potenziert sich das schnell. 

  • Authorization über einen einzigen Enforcement Point laufen lassen. Eine Stelle, die Access-Entscheidungen trifft und sie konsistent über alle Services anwendet. Auth-Logik über einzelne Endpoints zu verteilen ist der Weg, auf dem Checks vergessen werden.
  • Tokens und Sessions kurzlebig halten. Stelle Tokens mit kurzen Gültigkeitsfenstern aus und erzwinge in sinnvollen Intervallen eine erneute Validierung. Ändern sich die Berechtigungen einer Userin mitten in der Session, sollte das im nächsten Request berücksichtigt sein.
  • Interne APIs genauso behandeln wie externe. „Dieser Endpoint wird nur von unseren eigenen Services aufgerufen” ist keine Sicherheitsgrenze. Service-to-Service-Calls brauchen ebenfalls Auth Checks.

Praktik 4: Sichere Defaults setzen, nicht auf Konfiguration verlassen 

Ein*e Entwickler*In generiert einen neuen Service per Scaffolding, pusht ihn ins Staging, und es funktioniert. Niemand hinterfragt die Defaults. Monate später deckt ein Security Review auf, dass CORS so eingestellt ist, dass alle Origins erlaubt sind, Session Cookies das Secure Flag nicht gesetzt haben und das Admin Panel ohne Authentication erreichbar ist. Frameworks und Cloud-Plattformen sind darauf optimiert, dass man schnell loslegen kann, und das bedeutet Auslieferung mit permissiven Einstellungen. Die Annahme ist, dass Teams die Dinge vor Production härten. In der Praxis ist die Konfiguration, die in Development funktioniert hat, die Konfiguration, die ausgeliefert wird. 

  • Projekt-Templates härten. Wenn dein Team Starter Repos, Helm Charts oder Terraform Module nutzt, sperre sie ab: striktes CORS, sichere Cookie Flags (z. B. Secure, HttpOnly, SameSite), TLS verpflichtend, Admin-Interfaces deaktiviert. Jedes Projekt, das diese Templates erbt, startet sicher, ohne dass sich jemand daran erinnern muss.
  • Riskante Features auf Opt-in, nicht Opt-out. Debug Modes, Verbose Logging, permissive Firewall Rules: Das Aktivieren sollte eine bewusste Änderung erfordern. Wenn das Einschalten von etwas Gefährlichem so einfach ist wie das Nicht-Ausschalten, wird es jemand versehentlich ausliefern.
  • Bei fehlender Konfiguration scheitern. Fehlt eine Umgebungsvariable, sollte die Anwendung sich weigern zu starten, statt stillschweigend einen permissiven Fallback anzuwenden. Ein lautes Startup Failure ist besser als ein leise laufender Service mit offenen Berechtigungen.

Ein Docker-Compose-File ist eine praktische Stelle, an der diese Lücke sichtbar wird. Beide Konfigurationen lassen denselben Service laufen, was sich ändert, ist, wie viel vom Host der Container erreichen kann. 

Vorher: Was Frameworks standardmäßig generieren

services: 
  api: 
    image: my-api:latest 
    ports: 
      - "3000:3000" 
    environment: 
      - NODE_ENV=production

Nachher: Gehärtete Version

services: 
  api: 
    image: my-api:1.4.2@sha256:abc123def456...   # pinned digest, not :latest 
    ports: 
      - "127.0.0.1:3000:3000"                    # bind to loopback, expose via reverse proxy 
    environment: 
      - NODE_ENV=production 
    user: "10001:10001"                          # non-root UID/GID 
    read_only: true                              # immutable root filesystem 
    tmpfs: 
      - /tmp:size=64M,mode=1777                  # writable scratch space only where needed 
    cap_drop: 
      - ALL                                      # drop all Linux capabilities, add back only what's needed 
    networks: 
      - backend 
    deploy: 
      resources: 
        limits: 
          memory: 512M 
          cpus: '0.5' 
    healthcheck: 
      test: ["CMD", "node", "healthcheck.js"] 
      interval: 30s 
      timeout: 5s 
      retries: 3 
    restart: unless-stopped 

networks: 
  backend: 
    driver: bridge 
    internal: true                               # no direct egress; outbound via explicit proxy---

Zwei Dinge in der gehärteten Version werden Anwendungen beim ersten Start kaputtmachen: read_only: true scheitert sofort, wenn die Anwendung in einen Pfad schreibt, der nicht explizit durch ein tmpfs Mount abgedeckt ist – prüfe vor dem Aktivieren auf Log-Verzeichnisse, Temp Files und PID Files. cap_drop: ALL entfernt alle Linux Capabilities, was Anwendungen kaputtmacht, die sich an Port 80 binden oder auf CAP_NET_BIND_SERVICE angewiesen sind; füge nur die spezifische Capability wieder hinzu, die du brauchst. internal: true kappt sämtlichen ausgehenden Traffic aus dem Container – entferne es, wenn der Service legitime Calls an externe APIs macht, und leite ausgehenden Traffic stattdessen über einen expliziten Proxy. 

Über diese Einschränkungen hinaus: :latest wird durch eine gepinnte Version und einen SHA256 Digest ersetzt, was verhindert, dass stille Upstream-Image-Swaps Änderungen einbringen, die du nicht reviewt hast. Das Port Binding ist auf 127.0.0.1 beschränkt, sodass externer Traffic über einen Reverse Proxy laufen muss und nicht direkt am Container ankommt. Der Container läuft als Non-Root-User – UID/GID 10001 – was begrenzt, was ein kompromittierter Prozess auf dem Host anrichten kann. Memory und CPU Limits sind explizit gesetzt, was den Blast Radius eines Resource-Exhaustion-Angriffs oder eines durchgerutschten Cryptomining-Payloads begrenzt. 

Praktik 5: Heute einen Security Check in deiner Pipeline automatisieren 

Du lässt vor dem Merge bereits Linter und Tests laufen. Einen Security Check hinzuzufügen folgt demselben Prinzip: Probleme automatisch abfangen, bevor sie Production erreichen. Trotzdem findet Security-Validierung in vielen Teams immer noch manuell oder gar nicht statt – bis wenige Wochen vor einem Release. Der andere Failure Mode ist Überkorrektur: drei Scanner gleichzeitig, eine Flut von False Positives, und innerhalb eines Monats klickt das Team bei allem „ignorieren”. Fang mit einem Check an. Bring den Noise-Pegel unter Kontrolle, dann erweitere. 

  • Dependency Scanning ist der reibungsärmste Einstieg. Tools wie Dependabot oder OWASP Dependency-Check melden bekannte Vulnerabilities mit minimaler Konfiguration. Ein CVE in einer Dependency ist ein verifizierbarer Fakt, keine heuristische Schätzung – das Signal-Rausch-Verhältnis ist besser als bei den meisten SAST Tools.
  • Ergänze deine Dependency Checks um einen Secret Scanner. Tools wie gitleaks oder TruffleHog scannen Commits nach API Keys, Tokens und hardcodierten Secrets, bevor sie einen Shared Branch erreichen.
  • Setze einen Severity-Schwellenwert, an den sich dein Team tatsächlich hält. Build bei Critical und High brechen, Medium als Warning anzeigen, Low unterdrücken. Justiere nach dem, worauf dein Team tatsächlich reagiert. Wenn alles blockiert, werden Entwicklerinnen und Entwickler den Check umgehen.
  • Ownership für Findings zuweisen, nicht nur für das Tool. Ein Scanner ohne jemanden, der für die Triage seines Outputs verantwortlich ist, ist ein Scanner, der ignoriert wird. Lege im Voraus fest, wer reviewt, wer über Fix oder Suppress entscheidet und wie schnell.

Hier ein minimaler GitHub Actions Workflow mit Grype – ein Scanner, ein Severity-Schwellenwert und die Ergebnisse an einem Ort, an dem tatsächlich jemand für sie verantwortlich ist. 

GitHub Actions: Grype

name: security-scan

on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'        # weekly, Mondays 06:00 UTC - catches CVEs disclosed after last commit
  workflow_dispatch:             # allow manual triggering

jobs:
  dependency-scan:
    name: Dependency vulnerability scan
    runs-on: ubuntu-latest
    timeout-minutes: 10          # prevent hung jobs from blocking the queue
    permissions:
      contents: read
      security-events: write     # required to upload SARIF
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Scan repository with Grype
        id: scan
        uses: anchore/scan-action@v4   # pin to specific version; verify latest before publish
        with:
          path: "."
          severity-cutoff: high        # fail build on HIGH or CRITICAL findings
          fail-build: true
          output-format: sarif
          only-fixed: true             # skip vulns with no available fix - noise reduction

      - name: Upload findings to GitHub Security tab
        if: always()                   # upload even when the previous step fails the build
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: ${{ steps.scan.outputs.sarif }}
          category: grype              # disambiguates findings if you add more scanners later

Der Workflow läuft bei jedem Pull Request, bei jedem Push auf main und nach einem wöchentlichen Schedule – der Schedule ist wichtig, weil CVEs kontinuierlich offengelegt werden, nicht nur dann, wenn sich Code ändert. severity-cutoff: high bedeutet, dass HIGH– und CRITICAL-Findings den Build brechen; alles darunter erscheint informativ, ohne die Pipeline zu blockieren. only-fixed: true ist erwähnenswert: Es filtert Vulnerabilities ohne verfügbaren Patch heraus, was eine bedeutende Quelle von Alert Fatigue eliminiert. Findings werden als SARIF exportiert und in den GitHub Security Tab hochgeladen, was ihnen eine UI, einen Triage-Workflow und einen zuweisbaren Owner gibt – statt in CI Logs zu verschwinden, die nach einem erfolgreichen Build niemand mehr ansieht. 

Vom ersten Schritt zur Secure-by-Design-Gewohnheit 

Fünf Praktiken, keine davon erfordert eine Reorganisation. Service Accounts eingrenzen, fail-secure scheitern, jeden Request validieren, Defaults härten, einen automatisierten Check hinzufügen. 

Wenn du eine umgesetzt hast und sie funktioniert hat, nimm dir die nächste vor. So entsteht Security-Praxis: durch inkrementelle Änderungen, die sich über die Zeit aufaddieren. Der schwierige Teil ist keine einzelne Praktik. Es ist die Gewohnheit, Security als Teil der Arbeit zu behandeln und nicht als separate Aufgabe. 

Häufig gestellte Fragen

Welche dieser Praktiken setzt dein Team bereits um?

Wir arbeiten gemeinsam mit deinem Engineering-Team heraus, wo Sicherheit bereits in eurer Architektur verankert ist und wo die Defaults noch gegen euch arbeiten. Wir übersetzen unsere Ergebnisse in priorisierte, umsetzbare Schritte, die ihr sofort angehen könnt.

Security Assessment anfragen
Max

Max

Secure-by-Design Consultant
Max ist einer unserer Secure-by-Design-Consultants mit Schwerpunkt auf Bedrohungsmodellierung und Audit-Vorbereitung. Er hat Erfahrung darin, interne Auditprozesse aufzubauen und begleitet Unternehmen dabei, sich strukturiert auf externe Prüfungen vorzubereiten. Was ihn fachlich antreibt: die Beobachtung, dass Organisationen oft brillante Lösungen entwickeln, Security jedoch zu spät mitgedacht wird. Genau an dieser Schnittstelle setzt er an.

Insights

Insights

Zum Beitrag: Secure by Design 101: 12 Secure by Design Prinzipien
An astronaut floating in outerspace in front of a holographic blueprint of a spaceship.

Secure by Design

Secure by Design 101

Secure by Design 101: 12 Secure by Design Prinzipien

Was steckt hinter Secure by Design jenseits des Buzzwords? Ein kompakter Überblick über zwölf Prinzipien mit einem genaueren Blick auf Least Privilege, Fail Securely und Complete Mediation sowie den Fehlern, die sie in der Praxis untergraben.

Weiterlesen
Zum Beitrag: Secure by Design 101: Sicherheit in Wachstum und Wettbewerbsvorteile verwandeln

Secure by Design

Secure by Design 101

Secure by Design 101: Sicherheit in Wachstum und Wettbewerbsvorteile verwandeln

Sicherheit wird in vielen Organisationen noch nicht konsequent von Beginn an berücksichtigt – dabei liegt hier der Schlüssel zu Effizienz und Vertrauen. Secure by Design ändert dieses Paradigma, indem Sicherheit von Anfang an in jede Entscheidung eingebettet wird. Entdecken Sie, wie dieser Ansatz Risikominimierung in einen echten Geschäftsvorteil verwandelt.

Weiterlesen
Zum Beitrag: Secure by Design 101: Warum es sich trotzdem lohnt, jetzt anzufangen
An astronaut mechanic works on his spaceship to add security features such as locks and firewalls.

Secure by Design

Secure by Design 101

Secure by Design 101: Warum es sich trotzdem lohnt, jetzt anzufangen

Die meisten Projekte starten nicht mit Secure by Design. Das bedeutet nicht, dass es zu spät ist. Praktische Schritte für Project Manager, um Security in bestehenden Systemen zu verbessern.

Weiterlesen

CLOUDYRION verbindet IT-Sicherheit mit einer Sicherheitskultur, die Ihre Projekte stärkt. Wir entwickeln gemeinsam sichere Architekturen, Prozesse und Lösungen, die Ihre IT-Strategie und Unternehmenskultur optimal unterstützen