mirror of
https://github.com/community-unscripted/telemetry-service.git
synced 2026-06-30 20:57:55 -04:00
fix: integrate podium into stats row as compact card
This commit is contained in:
+106
@@ -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*
|
||||
+191
-3
@@ -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
|
||||
}
|
||||
|
||||
+71
-80
@@ -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 {
|
||||
<select id="repoFilter" class="custom-select" onchange="refreshData()">
|
||||
<option value="ProxmoxVE" selected>ProxmoxVE</option>
|
||||
<option value="ProxmoxVED">ProxmoxVED</option>
|
||||
<option value="Proxmox VE">Proxmox VE (Legacy)</option>
|
||||
<option value="external">External</option>
|
||||
<option value="all">All Sources</option>
|
||||
</select>
|
||||
@@ -2105,29 +2091,29 @@ func DashboardHTML() string {
|
||||
<div class="stat-card-value" id="failedCount">-</div>
|
||||
<div class="stat-card-subtitle">Installations encountered errors</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Most Popular Podium -->
|
||||
<div class="podium-section">
|
||||
<h2>🏆 Most Popular Applications</h2>
|
||||
<div class="podium">
|
||||
<div class="podium-step second">
|
||||
<div class="podium-medal">🥈</div>
|
||||
<div class="podium-app" id="podium2App">-</div>
|
||||
<div class="podium-count" id="podium2Count">-</div>
|
||||
<div class="podium-bar"></div>
|
||||
|
||||
<!-- Most Popular Card -->
|
||||
<div class="stat-card podium-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">Most Popular</span>
|
||||
<div class="stat-card-icon" style="font-size: 20px;">🏆</div>
|
||||
</div>
|
||||
<div class="podium-step first">
|
||||
<div class="podium-medal">🥇</div>
|
||||
<div class="podium-app" id="podium1App">-</div>
|
||||
<div class="podium-count" id="podium1Count">-</div>
|
||||
<div class="podium-bar"></div>
|
||||
</div>
|
||||
<div class="podium-step third">
|
||||
<div class="podium-medal">🥉</div>
|
||||
<div class="podium-app" id="podium3App">-</div>
|
||||
<div class="podium-count" id="podium3Count">-</div>
|
||||
<div class="podium-bar"></div>
|
||||
<div class="mini-podium">
|
||||
<div class="mini-podium-item gold">
|
||||
<span class="medal">🥇</span>
|
||||
<span class="app-name" id="podium1App">-</span>
|
||||
<span class="count" id="podium1Count">-</span>
|
||||
</div>
|
||||
<div class="mini-podium-item silver">
|
||||
<span class="medal">🥈</span>
|
||||
<span class="app-name" id="podium2App">-</span>
|
||||
<span class="count" id="podium2Count">-</span>
|
||||
</div>
|
||||
<div class="mini-podium-item bronze">
|
||||
<span class="medal">🥉</span>
|
||||
<span class="app-name" id="podium3App">-</span>
|
||||
<span class="count" id="podium3Count">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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
|
||||
|
||||
+166
@@ -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.*
|
||||
+146
@@ -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.*
|
||||
+10
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user