agentmon uses seccomp-bpf to enforce syscall-level security controls on agent processes.
When enabled, seccomp filtering provides four types of protection:
- Unix Socket Monitoring: Intercepts socket operations for policy-based access control
- File Monitoring: Intercepts filesystem operations for policy-based access control
- Signal Interception: Intercepts signal delivery for policy-based allow/deny/redirect
- Syscall Blocking: Denies (and optionally kills) processes that attempt blocked syscalls
sandbox:
seccomp:
enabled: true
mode: enforce # enforce | audit | disabled
unix_socket:
enabled: true
action: enforce # enforce | audit
signal_filter:
enabled: true
action: enforce # enforce | audit
file_monitor:
enabled: true
enforce_without_fuse: true
intercept_metadata: false
write_only_opens: true # default: true when intercept_metadata is false
syscalls:
default_action: allow # allow | block
block:
- ptrace
- process_vm_readv
- process_vm_writev
- mount
- umount2
# ... see defaults below
on_block: errno # errno | kill | log | log_and_kill (default: errno)
# Per-AF_* socket family blocking on socket(2) and socketpair(2).
# Unset → recommended-default list applied (12 families at errno).
# Set to [] → opt out of all family blocking entirely.
# Non-empty list → overrides defaults.
blocked_socket_families:
- family: AF_ALG # by name (preferred) or numeric ("38")
action: errno # errno | kill | log | log_and_kill (default: errno)
- family: AF_VSOCK
action: log_and_kill
# Advisory mitigation sets. Built-ins are embedded in agentmon; external
# directories are optional and only requested IDs are loaded.
# mitigation_sets:
# - dirtyfrag-conservative
# mitigation_dirs:
# - /etc/agentmon/mitigations
# Lower-level socket tuple rules are also available as an alternative.
# socket_rules:
# - name: dirtyfrag-conservative-rxrpc
# family: AF_RXRPC
# action: log_and_kill
# - name: dirtyfrag-conservative-xfrm
# family: AF_NETLINK
# protocol: NETLINK_XFRM
# action: log_and_killFile monitoring uses SECCOMP_RET_USER_NOTIF to evaluate filesystem policy for file syscalls. When write_only_opens is true, the seccomp filter only traps openat and legacy open calls whose flags request write/create behavior (O_WRONLY, O_RDWR, O_CREAT, O_TMPFILE, O_TRUNC, or O_APPEND). Read-only opens stay on the kernel fast path and do not emit file-open events.
openat2 is still trapped when file monitoring is enabled because its flags live in the user-space open_how struct; seccomp-BPF cannot dereference that pointer safely in the kernel filter.
Signal filtering uses SECCOMP_RET_USER_NOTIF to intercept signal-related syscalls before they execute. This allows agentmon to evaluate policy rules and decide whether to allow, deny, redirect, or audit the signal.
| Syscall | Purpose |
|---|---|
kill |
Send signal to process by PID |
tkill |
Send signal to thread by TID |
tgkill |
Send signal to thread in specific process |
rt_sigqueueinfo |
Queue signal with additional data |
pidfd_send_signal |
Send signal via process file descriptor |
- Process calls
kill(pid, SIGTERM)or similar - seccomp traps the syscall and notifies agentmon via the user-notify fd
- agentmon classifies the target (self, child, external, system, etc.)
- Policy rules are evaluated for the signal/target combination
- Decision is executed:
- allow: Syscall continues normally
- deny: Returns EPERM to caller
- redirect: Signal number is modified (e.g., SIGKILL → SIGTERM)
- audit: Syscall allowed, event logged
Signal rules are defined in the policy file:
signal_rules:
# Allow signals to self and children
- name: allow-self
signals: ["@all"]
target:
type: self
decision: allow
- name: allow-children
signals: ["@all"]
target:
type: children
decision: allow
# Block fatal signals to external processes
- name: deny-external-fatal
signals: ["@fatal"]
target:
type: external
decision: deny
# Redirect SIGKILL to SIGTERM for graceful shutdown
- name: graceful-kill
signals: ["SIGKILL"]
target:
type: descendants
decision: redirect
redirect_to: SIGTERM| Group | Signals |
|---|---|
@all |
All signals (1-31) |
@fatal |
SIGKILL, SIGTERM, SIGQUIT, SIGABRT |
@job |
SIGSTOP, SIGCONT, SIGTSTP, SIGTTIN, SIGTTOU |
@reload |
SIGHUP, SIGUSR1, SIGUSR2 |
{
"type": "signal_blocked",
"timestamp": "2026-01-11T10:30:00Z",
"session_id": "sess_abc123",
"sender_pid": 12345,
"target_pid": 1,
"signal": "SIGKILL",
"signal_number": 9,
"target_type": "system",
"decision": "deny",
"policy_rule": "deny-system-signals"
}See Policy Documentation for full configuration options.
Execve interception uses SECCOMP_RET_USER_NOTIF to trap execve and execveat syscalls, allowing agentmon to evaluate command execution against policy before it happens.
Before policy evaluation, agentmon resolves the executable path using filepath.EvalSymlinks. This defeats bypass attacks using:
- Symlinks to blocked binaries (e.g.,
ln -s /usr/bin/wget /tmp/safe && /tmp/safe) /proc/self/rootpaths (e.g.,/proc/self/root/usr/bin/wget)- Relative path tricks
The original (pre-canonicalization) path is preserved in audit events as raw_filename for forensic analysis.
When a wrapper command (like env, sudo, or ld-linux) is detected, agentmon "unwraps" it to find the real payload command and evaluates both against policy. The most restrictive decision wins.
Example: env wget http://evil.com
envis recognized as transparent → unwrap- Payload
wgetfound after skipping flags/assignments - Both
envandwgetevaluated: ifwgetis denied, the whole execution is denied
See Policy Documentation for configuration.
{
"type": "execve",
"timestamp": "2026-03-04T10:30:00Z",
"session_id": "sess_abc123",
"pid": 12345,
"parent_pid": 12300,
"depth": 1,
"filename": "/usr/bin/wget",
"raw_filename": "/proc/self/root/usr/bin/wget",
"argv": ["wget", "https://example.com"],
"unwrapped_from": "/usr/bin/env",
"payload_command": "wget",
"effective_action": "blocked",
"policy": {
"decision": "deny",
"effective_decision": "deny",
"rule": "block-wget"
}
}When seccomp is enabled, these syscalls are blocked by default:
| Syscall | Reason |
|---|---|
| ptrace | Process debugging/injection |
| process_vm_readv | Cross-process memory read |
| process_vm_writev | Cross-process memory write |
| personality | Execution domain changes |
| mount | Filesystem mounting |
| umount2 | Filesystem unmounting |
| pivot_root | Root filesystem changes |
| reboot | System reboot |
| kexec_load | Kernel replacement |
| init_module | Kernel module loading |
| finit_module | Kernel module loading (fd) |
| delete_module | Kernel module unloading |
sandbox.seccomp.syscalls.on_block selects what happens when a process invokes a syscall that appears in block:. Four values are supported; the default is errno.
| Value | Kernel mechanism | Effect on caller | Event emitted |
|---|---|---|---|
errno |
SCMP_ACT_ERRNO(EPERM) |
syscall returns EPERM |
no (kernel-only) |
kill |
SCMP_ACT_KILL_PROCESS |
process killed by SIGSYS | no (kernel-only) |
log |
SCMP_ACT_NOTIFY + handler responds EPERM |
syscall returns EPERM |
seccomp_blocked, outcome denied |
log_and_kill |
SCMP_ACT_NOTIFY + handler sends SIGKILL |
process killed by SIGKILL | seccomp_blocked, outcome killed |
Why four modes: errno is the lowest-cost default — well-behaved agents get a predictable EPERM and carry on; misbehaving ones are stopped at the kernel. kill is the irrevocable stance. log / log_and_kill take a user-notify round-trip per blocked call, so they are observable but more expensive; reach for them when you want an audit trail of every attempted violation.
Startup warning: when on_block is log or log_and_kill but no audit sink is registered, agentmon logs a warning at startup so operators don't wonder where events went.
sandbox.seccomp.blocked_socket_families blocks creation of specified AF_* socket families on socket(2) and socketpair(2). Mitigates the recurring CVE class where socket(AF_<niche-family>, ...) is the kernel attack entry point — see copy.fail for the AF_ALG case that motivated this feature.
When blocked_socket_families is omitted from config, agentmon applies a recommended-default list of 12 families at action: errno. Set the field to [] to opt out entirely; set it to a non-empty list to override the defaults.
| Family | Number | Why default |
|---|---|---|
AF_ALG |
38 | copy.fail mitigation; near-zero legitimate userspace use |
AF_VSOCK |
40 | Niche (VM-host); multiple historical CVEs |
AF_RDS |
21 | Reliable Datagram Sockets; multiple CVEs; effectively dead |
AF_TIPC |
30 | Niche cluster protocol; multiple CVEs |
AF_KCM |
41 | Kernel Connection Multiplexor; niche; CVEs |
AF_X25, AF_AX25, AF_NETROM, AF_ROSE, AF_DECnet, AF_APPLETALK, AF_IPX |
various | Legacy/dead protocols, pure attack surface |
Not in defaults (too widely used; opt-in only): AF_NETLINK, AF_PACKET, AF_BLUETOOTH, AF_CAN.
Each entry's family field accepts either a name (AF_ALG) or a numeric string (38). Names resolve via a built-in table; numbers in [0, 64) are accepted as a fallback for families the table doesn't yet know. Unknown names and out-of-range numbers are rejected at config-load time.
| Action | Effect on socket(AF_X, ...) |
Audit event |
|---|---|---|
errno |
Returns EAFNOSUPPORT (97) — the standard "this family isn't supported" code |
none (kernel-side) |
kill |
Process killed by SCMP_ACT_KILL_PROCESS |
none (kernel-side) |
log |
Returns EAFNOSUPPORT + emits audit event |
seccomp_socket_family_blocked, outcome denied |
log_and_kill |
Process killed by SIGKILL + emits audit event |
seccomp_socket_family_blocked, outcome killed |
errno is the right default for security tooling — well-behaved userspace falls back gracefully when a family isn't supported.
Family blocking has two engines that share the same config and emit identical audit-event shapes:
- seccomp-bpf (primary) — adds an
AddRuleConditionalrule onsocket(2)arg0 to the existing seccomp filter. Cheap, kernel-side. Used when seccomp is available AND the agentmon-unixwrap binary will run. - ptrace (fallback + defensive) — when
sandbox.ptrace.enabled: true, the family checker is also wired into the ptrace tracer regardless of which engine the selector reports. Runtime dispatch is mutually exclusive between engines, so this is safe and ensures coverage in hybrid configurations where the seccomp wrapper is skipped.
If neither engine is available on the host, agentmon logs a startup warning and continues — families are not blocked.
{
"type": "seccomp_socket_family_blocked",
"timestamp": "2026-04-29T18:00:00Z",
"session_id": "sess_abc123",
"source": "seccomp",
"pid": 12345,
"fields": {
"family_name": "AF_ALG",
"family_number": 38,
"syscall": "socket",
"action": "log_and_kill",
"outcome": "killed",
"engine": "seccomp"
}
}| Field | Meaning |
|---|---|
family_name |
Original config name (e.g. AF_ALG); empty if the entry was numeric-only |
family_number |
Resolved AF_* number |
syscall |
socket or socketpair |
action |
Value of action that matched (log or log_and_kill) |
outcome |
denied (errno path) / killed (kill landed) / vanished (tracee gone before enforcement) / deny_failed / deny_fallback_failed |
engine |
seccomp or ptrace — same audit shape regardless |
When a family is in blocked_socket_families AND socket is in blocked_syscalls, the family rule wins (more specific). When unix_socket.enabled: true is also set, libseccomp's action-precedence ensures family errno/kill rules outrank the unconditional ActNotify on socket(2) — AF_UNIX traffic still flows through unix-socket monitoring; AF_ALG (etc.) is denied.
Config typos fail fast at startup with a clear error:
sandbox.seccomp.blocked_socket_families[0].family: "AF_ALGOG" is not a valid AF_* name or number
sandbox.seccomp.blocked_socket_families[1].action: "deny" is not valid (allowed: errno, kill, log, log_and_kill)
sandbox.seccomp.socket_rules blocks specific socket(2) and socketpair(2) tuples. Both syscalls match the same fields: family, optional type, and optional protocol.
Use this when a mitigation should be narrower than an entire AF_* family. The manual syntax is:
sandbox:
seccomp:
socket_rules:
- name: dirtyfrag-conservative-rxrpc
family: AF_RXRPC
action: log_and_kill
- name: dirtyfrag-conservative-xfrm
family: AF_NETLINK
protocol: NETLINK_XFRM
action: log_and_killFields:
| Field | Required | Meaning |
|---|---|---|
name |
yes | Stable rule name used in audit events; names must be unique after mitigation sets are expanded |
family |
yes | AF_* name or numeric string |
type |
no | SOCK_* name or numeric socket type; flags such as SOCK_CLOEXEC are masked out before matching |
protocol |
no | Numeric protocol string, or a named NETLINK_* protocol when family: AF_NETLINK |
action |
no | errno, kill, log, or log_and_kill; defaults to errno when omitted |
Named NETLINK_* protocol values are valid only with family: AF_NETLINK. A protocol-scoped netlink rule does not block other netlink protocols.
sandbox.seccomp.mitigation_sets loads named mitigation YAML files and expands them into ordinary seccomp rules. agentmon ships built-in mitigations and can also load external mitigation files from opt-in mitigation_dirs.
External mitigation IDs are loaded from <id>.yaml or <id>.yml files in mitigation_dirs. Duplicate mitigation IDs across built-in and external sources are rejected.
sandbox:
seccomp:
mitigation_sets:
- dirtyfrag-conservative
mitigation_dirs:
- /etc/agentmon/mitigationsThe built-in dirtyfrag-conservative set is a conservative mitigation for the Openwall Dirty Frag advisory dated May 7, 2026. It expands to two socket_rules: one for AF_RXRPC, and one for AF_NETLINK with protocol NETLINK_XFRM. Both rules use action: log_and_kill, so matching processes are killed and audit events are emitted. It does not block all AF_NETLINK.
log and log_and_kill socket rules emit seccomp_socket_rule_blocked; errno and kill are enforced kernel-side and do not emit an event.
{
"type": "seccomp_socket_rule_blocked",
"timestamp": "2026-05-07T18:00:00Z",
"session_id": "sess_abc123",
"source": "seccomp",
"pid": 12345,
"fields": {
"rule_name": "dirtyfrag-conservative-xfrm",
"family_name": "AF_NETLINK",
"family_number": 16,
"protocol_name": "NETLINK_XFRM",
"protocol_number": 6,
"syscall": "socket",
"syscall_nr": 41,
"action": "log_and_kill",
"outcome": "killed",
"arch": "amd64",
"engine": "seccomp"
}
}type_name / type_number appear only when the matching rule includes type; protocol_name / protocol_number appear only when it includes protocol. Current socket-rule events are emitted by the seccomp engine, so engine is seccomp.
When a block-listed syscall traps under log or log_and_kill, a seccomp_blocked event is emitted. errno and kill do not emit — enforcement is kernel-side and no user-notify round trip occurs.
{
"type": "seccomp_blocked",
"timestamp": "2026-04-15T10:30:00Z",
"session_id": "sess_abc123",
"source": "seccomp",
"pid": 12345,
"fields": {
"syscall": "ptrace",
"syscall_nr": 101,
"action": "log_and_kill",
"outcome": "killed",
"arch": "arm64"
}
}| Field | Meaning |
|---|---|
pid |
TID of the thread that made the syscall (for multi-threaded agents, not always the TGID). |
syscall |
Human-readable syscall name resolved via libseccomp, or unknown(N) if unresolvable. |
syscall_nr |
Raw syscall number from struct seccomp_notif.data.syscall. |
action |
Value of on_block that matched (log or log_and_kill). |
outcome |
denied under log; killed under log_and_kill when the kill landed; denied under log_and_kill if the kill could not be delivered. |
arch |
Go runtime arch (amd64, arm64) — surfaces the filter's native architecture. |
- Linux kernel 5.0+ with seccomp user-notify support
- libseccomp installed (for syscall name resolution)
- CAP_SYS_ADMIN or no_new_privs for filter installation
Tip: Use agentmon detect to check if seccomp is available in your environment. See Cross-Platform Notes.