Datenschutz ist kein Feature, das man nachträglich einbaut. Es ist ein Design-Constraint, der von Anfang an mitgedacht werden muss. APIs, die personenbezogene Daten verarbeiten, müssen die Rechte der Betroffenen respektieren – nicht nur aus rechtlichen Gründen, sondern weil es das Richtige ist.
Zielbild
Nach diesem Artikel kannst du:
- Data Minimization als Design-Prinzip anwenden
- Retention Policies für verschiedene Datentypen definieren
- DSGVO-Betroffenenrechte (Auskunft, Löschung, Export) in APIs umsetzen
- PII in Logs und Non-Prod-Umgebungen schützen
Kernfragen
- Welche Daten dürfen wir speichern, wie lange, wo?
- Wie unterstützen wir DSGVO-Anfragen?
- Wie schützen wir PII außerhalb von Production?
Data Minimization
Das Prinzip der Datensparsamkeit: Nur erheben, was wirklich benötigt wird.
Entscheidungsframework
Beispiel: User-Registrierung
| Feld | Notwendig? | Entscheidung |
|---|---|---|
| Ja | Speichern (Login) | |
| Passwort-Hash | Ja | Speichern (Auth) |
| Name | Ja | Speichern (Anzeige) |
| Geburtsdatum | Nein* | Nicht erheben |
| Telefon | Optional | Nur wenn 2FA gewünscht |
| Adresse | Nein* | Erst bei Bestellung |
| IP-Adresse | Temporär | Log, 7–30 Tage Retention |
| User-Agent | Temporär | Log, 7–30 Tage Retention |
Hinweis: außer bei gesetzlicher Pflicht (z. B. Altersprüfung)
API-Design für Datensparsamkeit
// SCHLECHT: Zu viele Daten auf einmal erfragen
interface BadRegistrationRequest {
email: string;
password: string;
first_name: string;
last_name: string;
date_of_birth: string; // Nicht nötig für Registrierung
phone: string; // Nicht nötig für Registrierung
address: Address; // Nicht nötig für Registrierung
marketing_consent: boolean;
}
// GUT: Nur das Nötigste, Rest später bei Bedarf
interface RegistrationRequest {
email: string;
password: string;
display_name: string;
}
// Später, wenn Adresse benötigt wird (z.B. Checkout)
interface AddShippingAddressRequest {
order_id: string; // Kontext: Wofür wird Adresse gebraucht
address: Address;
}
Retention Policies
Daten haben ein Verfallsdatum. Retention Policies definieren, wann was gelöscht wird. Aufbewahrungsfristen hängen von Rechtsraum und Branche ab. In Deutschland gelten je nach Dokumenttyp häufig 6/8/10 Jahre (z. B. HGB § 257, AO § 147).
Retention-Matrix
| Datentyp | Retention | Begründung | Löschmethode |
|---|---|---|---|
| Account-Daten | Bis Zweckende + Inaktivitätsfrist | Vertrag/legitimes Interesse | Hard Delete |
| Buchungsbelege/Rechnungen (DE) | 8–10 Jahre | HGB § 257, AO § 147 | Archivierung/Anonymisieren |
| Handels-/Geschäftsbriefe (DE) | 6 Jahre | HGB § 257, AO § 147 | Archivierung |
| Audit Logs | 6–12 Monate (Beispiel) | Security/Compliance | Rotation |
| Access Logs | 7–30 Tage (Beispiel) | Security/Debugging | Rotation |
| Session Tokens | 24h / 30d | Security | Auto-Expire |
| Password Reset | 1 Stunde | Security | Auto-Expire |
| Gelöschte Accounts (Self-Service) | 14–30 Tage | Undo-Option | Hard Delete |
| Analytics (PII) | 0 | Privacy | Aggregieren/Anonymisieren |
| Analytics (anon) | 12–24 Monate | Business | Aggregierung/Archivierung |
Implementierung: Soft Delete mit Retention
Soft Delete mit Grace Period eignet sich für selbst initiierte Löschungen mit Undo. Bei DSGVO-Löschanfragen gilt: ohne unangemessene Verzögerung löschen, und nur dort aufbewahren, wo eine rechtliche Pflicht besteht.
// Entity mit Soft Delete
@Entity()
class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column({nullable: true})
deletedAt: Date | null;
@Column({nullable: true})
scheduledPurgeAt: Date | null; // Endgültige Löschung
}
// Lösch-Service
class UserDeletionService {
async softDelete(userId: string): Promise<void> {
const now = new Date();
const purgeDate = addDays(now, 30); // 30 Tage Grace Period (Self-Service)
const anonymizedEmail = `deleted-${crypto.randomUUID()}@example.invalid`; // z. B. randomUUID()
// Benachrichtigung vor Anonymisierung (falls wiederherstellbar)
await this.notificationService.sendDeletionConfirmation(userId);
await this.userRepository.update(userId, {
deletedAt: now,
scheduledPurgeAt: purgeDate,
// PII sofort pseudonymisieren (Unique-Constraints beachten)
email: anonymizedEmail,
displayName: 'Deleted User',
});
}
// Scheduled Job: Endgültige Löschung
@Cron('0 2 * * *') // Täglich um 02:00
async purgeExpiredUsers(): Promise<void> {
const users = await this.userRepository.find({
where: {
scheduledPurgeAt: LessThan(new Date()),
deletedAt: Not(IsNull()),
},
});
for (const user of users) {
await this.hardDelete(user.id);
}
}
private async hardDelete(userId: string): Promise<void> {
// Reihenfolge beachten wegen Foreign Keys
await this.auditLogRepository.anonymize({userId});
await this.orderRepository.anonymize({userId});
await this.userRepository.delete(userId);
this.logger.info('User purged', {userId, reason: 'retention_policy'});
}
}
Automatische Log-Retention
# Elasticsearch ILM Policy
PUT _ilm/policy/api-logs
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 }
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": { }
}
}
}
}
}
DSGVO-Betroffenenrechte
Die DSGVO definiert Rechte, die APIs aktiv unterstützen müssen. Antworten müssen grundsätzlich innerhalb eines Monats erfolgen; bei komplexen oder zahlreichen Anfragen ist eine Verlängerung um bis zu zwei weitere Monate möglich (mit Begründung).
Übersicht der Rechte
| Artikel | Recht | Beschreibung | API |
|---|---|---|---|
| Art. 15 | Auskunftsrecht | Betroffene können Auskunft über ihre Daten verlangen. | GET /users/{id}/data-export |
| Art. 17 | Recht auf Löschung (Vergessenwerden) | Betroffene können Löschung ihrer Daten verlangen. | DELETE /users/{id} oder POST /users/{id}/deletion |
| Art. 20 | Recht auf Datenübertragbarkeit | Daten in maschinenlesbarem Format exportieren. | GET /users/{id}/data-export?format=json |
| Art. 16 | Recht auf Berichtigung | Unrichtige Daten korrigieren lassen. | PATCH /users/{id} |
| Art. 21 | Widerspruchsrecht | Verarbeitung zu bestimmten Zwecken widersprechen. | PATCH /users/{id}/consents |
Hinweis: Art. 20 (Datenübertragbarkeit) gilt nur für Daten, die die betroffene Person bereitgestellt hat, die auf Einwilligung oder Vertrag beruhen und automatisiert verarbeitet werden. Abgeleitete/analytische Daten sind davon ausgenommen. Art. 15 (Auskunft) ist in der Regel weiter gefasst.
Implementierung: Datenexport (Art. 15 & 20)
// Controller
@Controller('users')
class UserDataController {
@Get(':id/data-export')
@UseGuards(AuthGuard, SameUserGuard) // Nur eigene Daten
async exportData(
@Param('id') userId: string,
@Query('format') format: 'json' | 'csv' = 'json',
@Query('scope') scope: 'access' | 'portability' = 'access'
): Promise<DataExportResponse> {
// Async Job starten (kann bei vielen Daten dauern)
const exportJob = await this.dataExportService.startExport(userId, format, scope);
return {
jobId: exportJob.id,
status: 'processing',
estimatedCompletion: exportJob.estimatedCompletion,
downloadUrl: null, // Wird nach Fertigstellung gesetzt
};
}
@Get(':id/data-export/:jobId')
@UseGuards(AuthGuard, SameUserGuard)
async getExportStatus(
@Param('id') userId: string,
@Param('jobId') jobId: string
): Promise<DataExportResponse> {
const job = await this.dataExportService.getJob(jobId, userId);
if (job.status === 'completed') {
return {
jobId: job.id,
status: 'completed',
downloadUrl: job.downloadUrl, // Signed URL, 24h gültig
expiresAt: job.downloadExpiresAt,
};
}
return {
jobId: job.id,
status: job.status,
estimatedCompletion: job.estimatedCompletion,
};
}
}
// Service
class DataExportService {
async generateExport(
userId: string,
format: string,
scope: 'access' | 'portability'
): Promise<Buffer> {
// Alle Daten des Users sammeln
const userData = {
profile: await this.userRepository.findById(userId),
orders: await this.orderRepository.findByUser(userId),
preferences: await this.preferenceRepository.findByUser(userId),
consents: await this.consentRepository.findByUser(userId),
activityLog: await this.activityRepository.findByUser(userId, {
limit: 1000, // Letzte 1000 Aktivitäten
}),
};
// Interne IDs und sensible Felder entfernen
const sanitized = this.sanitizeForExport(userData);
const payload = scope === 'portability'
? this.filterPortableData(sanitized)
: sanitized;
if (format === 'json') {
return Buffer.from(JSON.stringify(payload, null, 2));
}
return this.convertToCsv(payload);
}
private sanitizeForExport(data: any): any {
// Entfernen: interne IDs, Passwort-Hashes, etc.
const {passwordHash, internalNotes, ...safe} = data.profile;
return {
...data,
profile: safe,
exportedAt: new Date().toISOString(),
exportFormat: 'GDPR Article 15/20 Data Export',
};
}
private filterPortableData(data: any): any {
// Nur Daten, die die Person bereitgestellt hat bzw. beobachtet wurden
return {
profile: data.profile,
orders: data.orders,
preferences: data.preferences,
consents: data.consents,
activityLog: data.activityLog,
};
}
}
Implementierung: Löschung (Art. 17)
Wichtig: Aufbewahrungspflichten blockieren den Vorgang nicht komplett. Du löschst alles, was gelöscht werden darf, anonymisierst den Rest und informierst die betroffene Person transparent.
// Controller
@Controller('users')
class UserDeletionController {
@Post(':id/deletion-request')
@UseGuards(AuthGuard, SameUserGuard)
async requestDeletion(
@Param('id') userId: string,
@Body() request: DeletionRequest
): Promise<DeletionResponse> {
// Prüfen ob Löschung möglich (z.B. offene Bestellungen)
const {blockers, retentionNotice} = await this.deletionService.checkBlockers(userId);
if (blockers.length > 0) {
throw new ConflictException({
type: 'https://api.example.com/errors/deletion-blocked',
title: 'Deletion currently blocked',
detail: 'Please resolve the following issues before deletion',
blockers: blockers.map(b => ({
reason: b.reason,
resolution: b.resolution,
})),
});
}
// Löschung einleiten
const deletion = await this.deletionService.initiate(userId, {
reason: request.reason,
feedback: request.feedback, // Optional
});
return {
status: 'scheduled',
scheduledAt: deletion.scheduledAt,
gracePeriodEnds: deletion.gracePeriodEnds,
retentionNotice,
canCancel: true,
cancelUrl: `/users/${userId}/deletion-request/${deletion.id}/cancel`,
};
}
@Delete(':id/deletion-request/:requestId')
@UseGuards(AuthGuard, SameUserGuard)
async cancelDeletion(
@Param('id') userId: string,
@Param('requestId') requestId: string
): Promise<{ status: string }> {
await this.deletionService.cancel(requestId);
return {status: 'cancelled'};
}
}
// Lösch-Service mit Downstream-Propagation
class DeletionService {
async checkBlockers(
userId: string
): Promise<{blockers: DeletionBlocker[]; retentionNotice?: string}> {
const blockers: DeletionBlocker[] = [];
let retentionNotice: string | undefined;
// Offene Bestellungen
const openOrders = await this.orderRepository.findOpen(userId);
if (openOrders.length > 0) {
blockers.push({
reason: 'open_orders',
resolution: 'Please wait for orders to complete or cancel them',
details: openOrders.map(o => o.id),
});
}
// Aktives Abo
const subscription = await this.subscriptionRepository.findActive(userId);
if (subscription) {
blockers.push({
reason: 'active_subscription',
resolution: 'Please cancel your subscription first',
});
}
// Gesetzliche Aufbewahrungspflicht
const retentionRequired = await this.checkLegalRetention(userId);
if (retentionRequired) {
retentionNotice =
'Einige Daten unterliegen gesetzlichen Aufbewahrungspflichten ' +
'und werden anonymisiert, aber nicht gelöscht.';
}
return {blockers, retentionNotice};
}
async executeDelete(userId: string): Promise<void> {
// 1. Downstream-Services benachrichtigen
await this.eventBus.publish('user.deletion.started', {userId});
// 2. Daten anonymisieren (wo Retention gilt)
await this.anonymizeRetainedData(userId);
// 3. Daten löschen (wo keine Retention)
await this.deleteUserData(userId);
// 4. Bestätigung
await this.eventBus.publish('user.deletion.completed', {userId});
}
private async anonymizeRetainedData(userId: string): Promise<void> {
// Transaktionen: PII entfernen, aber Summen behalten
await this.db.query(`
UPDATE orders
SET
customer_name = 'ANONYMIZED',
customer_email = 'deleted@anonymized.local',
shipping_address = NULL,
anonymized_at = NOW()
WHERE user_id = $1
`, [userId]);
// Audit Logs: User-Referenz entfernen
await this.db.query(`
UPDATE audit_logs
SET
user_id = NULL,
user_email = 'ANONYMIZED',
ip_address = NULL
WHERE user_id = $1
`, [userId]);
}
}
Consent Management
// Consent-Modell
interface UserConsent {
userId: string;
purpose: ConsentPurpose;
granted: boolean;
grantedAt: Date | null;
revokedAt: Date | null;
source: 'registration' | 'settings' | 'api';
version: string; // Version der Datenschutzerklärung
}
type ConsentPurpose =
| 'essential' // Immer erlaubt (Vertrag)
| 'analytics' // Nutzungsanalyse
| 'marketing_email' // Marketing per Email
| 'marketing_push' // Push-Benachrichtigungen
| 'third_party'; // Weitergabe an Dritte
// API
@Controller('users/:id/consents')
class ConsentController {
@Get()
@UseGuards(AuthGuard, SameUserGuard)
async getConsents(@Param('id') userId: string): Promise<ConsentStatus[]> {
return this.consentService.getAll(userId);
}
@Patch()
@UseGuards(AuthGuard, SameUserGuard)
async updateConsents(
@Param('id') userId: string,
@Body() updates: ConsentUpdate[]
): Promise<ConsentStatus[]> {
for (const update of updates) {
if (update.purpose === 'essential') {
throw new BadRequestException('Essential consent cannot be modified');
}
await this.consentService.update(userId, update.purpose, update.granted);
// Bei Widerruf: Verarbeitung stoppen
if (!update.granted) {
await this.stopProcessing(userId, update.purpose);
}
}
return this.consentService.getAll(userId);
}
private async stopProcessing(userId: string, purpose: ConsentPurpose): Promise<void> {
switch (purpose) {
case 'marketing_email':
await this.emailService.unsubscribe(userId);
break;
case 'analytics':
await this.analyticsService.excludeUser(userId);
break;
case 'third_party':
await this.partnerService.revokeAccess(userId);
break;
}
}
}
PII-Schutz in Logs und Non-Prod
Personenbezogene Daten dürfen nicht unkontrolliert in Logs oder Test-Umgebungen landen. IP-Adressen sind personenbezogen; besser kürzen oder hashen statt vollständig loggen.
PII in Logs
// Logger mit automatischer PII-Maskierung
class SecureLogger {
private piiPatterns = [
{name: 'email', pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g},
{name: 'phone', pattern: /\+?[0-9]{10,15}/g},
{name: 'ip', pattern: /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g},
{name: 'ipv6', pattern: /([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/g},
{name: 'credit_card', pattern: /\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}/g},
{name: 'iban', pattern: /[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}/g},
];
private sensitiveFields = [
'password', 'token', 'secret', 'authorization',
'creditCard', 'ssn', 'dateOfBirth', 'address'
];
log(level: string, message: string, context?: Record<string, any>): void {
const sanitizedMessage = this.maskPii(message);
const sanitizedContext = this.sanitizeContext(context);
this.writeLog({
level,
message: sanitizedMessage,
context: sanitizedContext,
timestamp: new Date().toISOString(),
});
}
private maskPii(text: string): string {
let masked = text;
for (const {pattern} of this.piiPatterns) {
masked = masked.replace(pattern, '[REDACTED]');
}
return masked;
}
private sanitizeContext(context?: Record<string, any>): Record<string, any> {
if (!context) return {};
const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(context)) {
if (this.sensitiveFields.some(f => key.toLowerCase().includes(f))) {
sanitized[key] = '[REDACTED]';
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = this.sanitizeContext(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
}
// Verwendung
logger.log('info', 'User login', {
userId: '123',
email: 'user@example.com', // → '[REDACTED]'
ip: '192.168.1.1', // → '[REDACTED]'
userAgent: 'Mozilla/5.0', // → bleibt erhalten
});
PII-Maskierung für Non-Prod
-- Anonymisierungs-Script für Staging/Dev
-- NIEMALS auf Production ausführen!
-- Prüfung: Nicht auf Production ausführen
DO
$$
BEGIN
IF
current_database() = 'production' THEN
RAISE EXCEPTION 'Cannot run anonymization on production!';
END IF;
END $$;
-- Users anonymisieren
UPDATE users
SET email = 'user_' || id || '@test.local',
first_name = 'Test',
last_name = 'User_' || id,
phone = '+49000' || LPAD(id::text, 7, '0'),
date_of_birth = '1990-01-01'::date + (random() * 10000)::int;
-- Adressen anonymisieren
UPDATE addresses
SET street = 'Teststraße ' || id,
city = 'Teststadt',
postal_code = '12345',
country = 'DE';
-- Zahlungsdaten entfernen
UPDATE payment_methods
SET card_number = NULL,
card_holder = 'TEST USER',
expiry = NULL;
-- Nachrichten/Kommentare
UPDATE messages
SET content = 'Lorem ipsum dolor sit amet...';
-- Audit-Log (sensible Details entfernen)
UPDATE audit_logs
SET ip_address = '127.0.0.1',
user_agent = 'Anonymized';
Synthetic Data Generation
// Faker-basierte Testdaten statt Produktionsdaten
import {faker} from '@faker-js/faker';
class TestDataGenerator {
generateUser(): TestUser {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
first_name: faker.person.firstName(),
last_name: faker.person.lastName(),
phone: faker.phone.number(),
date_of_birth: faker.date.birthdate({min: 18, max: 80, mode: 'age'}),
address: {
street: faker.location.streetAddress(),
city: faker.location.city(),
postal_code: faker.location.zipCode(),
country: faker.location.countryCode(),
},
};
}
generateOrder(userId: string): TestOrder {
return {
id: faker.string.uuid(),
user_id: userId,
items: Array.from({length: faker.number.int({min: 1, max: 5})}, () => ({
product_id: faker.string.uuid(),
name: faker.commerce.productName(),
quantity: faker.number.int({min: 1, max: 3}),
price: parseFloat(faker.commerce.price()),
})),
total: parseFloat(faker.commerce.price({min: 10, max: 500})),
created_at: faker.date.recent({days: 30}),
};
}
}
Data Residency
Für Multi-Tenant-Systeme oder internationale Dienste: Wo werden Daten gespeichert? Die DSGVO verlangt keine generelle EU-Speicherung, aber Drittlandtransfers brauchen eine Rechtsgrundlage (z. B. Angemessenheitsbeschluss oder geeignete Garantien wie SCC/BCR).
Residency-Anforderungen
EU/EWR
- Verarbeitung in EU/EWR vereinfacht Compliance
- Drittlandtransfer nur mit Rechtsgrundlage (Art. 45 Angemessenheit oder Art. 46 Garantien)
Branchenspezifisch
- Zusätzliche Vorgaben je nach Branche/Vertrag prüfen
Schweiz/UK
- EU-Angemessenheitsbeschluss (Liste prüfen)
USA
- Angemessenheit nur für EU-US DPF-zertifizierte Firmen
- Alternativ: SCC/BCR + TIA
Multi-Region Architektur
// Tenant-basiertes Routing
class DataResidencyRouter {
private regionConfigs = new Map<string, RegionConfig>([
['eu', {
database: 'postgres-eu.example.com',
storage: 's3-eu-central-1',
allowedCountries: ['DE', 'FR', 'IT', 'ES', 'NL', /* ... */],
}],
['us', {
database: 'postgres-us.example.com',
storage: 's3-us-east-1',
allowedCountries: ['US', 'CA', 'MX'],
}],
['ch', {
database: 'postgres-ch.example.com',
storage: 'azure-switzerland-north',
allowedCountries: ['CH', 'LI'],
}],
]);
getConnectionForTenant(tenantId: string): DatabaseConnection {
const tenant = this.tenantRepository.findById(tenantId);
const region = this.regionConfigs.get(tenant.dataRegion);
if (!region) {
throw new Error(`Unknown region: ${tenant.dataRegion}`);
}
return this.connectionPool.get(region.database);
}
validateDataTransfer(
fromRegion: string,
toRegion: string,
basis: 'adequacy' | 'scc' | 'bcr'
): boolean {
if (fromRegion === toRegion) {
return true;
}
if (basis === 'adequacy') {
if (fromRegion === 'eu' && toRegion === 'us') {
return this.isDpfCertified(toRegion);
}
return this.isAdequateDestination(fromRegion, toRegion);
}
// SCC/BCR: zusätzliche Garantien + TIA erforderlich
return this.hasSafeguards(fromRegion, toRegion);
}
private isAdequateDestination(fromRegion: string, toRegion: string): boolean {
const adequate = new Map([
['eu', ['ch', 'uk']],
['ch', ['eu']],
['uk', ['eu']],
]);
return adequate.get(fromRegion)?.includes(toRegion) ?? false;
}
private isDpfCertified(toRegion: string): boolean {
// Beispiel: Registry/API-Abfrage; hier vereinfacht via Allowlist
const certifiedStorageByRegion = new Map<string, string[]>([
['us', ['s3-us-east-1']],
]);
const recipientStorage = this.regionConfigs.get(toRegion)?.storage;
return recipientStorage
? certifiedStorageByRegion.get(toRegion)?.includes(recipientStorage) ?? false
: false;
}
private hasSafeguards(fromRegion: string, toRegion: string): boolean {
// Beispiel: SCC/BCR + Transfer Impact Assessment als Policy-Matrix
const safeguards = new Map<string, {scc: boolean; tia: boolean}>([
['eu->us', {scc: true, tia: true}],
['eu->ch', {scc: true, tia: true}],
['ch->us', {scc: true, tia: false}],
]);
const key = `${fromRegion}->${toRegion}`;
const measures = safeguards.get(key);
return Boolean(measures?.scc && measures?.tia);
}
}
Regeln und Anti-Patterns
Do
- Data Minimization: Nur erheben, was wirklich benötigt wird
- Retention Policies: Automatische Löschung nach definierten Fristen
- Soft Delete mit Grace Period: Versehentliche Löschungen rückgängig machen
- PII-Maskierung: In Logs und Non-Prod keine echten Daten
- Consent-Management: Einwilligungen sauber verwalten und respektieren
- Export-Format: Maschinenlesbar (JSON, CSV), nicht PDF
Don't
- PII in URLs: Keine E-Mail-Adressen oder Namen in Query-Parametern
- Logs ohne Maskierung: PII in Logs ist ein Audit-Failure
- Prod-Daten in Dev: Immer synthetische oder anonymisierte Daten
- Löschung ignorieren: DSGVO-Anfragen haben Fristen (1 Monat, ggf. verlängert)
- Consent vorauswählen: Opt-in, nicht Opt-out
- Unbegrenzte Retention: Jedes Datum braucht ein Verfallsdatum
Artefakt: Data-Lifecycle-Policy
# Data Lifecycle Policy
## 1. Datenklassifizierung
| Klasse | Beschreibung | Beispiele | Schutzlevel |
|--------|--------------|-----------|-------------|
| PII | Personenbezogene Daten | Name, Email, Adresse | Hoch |
| Sensitive PII | Besonders schützenswert | Gesundheit, Finanzen | Sehr hoch |
| Business | Geschäftsdaten | Bestellungen, Verträge | Mittel |
| Technical | Technische Daten | Logs, Metriken | Niedrig |
| Public | Öffentliche Daten | Produktinfos | Keine Beschränkung |
## 2. Retention Schedule
| Datentyp | Retention | Rechtliche Basis | Löschmethode |
|----------|-----------|------------------|--------------|
| Account (aktiv) | Bis Zweckende + Inaktivitätsfrist | Vertrag | - |
| Account (gelöscht, Self-Service) | 14–30 Tage | Wiederherstellung | Hard Delete |
| Buchungsbelege/Rechnungen (DE) | 8–10 Jahre | HGB §257, AO §147 | Archiv → Delete |
| Handels-/Geschäftsbriefe (DE) | 6 Jahre | HGB §257, AO §147 | Archiv → Delete |
| Audit Logs | 6–12 Monate | Compliance | Rotation |
| Access Logs | 7–30 Tage | Debugging/Security | Rotation |
| Marketing-Daten | Bis Widerruf | Einwilligung | Hard Delete |
| Analytics (PII) | 0 | - | Aggregieren/Anonymisieren |
| Session Data | 24h-30d | Funktion | Auto-Expire |
## 3. DSGVO-Prozesse
### Auskunftsanfrage (Art. 15)
- **Frist**: 1 Monat (+ bis zu 2 Monate möglich, informieren)
- **Format**: JSON, optional PDF-Zusammenfassung
- **Prozess**: Automatisierter Export via API
- **Verantwortlich**: Automatisch / Datenschutzbeauftragter bei Sonderfällen
### Löschanfrage (Art. 17)
- **Frist**: 1 Monat (+ bis zu 2 Monate möglich, informieren)
- **Prüfung**: Automatische Blocker-Erkennung
- **Grace Period**: 14–30 Tage (nur Self-Service, nicht bei Art. 17)
- **Ausnahmen**: Gesetzliche Aufbewahrungspflichten (anonymisieren statt löschen)
### Berichtigung (Art. 16)
- **Frist**: 1 Monat (+ bis zu 2 Monate möglich, informieren)
- **Prozess**: Self-Service via UI/API
- **Audit**: Änderungen protokollieren
### Datenübertragbarkeit (Art. 20)
- **Format**: JSON, CSV
- **Scope**: Vom Nutzer bereitgestellte/observierte Daten; Einwilligung/Vertrag
- **Prozess**: Async Export mit Download-Link
## 4. PII-Handling
### In Logs
- Automatische Maskierung aktiv
- Maskierte Felder: E-Mail, IP, Telefon, Kreditkarte
- Review: Quartalsweise Log-Audit
### In Non-Prod
- Keine Produktionsdaten ohne Anonymisierung
- Anonymisierungs-Script: `scripts/anonymize-db.sql`
- Synthetische Daten: Faker.js
### In Backups
- Verschlüsselung: AES-256
- Retention: 30 Tage
- Geo-Redundanz: Nur innerhalb der Datenregion
- Löschungen: Wirksam nach Ablauf der Backup-Retention; Restore → Re-Delete
## 5. Data Residency
| Region | Speicherort | Erlaubte Transfers |
|--------|-------------|-------------------|
| EU | eu-central-1 | EU, CH, UK (Angemessenheit) |
| CH | switzerland-north | CH, EU (Angemessenheit) |
| US | us-east-1 | US (DPF) oder SCC/BCR |
## 6. Verantwortlichkeiten
| Rolle | Verantwortung |
|-------|---------------|
| Datenschutzbeauftragter | Policy-Owner, DSGVO-Anfragen |
| Engineering | Technische Umsetzung |
| Operations | Backup, Retention, Löschung |
| Legal | Rechtliche Bewertung, Verträge |
Checkliste
Must-have vor Go-Live
- [ ] Data Minimization: Nur notwendige Daten erheben
- [ ] Retention Policies für alle Datentypen definiert
- [ ] Automatische Löschung implementiert
- [ ] Datenexport (Art. 15/20) möglich
- [ ] Löschprozess (Art. 17) implementiert
- [ ] PII-Maskierung in Logs aktiv
- [ ] Non-Prod ohne echte PII
Should-have
- [ ] Consent-Management mit Widerrufsmöglichkeit
- [ ] Soft Delete mit Grace Period
- [ ] Anonymisierungs-Scripts für Non-Prod
- [ ] Data Residency dokumentiert
Nice-to-have
- [ ] Self-Service Datenexport
- [ ] Automatische Consent-Versionierung
- [ ] Data Lineage Tracking
Wie es weitergeht
Im letzten Teil der Serie behandeln wir Go-Live-Readiness – die finale Checkliste, die alle Themen zusammenführt und sicherstellt, dass deine API bereit für den Launch ist.
Dies ist Teil 21 der Serie API-Design. Alle Teile findest du in der Serie: API-Design.