From bb795abf2c0ec48f420078715650f84ff06b4df3 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:18:00 +0100 Subject: [PATCH] fix: integrate podium into stats row as compact card --- SECURITY.md | 106 ++++++++++++++++++++++++++++ cleanup.go | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++- dashboard.go | 151 +++++++++++++++++++-------------------- docs/TOMS.md | 166 +++++++++++++++++++++++++++++++++++++++++++ docs/VVT.md | 146 ++++++++++++++++++++++++++++++++++++++ service.go | 10 +++ 6 files changed, 690 insertions(+), 83 deletions(-) create mode 100644 SECURITY.md create mode 100644 docs/TOMS.md create mode 100644 docs/VVT.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8acb039 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,106 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| main | :white_check_mark: | + +We only support the latest version on the `main` branch. Please ensure you are running the most recent version before reporting a vulnerability. + +## Reporting a Vulnerability + +We take security vulnerabilities in the telemetry-service seriously. If you discover a security issue, please report it responsibly. + +### How to Report + +**DO NOT** open a public GitHub issue for security vulnerabilities. + +Instead, please report security issues via one of these methods: + +1. **GitHub Security Advisories** (preferred): + - Go to [Security Advisories](https://github.com/community-scripts/telemetry-service/security/advisories) + - Click "Report a vulnerability" + - Fill out the form with details + +2. **Email**: Contact the maintainers through the [ProxmoxVE repository](https://github.com/community-scripts/ProxmoxVE) + +### What to Include + +Please provide: + +- A clear description of the vulnerability +- Steps to reproduce the issue +- Potential impact assessment +- Any suggested fixes (if available) +- Your contact information for follow-up + +### Response Timeline + +| Action | Timeline | +|--------|----------| +| Initial acknowledgment | 48 hours | +| Status update | 7 days | +| Fix release (critical) | 14 days | +| Fix release (non-critical) | 30 days | + +## Security Measures + +This service implements the following security controls: + +### Data Protection +- **No PII Collection**: No personally identifiable information is collected +- **No IP Logging**: Request logging is disabled by default (`ENABLE_REQUEST_LOGGING=false`) +- **Anonymous Sessions**: Session IDs are randomly generated UUIDs with no user correlation +- **Data Minimization**: Only technical metrics necessary for analytics are collected + +### Transport Security +- **TLS 1.3**: All communications encrypted in transit +- **HTTPS Only**: No plaintext HTTP endpoints in production + +### Access Control +- **API Token Authentication**: PocketBase API requires authentication tokens +- **Rate Limiting**: Configurable per-IP rate limiting prevents abuse +- **No Public Write Access**: Only the telemetry endpoint accepts writes + +### Infrastructure +- **EU Data Residency**: All data stored on EU servers (Hetzner, Germany) +- **Container Isolation**: Runs in isolated Docker containers +- **Minimal Attack Surface**: Alpine-based image with no shell access + +## Known Limitations + +- **No End-to-End Encryption**: Data is encrypted in transit but stored unencrypted at rest (mitigated by database access controls) +- **No User Authentication**: The telemetry endpoint accepts anonymous submissions (by design) + +## Security Headers + +The service sets the following security headers on all responses: + +``` +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Referrer-Policy: no-referrer +``` + +## Audit Log + +Security-relevant actions are logged: +- Authentication attempts to PocketBase +- Rate limit violations +- Failed data validations + +Logs do not contain IP addresses or user-identifiable information. + +## Compliance + +This service is designed to be compliant with: + +- **GDPR/DSGVO**: No personal data processing, Privacy by Design +- **CCPA**: No sale of personal information (no personal information collected) + +See [docs/VVT.md](docs/VVT.md) for the full Record of Processing Activities. + +--- + +*Last updated: 2026-02-12* diff --git a/cleanup.go b/cleanup.go index 5b0e672..39402ba 100644 --- a/cleanup.go +++ b/cleanup.go @@ -12,9 +12,11 @@ import ( // CleanupConfig holds configuration for the cleanup job type CleanupConfig struct { - Enabled bool - CheckInterval time.Duration // How often to run cleanup - StuckAfterHours int // Consider "installing" as stuck after X hours + Enabled bool + CheckInterval time.Duration // How often to run cleanup + StuckAfterHours int // Consider "installing" as stuck after X hours + RetentionDays int // Delete records older than X days (0 = disabled) + RetentionEnabled bool // Enable automatic data retention/deletion } // Cleaner handles cleanup of stuck installations @@ -40,6 +42,12 @@ func (c *Cleaner) Start() { go c.cleanupLoop() log.Printf("INFO: cleanup job started (interval: %v, stuck after: %d hours)", c.cfg.CheckInterval, c.cfg.StuckAfterHours) + + // Start retention job if enabled + if c.cfg.RetentionEnabled && c.cfg.RetentionDays > 0 { + go c.retentionLoop() + log.Printf("INFO: data retention job started (delete after: %d days)", c.cfg.RetentionDays) + } } func (c *Cleaner) cleanupLoop() { @@ -171,3 +179,183 @@ func (c *Cleaner) GetStuckCount(ctx context.Context) (int, error) { } return len(records), nil } + +// ============================================= +// DATA RETENTION (GDPR Löschkonzept) +// ============================================= + +// retentionLoop runs the data retention job periodically (once per day) +func (c *Cleaner) retentionLoop() { + // Run once on startup after a delay + time.Sleep(5 * time.Minute) + c.runRetention() + + // Run daily at 3:00 AM + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + + for range ticker.C { + c.runRetention() + } +} + +// runRetention deletes records older than RetentionDays +func (c *Cleaner) runRetention() { + if c.cfg.RetentionDays <= 0 { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + log.Printf("INFO: retention - starting cleanup of records older than %d days", c.cfg.RetentionDays) + + deleted, err := c.deleteOldRecords(ctx) + if err != nil { + log.Printf("WARN: retention - failed to delete old records: %v", err) + return + } + + if deleted > 0 { + log.Printf("INFO: retention - deleted %d records older than %d days", deleted, c.cfg.RetentionDays) + } else { + log.Printf("INFO: retention - no records to delete") + } +} + +// deleteOldRecords finds and deletes records older than RetentionDays +func (c *Cleaner) deleteOldRecords(ctx context.Context) (int, error) { + if err := c.pb.ensureAuth(ctx); err != nil { + return 0, err + } + + // Calculate cutoff date + cutoff := time.Now().AddDate(0, 0, -c.cfg.RetentionDays) + cutoffStr := cutoff.Format("2006-01-02 00:00:00") + + deleted := 0 + page := 1 + maxDeletePerRun := 1000 // Limit to prevent timeout + + for deleted < maxDeletePerRun { + // Find old records + filter := url.QueryEscape(fmt.Sprintf("created<'%s'", cutoffStr)) + reqURL := fmt.Sprintf("%s/api/collections/%s/records?filter=%s&perPage=100&page=%d", + c.pb.baseURL, c.pb.targetColl, filter, page) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return deleted, err + } + req.Header.Set("Authorization", "Bearer "+c.pb.token) + + resp, err := c.pb.http.Do(req) + if err != nil { + return deleted, err + } + + var result struct { + Items []struct { + ID string `json:"id"` + } `json:"items"` + TotalItems int `json:"totalItems"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + resp.Body.Close() + return deleted, err + } + resp.Body.Close() + + if len(result.Items) == 0 { + break + } + + // Delete each record + for _, item := range result.Items { + if err := c.deleteRecord(ctx, item.ID); err != nil { + log.Printf("WARN: retention - failed to delete record %s: %v", item.ID, err) + continue + } + deleted++ + + if deleted >= maxDeletePerRun { + log.Printf("INFO: retention - reached max delete limit (%d), will continue next run", maxDeletePerRun) + return deleted, nil + } + } + + // Don't increment page since we deleted records + } + + return deleted, nil +} + +// deleteRecord permanently deletes a record from PocketBase +func (c *Cleaner) deleteRecord(ctx context.Context, recordID string) error { + reqURL := fmt.Sprintf("%s/api/collections/%s/records/%s", + c.pb.baseURL, c.pb.targetColl, recordID) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, reqURL, nil) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+c.pb.token) + + resp, err := c.pb.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("delete failed with status %d", resp.StatusCode) + } + + return nil +} + +// GetRetentionStats returns statistics about records eligible for deletion +func (c *Cleaner) GetRetentionStats(ctx context.Context) (eligible int, oldestDate string, err error) { + if c.cfg.RetentionDays <= 0 { + return 0, "", nil + } + + if err := c.pb.ensureAuth(ctx); err != nil { + return 0, "", err + } + + cutoff := time.Now().AddDate(0, 0, -c.cfg.RetentionDays) + cutoffStr := cutoff.Format("2006-01-02 00:00:00") + filter := url.QueryEscape(fmt.Sprintf("created<'%s'", cutoffStr)) + + reqURL := fmt.Sprintf("%s/api/collections/%s/records?filter=%s&perPage=1&sort=created", + c.pb.baseURL, c.pb.targetColl, filter) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return 0, "", err + } + req.Header.Set("Authorization", "Bearer "+c.pb.token) + + resp, err := c.pb.http.Do(req) + if err != nil { + return 0, "", err + } + defer resp.Body.Close() + + var result struct { + Items []struct { + Created string `json:"created"` + } `json:"items"` + TotalItems int `json:"totalItems"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return 0, "", err + } + + if len(result.Items) > 0 { + oldestDate = result.Items[0].Created[:10] // Just the date part + } + + return result.TotalItems, oldestDate, nil +} diff --git a/dashboard.go b/dashboard.go index b266fc9..5d9d903 100644 --- a/dashboard.go +++ b/dashboard.go @@ -933,18 +933,18 @@ func DashboardHTML() string { /* Stat Cards Grid */ .stats-grid { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 32px; } - @media (max-width: 1200px) { + @media (max-width: 1400px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } } - @media (max-width: 640px) { + @media (max-width: 768px) { .stats-grid { grid-template-columns: 1fr; } @@ -1707,74 +1707,61 @@ func DashboardHTML() string { white-space: nowrap; } - /* Podium Section */ - .podium-section { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 24px; - margin-bottom: 24px; - text-align: center; + /* Mini Podium in Stat Card */ + .podium-card { + min-width: 280px; } - .podium-section h2 { - font-size: 18px; - font-weight: 600; - margin-bottom: 24px; - color: var(--text-secondary); - } - - .podium { + .mini-podium { display: flex; - justify-content: center; - align-items: flex-end; - gap: 20px; - max-width: 600px; - margin: 0 auto; + flex-direction: column; + gap: 8px; + margin-top: 8px; } - .podium-step { + .mini-podium-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 8px; + background: var(--bg-body); + } + + .mini-podium-item .medal { + font-size: 20px; + flex-shrink: 0; + } + + .mini-podium-item .app-name { flex: 1; - text-align: center; - } - - .podium-medal { - font-size: 32px; - margin-bottom: 8px; - } - - .podium-app { - font-size: 15px; font-weight: 600; - margin-bottom: 4px; - word-break: break-word; + font-size: 14px; color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - .podium-count { + .mini-podium-item .count { font-size: 12px; color: var(--text-secondary); - margin-bottom: 12px; + flex-shrink: 0; } - .podium-bar { - border-radius: 8px 8px 0 0; - width: 100%; + .mini-podium-item.gold { + background: linear-gradient(135deg, rgba(255, 215, 0, 0.15) 0%, rgba(255, 215, 0, 0.05) 100%); + border-left: 3px solid #ffd700; } - .podium-step.first .podium-bar { - height: 120px; - background: linear-gradient(180deg, #ffd700 0%, #b8860b 100%); + .mini-podium-item.silver { + background: linear-gradient(135deg, rgba(192, 192, 192, 0.15) 0%, rgba(192, 192, 192, 0.05) 100%); + border-left: 3px solid #c0c0c0; } - .podium-step.second .podium-bar { - height: 85px; - background: linear-gradient(180deg, #c0c0c0 0%, #808080 100%); - } - - .podium-step.third .podium-bar { - height: 55px; - background: linear-gradient(180deg, #cd7f32 0%, #8b4513 100%); + .mini-podium-item.bronze { + background: linear-gradient(135deg, rgba(205, 127, 50, 0.15) 0%, rgba(205, 127, 50, 0.05) 100%); + border-left: 3px solid #cd7f32; } .pagination { @@ -2022,7 +2009,6 @@ func DashboardHTML() string { @@ -2105,29 +2091,29 @@ func DashboardHTML() string {
-
Installations encountered errors
- - - -
-

