Skip to content

Sicherheitsarchitektur

TCM365 implementiert ein Defense-in-Depth-Sicherheitsmodell mit mehreren unabhängigen Schichten. Dieses Dokument behandelt Authentifizierung, Autorisierung, Tenant-Isolation, Verschlüsselung und operationale Sicherheitskontrollen.


Sicherheitsschichten im Ueberblick

graph TB
    subgraph NET["Netzwerkschicht"]
        N1["Helmet (Security Headers)"]
        N2["CORS-Konfiguration"]
        N3["Rate Limiting"]
    end

    subgraph AUTH["Authentifizierungsschicht"]
        A1["JWT Bearer Tokens"]
        A2["Local Strategy (bcrypt)"]
        A3["Azure AD Integration"]
        A4["Token Blacklisting"]
    end

    subgraph AUTHZ["Autorisierungsschicht"]
        Z1["Rollenhierarchie (4 Stufen)"]
        Z2["Permission System (resource:action)"]
        Z3["Route Guards"]
    end

    subgraph TENANT["Tenant-Isolationsschicht"]
        T1["Schicht 1: TenantAccessService (IDOR-Prävention)"]
        T2["Schicht 2: PostgreSQL Row-Level Security"]
        T3["Schicht 3: Schema-per-Tenant (physische Trennung)"]
    end

    subgraph DATA["Datenschutzschicht"]
        D1["Per-Tenant-Verschlüsselung (AES-256 / Azure Key Vault)"]
        D2["Credential-Verschlüsselung im Ruhezustand"]
        D3["UUID-Primärschlüssel (keine Enumeration)"]
    end

    subgraph AUDIT["Auditschicht"]
        AU1["Unveränderliche Audit Logs"]
        AU2["Request Correlation (X-Request-ID)"]
        AU3["AuditLog Interceptor"]
        AU4["Strukturiertes Logging"]
    end

    NET --> AUTH --> AUTHZ --> TENANT --> DATA --> AUDIT

Authentifizierung

JWT-Authentifizierung

TCM365 verwendet JWT (JSON Web Tokens) für die API-Authentifizierung. Tokens werden bei erfolgreicher Anmeldung ausgestellt und müssen im Authorization-Header nachfolgender Anfragen enthalten sein.

Token-Lebenszyklus:

Schritt Beschreibung
Login POST /api/v1/auth/login mit E-Mail + Passwort
Token-Ausstellung Server gibt signiertes JWT mit User ID, Rolle, Org ID zurück
Request Auth Client sendet Authorization: Bearer <token>
Validierung JwtStrategy extrahiert und validiert das Token
Refresh POST /api/v1/auth/refresh vor Token-Ablauf
Logout Token wird zur Blacklist hinzugefügt (Redis oder In-Memory)

Token Payload:

{
  "sub": "user-uuid",
  "email": "user@example.com",
  "role": "TENANT_ADMIN",
  "orgId": "org-uuid",
  "permissions": ["tenants:read", "tenants:write", "snapshots:read"],
  "iat": 1705312200,
  "exp": 1705314000
}

Token-Konfiguration:

Einstellung Standard Umgebungsvariable
Signaturalgorithmus HS256 --
Ablaufzeit 30 Minuten APP_ACCESS_TOKEN_EXPIRE_MINUTES
Secret Key -- APP_SECRET_KEY (min. 32 Zeichen)

Lokale Authentifizierung (Passwort)

Für lokale Benutzerkonten (nicht Azure AD SSO) werden Passwörter mit bcrypt gehasht:

// Passwort-Hashing (bei Registrierung/Änderung)
const hash = await bcrypt.hash(plaintext, 12); // 12 Salt Rounds

// Passwort-Verifizierung (bei Login)
const isValid = await bcrypt.compare(plaintext, hash);

Passwortsicherheit

  • Bcrypt mit 12 Salt Rounds (adaptiver Kostenfaktor)
  • Passwörter werden niemals im Klartext gespeichert
  • Passwortänderung invalidiert bestehende Sessions

Azure AD-Authentifizierung

TCM365 unterstützt Azure AD SSO über MSAL (Microsoft Authentication Library):

const msalConfig = {
  auth: {
    clientId: configService.get('AZURE_AD_CLIENT_ID'),
    authority: configService.get('AZURE_AD_AUTHORITY'),
    clientSecret: configService.get('AZURE_AD_CLIENT_SECRET'),
  },
};

Autorisierung

Rollenhierarchie

TCM365 implementiert ein hierarchisches Rollensystem, bei dem höhere Rollen alle Berechtigungen niedrigerer Rollen erben:

Rolle Stufe Erbt von Beschreibung
SUPER_ADMIN 100 Alle Rollen Vollständiger Plattformzugriff, Benutzerverwaltung, Systemeinstellungen
TENANT_ADMIN 75 WORKFLOW_MANAGER Tenant-Konfiguration, Verbindungsverwaltung
WORKFLOW_MANAGER 50 VIEWER Workflow-Erstellung, Snapshot Capture, Rollback-Ausführung
VIEWER 25 -- Lesezugriff auf Dashboards, Snapshots, Reports

Rollendurchsetzung in Controllern:

@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.TENANT_ADMIN) // Minimale Rolle: TENANT_ADMIN oder höher
@Post()
async createTenant(@Body() dto: CreateTenantDto) {
  // Nur TENANT_ADMIN und SUPER_ADMIN können darauf zugreifen
}

Permission System

Über Rollen hinaus unterstützt TCM365 feingranulare Berechtigungen im resource:action-Format:

Ressourcen:

Ressource Beschreibung
users Benutzerverwaltung
tenants Tenant-Verbindungen
workflows Workflow-Operationen
snapshots Snapshot-Operationen
audit Audit Log-Zugriff
settings Systemeinstellungen

Aktionen:

Aktion Beschreibung
read Ressourcen anzeigen/auflisten
write Ressourcen erstellen/aktualisieren
delete Ressourcen entfernen
connect Verbindungen herstellen (Tenants)
execute Operationen ausführen (Workflows, Snapshots)
rollback Rollback-Operationen ausführen

Berechtigungsdurchsetzung:

@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('snapshots:execute')
@Post('capture')
async captureSnapshot(@Body() dto: CaptureSnapshotDto) {
  // Nur Benutzer mit snapshots:execute-Berechtigung können zugreifen
}

Guards-Stack

Mehrere Guards können auf einem einzelnen Endpoint kombiniert werden:

@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
@Roles(UserRole.WORKFLOW_MANAGER)
@Permissions('workflows:execute')
@Post(':id/execute')
async executeWorkflow(@Param('id') id: string) {
  // Erfordert: gueltiges JWT + WORKFLOW_MANAGER-Rolle + workflows:execute-Berechtigung
}

Drei-Schichten Tenant-Isolation

TCM365 erzwingt Tenant-Datenisolation auf drei unabhängigen Schichten. Wenn eine einzelne Schicht kompromittiert wird, schützen die verbleibenden Schichten weiterhin die Tenant-Daten.

Schicht 1: TenantAccessService (Anwendungsschicht)

Der TenantAccessService verhindert Insecure Direct Object Reference (IDOR)-Angriffe, indem er validiert, dass der authentifizierte Benutzer Zugriff auf die Organisation der angeforderten Ressource hat.

@Injectable()
export class TenantAccessService {
  async validateAccess(userId: string, resourceOrgId: string): Promise<void> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      relations: ['organization'],
    });

    if (user.organization.id !== resourceOrgId) {
      this.logger.warn('TENANT_ACCESS_DENIED', {
        userId,
        attemptedOrgId: resourceOrgId,
        userOrgId: user.organization.id,
      });
      throw new ForbiddenException('Access denied to this resource');
    }
  }
}

Verwendung in Controllern:

@Get(':id')
async getSnapshot(@Param('id') id: string, @CurrentUser() user: User) {
  const snapshot = await this.snapshotRepo.findOne({ where: { id } });
  await this.tenantAccess.validateAccess(user.id, snapshot.orgId);
  return snapshot;
}

IDOR-Prävention

Jeder Controller, der auf tenant-scoped Daten zugreift, muss TenantAccessService.validateAccess() aufrufen, bevor Daten zurueckgegeben werden. Dies verhindert, dass Benutzer auf Daten anderer Organisationen zugreifen, indem sie UUIDs in API-Anfragen manipulieren.

Schicht 2: Row-Level Security (Datenbankschicht)

PostgreSQL Row-Level Security filtert Abfrageergebnisse automatisch, sodass nur Zeilen der aktuellen Organisation enthalten sind.

-- RLS-Policy (angewendet auf 18 tenant-scoped Tabellen)
CREATE POLICY tenant_isolation ON snapshots
  USING (org_id = current_setting('app.current_org_id')::uuid);

-- Anwendung setzt die Session-Variable pro Anfrage
SET app.current_org_id = 'org-uuid-hier';

Zwei-Rollen-Architektur:

Rolle Verhalten Anwendungsfall
tcm_admin BYPASSRLS Migrationen, Schema-Verwaltung, Admin-Skripte
tcm_app Respektiert RLS Alle Anwendungsabfragen

Selbst wenn die Anwendungsschicht die Filterung nach Organisation nicht durchfuehrt, stellt RLS sicher, dass die Datenbank niemals Cross-Tenant-Daten zurueckgibt.

Schicht 3: Schema-per-Tenant (Physische Trennung)

Data-Plane-Tabellen werden physisch in Per-Tenant PostgreSQL-Schemas getrennt:

public Schema           -- Control-Plane (users, groups, system_settings)
tenant_<org-uuid>       -- Data-Plane pro Organisation (snapshots, drifts etc.)

Jedes Tenant-Schema enthält 23 Tabellen mit operativen Daten. Der TenantSchemaManager-Service leitet Abfragen basierend auf der Organisation des authentifizierten Benutzers an das korrekte Schema weiter.

Isolationsgarantie: Selbst bei fehlerhaft konfigurierten RLS-Policies werden Data-Plane-Abfragen gegen das korrekte Tenant-Schema ausgeführt, wodurch Cross-Tenant-Datenlecks für Data-Plane-Operationen architektonisch ausgeschlossen sind.


Per-Tenant-Verschlüsselung

Sensible Tenant-Credentials (Azure AD Secrets, Zscaler API Keys, Atlassian OAuth Tokens, Passwörter) werden im Ruhezustand über ein erweiterbares SecretBackend-Interface verschlüsselt.

SecretBackend Interface

interface SecretBackend {
  encrypt(plaintext: string, tenantId: string): Promise<string>;
  decrypt(ciphertext: string, tenantId: string): Promise<string>;
}

Implementierungen

Backend Umgebung Verschluesselungsmethode
LocalAES Entwicklung AES-256-GCM mit per-tenant abgeleiteten Schluesseln
AzureKeyVault Produktion Azure Key Vault Managed Keys

LocalAES Details:

  • Algorithmus: AES-256-GCM
  • Schluesselableitung: HKDF aus Master Key + Tenant ID (eindeutiger Schlüssel pro Tenant)
  • IV: Zufaellige 12 Bytes pro Verschluesselungsvorgang
  • Ausgabeformat: iv:ciphertext:authTag (Base64-kodiert)

Verschluesselte Spalten in VendorTenant:

Spalte Entity Inhalt
zia_api_key ZscalerVendorTenant Zscaler ZIA API Key
zia_admin_password ZscalerVendorTenant Zscaler ZIA Admin-Passwort
zpa_client_secret ZscalerVendorTenant Zscaler ZPA Client Secret
write_credentials M365VendorTenant M365 Schreiboperations-Credentials (JSONB)
oauth_access_token AtlassianVendorTenant Atlassian OAuth 2LO Access Token
oauth_refresh_token AtlassianVendorTenant Atlassian OAuth 2LO Refresh Token

Capture Lock Service

Der Capture Lock Service verhindert gleichzeitige Snapshot Captures gegen denselben Tenant, was zu API Rate Limit-Verletzungen oder inkonsistenten Daten führen koennte.

@Injectable()
export class CaptureLockService {
  // Exklusiven Lock für den Tenant erwerben
  async acquire(tenantId: string, ttlMs: number): Promise<boolean>;

  // Lock nach Abschluss des Captures freigeben
  async release(tenantId: string): Promise<void>;
}
Backend Implementierung Geeignet für
Redis SET key NX EX ttl (atomar) Multi-Instance-Bereitstellungen
In-Memory Map<string, boolean> mit TTL Single-Instance-Bereitstellungen

Der Lock wird nach Ablauf der TTL automatisch freigegeben (auch wenn der Capture-Prozess abstuerzt), wodurch permanente Lock-Zustaende verhindert werden.


HTTP-Sicherheit

Helmet

Helmet setzt sichere HTTP-Header auf alle Antworten:

Header Wert Zweck
X-Content-Type-Options nosniff MIME-Type-Sniffing verhindern
X-Frame-Options SAMEORIGIN Clickjacking verhindern
X-XSS-Protection 1; mode=block XSS-Filter
Strict-Transport-Security max-age=31536000 HTTPS erzwingen
Content-Security-Policy Pro Deployment konfiguriert Ressourcenladen beschränken

