feat(filesystem): introduce FileOwnership and ProcessUser value objects

- Add `FileOwnership` to encapsulate file owner and group information.
- Add `ProcessUser` to represent and manage system process user details.
- Enhance ownership matching and debugging with structured data objects.
- Include new documentation on file ownership handling and permission improvements.
- Prepare infrastructure for enriched error handling in filesystem operations.
This commit is contained in:
2025-11-04 00:56:49 +01:00
parent 30d15d1b20
commit 3085739e34
9 changed files with 1364 additions and 22 deletions

View File

@@ -0,0 +1,226 @@
# FilePermissionException Verbesserungs-Plan
## Ziel
Verbesserung der `FilePermissionException` um detaillierte Permission- und User-Informationen f?r besseres Debugging.
## Problem-Analyse
### Aktueller Zustand
Die `FilePermissionException` gibt momentan nur minimale Informationen:
```php
FilePermissionException::delete($path, 'Directory is not writable: ' . $dir);
// Output: "Permission denied for delete on file: /path/to/file (Directory is not writable: /path/to/dir)"
```
**Fehlende Informationen:**
- Aktuelle Permissions der Datei/Verzeichnisses
- Owner/Group der Datei
- Aktueller Process-User
- Erwartete Permissions
- Octal-Darstellung der Permissions
### Verf?gbare Infrastruktur
? **PermissionChecker** hat bereits `getDiagnosticInfo()`:
- Liefert owner, group, permissions, parent_dir info
- Nutzt `posix_getpwuid()` und `posix_getgrgid()`
? **ExceptionContext** hat `withData()`, `withDebug()`, `withMetadata()`
? **posix_geteuid()** verf?gbar f?r Process-User
## L?sungsplan
### L?sung 1: Erweitere FilePermissionException (HOHE PRIORIT?T)
**Neue statische Factory-Methoden mit Permission-Details:**
```php
public static function delete(
string $path,
?string $reason = null,
?PermissionChecker $permissionChecker = null
): self {
$diagnosticInfo = $permissionChecker?->getDiagnosticInfo($path) ?? [];
$currentUser = self::getCurrentUserInfo();
return new self(
path: $path,
operation: 'delete',
reason: $reason,
diagnosticInfo: $diagnosticInfo,
currentUser: $currentUser
);
}
```
**Neue Properties:**
- `?array $diagnosticInfo` - Permission-Details vom PermissionChecker
- `?array $currentUser` - Aktueller Process-User Info
**Verbesserte Message-Formatierung:**
```php
$message = "Permission denied for {$operation} on file: {$path}";
if ($reason) {
$message .= " ({$reason})";
}
// Add detailed info if available
if ($this->diagnosticInfo) {
$message .= sprintf(
"\n File owner: %s, group: %s, permissions: %s",
$this->diagnosticInfo['owner'] ?? 'unknown',
$this->diagnosticInfo['group'] ?? 'unknown',
$this->diagnosticInfo['permissions'] ?? 'unknown'
);
if (isset($this->diagnosticInfo['parent_dir'])) {
$parentInfo = $permissionChecker?->getDiagnosticInfo($this->diagnosticInfo['parent_dir']) ?? [];
$message .= sprintf(
"\n Parent directory: %s (owner: %s, permissions: %s)",
$this->diagnosticInfo['parent_dir'],
$parentInfo['owner'] ?? 'unknown',
$parentInfo['permissions'] ?? 'unknown'
);
}
}
if ($this->currentUser) {
$message .= sprintf(
"\n Current process user: %s (uid: %d, gid: %d)",
$this->currentUser['name'],
$this->currentUser['uid'],
$this->currentUser['gid']
);
}
```
### L?sung 2: Helper-Methode f?r Process-User Info
**Neue private Methode in FilePermissionException:**
```php
private static function getCurrentUserInfo(): ?array
{
if (!function_exists('posix_geteuid')) {
return null;
}
$uid = posix_geteuid();
$gid = posix_getegid();
$userInfo = posix_getpwuid($uid);
$groupInfo = posix_getgrgid($gid);
return [
'uid' => $uid,
'gid' => $gid,
'name' => $userInfo['name'] ?? 'unknown',
'group' => $groupInfo['name'] ?? 'unknown',
];
}
```
### L?sung 3: Integration in FileStorage-Methoden
**Anpassung aller FilePermissionException Aufrufe:**
**Vorher:**
```php
if (! is_writable($dir)) {
throw FilePermissionException::delete($path, 'Directory is not writable: ' . $dir);
}
```
**Nachher:**
```php
if (! is_writable($dir)) {
throw FilePermissionException::delete(
path: $path,
reason: 'Directory is not writable: ' . $dir,
permissionChecker: $this->permissions
);
}
```
**Betroffene Stellen:**
1. `FileStorage::get()` - read exception
2. `FileStorage::put()` - write exception, createDirectory exception
3. `FileStorage::delete()` - delete exception
4. `FileStorage::createDirectory()` - createDirectory exception
5. Alle anderen Stellen, die FilePermissionException werfen
### L?sung 4: ExceptionContext erweitern
**Erweiterte Context-Daten in Exception:**
```php
$context = ExceptionContext::forOperation('file.permission', 'filesystem')
->withData([
'path' => $path,
'operation' => $operation,
'reason' => $reason,
])
->withDebug([
'file_permissions' => $diagnosticInfo['permissions'] ?? null,
'file_owner' => $diagnosticInfo['owner'] ?? null,
'file_group' => $diagnosticInfo['group'] ?? null,
'parent_dir' => $diagnosticInfo['parent_dir'] ?? null,
'parent_writable' => $diagnosticInfo['parent_writable'] ?? null,
])
->withMetadata([
'current_user' => $currentUser,
'file_exists' => $diagnosticInfo['exists'] ?? false,
'is_file' => $diagnosticInfo['is_file'] ?? false,
'is_dir' => $diagnosticInfo['is_dir'] ?? false,
]);
```
## Implementierungsreihenfolge
1. ? **L?sung 2** - Helper-Methode f?r Process-User (einfach, keine Dependencies)
2. ? **L?sung 1** - Erweitere FilePermissionException Constructor und Factory-Methoden
3. ? **L?sung 3** - Integration in FileStorage-Methoden
4. ? **L?sung 4** - ExceptionContext erweitern
## Beispiel-Output
### Vorher:
```
Permission denied for delete on file: /var/www/html/storage/cache/file.cache.php (Directory is not writable: /var/www/html/storage/cache)
```
### Nachher:
```
Permission denied for delete on file: /var/www/html/storage/cache/file.cache.php (Directory is not writable: /var/www/html/storage/cache)
File owner: www-data, group: www-data, permissions: drwxrwxr-x
Parent directory: /var/www/html/storage/cache (owner: root, permissions: drwxr-xr-x)
Current process user: www-data (uid: 33, gid: 33)
```
## Vorteile
1. **Besseres Debugging**: Sofort sichtbar, welche Permissions falsch sind
2. **User-Info**: Zeigt Owner-Mismatch sofort
3. **Context**: Vollst?ndige Permission-Info in Exception-Context f?r Logging
4. **R?ckw?rtskompatibel**: Alte Aufrufe funktionieren weiterhin (optional Parameter)
## Performance-?berlegungen
- `getDiagnosticInfo()` ist relativ teuer (mehrere `stat()` Calls)
- **L?sung**: Nur bei Exception-Throw aufrufen (nicht bei jedem Check)
- Optional: K?nnte in `withDebug()` ausgelagert werden, wenn Performance kritisch ist
## Testing
1. **Unit-Tests**: FilePermissionException mit Permission-Details
2. **Integration-Tests**: FileStorage-Methoden mit verschiedenen Permission-Szenarien
3. **E2E-Tests**: Cache-Operationen mit Permission-Problemen
## Dokumentation
Nach Implementierung aktualisieren:
- `docs/claude/error-handling.md` - FilePermissionException mit Details
- `docs/deployment/cache-configuration.md` - Exception-Beispiele