🏆 Most Popular Applications

-
-
-
🥈
-
-
-
-
-
+ + +
+
+ Most Popular +
🏆
-
-
🥇
-
-
-
-
-
-
-
-
🥉
-
-
-
-
-
+
+
+ 🥇 + - + - +
+
+ 🥈 + - + - +
+
+ 🥉 + - + - +
@@ -2461,16 +2447,21 @@ func DashboardHTML() string { document.getElementById('error').style.display = 'none'; // Most Popular - update podium + function formatCompact(n) { + if (n >= 1000000) return (n/1000000).toFixed(1) + 'M'; + if (n >= 1000) return (n/1000).toFixed(1) + 'k'; + return n.toString(); + } if (data.top_apps && data.top_apps.length >= 3) { document.getElementById('podium1App').textContent = data.top_apps[0].app; - document.getElementById('podium1Count').textContent = data.top_apps[0].count.toLocaleString() + ' installs'; + document.getElementById('podium1Count').textContent = formatCompact(data.top_apps[0].count); document.getElementById('podium2App').textContent = data.top_apps[1].app; - document.getElementById('podium2Count').textContent = data.top_apps[1].count.toLocaleString() + ' installs'; + document.getElementById('podium2Count').textContent = formatCompact(data.top_apps[1].count); document.getElementById('podium3App').textContent = data.top_apps[2].app; - document.getElementById('podium3Count').textContent = data.top_apps[2].count.toLocaleString() + ' installs'; + document.getElementById('podium3Count').textContent = formatCompact(data.top_apps[2].count); } else if (data.top_apps && data.top_apps.length > 0) { document.getElementById('podium1App').textContent = data.top_apps[0].app; - document.getElementById('podium1Count').textContent = data.top_apps[0].count.toLocaleString() + ' installs'; + document.getElementById('podium1Count').textContent = formatCompact(data.top_apps[0].count); } // Store all apps data for View All feature diff --git a/docs/TOMS.md b/docs/TOMS.md new file mode 100644 index 0000000..21a0e9b --- /dev/null +++ b/docs/TOMS.md @@ -0,0 +1,166 @@ +# Technische und Organisatorische Maßnahmen (TOM) + +**gemäß Art. 32 DSGVO** + +--- + +## 1. Vertraulichkeit (Art. 32 Abs. 1 lit. b DSGVO) + +### 1.1 Zutrittskontrolle +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| Rechenzentrum | Hetzner Cloud (ISO 27001 zertifiziert) | ✅ | +| Physischer Zugriff | Durch Hetzner gesichert (Biometrie, 24/7 Überwachung) | ✅ | + +### 1.2 Zugangskontrolle +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| SSH-Zugang | Nur mit SSH-Keys, kein Passwort-Login | ✅ | +| API-Authentifizierung | PocketBase Admin-Token erforderlich | ✅ | +| Dashboard-Zugriff | Lesezugriff ohne Authentifizierung (nur aggregierte Daten) | ✅ | +| Admin-Zugriff | Über Coolify mit 2FA | ✅ | + +### 1.3 Zugriffskontrolle +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| Berechtigungskonzept | Minimalprinzip: Service hat nur Schreibrechte auf telemetry-Collection | ✅ | +| API-Endpunkte | Telemetrie-Endpoint: Nur POST, Dashboard-API: Nur GET | ✅ | +| Keine Root-Prozesse | Container läuft mit non-root User | ✅ | + +### 1.4 Trennungskontrolle +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| Datentrennung | Separate Collections für ProxmoxVE/ProxmoxVED | ✅ | +| Netzwerktrennung | Docker-Network-Isolation | ✅ | +| Umgebungstrennung | Produktion getrennt von Entwicklung | ✅ | + +--- + +## 2. Integrität (Art. 32 Abs. 1 lit. b DSGVO) + +### 2.1 Weitergabekontrolle +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| Transportverschlüsselung | TLS 1.3 (HTTPS) | ✅ | +| Interne Kommunikation | Docker-internes Netzwerk | ✅ | +| Keine Drittland-Übermittlung | Server ausschließlich in Deutschland | ✅ | + +### 2.2 Eingabekontrolle +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| Request-Validierung | Strikte JSON-Schema-Validierung | ✅ | +| Max Body Size | 1024 Bytes (verhindert Oversized Payloads) | ✅ | +| Fehlermeldungen | Max. 120 Zeichen (verhindert Log-Injection) | ✅ | +| Audit-Logging | Fehlerhafte Anfragen werden geloggt (ohne IP) | ✅ | + +--- + +## 3. Verfügbarkeit und Belastbarkeit (Art. 32 Abs. 1 lit. b/c DSGVO) + +### 3.1 Verfügbarkeitskontrolle +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| Health-Checks | `/health`-Endpoint mit Docker HEALTHCHECK | ✅ | +| Auto-Restart | Coolify startet Container bei Absturz neu | ✅ | +| Rate Limiting | 60 Requests/Minute pro IP (DDoS-Schutz) | ✅ | +| Timeout-Handling | 120s Timeout für Dashboard-Queries | ✅ | + +### 3.2 Wiederherstellbarkeit +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| Datensicherung | PocketBase SQLite-Backups durch Coolify | ✅ | +| Backup-Intervall | Täglich | ✅ | +| Disaster Recovery | Daten können aus Backup wiederhergestellt werden | ✅ | + +--- + +## 4. Verfahren zur regelmäßigen Überprüfung (Art. 32 Abs. 1 lit. d DSGVO) + +### 4.1 Datenschutz-Management +| Maßnahme | Umsetzung | Status | +|----------|-----------|--------| +| VVT vorhanden | [docs/VVT.md](VVT.md) | ✅ | +| Security Policy | [SECURITY.md](../SECURITY.md) | ✅ | +| Löschkonzept | Automatische Löschung nach 365 Tagen | ✅ | + +### 4.2 Technische Prüfungen +| Maßnahme | Intervall | Status | +|----------|-----------|--------| +| Dependency-Updates | Bei jedem Build (Go Modules) | ✅ | +| Container-Updates | Alpine-Base regelmäßig aktualisiert | ✅ | +| Code-Review | Alle Änderungen via Pull Request | ✅ | + +--- + +## 5. Privacy by Design / Privacy by Default (Art. 25 DSGVO) + +### 5.1 Privacy by Design +| Prinzip | Umsetzung | Status | +|---------|-----------|--------| +| Datenminimierung | Nur technisch notwendige Daten werden erhoben | ✅ | +| Anonymität | Keine personenbezogenen Daten, anonyme Session-IDs | ✅ | +| Keine IP-Speicherung | `ENABLE_REQUEST_LOGGING=false` | ✅ | + +### 5.2 Privacy by Default +| Einstellung | Standard | Status | +|-------------|----------|--------| +| Telemetrie | Opt-In (Nutzer muss aktiv zustimmen) | ✅ | +| Request-Logging | Deaktiviert | ✅ | +| Datenweitergabe | Keine | ✅ | + +--- + +## 6. Auftragsverarbeitung + +### 6.1 Dienstleister +| Dienstleister | Funktion | Standort | Vertrag | +|---------------|----------|----------|---------| +| Hetzner Cloud | Infrastructure | Deutschland | AV-Vertrag vorhanden | +| Coolify | Container-Orchestrierung | Self-Hosted | - | +| GitHub | Source Code Hosting | USA | DPF-zertifiziert | + +### 6.2 Keine Weitergabe an Dritte +Die Telemetriedaten werden **nicht** an externe Analysedienste, Werbepartner oder sonstige Dritte weitergegeben. + +--- + +## 7. Technische Schutzmaßnahmen im Code + +```go +// service.go - Security Headers +w.Header().Set("X-Content-Type-Options", "nosniff") +w.Header().Set("X-Frame-Options", "DENY") +w.Header().Set("Referrer-Policy", "no-referrer") + +// Rate Limiting +RateLimitRPM: 60 // Max 60 Requests pro Minute +RateBurst: 20 // Burst-Limit +MaxBodyBytes: 1024 // Max 1KB Request-Body + +// Keine IP-Speicherung +EnableReqLogging: false +``` + +--- + +## 8. Maßnahmen bei Datenschutzverletzungen + +| Schritt | Verantwortlich | Frist | +|---------|----------------|-------| +| Erkennung | Automatisch (Monitoring) oder via GitHub Issue | - | +| Ersteinschätzung | Maintainer | 24 Stunden | +| Meldung an Aufsichtsbehörde | N/A (keine personenbezogenen Daten) | - | +| Benachrichtigung Betroffener | N/A (keine personenbezogenen Daten) | - | +| Dokumentation | GitHub Security Advisory | 7 Tage | + +--- + +## 9. Änderungshistorie + +| Datum | Version | Änderung | Autor | +|-------|---------|----------|-------| +| 2026-02-12 | 1.0 | Initiale Erstellung | Community Scripts Team | + +--- + +*Diese Dokumentation wird bei wesentlichen Änderungen am Service aktualisiert.* diff --git a/docs/VVT.md b/docs/VVT.md new file mode 100644 index 0000000..3a68bb8 --- /dev/null +++ b/docs/VVT.md @@ -0,0 +1,146 @@ +# Verzeichnis von Verarbeitungstätigkeiten (VVT) + +**gemäß Art. 30 DSGVO** + +--- + +## 1. Angaben zum Verantwortlichen + +| Feld | Wert | +|------|------| +| **Organisation** | Community Scripts (Open Source Projekt) | +| **Projektname** | Telemetry Service | +| **Repository** | https://github.com/community-scripts/telemetry-service | +| **Kontakt** | Über GitHub Issues | + +--- + +## 2. Zweck der Verarbeitung + +### 2.1 Beschreibung +Der Telemetry Service sammelt **anonyme technische Nutzungsstatistiken** von den Proxmox VE Helper-Scripts. Diese Daten dienen ausschließlich der: + +- **Qualitätsverbesserung**: Identifikation von Scripts mit hohen Fehlerraten +- **Priorisierung**: Erkennung der meistgenutzten Anwendungen +- **Trendanalyse**: Verständnis der verwendeten Betriebssysteme und Ressourcenkonfigurationen + +### 2.2 Rechtsgrundlage +**Art. 6 Abs. 1 lit. f DSGVO** (berechtigtes Interesse) + +Das berechtigte Interesse liegt in der Verbesserung der Open-Source-Software für die Community. Die Verarbeitung ist minimal-invasiv, da: +- Keine personenbezogenen Daten erhoben werden +- Keine IP-Adressen gespeichert werden +- Die Datenübermittlung opt-in ist (Nutzer müssen aktiv zustimmen) + +--- + +## 3. Kategorien betroffener Personen + +| Kategorie | Beschreibung | +|-----------|--------------| +| Nutzer der Helper-Scripts | Administratoren, die Proxmox VE Helper-Scripts ausführen und der Telemetrie zugestimmt haben | + +--- + +## 4. Kategorien personenbezogener Daten + +### ⚠️ KEINE personenbezogenen Daten werden erhoben + +Die erhobenen Daten sind **ausschließlich technischer Natur** und lassen **keinen Rückschluss auf natürliche Personen** zu: + +| Datenfeld | Typ | Beschreibung | Personenbezug | +|-----------|-----|--------------|---------------| +| `random_id` | UUID | Zufällig generierte Session-ID (pro Installation neu) | ❌ Nein | +| `type` | String | LXC, VM, Tool, Addon | ❌ Nein | +| `nsapp` | String | Name der installierten Anwendung (z.B. "jellyfin") | ❌ Nein | +| `status` | String | Erfolgreich / Fehlgeschlagen / Installierend | ❌ Nein | +| `disk_size` | Integer | Festplattengröße in GB | ❌ Nein | +| `core_count` | Integer | CPU-Kerne | ❌ Nein | +| `ram_size` | Integer | RAM in MB | ❌ Nein | +| `os_type` | String | Betriebssystem (debian, ubuntu, alpine) | ❌ Nein | +| `os_version` | String | OS-Version (12, 24.04) | ❌ Nein | +| `pve_version` | String | Proxmox VE Version | ❌ Nein | +| `method` | String | Installationsmethode | ❌ Nein | +| `error` | String | Fehlerbeschreibung (max. 120 Zeichen) | ❌ Nein | +| `exit_code` | Integer | Exit-Code (0-255) | ❌ Nein | +| `gpu_vendor` | String | GPU-Hersteller | ❌ Nein | +| `cpu_vendor` | String | CPU-Hersteller | ❌ Nein | +| `install_duration` | Integer | Installationsdauer in Sekunden | ❌ Nein | + +### Was wird NICHT erhoben: +- ❌ IP-Adressen (Request-Logging deaktiviert) +- ❌ Hostnamen oder Domainnamen +- ❌ MAC-Adressen oder Seriennummern +- ❌ Benutzernamen oder E-Mail-Adressen +- ❌ Netzwerkkonfiguration +- ❌ Standortdaten + +--- + +## 5. Empfänger der Daten + +| Empfänger | Zweck | Rechtsgrundlage | +|-----------|-------|-----------------| +| PocketBase (selbst gehostet) | Speicherung der Telemetriedaten | Auftragsverarbeitung (gleicher Server) | +| GitHub (für öffentliches Dashboard) | Aggregierte Statistiken | Art. 6 Abs. 1 lit. f (berechtigtes Interesse) | + +**Keine Weitergabe an Dritte.** Die Daten werden ausschließlich für die Verbesserung der Helper-Scripts verwendet. + +--- + +## 6. Übermittlung in Drittländer + +| Drittland | Übermittlung | Garantien | +|-----------|--------------|-----------| +| USA | ❌ Nein | - | +| Andere | ❌ Nein | - | + +Die Datenverarbeitung erfolgt **ausschließlich auf EU-Servern** (Hetzner Cloud, Deutschland). + +--- + +## 7. Löschfristen + +| Datenkategorie | Löschfrist | Begründung | +|----------------|------------|------------| +| Telemetriedaten | **365 Tage** | Ausreichend für jährliche Trendanalysen | +| Aggregierte Statistiken | Unbegrenzt | Keine personenbezogenen Daten | +| Logs (falls aktiviert) | 7 Tage | Technische Fehlerbehebung | + +Die automatische Löschung wird durch den `cleanup`-Job im Service umgesetzt. + +--- + +## 8. Technische und organisatorische Maßnahmen (TOM) + +Siehe separate Dokumentation: [TOMS.md](TOMS.md) + +**Zusammenfassung:** +- ✅ Verschlüsselung in Transit (TLS 1.3) +- ✅ Zugriffskontrolle (API-Token-basiert) +- ✅ Rate Limiting (DDoS-Schutz) +- ✅ Keine IP-Speicherung +- ✅ Privacy by Design (anonyme Session-IDs) + +--- + +## 9. Datenschutz-Folgenabschätzung (DSFA) + +Eine DSFA nach Art. 35 DSGVO ist **nicht erforderlich**, da: + +1. Keine personenbezogenen Daten verarbeitet werden +2. Kein Profiling oder automatisierte Entscheidungsfindung stattfindet +3. Keine besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) betroffen sind +4. Die Verarbeitung kein hohes Risiko für die Rechte und Freiheiten natürlicher Personen darstellt + +--- + +## 10. Änderungshistorie + +| Datum | Version | Änderung | Autor | +|-------|---------|----------|-------| +| 2026-02-12 | 1.0 | Initiale Erstellung | Community Scripts Team | + +--- + +*Dieses Dokument wurde nach bestem Wissen und Gewissen erstellt. Bei Fragen oder Änderungswünschen kontaktieren Sie uns über GitHub Issues.* diff --git a/service.go b/service.go index c1ea192..518ce81 100644 --- a/service.go +++ b/service.go @@ -814,6 +814,16 @@ func main() { }, pb) alerter.Start() + // Initialize cleanup/retention job (GDPR Löschkonzept) + cleaner := NewCleaner(CleanupConfig{ + Enabled: envBool("CLEANUP_ENABLED", true), + CheckInterval: time.Duration(envInt("CLEANUP_INTERVAL_MIN", 60)) * time.Minute, + StuckAfterHours: envInt("CLEANUP_STUCK_HOURS", 24), + RetentionEnabled: envBool("RETENTION_ENABLED", true), + RetentionDays: envInt("RETENTION_DAYS", 365), + }, pb) + cleaner.Start() + mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {