syncova-backup/packages/agentregistry/registry.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

221 lines
8.5 KiB
Go

// Package agentregistry verwaltet die am Control Server angemeldeten Agents.
//
// Leitgedanke aus PROMPT.md §59: Ein kompromittierter Agent darf nicht
// automatisch vollständigen Zugriff auf die Backup-Infrastruktur erhalten.
// Daraus folgt die Trennung in zwei Tokenarten:
//
// - Ein Aufnahme-Token gilt einmalig und nur zur Registrierung.
// - Ein Betriebs-Token gilt dauerhaft, trägt aber ausschließlich
// agentspezifische Rechte und ist einzeln widerrufbar.
package agentregistry
import (
"context"
"errors"
"time"
"github.com/google/uuid"
)
// AgentStatus ist der Betriebszustand eines Agents.
type AgentStatus string
const (
// AgentStatusActive bedeutet: der Agent darf arbeiten.
AgentStatusActive AgentStatus = "active"
// AgentStatusDisabled bedeutet: der Agent ist vorübergehend abgeschaltet.
AgentStatusDisabled AgentStatus = "disabled"
// AgentStatusRevoked bedeutet: der Agent ist dauerhaft gesperrt.
AgentStatusRevoked AgentStatus = "revoked"
)
// Platform benennt das Betriebssystem eines Agents.
type Platform string
const (
// PlatformWindows ist ein Windows-System.
PlatformWindows Platform = "windows"
// PlatformLinux ist ein Linux-System.
PlatformLinux Platform = "linux"
// PlatformDarwin ist ein macOS-System.
PlatformDarwin Platform = "darwin"
)
// IsSupported meldet, ob eine Plattform bekannt ist.
func (platform Platform) IsSupported() bool {
switch platform {
case PlatformWindows, PlatformLinux, PlatformDarwin:
return true
default:
return false
}
}
// Agent beschreibt einen registrierten Agent.
type Agent struct {
// ID ist der öffentliche Bezeichner.
ID uuid.UUID `json:"id"`
// Name ist die sprechende Bezeichnung.
Name string `json:"name"`
// Hostname ist der gemeldete Rechnername.
Hostname string `json:"hostname,omitempty"`
// Platform ist das Betriebssystem.
Platform Platform `json:"platform"`
// Architecture ist die Rechnerarchitektur.
Architecture string `json:"architecture,omitempty"`
// Version ist die Programmversion des Agents.
Version string `json:"version,omitempty"`
// Status ist der Betriebszustand.
Status AgentStatus `json:"status"`
// LastHeartbeatAt ist der Zeitpunkt der letzten Lebendmeldung in UTC.
LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty"`
// LastIPAddress ist die zuletzt gesehene Absenderadresse.
LastIPAddress string `json:"last_ip_address,omitempty"`
// RegisteredAt ist der Zeitpunkt der Aufnahme in UTC.
RegisteredAt time.Time `json:"registered_at"`
}
// HeartbeatAge liefert die Zeit seit der letzten Lebendmeldung.
//
// Der zweite Rückgabewert meldet, ob überhaupt je eine einging. Ein Agent ohne
// Lebendmeldung ist nicht dasselbe wie einer mit alter Meldung.
func (agent Agent) HeartbeatAge(referenceTime time.Time) (time.Duration, bool) {
if agent.LastHeartbeatAt == nil {
return 0, false
}
return referenceTime.Sub(*agent.LastHeartbeatAt), true
}
// IsOffline meldet, ob ein Agent länger als die erlaubte Frist stumm ist.
//
// Ein stummer Agent bedeutet: von diesem System kommen keine Backups mehr.
// Das ist eine Warnung wert (PROMPT.md §38).
func (agent Agent) IsOffline(referenceTime time.Time, allowedSilence time.Duration) bool {
heartbeatAge, hasHeartbeat := agent.HeartbeatAge(referenceTime)
if !hasHeartbeat {
// Ein Agent, der sich nie gemeldet hat, gilt ab der Frist nach seiner
// Aufnahme als offline.
return referenceTime.Sub(agent.RegisteredAt) > allowedSilence
}
return heartbeatAge > allowedSilence
}
// EnrollmentToken ist ein einmaliges Aufnahme-Token.
type EnrollmentToken struct {
// ID ist der Bezeichner des Tokens.
ID uuid.UUID `json:"id"`
// Token ist der Klartext.
//
// Er verlässt den Server genau einmal bei der Ausstellung; danach liegt
// nur noch sein Hash vor.
Token string `json:"token"`
// AgentName ist der vorgesehene Name des aufzunehmenden Agents.
AgentName string `json:"agent_name"`
// ExpiresAt ist die Ablaufzeit in UTC.
ExpiresAt time.Time `json:"expires_at"`
}
// RegistrationResult ist das Ergebnis einer Agent-Registrierung.
type RegistrationResult struct {
// Agent ist der angelegte Agent.
Agent Agent `json:"agent"`
// AgentToken ist das Betriebstoken im Klartext.
//
// Es verlässt den Server genau einmal. Geht es verloren, muss der Agent
// neu aufgenommen oder sein Token gewechselt werden.
AgentToken string `json:"agent_token"`
}
// RegistrationRequest beschreibt einen sich anmeldenden Agent.
type RegistrationRequest struct {
// EnrollmentToken ist das Aufnahme-Token.
EnrollmentToken string
// Hostname ist der Rechnername des Systems.
Hostname string
// Platform ist das Betriebssystem.
Platform Platform
// Architecture ist die Rechnerarchitektur.
Architecture string
// Version ist die Programmversion des Agents.
Version string
// IPAddress ist die Absenderadresse.
IPAddress string
}
// HeartbeatRequest ist eine Lebendmeldung eines Agents.
type HeartbeatRequest struct {
// Version ist die aktuelle Programmversion des Agents.
//
// Sie wird bei jeder Meldung übernommen, damit ein Update sofort sichtbar
// ist und veraltete Agents erkennbar bleiben (PROMPT.md §90).
Version string
// IPAddress ist die Absenderadresse.
IPAddress string
}
// Fehler der Agent-Verwaltung.
var (
// ErrEnrollmentTokenInvalid meldet ein unbrauchbares Aufnahme-Token.
//
// Der Fehler unterscheidet bewusst nicht zwischen unbekannt, abgelaufen und
// bereits eingelöst: eine Unterscheidung erlaubte es, gültige Tokens zu erraten.
ErrEnrollmentTokenInvalid = errors.New("das aufnahme-token ist ungültig, abgelaufen oder bereits verwendet")
// ErrAgentTokenInvalid meldet ein unbrauchbares Betriebstoken.
ErrAgentTokenInvalid = errors.New("das agent-token ist ungültig oder widerrufen")
// ErrAgentNotFound meldet einen nicht vorhandenen Agent.
ErrAgentNotFound = errors.New("der agent existiert nicht")
// ErrAgentRevoked meldet einen gesperrten Agent.
ErrAgentRevoked = errors.New("der agent ist gesperrt")
// ErrUnsupportedPlatform meldet eine unbekannte Plattform.
ErrUnsupportedPlatform = errors.New("die plattform wird nicht unterstützt")
)
// defaultEnrollmentValidity ist die Gültigkeitsdauer eines Aufnahme-Tokens.
//
// Eine Stunde reicht für eine geplante Installation und begrenzt zugleich das
// Zeitfenster, in dem ein abgefangenes Token nützt.
const defaultEnrollmentValidity = time.Hour
// AgentFilter schränkt eine Agent-Abfrage ein.
type AgentFilter struct {
// Status beschränkt auf einen Betriebszustand.
Status string
// Platform beschränkt auf ein Betriebssystem.
Platform string
// Page ist die gewünschte Seite, beginnend bei 1.
Page int
// PageSize ist die Anzahl der Einträge je Seite.
PageSize int
}
// Store kapselt die Ablage der Agent-Daten.
//
// Die Schnittstelle trennt die Domänenlogik von der Datenbank und macht sie
// einzeln prüfbar.
type Store interface {
// CreateEnrollmentToken legt ein Aufnahme-Token an.
CreateEnrollmentToken(createContext context.Context, tokenHash string, agentName string, expiresAt time.Time, createdBy *uuid.UUID) (uuid.UUID, error)
// ConsumeEnrollmentToken löst ein Aufnahme-Token ein und liefert den vorgesehenen Namen.
ConsumeEnrollmentToken(consumeContext context.Context, tokenHash string, agentID uuid.UUID) (agentName string, consumeError error)
// PeekEnrollmentToken liest den vorgesehenen Namen, ohne einzulösen.
PeekEnrollmentToken(peekContext context.Context, tokenHash string) (agentName string, peekError error)
// CreateAgent legt einen Agent an.
CreateAgent(createContext context.Context, agentToCreate Agent) (uuid.UUID, error)
// CreateAgentToken legt ein Betriebstoken an.
CreateAgentToken(createContext context.Context, agentID uuid.UUID, tokenHash string) (uuid.UUID, error)
// FindAgentByTokenHash sucht einen Agent anhand seines Betriebstokens.
FindAgentByTokenHash(queryContext context.Context, tokenHash string) (Agent, error)
// FindAgentByID sucht einen Agent anhand seiner Kennung.
FindAgentByID(queryContext context.Context, agentID uuid.UUID) (Agent, error)
// ListAgents liefert eine Seite von Agents.
ListAgents(queryContext context.Context, agentFilter AgentFilter) ([]Agent, int64, error)
// RecordHeartbeat vermerkt eine Lebendmeldung.
RecordHeartbeat(updateContext context.Context, agentID uuid.UUID, agentVersion string, ipAddress string) error
// RevokeAgent sperrt einen Agent und alle seine Tokens.
RevokeAgent(updateContext context.Context, agentID uuid.UUID) error
// RotateAgentToken widerruft alle Tokens eines Agents und legt ein neues an.
RotateAgentToken(updateContext context.Context, agentID uuid.UUID, newTokenHash string) error
}