Cloud-Architektur · High-Safety
Konzept · v1.0K8s Auto-Scaling · Global Load Balancer · Multi-Region · E2E AES-256
Region-Status · Live Preview
Kein eigener Cluster
24ms
Audio-Latenz
< 1s
Failover RTO
AES-256
Verschlüsselung
99.99%
Uptime SLA
# ─── ARCHITEKTUR-DIAGRAMM · GESAMTÜBERSICHT ─────────────────────
INTERNET
┌─────────────────────────────────────┐
│ Nutzer-Device (iOS / Android / PWA) │
│ ┌─────────────────────────────────┐ │
│ │ AES-256-GCM verschlüsselter │ │
│ │ Audio-Stream (WebRTC + TURN) │ │
│ └──────────────┬──────────────────┘ │
└─────────────────┼───────────────────-┘
│ WSS / WebRTC (TLS 1.3)
▼
┌─────────────────────────────────────────────────┐
│ AWS GLOBAL ACCELERATOR │
│ (Anycast IP — < 15ms zum Edge) │
└────────────────┬──────────────────┬────────────┘
│ │
┌───────────▼──────┐ ┌────────▼──────────┐
│ PoP Frankfurt │ │ PoP Amsterdam │
│ (Primary Edge) │ │ (Overflow Edge) │
└───────────┬──────┘ └────────┬──────────┘
│ AWS Backbone │
▼ ▼
╔═══════════════════════════════════════════════╗
║ REGION PRIMARY: eu-central-1 ║
║ AWS FRANKFURT ║
║ ║
║ ┌─────────────────────────────────────────┐ ║
║ │ Application Load Balancer (ALB) │ ║
║ │ (SSL Termination, Health Checks) │ ║
║ └─────────────┬───────────┬───────────────┘ ║
║ │ │ ║
║ ┌────────────▼──┐ ┌────▼─────────────┐ ║
║ │ K8s Cluster │ │ TURN-Server Pool │ ║
║ │ ───────────── │ │ ─────────────────│ ║
║ │ Signal-Server │ │ coturn (WebRTC) │ ║
║ │ Pods: 3-50 │ │ Pods: 5-20 │ ║
║ │ (HPA + KEDA) │ │ │ ║
║ └────────────┬──┘ └────────────────── ┘ ║
║ │ ║
║ ┌────────────▼──────────────────────────┐ ║
║ │ Redis Cluster (Primary) │ ║
║ │ Session State · GPS · Audio Meta │ ║
║ └────────────────────────────────────────┘ ║
║ │ ║
║ ┌────────────▼──────────────────────────┐ ║
║ │ PostgreSQL (RDS Multi-AZ) │ ║
║ │ Alarms · Users · Audit-Logs │ ║
║ └────────────────────────────────────────┘ ║
╚══════════════════╦════════════════════════════╝
║ Async Replication (< 50ms)
╔══════════════════╩════════════════════════════╗
║ REGION BACKUP: eu-west-1 ║
║ AWS IRELAND ║
║ (Identische Infrastruktur) ║
║ STANDBY — Failover < 1 Sekunde ║
╚════════════════════════════════════════════════╝
LATENZ-BUDGET (Europa):
Anycast Edge: 5-15ms
ALB Frankfurt: 5-10ms
Signal-Pod: 2-5ms
WebRTC TURN: 3-8ms
Guardian-Device: 5-15ms
───────────────────────
GESAMT: 20-53ms ✅ Ziel: < 100ms3
Min. Pods (HA)
50
Max. Pods (Peak)
60%
CPU Scale-Trigger
HPA: Skaliert bei CPU >60% oder >3000 WebSocket-Verbindungen pro Pod.
KEDA Cron: Freitag 22:00 Uhr — 20 Pods pre-gestartet, bevor Wochenend-Last kommt.
PodAntiAffinity: Pods verteilt auf verschiedene Nodes — ein Node-Ausfall = kein Ausfall.
# ─── MODUL 1: KUBERNETES AUTO-SCALING · SIGNAL-SERVER ──────────
# HPA (Horizontal Pod Autoscaler) + KEDA für WebSocket-Metriken
# ── signal-server-deployment.yaml ───────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: secsi-signal-server
namespace: secsi-prod
spec:
replicas: 3 # Minimum: 3 Pods (HA)
selector:
matchLabels:
app: signal-server
template:
spec:
containers:
- name: signal-server
image: secsi/signal-server:latest
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
env:
- name: REGION
valueFrom:
fieldRef:
fieldPath: metadata.annotations['topology.kubernetes.io/region']
- name: MAX_WEBSOCKET_CONNS
value: "5000" # Pro Pod max. 5000 WS-Verbindungen
affinity:
podAntiAffinity: # Pods auf verschiedene Nodes verteilen
requiredDuringScheduling:
- labelSelector:
matchLabels: {app: signal-server}
topologyKey: kubernetes.io/hostname
---
# ── HPA: CPU + Custom WebSocket Metric ──────────────────────────
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: signal-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: secsi-signal-server
minReplicas: 3
maxReplicas: 50 # Wochenende Nacht: bis zu 50 Pods
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # Scale-out bei >60% CPU
- type: External
external:
metric:
name: websocket_active_connections
target:
type: AverageValue
averageValue: "3000" # Scale-out bei >3000 WS/Pod
---
# ── KEDA ScaledObject: Cron-basiertes Pre-Scaling ────────────────
# Wochenende Nacht vorausschauend skalieren (bevor Last kommt)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: signal-server-weekend-scaler
spec:
scaleTargetRef:
name: secsi-signal-server
triggers:
- type: cron
metadata:
timezone: Europe/Berlin
start: "0 22 * * 5" # Freitag 22:00 Uhr
end: "0 5 * * 1" # Montag 05:00 Uhr
desiredReplicas: "20" # Wochenende: 20 Pods bereit
- type: cron
metadata:
start: "0 5 * * 1"
end: "0 22 * * 5"
desiredReplicas: "3" # Wochentag: 3 Pods# ─── MODUL 2: GLOBAL LOAD BALANCER · ANYCAST + EDGE ────────────
# Ziel: Audio-Stream Latenz < 100ms überall in Europa
# Technologie: AWS Global Accelerator + CloudFront + ALB
# ── Global Accelerator (Anycast IP) ─────────────────────────────
# Zwei statische Anycast-IPs — DNS löst zur nächsten Edge auf
AWS::GlobalAccelerator::Accelerator:
Name: secsi-global
IpAddressType: IPV4
Enabled: true
AWS::GlobalAccelerator::Listener:
Protocol: TCP
PortRanges:
- FromPort: 443 # HTTPS / WSS
ToPort: 443
- FromPort: 3478 # STUN/TURN für WebRTC Audio
ToPort: 3479
AWS::GlobalAccelerator::EndpointGroup:
# Region 1: Frankfurt (Primary)
- EndpointGroupRegion: eu-central-1
TrafficDialPercentage: 100
HealthCheckProtocol: TCP
HealthCheckPort: 443
ThresholdCount: 2 # 2 Fehler → Failover
Endpoints:
- EndpointId: arn:aws:elasticloadbalancing:eu-central-1:...
Weight: 100
# Region 2: Ireland (Backup / Overflow)
- EndpointGroupRegion: eu-west-1
TrafficDialPercentage: 0 # Normalfall: kein Traffic
ThresholdCount: 2
Endpoints:
- EndpointId: arn:aws:elasticloadbalancing:eu-west-1:...
Weight: 100
# ── WebRTC TURN-Server: Geo-Routing ─────────────────────────────
# ICE-Kandidaten liefern immer den nächsten TURN-Server
TURN_CONFIG:
servers:
- url: "turn:fra.turn.sec-si.de:3478" # Frankfurt
region: eu-central-1
priority: 1
- url: "turn:dub.turn.sec-si.de:3478" # Dublin
region: eu-west-1
priority: 2
- url: "turn:ams.turn.sec-si.de:3478" # Amsterdam
region: eu-west-3
priority: 3
# NGINX Stream-Proxy für UDP/TCP Load Balancing
nginx_stream:
upstream turn_backends:
least_conn; # Least-Connection für Streams
server turn1.internal:3478 weight=10;
server turn2.internal:3478 weight=10;
server turn3.internal:3478 weight=10;
# ── Latenz-Budget: User → Audio beim Partner ────────────────────
#
# User-Device ──(Anycast)──► Edge PoP: 5-15ms
# Edge PoP ────(AWS Backbone)─► ALB Frankfurt: 5-10ms
# ALB ────────────────────────► Signal-Pod: 2-5ms
# Signal-Pod ──(WebRTC ICE)───► TURN: 3-8ms
# TURN ─────────────────────── Guardian: 5-15ms
# ─────────
# GESAMT: 20-53ms ✅ < 100msGlobal Accelerator statt DNS: Failover in <1s — kein DNS-TTL-Warten.
Redis Cross-Region: Session-State async repliziert (<50ms Lag) — Client reconnectet nahtlos.
Health-Check: 1 Fehler genügt für Failover (nicht erst nach 3) — maximale Reaktionsgeschwindigkeit.
# ─── MODUL 3: MULTI-REGION REDUNDANZ · 1-SEKUNDEN-FAILOVER ────
# Primär: AWS Frankfurt (eu-central-1)
# Backup: AWS Ireland (eu-west-1)
# RTO: < 1 Sekunde · RPO: 0 (kein Datenverlust)
# ── Route 53 Health Checks + Failover Policy ─────────────────────
aws route53 create-health-check --caller-reference "secsi-fra-$(date +%s)" --health-check-config '{
"Type": "HTTPS",
"FullyQualifiedDomainName": "api-fra.sec-si.de",
"Port": 443,
"RequestInterval": 10, # Check alle 10s
"FailureThreshold": 1, # 1 Fehler = sofort Failover
"EnableSNI": true,
"Regions": ["eu-central-1","eu-west-1","ap-southeast-1"]
}'
# ── DNS Failover Records ─────────────────────────────────────────
# Primary → Frankfurt
aws route53 change-resource-record-sets --change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "api.sec-si.de",
"Type": "A",
"Failover": "PRIMARY",
"SetIdentifier": "Frankfurt",
"HealthCheckId": "HEALTH-CHECK-FRA-ID",
"AliasTarget": {
"HostedZoneId": "Z215JYRZR1TBD5",
"DNSName": "secsi-alb-fra.eu-central-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
}]
}'
# Secondary → Ireland (automatisch aktiv wenn Primary down)
# (identische Config mit Failover: "SECONDARY" + Ireland ALB)
# ── Active Session Migration bei Failover ────────────────────────
# Problem: WebSocket-Verbindungen brechen bei Failover ab
# Lösung: Session-State in Redis Cluster (Cross-Region Replication)
Redis:
Primary: redis-fra.sec-si.internal # Frankfurt
Replica: redis-dub.sec-si.internal # Dublin (async Replikation)
ReplicationLag: < 50ms
SESSION_MIGRATION_PROTOCOL:
1. Health-Check schlägt fehl → Route 53 schaltet um (≈ 5-10s DNS TTL!)
→ LÖSUNG: AWS Global Accelerator (kein DNS) → < 1s Umschaltung
2. Client-SDK erkennt WS-Disconnect:
ws.onclose = () => {
reconnect({ servers: FALLBACK_ENDPOINTS, maxRetries: 20 })
// Session-Token im Client gespeichert → nahtlose Wiederverbindung
}
3. Ireland-Server lädt Session aus Redis-Replica:
session = await redis.get("session:" + token)
// Alle Alarm-Daten, GPS, Audio-Buffer sofort verfügbar
FAILOVER-ZEITLINIE:
T+0ms: Frankfurt-ALB antwortet nicht
T+200ms: Global Accelerator erkennt Ausfall (2x Health-Check)
T+300ms: Traffic-Switch zu Ireland-Endpoint
T+500ms: Client-SDK reconnect, Session aus Redis geladen
T+800ms: Audio-Stream aktiv auf Ireland-TURN-Server
GESAMT: < 1 Sekunde ✅Zero-Knowledge Server
ECDH P-384 Key-Exchange — Server sieht nur verschlüsselte Pakete. Selbst wir als Betreiber können nicht mithören.
Emergency Protocol
2-of-3 Shamir Secret Sharing — Entschlüsselung nur mit richterlichem Beschluss + 2 Key-Custodians.
# ─── MODUL 4: END-TO-END VERSCHLÜSSELUNG · AES-256-GCM ─────────
# Ziel: Selbst Server-Operator kann Audio NICHT mithören
# Ausnahme: Emergency Protocol (richterliche Anordnung oder aktiver SOS)
# ── Key-Exchange: ECDH P-384 (Perfect Forward Secrecy) ───────────
CLIENT_SETUP (beim Session-Start):
# 1. Ephemeral Keypair generieren (nie persistent gespeichert!)
clientPrivKey, clientPubKey = crypto.generateKeyPair("P-384")
# 2. Öffentlichen Key an Guardian senden (via Signal-Server)
Signal.send({
type: "KEY_EXCHANGE_INIT",
public_key: clientPubKey.export("base64"),
session_id: SESSION.id
})
GUARDIAN_RESPONSE:
guardianPrivKey, guardianPubKey = crypto.generateKeyPair("P-384")
# 3. ECDH Shared Secret ableiten
sharedSecret = ECDH.derive(guardianPrivKey, clientPubKey)
# 4. AES-256-GCM Key aus SharedSecret ableiten (HKDF)
sessionKey = HKDF(
inputKeyMaterial: sharedSecret,
salt: SESSION.id + now(),
info: "secsi-audio-v1",
length: 32 # 256 Bit
)
Signal.send({
type: "KEY_EXCHANGE_COMPLETE",
public_key: guardianPubKey.export("base64")
})
CLIENT_FINALIZE:
sessionKey = HKDF(ECDH.derive(clientPrivKey, guardianPubKey), ...)
# Beide Seiten haben denselben sessionKey — Server hat ihn NIE gesehen!
# ── Audio Encryption: AES-256-GCM ────────────────────────────────
AUDIO_ENCRYPTION (pro Audio-Chunk, alle ~20ms):
IV = crypto.randomBytes(12) # 96-Bit IV — NIEMALS wiederverwenden
encryptedChunk = AES_256_GCM.encrypt(
key: sessionKey,
iv: IV,
plaintext: audioChunk,
aad: JSON.stringify({ # Authenticated Additional Data
session_id: SESSION.id,
seq: chunkSequenceNumber, # Replay-Schutz
ts: now()
})
)
# Paket = IV (12B) + AuthTag (16B) + EncryptedAudio (variabel)
packet = IV + encryptedChunk.authTag + encryptedChunk.ciphertext
WebRTC.send(packet)
AUDIO_DECRYPTION (Guardian-Seite):
IV = packet.slice(0, 12)
authTag = packet.slice(12, 28)
ciphertext = packet.slice(28)
plainAudio = AES_256_GCM.decrypt(
key: sessionKey,
iv: IV,
authTag: authTag,
aad: ...,
input: ciphertext
)
# TAMPERING: Wenn authTag falsch → Exception → Paket verworfen
# ── Emergency Protocol: Temporäre Entschlüsselung ────────────────
# Nur bei aktivem SOS + richterlicher Anordnung (§100a StPO)
EMERGENCY_DECRYPT_PROTOCOL:
# Session-Key wird mit 2-of-3 Shamir Secret Sharing gesichert
# Drei Key-Custodians: CEO + CTO + Externer Datenschutzbeauftragter
# Mindestens 2 müssen zustimmen
keyShares = ShamirSecretSharing.split(
secret: sessionKey,
shares: 3,
threshold: 2
)
# Shares gespeichert bei:
share_1 → HSM (Hardware Security Module) Frankfurt
share_2 → HSM Ireland
share_3 → Externer Treuhänder (Notar / Datenschützer)
# Rekonstruktion nur mit gerichtlichem Beschluss + 2 Custodians
sessionKey = ShamirSecretSharing.combine([share_1, share_2])/emergency/sync
/emergency/session
/emergency/audio/signal
/ws/emergency/{id}
# ─── EMERGENCY_SYNC API-SPEZIFIKATION ──────────────────────────
# Alle Endpunkte: TLS 1.3 · Bearer JWT · Rate-Limit 1000 req/min
# ═══════════════════════════════════════════════════════════════
# POST /api/v2/emergency/sync
# Haupt-Sync-Endpunkt: Session-State zwischen Regionen
# ═══════════════════════════════════════════════════════════════
REQUEST:
Authorization: Bearer {session_jwt}
Content-Type: application/json
{
"session_id": "ses_abc123",
"alert_id": "alr_xyz789",
"event_type": "sos_triggered" | "gps_update" | "audio_chunk" | "heartbeat",
"payload": {
"gps": { "lat": 48.137, "lng": 11.575, "accuracy_m": 8 },
"audio_meta": { "codec": "opus", "bitrate": 24000, "encrypted": true },
"escalation": "intervention",
"timestamp": "2026-03-11T22:14:33.123Z"
},
"integrity_hmac": "sha256=abc123..." # HMAC über payload+ts
}
RESPONSE 200:
{
"synced": true,
"region": "eu-central-1",
"failover_ready": true, # Backup-Region hat State
"latency_ms": 18,
"next_heartbeat": 5000 # ms bis nächster Heartbeat erwartet
}
RESPONSE 503 (Region down → Failover):
Retry-After: 0 # Sofort zu Backup
X-Failover-Endpoint: https://api-dub.sec-si.de/api/v2/emergency/sync
# ═══════════════════════════════════════════════════════════════
# GET /api/v2/emergency/session/{session_id}
# Session-State abrufen (für Failover-Reconnect)
# ═══════════════════════════════════════════════════════════════
RESPONSE 200:
{
"session_id": "ses_abc123",
"status": "active",
"alert_id": "alr_xyz789",
"user_id": "usr_111",
"guardians": ["guardian@sec-si.de"],
"last_gps": { "lat": 48.137, "lng": 11.575 },
"escalation": "intervention",
"encryption": {
"algorithm": "AES-256-GCM",
"pfs": true,
"key_id": "key_2026_03_11" # Nur Key-Referenz, nie Key selbst!
},
"region_primary": "eu-central-1",
"region_backup": "eu-west-1",
"created_at": "2026-03-11T22:00:00Z",
"last_sync_at": "2026-03-11T22:14:33Z"
}
# ═══════════════════════════════════════════════════════════════
# POST /api/v2/emergency/audio/signal
# WebRTC Signaling für Audio-Kanal (SDP Exchange)
# ═══════════════════════════════════════════════════════════════
REQUEST:
{
"session_id": "ses_abc123",
"signal_type": "offer" | "answer" | "ice_candidate",
"sdp": "v=0
o=- ...", # SDP für offer/answer
"candidate": { "candidate": "...", "sdpMid": "audio" }
}
RESPONSE 200:
{ "forwarded": true, "to_peer": "guardian@sec-si.de" }
# ═══════════════════════════════════════════════════════════════
# POST /api/v2/emergency/escalate
# Eskalation: Pre-Alarm → Validation → Intervention → Police
# ═══════════════════════════════════════════════════════════════
REQUEST:
{
"session_id": "ses_abc123",
"from_level": "validation",
"to_level": "intervention",
"trigger": "no_checkin" | "audio_score" | "guardian_manual" | "duress_pin",
"auto_police": false # true = 110 automatisch auslösen
}
RESPONSE 200:
{
"new_level": "intervention",
"notified": ["guardian@sec-si.de", "leitstelle@sec-si.de"],
"police_dispatched": false,
"eta_police_s": null
}
# ═══════════════════════════════════════════════════════════════
# WebSocket: wss://api.sec-si.de/ws/emergency/{session_id}
# Bi-direktionaler Echtzeit-Kanal für alle Events
# ═══════════════════════════════════════════════════════════════
EVENTS CLIENT → SERVER:
{ "type": "GPS_UPDATE", "gps": {...} }
{ "type": "HEARTBEAT", "ts": "..." }
{ "type": "AUDIO_READY", "turn_candidates": [...] }
{ "type": "ALARM_CANCEL", "pin_type": "normal" | "duress" }
EVENTS SERVER → CLIENT:
{ "type": "GUARDIAN_JOINED", "guardian": "..." }
{ "type": "ESCALATION_CHANGE", "level": "intervention" }
{ "type": "ALIBI_CALL", "payload": {...} }
{ "type": "FAILOVER", "endpoint": "wss://api-dub.sec-si.de/..." }
{ "type": "SESSION_CLOSED", "reason": "resolved" }Compliance & Zertifizierungen (Ziel)
ISO 27001
ISMS — Informationssicherheits-Managementsystem
DSGVO Art. 32
Technisch-organisatorische Maßnahmen (TOMs)
BSI IT-Grundschutz
Kritische Infrastruktur — KRITIS-relevant
SOC 2 Type II
Audit: Security, Availability, Confidentiality
TLS 1.3 only
Kein TLS 1.2 — nur aktuelle Cipher-Suites
§100a StPO
Notfall-Entschlüsselung nur mit Gerichtsbeschluss