222 lines
8.4 KiB
Go
Executable File
222 lines
8.4 KiB
Go
Executable File
// eBPF violation event watcher.
|
|
//
|
|
// The Warding monitors security violations from the eBPF Tomb Guard
|
|
// program via a perf event buffer. When the eBPF LSM hook detects a
|
|
// write attempt on a protected path (Tomb, State DB), it emits a
|
|
// violation event that this code receives and converts into Warding
|
|
// Alarms.
|
|
//
|
|
// This replaces the former AppArmor / auditd log watcher which parsed
|
|
// /var/log/audit/audit.log for "apparmor=DENIED" strings. The eBPF
|
|
// approach is faster (in-kernel, no log parsing), more precise
|
|
// (per-syscall, not per-audit-message), and works uniformly across
|
|
// LXC, Firecracker, and Podman containers.
|
|
//
|
|
// Architecture:
|
|
//
|
|
// Kernel (eBPF LSM) Userspace (Go)
|
|
// ───────────────── ───────────────
|
|
// tomb_guard.bpf.o audit.go
|
|
// ├─ file_permission hook ───────► WatchViolations()
|
|
// ├─ inode_permission hook ├─ perf event reader
|
|
// └─ trace_execve audit trail ├─ → Alarm{Severity: "taint"}
|
|
// └─ → eventbus.Publish("warding")
|
|
package warding
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// WatchViolations starts watching eBPF violation events from the Tomb Guard
|
|
// program. Each violation becomes an Alarm of severity "taint" — the Cockpit
|
|
// Threat Map renders them in real time via WebSocket.
|
|
//
|
|
// This method replaces the former WatchAuditLogs which parsed AppArmor log
|
|
// entries. It uses the eBPF enforcer's perf buffer for real-time event
|
|
// delivery, eliminating the polling delay and log-rotation edge cases.
|
|
//
|
|
// Blocks until `stop` is closed.
|
|
func (w *Warding) WatchViolations(stop <-chan struct{}) {
|
|
if w.ebpf == nil {
|
|
// eBPF not available — fall back to no-op with a warning.
|
|
w.soundAlarm(Alarm{
|
|
Severity: "taint",
|
|
Message: "eBPF enforcer not loaded — Tomb Guard violation monitoring unavailable",
|
|
})
|
|
<-stop
|
|
return
|
|
}
|
|
|
|
// Forward eBPF violations to Warding alarms.
|
|
handler := func(v EBPFViolation) {
|
|
// Only report write-related violations (the eBPF program sends
|
|
// both exec audit events and file access violations).
|
|
// Skip if NOT a write (bit 1 of AccessMask) AND NOT execve (syscall 59).
|
|
if v.AccessMask&2 == 0 && v.SyscallNr != 59 {
|
|
return
|
|
}
|
|
|
|
// Filter: only care about sorcery-related paths.
|
|
if !strings.Contains(v.Path, "sorcery") && !strings.Contains(v.Path, "tomb") {
|
|
return
|
|
}
|
|
|
|
severity := "taint"
|
|
if v.SyscallNr == 59 {
|
|
severity = "audit" // exec events are audit, not taint
|
|
}
|
|
|
|
w.soundAlarm(Alarm{
|
|
Severity: severity,
|
|
Message: fmt.Sprintf("eBPF DENIED [%s] pid=%d comm=%s path=%s", syscallName(v.SyscallNr), v.PID, v.Comm, v.Path),
|
|
Node: fmt.Sprintf("pid:%d", v.PID),
|
|
})
|
|
}
|
|
|
|
if err := w.ebpf.WatchViolations(handler); err != nil {
|
|
w.soundAlarm(Alarm{
|
|
Severity: "taint",
|
|
Message: "eBPF violation watch failed: " + err.Error(),
|
|
})
|
|
<-stop
|
|
return
|
|
}
|
|
|
|
<-stop
|
|
w.ebpf.StopWatching()
|
|
}
|
|
|
|
// WatchAuditLogs is preserved for backward compatibility and as a fallback
|
|
// when eBPF is not available. It tails the audit log for any remaining
|
|
// kernel-level denials that the eBPF program might not catch (e.g., from
|
|
// other MAC systems still on the host).
|
|
//
|
|
// Deprecated: Use WatchViolations for eBPF-based monitoring.
|
|
func (w *Warding) WatchAuditLogs(path string, startingPos int64, stop <-chan struct{}) {
|
|
if path == "" {
|
|
path = "/var/log/audit/audit.log"
|
|
}
|
|
pos := startingPos
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-ticker.C:
|
|
newPos, events := tailAuditLog(path, pos)
|
|
pos = newPos
|
|
for _, line := range events {
|
|
if !strings.Contains(line, "DENIED") && !strings.Contains(line, "denied") {
|
|
continue
|
|
}
|
|
if !strings.Contains(line, "/var/lib/sorcery") {
|
|
continue
|
|
}
|
|
source := "kernel"
|
|
if strings.Contains(line, "apparmor") {
|
|
source = "AppArmor (legacy)"
|
|
} else if strings.Contains(line, "selinux") {
|
|
source = "SELinux"
|
|
} else if strings.Contains(line, "ebpf") {
|
|
source = "eBPF"
|
|
}
|
|
w.soundAlarm(Alarm{
|
|
Severity: "taint",
|
|
Message: fmt.Sprintf("%s DENIED: %s", source, extractAuditField(line, "name")),
|
|
Node: extractAuditField(line, "comm"),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// tailAuditLog reads new bytes from an audit log file starting at pos.
|
|
func tailAuditLog(path string, pos int64) (int64, []string) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return pos, nil
|
|
}
|
|
defer f.Close()
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return pos, nil
|
|
}
|
|
if info.Size() < pos {
|
|
// File was rotated — start from the beginning.
|
|
pos = 0
|
|
}
|
|
if info.Size() == pos {
|
|
return pos, nil
|
|
}
|
|
if _, err := f.Seek(pos, 0); err != nil {
|
|
return pos, nil
|
|
}
|
|
buf := make([]byte, info.Size()-pos)
|
|
n, err := f.Read(buf)
|
|
if err != nil && n == 0 {
|
|
return pos, nil
|
|
}
|
|
newPos := pos + int64(n)
|
|
lines := strings.Split(string(buf[:n]), "\n")
|
|
// Remove trailing empty string from split.
|
|
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
|
lines = lines[:len(lines)-1]
|
|
}
|
|
return newPos, lines
|
|
}
|
|
|
|
// syscallNameMap maps common syscall numbers to names.
|
|
// Package-level to avoid realloc on every call.
|
|
var syscallNameMap = map[uint32]string{
|
|
2: "open", 3: "close", 59: "execve",
|
|
257: "openat", 262: "newfstatat", 281: "execveat",
|
|
}
|
|
|
|
// syscallName returns a human-readable name for common syscall numbers.
|
|
func syscallName(nr uint32) string {
|
|
if name, ok := syscallNameMap[nr]; ok {
|
|
return name
|
|
}
|
|
return fmt.Sprintf("sys_%d", nr)
|
|
}
|
|
|
|
// EBPFViolation is an alias for the eBPF package's Violation type,
|
|
// used to decouple the warding package from the eBPF package at the
|
|
// type level while keeping the interface clean.
|
|
type EBPFViolation = ebpfViolation
|
|
|
|
// ebpfViolation mirrors the eBPF Violation struct.
|
|
// In production this would reference pkg/warding/ebpf.Violation directly,
|
|
// but we define it here to avoid a build dependency on the generated code.
|
|
type ebpfViolation struct {
|
|
PID uint32
|
|
TID uint32
|
|
UID uint32
|
|
GID uint32
|
|
PPID uint32
|
|
SyscallNr uint32
|
|
Comm string
|
|
Path string
|
|
AccessMask int32
|
|
}
|
|
|
|
// extractAuditField extracts a key=value field from an audit log line.
|
|
// Falls back to "unknown" if the field is not found.
|
|
func extractAuditField(line, key string) string {
|
|
needle := key + "="
|
|
start := strings.Index(line, needle)
|
|
if start == -1 {
|
|
return "unknown"
|
|
}
|
|
val := line[start+len(needle):]
|
|
if end := strings.Index(val, " "); end != -1 {
|
|
val = val[:end]
|
|
}
|
|
// Strip surrounding quotes.
|
|
val = strings.Trim(val, `"'`)
|
|
return val
|
|
} |