// Package warding is the security boundary of the Coven. // // "The Eye of the Ward" watches for Taint (unauthorized file modifications) // via the eBPF Tomb Guard LSM program. The in-kernel eBPF program intercepts // write syscalls to protected paths (/var/lib/sorcery-go/tomb/** and // /var/lib/sorcery-go/state/**) and emits violation events to userspace // via a perf event buffer. // // Node admission is controlled at the network firewall layer (OPNsense / IPFire). // "Warp-Link Blocking" integrates with OpenSnitch or Portmaster to drop // unsanctioned Essence-sync traffic. // // Security architecture (replacing the former AppArmor-based system): // // Layer 1: eBPF LSM (Tomb Guard) — in-kernel MAC enforcement // Layer 2: eBPF cgroup filters — device + network control // Layer 3: Network firewall — OPNsense / IPFire isolates the Coven // Layer 4: Network gatekeeping — OpenSnitch / Portmaster // Layer 5: Integrity verification — Merkle root + per-blob hashing // Layer 6: Quarantine — cgroup freezer (multi-runtime) // // On boot, the Warding runs Inspect() on every Essence a Sanctum tries to // reanimate. If the Merkle root computed from the on-disk bytes does not // match the recorded EssenceID, the Reanimation is rejected and an alarm // is broadcast to all connected nodes. // // The Warding also verifies PGP signatures on DETAILS files before the // Cauldron sources them — see pgp.go. package warding import ( "fmt" "sync" "time" "dcos.net/sorcery-go/pkg/eventbus" ) // TombReader is the read interface warding needs from the Tomb. Kept small // to avoid an import cycle. type TombReader interface { VerifyRoot(essenceID string) error VerifyBlobs(essenceID string) error } // EBPFEnforcer is the interface for the eBPF security enforcement layer. // This allows the warding package to use the eBPF enforcer without a // direct dependency on the cilium/ebpf library (which is platform-specific). type EBPFEnforcer interface { // Load compiles and loads eBPF programs into the kernel. Load() error // AttachCgroup attaches cgroup filter programs. AttachCgroup(cgroupPath string) error // SetEnforceMode sets permissive (0) or enforcing (1). SetEnforceMode(mode uint32) error // GetEnforceMode returns the current enforcement mode. GetEnforceMode() (uint32, error) // SetTombPath configures the protected Tomb path prefix. SetTombPath(path string) error // SetStatePath configures the protected State path prefix. SetStatePath(path string) error // AddTrustedPID adds a PID to the enforcement bypass allowlist. AddTrustedPID(pid uint32) error // SelfTrustedPID adds the current process to the trusted allowlist. SelfTrustedPID() error // WatchViolations starts the perf buffer reader for violation events. WatchViolations(handler func(Violation)) error // StopWatching stops the violation event reader. StopWatching() // Close releases all kernel resources. Close() // IsLoaded reports whether eBPF programs are loaded. IsLoaded() bool // IsCgroupAttached reports whether cgroup filters are attached. IsCgroupAttached() bool // Status returns a human-readable status summary. Status() string } // Violation represents an eBPF-detected security violation event. type Violation struct { PID uint32 `json:"pid"` TID uint32 `json:"tid"` UID uint32 `json:"uid"` GID uint32 `json:"gid"` PPID uint32 `json:"ppid"` SyscallNr uint32 `json:"syscall_nr"` Comm string `json:"comm"` Path string `json:"path"` AccessMask int32 `json:"access_mask"` } // maxAlarms caps the in-memory alarm ring. When exceeded, oldest // alarms are trimmed to prevent unbounded memory growth in long-running // deployments. const maxAlarms = 10000 // Warding is the central defensive service. type Warding struct { mu sync.Mutex Tomb TombReader Bus *eventbus.Bus // alarms are published here too Alarms []Alarm QuarantineList map[string]bool ebpf EBPFEnforcer // eBPF enforcement layer (nil if unavailable) } // Alarm is one event the Warding has flagged. type Alarm struct { Time time.Time `json:"time"` Severity string `json:"severity"` // "taint" | "egress-block" | "license-violation" | "quarantine" | "banish" | "pgp" | "audit" Node string `json:"node"` EssenceID string `json:"essence_id"` Message string `json:"message"` } // New returns a Warding backed by the given Tomb. func New(t TombReader, bus *eventbus.Bus) *Warding { return &Warding{ Tomb: t, Bus: bus, QuarantineList: make(map[string]bool), } } // NewWithEBPF returns a Warding with eBPF enforcement enabled. func NewWithEBPF(t TombReader, bus *eventbus.Bus, enforcer EBPFEnforcer) *Warding { return &Warding{ Tomb: t, Bus: bus, QuarantineList: make(map[string]bool), ebpf: enforcer, } } // InitEBPF loads the eBPF enforcement programs and configures them for // the given Tomb and State paths. This should be called once at startup. func (w *Warding) InitEBPF(tombPath, statePath string) error { if w.ebpf == nil { return fmt.Errorf("warding: no eBPF enforcer configured") } // Load eBPF programs into the kernel. if err := w.ebpf.Load(); err != nil { return fmt.Errorf("warding: eBPF load failed: %w", err) } // Configure protected paths. if tombPath != "" { if err := w.ebpf.SetTombPath(tombPath); err != nil { return fmt.Errorf("warding: set tomb path: %w", err) } } if statePath != "" { if err := w.ebpf.SetStatePath(statePath); err != nil { return fmt.Errorf("warding: set state path: %w", err) } } // Trust the current process. if err := w.ebpf.SelfTrustedPID(); err != nil { return fmt.Errorf("warding: trust self PID: %w", err) } return nil } // AttachEBPF attaches the eBPF cgroup filters to the given cgroup path. // This enables device whitelisting and network filtering for the cgroup. func (w *Warding) AttachEBPF(cgroupPath string) error { if w.ebpf == nil { return fmt.Errorf("warding: no eBPF enforcer configured") } return w.ebpf.AttachCgroup(cgroupPath) } // SetEnforceMode controls the eBPF enforcement mode: // 0 = permissive (log violations but don't block) // 1 = enforcing (block violations and log them) func (w *Warding) SetEnforceMode(enforcing bool) error { if w.ebpf == nil { return nil } mode := uint32(0) if enforcing { mode = 1 } return w.ebpf.SetEnforceMode(mode) } // EBPFStatus returns the status of the eBPF enforcement layer. func (w *Warding) EBPFStatus() string { if w.ebpf == nil { return "eBPF: not configured" } return w.ebpf.Status() } // Inspect verifies the Merkle root AND every blob of an Essence. If either // check fails the Essence is "Tainted" — the Warding sounds an alarm and // refuses to allow Reanimation. func (w *Warding) Inspect(essenceID string) error { if err := w.Tomb.VerifyRoot(essenceID); err != nil { w.soundAlarm(Alarm{ Severity: "taint", EssenceID: essenceID, Message: "merkle root mismatch: " + err.Error(), }) return fmt.Errorf("warding: rejected Essence %s: %w", essenceID, err) } if err := w.Tomb.VerifyBlobs(essenceID); err != nil { w.soundAlarm(Alarm{ Severity: "taint", EssenceID: essenceID, Message: "blob bit-rot: " + err.Error(), }) return fmt.Errorf("warding: blob verification failed for %s: %w", essenceID, err) } return nil } // Quarantine freezes a Sanctum via the configured runtime adapter and severs // its Essence links so memory-based exploits can't spread. Works across // LXC, Podman, Firecracker, and baremetal deployments. func (w *Warding) Quarantine(nodeID string) { w.mu.Lock() defer w.mu.Unlock() w.QuarantineList[nodeID] = true if err := freezeContainer(nodeID); err != nil { w.soundAlarmLocked(Alarm{ Severity: "quarantine", Node: nodeID, Message: "freeze failed: " + err.Error(), }) } else { w.soundAlarmLocked(Alarm{ Severity: "quarantine", Node: nodeID, Message: "Sanctum quarantined via cgroup/runtime freeze", }) } } // ThawUnfreeze resumes a previously quarantined Sanctum. func (w *Warding) ThawUnfreeze(nodeID string) error { w.mu.Lock() defer w.mu.Unlock() delete(w.QuarantineList, nodeID) return thawContainer(nodeID) } // IsQuarantined reports whether a node is currently frozen. func (w *Warding) IsQuarantined(nodeID string) bool { w.mu.Lock() defer w.mu.Unlock() return w.QuarantineList[nodeID] } // AlarmsSince returns all alarms emitted after `t`. The Cockpit "Sanctum" // heatmap renders these in real time via WebSocket. func (w *Warding) AlarmsSince(t time.Time) []Alarm { w.mu.Lock() defer w.mu.Unlock() var out []Alarm for _, a := range w.Alarms { if a.Time.After(t) { out = append(out, a) } } return out } // Banish quarantines a Sanctum by freezing its container runtime and // recording the banishment in the Warding alarm log. func (w *Warding) Banish(nodeID string) { w.mu.Lock() defer w.mu.Unlock() w.QuarantineList[nodeID] = true freezeErr := freezeContainer(nodeID) w.soundAlarmLocked(Alarm{ Severity: "quarantine", Node: nodeID, Message: "Sanctum quarantined via cgroup/runtime freeze", }) if freezeErr != nil { w.soundAlarmLocked(Alarm{ Severity: "quarantine", Node: nodeID, Message: "freeze failed: " + freezeErr.Error(), }) } w.soundAlarmLocked(Alarm{ Severity: "banish", Node: nodeID, Message: "node banished from the Coven", }) } // Close cleans up all resources, including the eBPF enforcement layer. func (w *Warding) Close() { w.mu.Lock() defer w.mu.Unlock() if w.ebpf != nil { w.ebpf.Close() } } // soundAlarm acquires the lock and sounds an alarm. Use this when the // caller does NOT already hold w.mu. func (w *Warding) soundAlarm(a Alarm) { w.mu.Lock() defer w.mu.Unlock() w.soundAlarmLocked(a) } // soundAlarmLocked appends an alarm and publishes it on the bus. // The caller MUST already hold w.mu. func (w *Warding) soundAlarmLocked(a Alarm) { if a.Time.IsZero() { a.Time = time.Now() } w.Alarms = append(w.Alarms, a) // Trim oldest alarms when the ring exceeds the cap. if len(w.Alarms) > maxAlarms { trimmed := make([]Alarm, maxAlarms) copy(trimmed, w.Alarms[len(w.Alarms)-maxAlarms:]) w.Alarms = trimmed } if w.Bus != nil { w.Bus.Publish("warding", eventbus.Event{ Type: eventbus.EventAlarm, Data: a.Severity + ": " + a.Message, }) } }