This document describes the internal architecture of AegisBPF, an eBPF-based runtime security agent.
AegisBPF uses eBPF (extended Berkeley Packet Filter) to monitor and optionally block process executions at the kernel level. It leverages the BPF LSM (Linux Security Module) hooks for enforcement and tracepoints for audit-only monitoring.
┌─────────────────────────────────────────────────────────────────┐
│ User Space │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ aegisbpf daemon │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │ │
│ │ │ Policy │ │ BPF │ │ Event │ │ Metrics │ │ │
│ │ │ Manager │ │ Ops │ │ Handler │ │ (Prometheus) │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────────┬────────┘ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌────┴───────────┴───────────────┘ │ │
│ │ │ │ │ │
│ │ │ ▼ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │
│ │ └─►│ libbpf │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ │ bpf() syscall │
├──────────────────────────────┼──────────────────────────────────┤
│ Kernel Space │
│ │ │
│ ┌───────────────────────────┴───────────────────────────────┐ │
│ │ BPF Subsystem │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐│ │
│ │ │ LSM Hook │ │ Tracepoint │ │ BPF Maps ││ │
│ │ │ file_open │ │ sched_exec │ │ ┌───────────────┐ ││ │
│ │ │ │ │ │ │ │ deny_inode │ ││ │
│ │ │ (enforce) │ │ (audit) │ │ │ deny_path │ ││ │
│ │ │ │ │ │ │ │ allow_cgroup │ ││ │
│ │ └──────┬──────┘ └──────┬──────┘ │ │ events (ring) │ ││ │
│ │ │ │ │ │ block_stats │ ││ │
│ │ └────────────────┴─────────┴──┴───────────────┴──┘│ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ exec() syscall │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The BPF program runs in kernel context and implements:
sched_process_exec: emits EXEC events for process executionssys_enter_openat: emits audit-only BLOCK events for deny_path matchesdeny_inode: Hash map of blocked (dev, inode) pairsdeny_path: Hash map of blocked path hashesallow_cgroup: Hash map of allowed cgroup IDsevents: Ring buffer for sending events to userspaceblock_stats: Global counters for blocks and dropsdeny_cgroup_stats: Per-cgroup block countersdeny_inode_stats: Per-inode block countersdeny_path_stats: Per-path block countersagent_meta: Agent metadata (layout version)Entry point and CLI interface:
BPF operations layer:
Key functions:
load_bpf(): Load and optionally pin BPF objectsattach_all(): Attach programs to LSM/tracepointsadd_deny_inode(): Add entry to deny listread_block_stats_map(): Read statisticsPolicy file management:
Event handling:
Utility functions:
SHA256 implementation:
Seccomp filter:
Structured logging:
Error handling:
Result<T> type for success/failureError class with code, message, contextTRY() macro for early return1. Process calls open("/etc/shadow")
│
▼
2. Kernel invokes file_open LSM hook
│
▼
3. BPF program handle_file_open runs
│
├─── Check allow_cgroup map
│ └─ If cgroup allowed → ALLOW
│
└─── Check deny_inode map
└─ If inode blocked → DENY + emit event
│
▼
4. Return 0 (allow) or -EPERM (deny)
│
▼
5. If denied, ring buffer event sent to userspace
│
▼
6. aegisbpf daemon receives event
│
▼
7. Event logged to journald/stdout
1. Process executes a binary (execve)
│
▼
2. execve() completes successfully
│
▼
3. Kernel fires sched_process_exec tracepoint
│
▼
4. BPF program handle_execve runs
│
▼
5. EXEC event emitted to ring buffer
│
▼
6. aegisbpf daemon receives event
│
▼
7. Event logged to journald/stdout
1. Process opens a file (open/openat)
│
▼
2. Kernel fires sys_enter_openat tracepoint
│
▼
3. BPF program handle_openat runs
│
▼
4. If path is in deny_path, emit audit-only BLOCK event
│
▼
5. aegisbpf daemon receives event
│
▼
6. Event logged to journald/stdout
Maps are pinned to /sys/fs/bpf/aegis/ for persistence:
/sys/fs/bpf/aegis/
├── deny_inode # Blocked inodes
├── deny_path # Blocked paths
├── allow_cgroup # Allowed cgroups
├── events # Ring buffer (not pinned)
├── block_stats # Global counters
├── deny_cgroup_stats # Per-cgroup stats
├── deny_inode_stats # Per-inode stats
├── deny_path_stats # Per-path stats
└── agent_meta # Layout version
Pinning allows:
AegisBPF uses a Result
Result<InodeId> path_to_inode(const std::string& path) {
struct stat st;
if (stat(path.c_str(), &st) != 0) {
return Error::system(errno, "stat failed");
}
return InodeId{static_cast<uint32_t>(st.st_dev), st.st_ino};
}
// Usage with TRY macro
Result<void> block_file(const std::string& path) {
auto inode = TRY(path_to_inode(path));
TRY(add_to_deny_map(inode));
return {};
}
Benefits:
| Feature | Minimum Version | Notes |
|---|---|---|
| BPF CO-RE | 5.5 | Compile-once, run-everywhere |
| BPF LSM | 5.7 | Required for enforce mode |
| Ring buffer | 5.8 | More efficient than perf buffer |
| CAP_BPF | 5.8 | Dedicated capability |
| BTF | 5.2 | Type information |