View File

@@ -0,0 +1,388 @@
# Redis ACL (Access Control List) - Erkl?rung
## Was ist Redis ACL?
**Redis ACL (Access Control List)** ist ein Authentifizierungs- und Autorisierungssystem, das in Redis 6.0 eingef?hrt wurde. Es erm?glicht feingranulare Kontrolle ?ber Benutzerrechte und Zugriff auf Redis-Befehle und -Daten.
### Unterschied: Legacy vs. ACL
#### Legacy Authentication (vor Redis 6.0)
**Wie es funktioniert:**
- Ein einzelnes Passwort f?r die gesamte Redis-Instanz
- Konfiguration via `requirepass` in `redis.conf` oder `--requirepass` Parameter
- Alle verbundenen Clients haben **vollst?ndigen Zugriff** auf alle Daten und Befehle
- Keine Unterscheidung zwischen Benutzern
**Beispiel:**
```bash
# redis.conf
requirepass "mein-passwort-123"
# Oder via Command Line
redis-server --requirepass "mein-passwort-123"
```
**PHP Code:**
```php
$redis = new Redis();
$redis->connect('localhost', 6379);
$redis->auth('mein-passwort-123'); // Einfache Passwort-Auth
// Jetzt hat der Client VOLLST?NDIGEN Zugriff
```
**Probleme:**
- ? Keine Benutzer-Trennung
- ? Keine feingranulare Kontrolle
- ? Alle Clients haben Admin-Rechte
- ? Keine M?glichkeit, verschiedene Berechtigungen zu vergeben
#### Redis ACL (ab Redis 6.0)
**Wie es funktioniert:**
- Mehrere Benutzer mit unterschiedlichen Berechtigungen
- Jeder Benutzer hat Username + Passwort
- Feingranulare Kontrolle ?ber:
- Welche Befehle erlaubt sind
- Auf welche Keys/Datenbanken zugegriffen werden kann
- Lese-/Schreib-Rechte
- Spezifische Key-Patterns
**Beispiel:**
```bash
# Redis CLI
ACL SETUSER cache-user on >cache-password123 ~cache:* +get +set +del +exists
ACL SETUSER queue-user on >queue-password456 ~queue:* +lpush +rpop +llen
ACL SETUSER admin on >admin-password789 ~* &* +@all
```
**PHP Code:**
```php
// F?r Cache-Benutzer
$redis = new Redis();
$redis->connect('localhost', 6379);
$redis->auth('cache-user', 'cache-password123'); // Username + Password
// F?r Admin
$redis->auth('admin', 'admin-password789'); // Username + Password
```
## ACL Konzepte im Detail
### 1. Benutzer (Users)
**Standard-Benutzer:**
- `default`: Voreingestellter Benutzer (ohne Passwort = kein Auth erforderlich)
- Benutzer k?nnen aktiv (`on`) oder deaktiviert (`off`) sein
**Beispiel:**
```redis
ACL SETUSER myuser on >mypassword
```
### 2. Passw?rter
**Passwort-Formate:**
- Plaintext: `>password123`
- Hash: `#5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8`
**Mehrere Passw?rter:**
```redis
ACL SETUSER myuser on >password1 >password2 >password3
```
### 3. Key-Patterns (Zugriff auf Keys)
**Pattern-Syntax:**
- `~*`: Zugriff auf alle Keys
- `~cache:*`: Zugriff nur auf Keys die mit `cache:` beginnen
- `~user:*`: Zugriff nur auf Keys die mit `user:` beginnen
- `~{user,cache}:*`: Zugriff auf mehrere Patterns
**Beispiel:**
```redis
ACL SETUSER cache-user on >pass ~cache:* ~session:*
```
### 4. Befehle (Commands)
**Berechtigungen:**
- `+command`: Befehl erlauben (z.B. `+get`, `+set`)
- `-command`: Befehl verbieten (z.B. `-flushdb`)
- `+@category`: Alle Befehle einer Kategorie erlauben
- `-@category`: Alle Befehle einer Kategorie verbieten
- `+@all`: Alle Befehle erlauben (Admin)
- `+@read`: Nur Lese-Befehle
- `+@write`: Nur Schreib-Befehle
- `+@keyspace`: Keyspace-Befehle
- `+@string`: String-Befehle
- `+@list`: List-Befehle
- `+@set`: Set-Befehle
- `+@sortedset`: Sorted Set-Befehle
- `+@hash`: Hash-Befehle
- `+@stream`: Stream-Befehle
- `+@pubsub`: Pub/Sub-Befehle
- `+@transaction`: Transaction-Befehle
- `+@connection`: Connection-Befehle
- `+@scripting`: Scripting-Befehle
- `+@admin`: Admin-Befehle
- `+@dangerous`: Gef?hrliche Befehle (flush, debug, etc.)
**Beispiel:**
```redis
# Cache-User: Nur GET, SET, DEL, EXISTS
ACL SETUSER cache-user on >pass ~cache:* +get +set +del +exists
# Queue-User: Nur List-Operationen
ACL SETUSER queue-user on >pass ~queue:* +lpush +rpop +llen +lrange
# Admin: Alle Befehle
ACL SETUSER admin on >pass ~* &* +@all
```
### 5. Datenbanken (Databases)
**Standard:**
- Alle Datenbanken sind erlaubt (`&*`)
- Spezifische Datenbanken: `&0`, `&1`, `&2`, etc.
**Beispiel:**
```redis
# Nur Datenbank 1 erlauben
ACL SETUSER myuser on >pass ~* &1 +@all
```
## Praktische Beispiele
### Beispiel 1: Cache-User (nur Lese/Schreib auf Cache-Keys)
```redis
ACL SETUSER cache-user on >cache-secret-password ~cache:* +get +set +del +exists +expire +ttl
```
**Was darf dieser User?**
- ? `GET cache:user:123`
- ? `SET cache:user:123 "value"`
- ? `DEL cache:user:123`
- ? `EXISTS cache:user:123`
- ? `GET user:123` (nicht `cache:` Prefix)
- ? `FLUSHDB` (nicht erlaubt)
- ? `KEYS *` (nicht erlaubt)
### Beispiel 2: Queue-User (nur Queue-Operationen)
```redis
ACL SETUSER queue-user on >queue-secret-password ~queue:* +lpush +rpop +llen +lrange +lrem +@list
```
**Was darf dieser User?**
- ? `LPUSH queue:emails "task"`
- ? `RPOP queue:emails`
- ? `LLEN queue:emails`
- ? `GET queue:emails` (String-Befehle nicht erlaubt)
- ? `FLUSHDB` (nicht erlaubt)
### Beispiel 3: Admin-User (vollst?ndiger Zugriff)
```redis
ACL SETUSER admin on >admin-secret-password ~* &* +@all
```
**Was darf dieser User?**
- ? Alle Befehle
- ? Alle Keys
- ? Alle Datenbanken
### Beispiel 4: Read-Only User (nur Lese-Zugriff)
```redis
ACL SETUSER readonly-user on >readonly-password ~* +@read
```
**Was darf dieser User?**
- ? `GET`, `HGET`, `SMEMBERS`, etc. (Lese-Befehle)
- ? `SET`, `DEL`, `FLUSHDB` (Schreib-Befehle)
## ACL vs. Legacy in unserem Framework
### Aktuelle Implementierung (Legacy)
**Konfiguration:**
```yaml
# docker-compose.staging.yml
redis-server --requirepass "password123"
```
**PHP Code:**
```php
// RedisConfig.php
password: $env->get(EnvKey::REDIS_PASSWORD, null)
// RedisConnection.php
$redis->auth($this->config->password); // Einfache Passwort-Auth
```
**Problem:**
- Alle Verbindungen (cache, queue, session) verwenden dasselbe Passwort
- Alle haben vollst?ndigen Zugriff auf alle Daten
- Keine Isolation zwischen verschiedenen Use Cases
### Potenzielle ACL-Implementierung
**Konfiguration:**
```yaml
# docker-compose.staging.yml
# Redis startet mit ACL
# ACL wird via init script oder config gesetzt:
ACL SETUSER cache-user on >cache-password ~cache:* +@read +@write -@dangerous
ACL SETUSER queue-user on >queue-password ~queue:* +@list +@read -@dangerous
ACL SETUSER session-user on >session-password ~session:* +@read +@write -@dangerous
ACL SETUSER admin on >admin-password ~* &* +@all
```
**Environment Variables:**
```env
# F?r Cache-Connection
REDIS_USERNAME=cache-user
REDIS_PASSWORD=cache-password
# F?r Queue-Connection
REDIS_USERNAME=queue-user
REDIS_PASSWORD=queue-password
# F?r Session-Connection
REDIS_USERNAME=session-user
REDIS_PASSWORD=session-password
```
**PHP Code:**
```php
// RedisConfig.php
public ?string $username = null;
public ?string $password = null;
// RedisConnection.php
if ($this->config->password) {
if ($this->config->username) {
// ACL: auth(username, password)
$authResult = $this->client->auth(
$this->config->username,
$this->config->password
);
} else {
// Legacy: auth(password)
$authResult = $this->client->auth($this->config->password);
}
}
```
**Vorteile:**
- ? Isolation: Cache-User kann nicht auf Queue-Keys zugreifen
- ? Sicherheit: Minimale Berechtigungen (Principle of Least Privilege)
- ? Audit: Verschiedene User f?r verschiedene Operationen
- ? Schutz: Kein User kann `FLUSHDB` ausf?hren (au?er Admin)
## Wann ACL verwenden?
### ACL ist sinnvoll wenn:
1. **Multi-Tenant Umgebungen**
- Verschiedene Anwendungen teilen sich Redis
- Jede Anwendung braucht isolierten Zugriff
2. **Sicherheits-Anforderungen**
- Compliance-Anforderungen (z.B. PCI-DSS)
- Feingranulare Zugriffskontrolle erforderlich
3. **Team-Isolation**
- Verschiedene Teams haben verschiedene Bereiche
- Cache-Team, Queue-Team, Session-Team
4. **Production vs. Staging**
- Staging: Weniger restriktiv
- Production: Strenge ACL-Regeln
### Legacy Auth ist ausreichend wenn:
1. **Single-Application**
- Nur eine Anwendung nutzt Redis
- Keine Multi-Tenant-Anforderungen
2. **Einfache Setups**
- Entwicklungs-Umgebungen
- Kleine Projekte ohne hohe Sicherheitsanforderungen
3. **Externe Isolation**
- Redis l?uft in isoliertem Netzwerk
- Zugriff wird auf Netzwerk-Ebene kontrolliert
## Migration von Legacy zu ACL
### Schritt 1: Redis mit ACL konfigurieren
```bash
# Redis startet mit Legacy Auth
# Dann ACL konfigurieren:
# Legacy User erstellen (f?r Backward Compatibility)
ACL SETUSER default on >legacy-password ~* &* +@all
# Spezialisierte User erstellen
ACL SETUSER cache-user on >cache-password ~cache:* +@read +@write
ACL SETUSER queue-user on >queue-password ~queue:* +@list
```
### Schritt 2: Code anpassen
```php
// Schrittweise Migration:
// 1. Code unterst?tzt beide Methoden (Backward Compatible)
// 2. Neue Connections verwenden ACL
// 3. Alte Connections k?nnen weiterhin Legacy Auth verwenden
```
### Schritt 3: Testing
```php
// Test mit verschiedenen Usern
// Verifizieren dass Isolation funktioniert
// Verifizieren dass Legacy Auth noch funktioniert
```
## Zusammenfassung
| Aspekt | Legacy Auth | Redis ACL |
|--------|-------------|-----------|
| **Benutzer** | 1 (implizit) | Viele (explizit) |
| **Passwort** | 1 Passwort f?r alle | Passwort pro User |
| **Berechtigungen** | Alles oder nichts | Feingranular |
| **Key-Isolation** | Nein | Ja (Patterns) |
| **Befehl-Kontrolle** | Nein | Ja (pro Command) |
| **Komplexit?t** | Niedrig | Mittel-Hoch |
| **Sicherheit** | Basis | Hoch |
| **Verwaltung** | Einfach | Komplexer |
## Empfehlung f?r unser Framework
**Aktuell (Staging):**
- Legacy Auth ist ausreichend
- Einfacher zu verwalten
- Keine Multi-Tenant-Anforderungen
**Zukunft (Production):**
- ACL k?nnte sinnvoll sein f?r:
- Bessere Isolation zwischen Cache/Queue/Session
- Compliance-Anforderungen
- Multi-Tenant-Szenarien
**Code-Vorbereitung:**
- Code sollte beide Methoden unterst?tzen (Backward Compatible)
- `REDIS_USERNAME` optional machen
- Wenn `username` gesetzt ? ACL Auth
- Wenn nur `password` ? Legacy Auth
## Weitere Ressourcen
- [Redis ACL Documentation](https://redis.io/docs/management/security/acl/)
- [Redis ACL Commands](https://redis.io/commands/acl/)
- [Redis Security Best Practices](https://redis.io/docs/management/security/)

View File

@@ -0,0 +1,364 @@
# Redis Authentication Problem Analysis & Refactoring Plan
## Problem Description
**Fehlermeldung:**
```
Direct Redis connection failed: Failed to connect to Redis (cache): WRONGPASS invalid username-password pair or user is disabled.
with Host: staging-redis and Password: gRUFGtFFthCGFHYjZ7o9yuiDRJ0JdG4Syiv1fVPb544=
```
## Root Cause Analysis
### Identified Issues
1. **Password Whitespace/Newline Problem**
- Das Passwort wird aus Docker Secrets geladen (`/run/secrets/redis_password`)
- `DockerSecretsResolver` verwendet `trim()`, aber m?glicherweise gibt es unsichtbare Zeichen (z.B. `\r\n`, `\n`, Tabs)
- Redis ist sehr strikt bei Passwort-?bereinstimmungen
2. **Fehlende Passwort-Validierung**
- Keine Validierung des Passworts vor der Verwendung
- Keine Sanitization au?erhalb von `trim()`
3. **Unzureichende Fehlerbehandlung**
- Fehlermeldung enth?lt das vollst?ndige Passwort (Security Risk!)
- Keine Unterscheidung zwischen verschiedenen Auth-Fehlertypen
- Keine detaillierte Diagnose-Informationen
4. **Keine Redis ACL Unterst?tzung**
- Aktuell nur Legacy Passwort-Auth (`requirepass`)
- Redis 6+ unterst?tzt ACL mit Username/Password
- Code ist nicht f?r zuk?nftige ACL-Nutzung vorbereitet
5. **Fehlende Passwort-Normalisierung**
- Verschiedene Whitespace-Zeichen werden nicht normalisiert
- Keine Behandlung von UTF-8 BOM oder anderen Steuerzeichen
## Current Implementation Analysis
### Code Flow
```
Environment::get(REDIS_PASSWORD)
?
DockerSecretsResolver::resolve()
?
file_get_contents() + trim()
?
RedisConfig::fromEnvironment()
?
RedisConnection::connect()
?
$client->auth($password) ? WRONGPASS Fehler hier
```
### Problematic Code Sections
**1. DockerSecretsResolver.php (Zeile 104)**
```php
$result = trim($content);
```
- Problem: Entfernt nur Whitespace am Anfang/Ende
- Fehlt: Normalisierung von `\r\n` zu `\n`, Entfernung von unsichtbaren Zeichen
**2. RedisConnection.php (Zeile 92-95)**
```php
if ($this->config->password) {
if (! $this->client->auth($this->config->password)) {
throw new RedisConnectionException("Redis authentication failed");
}
}
```
- Problem: Generische Fehlermeldung ohne Details
- Fehlt: Unterscheidung zwischen verschiedenen Auth-Fehlern
- Fehlt: Passwort-Validierung vor Auth-Versuch
**3. RedisConnection.php (Zeile 115)**
```php
throw new RedisConnectionException(
"Failed to connect to Redis ({$this->name}): " . $e->getMessage() . " with Host: {$this->config->host} and Password: {$this->config->password}",
previous: $e
);
```
- **KRITISCH**: Passwort wird in Exception-Message ausgegeben (Security Risk!)
- Passwort sollte niemals in Logs/Exceptions erscheinen
## Refactoring Vorschl?ge
### 1. Passwort-Normalisierung verbessern
**Problem:** `trim()` ist unzureichend f?r Docker Secrets
**L?sung:** Erweiterte Normalisierung in `DockerSecretsResolver`
```php
private function normalizeSecret(string $content): string
{
// Entferne alle Whitespace-Zeichen am Anfang und Ende
$normalized = trim($content);
// Entferne unsichtbare Steuerzeichen (au?er normale Zeichen)
$normalized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $normalized);
// Normalisiere Line Endings (falls vorhanden)
$normalized = str_replace(["\r\n", "\r"], "", $normalized);
// Entferne UTF-8 BOM falls vorhanden
if (str_starts_with($normalized, "\xEF\xBB\xBF")) {
$normalized = substr($normalized, 3);
}
return $normalized;
}
```
### 2. Passwort-Validierung hinzuf?gen
**Problem:** Keine Validierung vor Verwendung
**L?sung:** Validierung in `RedisConfig`
```php
private function validatePassword(?string $password): ?string
{
if ($password === null || $password === '') {
return null;
}
// Warnung f?r sehr kurze Passw?rter
if (strlen($password) < 8) {
// Log warning, aber nicht blockieren (kann gewollt sein)
}
// Warnung f?r Whitespace in der Mitte (nur trimmen, nicht blockieren)
$trimmed = trim($password);
if ($trimmed !== $password) {
// Log warning: Password hatte Whitespace am Anfang/Ende
}
return $trimmed !== '' ? $trimmed : null;
}
```
### 3. Sicherheit: Passwort aus Exceptions entfernen
**Problem:** Passwort erscheint in Exception-Messages
**L?sung:** Maskiertes Passwort in Fehlermeldungen
```php
private function maskPassword(?string $password): string
{
if ($password === null || $password === '') {
return '(not set)';
}
$length = strlen($password);
if ($length <= 4) {
return str_repeat('*', $length);
}
return substr($password, 0, 2) . str_repeat('*', $length - 4) . substr($password, -2);
}
```
### 4. Verbesserte Fehlerbehandlung
**Problem:** Generische Fehlermeldungen ohne Kontext
**L?sung:** Detaillierte Exception mit diagnostischen Informationen
```php
private function authenticate(): void
{
if (!$this->config->password) {
return; // Keine Authentifizierung erforderlich
}
try {
$authResult = $this->client->auth($this->config->password);
if (!$authResult) {
throw new RedisConnectionException(
"Redis authentication failed for connection '{$this->name}'",
context: $this->createAuthDiagnostics()
);
}
} catch (RedisException $e) {
$message = $e->getMessage();
// Spezifische Fehlermeldungen basierend auf Redis-Fehlercode
if (str_contains($message, 'WRONGPASS')) {
throw new RedisConnectionException(
"Redis authentication failed: Invalid password for connection '{$this->name}'",
context: $this->createAuthDiagnostics(),
previous: $e
);
}
// Andere Auth-Fehler
throw new RedisConnectionException(
"Redis authentication error for connection '{$this->name}': {$message}",
context: $this->createAuthDiagnostics(),
previous: $e
);
}
}
private function createAuthDiagnostics(): ExceptionContext
{
$context = ExceptionContext::forOperation('Redis Authentication');
$context->add('connection_name', $this->name);
$context->add('host', $this->config->host);
$context->add('port', $this->config->port);
$context->add('database', $this->config->database);
$context->add('password_set', $this->config->password !== null);
$context->add('password_length', $this->config->password ? strlen($this->config->password) : 0);
// KEIN Passwort in Context!
return $context;
}
```
### 5. Redis ACL Unterst?tzung (Optional, f?r Zukunft)
**Problem:** Keine Unterst?tzung f?r Redis 6+ ACL
**L?sung:** Optionale Username/Password Auth
```php
final readonly class RedisConfig
{
private function __construct(
public string $host = 'redis',
public int $port = 6379,
public ?string $password = null,
public ?string $username = null, // NEU f?r ACL
// ... rest
) {}
public static function fromEnvironment(Environment $env): self
{
return new self(
// ...
password: $env->get(EnvKey::REDIS_PASSWORD, null),
username: $env->get(EnvKey::REDIS_USERNAME, null), // NEU
// ...
);
}
}
// In RedisConnection.php
private function authenticate(): void
{
if (!$this->config->password) {
return;
}
// Redis 6+ ACL: auth(username, password)
if ($this->config->username) {
$authResult = $this->client->auth($this->config->username, $this->config->password);
} else {
// Legacy: auth(password)
$authResult = $this->client->auth($this->config->password);
}
if (!$authResult) {
throw new RedisConnectionException(/* ... */);
}
}
```
### 6. EnvKey Enum erweitern
**L?sung:** Optional `REDIS_USERNAME` f?r zuk?nftige ACL-Nutzung
```php
enum EnvKey: string
{
// ... existing ...
case REDIS_HOST = 'REDIS_HOST';
case REDIS_PORT = 'REDIS_PORT';
case REDIS_PASSWORD = 'REDIS_PASSWORD';
case REDIS_USERNAME = 'REDIS_USERNAME'; // NEU (optional)
case REDIS_PREFIX = 'REDIS_PREFIX';
}
```
## Implementierungsplan
### Phase 1: Kritische Sicherheitsfixes (SOFORT)
1. ? **Passwort aus Exception-Messages entfernen**
- `RedisConnection.php` Zeile 115: Maskiertes Passwort verwenden
- Security Hotfix
2. ? **Verbesserte Passwort-Normalisierung**
- `DockerSecretsResolver.php`: Erweiterte `normalizeSecret()` Methode
- Behebt wahrscheinlich das Hauptproblem
### Phase 2: Robustheit (HOCH)
3. ? **Passwort-Validierung**
- `RedisConfig`: Validierung und Sanitization
- Warnungen f?r problematische Passw?rter
4. ? **Verbesserte Fehlerbehandlung**
- Spezifische Exception-Messages
- Diagnostische Informationen (ohne Passwort!)
### Phase 3: Zukunftssicherheit (MITTEL)
5. ?? **Redis ACL Unterst?tzung** (Optional)
- Nur wenn Redis ACL verwendet werden soll
- Backward-kompatibel mit Legacy Auth
## Priorisierung
| Priorit?t | Task | Impact | Effort |
|-----------|------|--------|--------|
| ?? KRITISCH | Passwort aus Exceptions entfernen | Security | Low |
| ?? KRITISCH | Passwort-Normalisierung verbessern | Fixes Bug | Low |
| ?? HOCH | Passwort-Validierung | Robustheit | Low |
| ?? HOCH | Verbesserte Fehlerbehandlung | Debugging | Medium |
| ?? MITTEL | Redis ACL Unterst?tzung | Zukunft | Medium |
## Testing Recommendations
1. **Unit Tests f?r Passwort-Normalisierung**
```php
test('normalizes password with newlines', function () {
$resolver = new DockerSecretsResolver();
// Test with \r\n, \n, \r, etc.
});
```
2. **Integration Tests f?r Redis Auth**
```php
test('connects with password from Docker Secret', function () {
// Test with actual Redis instance
});
```
3. **Security Tests**
```php
test('does not expose password in exception messages', function () {
// Verify password is masked
});
```
## Expected Outcomes
Nach Implementierung:
- ? Passwort wird korrekt normalisiert (Whitespace entfernt)
- ? Keine Passw?rter in Logs/Exceptions
- ? Bessere Fehlermeldungen f?r Debugging
- ? Robustere Passwort-Behandlung
- ? Vorbereitung f?r Redis ACL (optional)
## Notes
- Das Hauptproblem ist wahrscheinlich **Whitespace/Newlines im Passwort**
- Die Security-Probleme (Passwort in Exceptions) sollten sofort behoben werden
- ACL-Unterst?tzung ist optional, aber gut f?r Zukunftssicherheit