CORS

Cross-Origin Resource Sharing wird über die CORS_ORIGINS-Umgebungsvariable konfiguriert:

app.enableCors({
  origin: configService.get('CORS_ORIGINS').split(','),
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
});

Rate Limiting

Rate Limiting schuetzt gegen Brute-Force-Angriffe und API-Missbrauch:

Bereich Limit Fenster Backend
Global 100 Anfragen 1 Minute Redis / In-Memory
Auth (Login) 5 Versuche 5 Minuten Redis / In-Memory
Snapshot Capture 1 pro Tenant Lock TTL Capture Lock Service

Audit Trail

AuditLog Interceptor

Der AuditLogInterceptor erfasst automatisch alle zustandsaendernden Operationen:

@Injectable()
export class AuditLogInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();

    if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
      return next.handle().pipe(
        tap(() => {
          this.auditLogService.create({
            userId: request.user?.id,
            action: `${request.method} ${request.path}`,
            resourceType: this.extractResourceType(request.path),
            resourceId: request.params?.id,
            requestId: request.headers['x-request-id'],
            metadata: { body: this.sanitize(request.body) },
          });
        }),
      );
    }
    return next.handle();
  }
}

Audit Log-Eintrag

Feld Beschreibung
id Eindeutige Audit-Eintrags-ID (UUID)
user_id Wer die Aktion durchgeführt hat
action Welche Aktion durchgeführt wurde
resource_type Typ der betroffenen Ressource
resource_id ID der betroffenen Ressource
request_id Korrelations-ID für Request Tracing
metadata Zusaetzlicher Kontext (bereinigter Request Body)
created_at Wann die Aktion stattfand

Unveränderliche Audit Logs

Audit Log-Einträge sind append-only. Es gibt keine Update- oder Delete-Funktionalität für Audit-Einträge, was einen manipulationssicheren Audit Trail gewaehrleistet.

Request Correlation

Jede Anfrage erhält eine eindeutige X-Request-ID (automatisch generiert oder vom Client übergeben). Diese ID wird:

  • In allen Log-Einträgen der Anfrage aufgenommen
  • In den Response-Headern zurueckgegeben
  • In Audit Log-Einträgen gespeichert
  • Zur Korrelation von Aktionen über Services hinweg verwendet

Sicherheits-Best-Practices für Entwickler

Empfohlen

  • Immer TenantAccessService.validateAccess() in Controllern verwenden, die tenant-scoped Daten behandeln
  • Konfiguration über ConfigService zugreifen, niemals process.env
  • class-validator-Dekoratoren auf allen DTOs für Eingabevalidierung verwenden
  • @UseGuards(JwtAuthGuard) auf allen nicht-öffentlichen Endpoints anwenden
  • UUID-Parameter (niemals sequentielle Integers) in URLs verwenden
  • Sicherheitsereignisse mit strukturiertem Logging inkl. Request IDs protokollieren
  • Alle Tenant-Credentials vor Speicherung verschluesseln

Vermeiden

  • Niemals RLS umgehen, indem die tcm_admin-Rolle im Anwendungscode verwendet wird
  • Niemals Passwörter im Klartext speichern oder reversible Verschlüsselung für Passwörter verwenden
  • Niemals interne Fehlerdetails in API-Antworten exponieren (Produktion)
  • Niemals client-bereitgestellten Organisations-IDs ohne Validierung vertrauen
  • Niemals Helmet oder CORS in der Produktion deaktivieren
  • Niemals Secrets (.env, API Keys, Zertifikate) in die Versionskontrolle committen
  • Niemals synchronize: true in der TypeORM-Konfiguration verwenden

Sicherheitstests

TCM365 enthält 70 Cross-Tenant-Isolationstests, die verifizieren:

  • Benutzer können nicht auf Snapshots anderer Organisationen zugreifen
  • Benutzer können nicht auf Tenants anderer Organisationen zugreifen
  • Benutzer können nicht auf Drifts anderer Organisationen zugreifen
  • Benutzer können nicht auf Workflows anderer Organisationen zugreifen
  • RLS filtert Abfragen korrekt nach Organisation
  • TenantAccessService blockiert IDOR-Versuche
  • Token Blacklisting verhindert die Nutzung widerrufener Tokens
cd backend-js
npm test -- --grep "cross-tenant"