01 / 19
▶ session recording  ·  mTLS  ·  mandatory acknowledgement  ·  process sandbox  ·  sudoers management
SUDO-LOGGER
Real-time sudo session recording with mandatory remote acknowledgement.
If the log server goes dark — so does your terminal.
$ sudo-logger-agent -server logserver:9876 

THE
BLIND
SPOT

Every sudo command grants root. Without verified remote logging, you have no tamper-proof record of what actually happened — or if it happened at all.

Traditional syslog is advisory. The admin can stop the logger, the log server can be unreachable, or the service can simply be killed. None of these edge cases should allow privileged execution to go unrecorded.

sudo audit log — /var/log/sudoreplay
09:14:02aluncat /etc/shadow
09:18:41alunsystemctl restart nginx
09:31:07alunbash
09:31:22alunrm -rf /var/log/*
09:44:12alunls /root
HOW IT WORKS
C Plugin
PLUGIN
Loaded by sudo for every invocation.
Captures all I/O streams.
Blocks sudo entirely if agent is unreachable at start.
Unix socket
Go Daemon
AGENT
Local bridge running as root.
ACK state per session.
Heartbeat every 400 ms.
Mutual TLS · 9876
Go Server
LOGSERVER
Central log receiver.
ed25519-signed ACKs per chunk.
Pluggable storage: local disk or S3 + PostgreSQL.
If the server is unreachable when sudo runs → the command is blocked before execution. No server, no sudo.
THE FREEZE MECHANISM
[ SUDO-LOGGER: log server unreachable — input frozen ]
Waiting for log server to come back...
🟢
Running
Session active. Server ACKs every chunk. Heartbeat every 400 ms.
📡
Network lost
No HEARTBEAT_ACK within 800 ms. Connection declared dead.
❄️
Frozen
cgroup.freeze=1 suspends the session. Banner shown on tty. No input reaches the shell.
Resumed
Network returns. ACK received. cgroup unfreezes. Session continues seamlessly.
Freeze latency: ~1 second  ·  Auto-recovery: < 2 seconds  ·  GUI apps frozen via SIGSTOP (pgid-aware)  ·  Ctrl+C always works
SECURITY PROPERTIES
🚫
Blocked at start
If the log server is unreachable when sudo runs, the session is rejected before the command executes. No logging capability = no execution.
🔐
Mutual TLS
Both client and server authenticate with certificates from a shared CA. Unknown clients are rejected at the TLS handshake.
ed25519-signed ACKs
The server signs every ACK with an ed25519 private key. Clients hold only the public key — a compromised client cannot forge ACKs fleet-wide.
🗄
Tamper-evident storage
Logs are written on a separate server the sudo-running user has no access to. Users cannot modify their own audit trail.
Session terminated if agent is killed
If the agent socket drops mid-session (EPIPE / ECONNRESET), the plugin sends SIGTERM to sudo within 150 ms. The attacker's shell is terminated — they cannot continue unlogged. The kill command itself is already in the log.
⚠️
Incomplete session detection
If the agent is killed mid-session, the server writes an INCOMPLETE marker and logs a SECURITY: warning. The replay UI flags the session with a red border and warning badge.
🛡️
Kernel-enforced process sandbox
eBPF LSM hooks prevent root from deleting audit logs, killing daemons, or modifying /etc/sudoers — even with full CAP_ALL. Not a policy. A kernel veto.
Complete I/O capture — browser replay
stdin · stdout · stderr · tty input · tty output — all recorded in real time with full timing data. Replay any session via the built-in web UI at http://localhost:8080.
HEARTBEAT & ACK PROTOCOL
PLUGIN
C · per sudo pid
Unix socket
AGENT
Go · local daemon
mutual TLS · 9876
LOGSERVER
Go · remote
plugin → agent → server
SESSION_START / tty_output (seq=N)
chunk + sequence number
server → agent → plugin
ACK (seq=N · 64-byte ed25519 sig)
server signs; plugin verifies
agent → server   every 400 ms
HEARTBEAT
liveness probe
server → agent
HEARTBEAT_ACK
missing > 800 ms → freeze
400 ms
heartbeat interval
800 ms
dead threshold → cgroup.freeze
150 ms
plugin monitor poll · socket drop → SIGTERM
CRYPTOGRAPHY
🔐   Mutual TLS (mTLS)
A private CA issues certificates for both server and all clients.
TLS handshake requires a valid client cert — unknown agents are rejected before any data is exchanged.
Client certificate CN is verified against the host field in SESSION_START — a compromised agent on host A cannot forge logs for host B.
All session I/O is encrypted in transit — no plaintext on the wire.
server cert CN=sudo-logserver ← signed by shared CA
client cert CN=<hostname> ← signed by shared CA
cert CN verified vs SESSION_START host field
✍   ed25519 ACK Signing
Server holds the ed25519 private key — never distributed to clients.
Each ACK is signed over a payload binding session ID, sequence number, and nanosecond timestamp — prevents replay of old ACKs.
Agent holds only the public key (ack-verify.key): can verify but cannot forge.
Even a fully compromised agent cannot generate valid ACKs fleet-wide.
ACK payload signed by server
  session_id variable ← ties ACK to session
  seq 8 bytes ← prevents replay of old ACK
  timestamp_ns 8 bytes ← freshness
  signature 64 bytes ← ed25519 over above
CGROUP FREEZE — INTERNALS
# agent creates one cgroup per session
/sys/fs/cgroup/
  sudo_logger/<session_id>/
    cgroup.procs PID # sudo pid; children inherit

# suspends every task in the cgroup instantly
cgroup.freeze = 1
# resumes all tasks instantly
cgroup.freeze = 0
Terminal (bash, vi, …)
Stays in cgroup — cgroup.freeze=1 is enough.
Plugin already blocks TTY input, SIGSTOP not needed (would trigger job control → bash to background).
GUI apps (gvim, okular, …)
systemd/GNOME moves them to app-*.scope — outside our cgroup. No controlling TTY → agent sends SIGSTOP directly (freeze, not kill). On resume: SIGCONT.
① session starts
Agent creates cgroup and adds the sudo PID. All child processes inherit the cgroup. The agent uses a `readyToFork` barrier to ensure `sudo` remains in the restricted cgroup until the server is ready, guaranteeing children are born inside the sandbox. Plugin calls unshare(CLONE_NEWCGROUP) — child processes see the session cgroup as their /sys/fs/cgroup root and cannot migrate to a parent cgroup to escape the freeze, even with CAP_SYS_ADMIN. Agent polls every 10 ms to catch GUI apps that systemd moves out.
② normal operation
I/O chunks flow, server replies with signed ACKs. Heartbeat every 400 ms.
③ ACK timeout — 800 ms
Agent writes 1 to cgroup.freeze — kernel suspends all tasks. GUI apps outside the cgroup receive SIGSTOP. Banner shown on /dev/tty.
④ frozen — input blocked
No keystrokes reach the shell. Ctrl+C / Ctrl+Z still work — monitor thread reclaims terminal foreground group every 150 ms.
⑤ network returns
ACK received → cgroup.freeze=0, SIGCONT to any SIGSTOP'd processes. Session resumes with no data loss.
WEB REPLAY INTERFACE

BROWSER
PLAYBACK

A self-contained HTTP server reads the iolog directories written by sudo-logger-server and serves a full terminal player — no database, no dependencies.

  • Sudoers tab — push rules to hosts; visual card editor; real-time diff & sync badges
  • Risk scoring 0–100 — configurable YAML rules
  • SIEM forwarding — JSON / CEF / OCSF · HTTPS · Syslog UDP/TCP/TLS · login/logout events
  • Summary tab — per-user stats, sortable, filterable
  • Anomalies tab — incomplete, high-risk, root shell, after-hours
  • Settings tab — risk rules + SIEM config in the browser
  • Full command + arguments in session list
  • Live search by user, host, or command
  • Play / pause · seek · speed 0.25×–16×
  • Keyboard: Space · ← / → · R
  • asciinema-player — pixel-accurate terminal replay
  • Single binary with embedded frontend
$ dnf install sudo-logger-replay-1.20.27-1.fc44.x86_64.rpm
$ systemctl enable --now sudo-replay
→ http://localhost:8080
sudo-replay web interface
SESSION PLAYER
v1.20.27
Sessions
Summary
Anomalies
Sudoers
Settings
Help
time
risk
dur
alun root CRIT 91
cat /etc/shadow
prod01 · 14:31 2s
alun root HIGH 72
bash -i
prod01 · 14:18 4m 22s
bob root INCOMPLETE
rm -rf /var/log/*
prod02 · 13:55
carl www-data MED 38
systemctl restart nginx
web03 · 12:47 1s
alun root ● LIVE
vi /etc/sudoers
prod01 · 15:02
useralun root hostprod01 cmdbash -i HIGH RISK 72
cwd/root dur4m 22s started2026-04-05 14:18:53
$bash -i
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
root@prod01:~# id
uid=0(root) gid=0(root) groups=0(root)
root@prod01:~# cat /etc/passwd | grep -v nologin
root:x:0:0:root:/root:/bin/bash
alun:x:1000:1000::/home/alun:/bin/bash
bob:x:1001:1001::/home/bob:/bin/bash
root@prod01:~# _
1:48
4:22
SUMMARY & ANOMALIES
v1.20.27
Sessions
Summary
Anomalies
Settings
Help
247
sessions
12
unique users
8
high-risk
3
incomplete
14.2s
avg duration
User statistics
user sessions max risk commands last seen top command
alun 89 CRIT 91 156 14:31:07 cat /etc/shadow
bob 45 HIGH 72 62 13:55:11 rm -rf /var/log/*
carl 113 MED 38 198 12:47:33 systemctl restart nginx
dana 32 LOW 12 41 11:20:05 journalctl -f
ANOMALY DETECTION & SETTINGS
Sessions
Summary
Anomalies
3
incomplete
8
high-risk
2
root shells
1
after-hours
Detected anomalies
typeuser / commandrisktime
HIGH RISK alun / cat /etc/shadow 91 14:31
INCOMPLETE bob / rm -rf /var/log/* 13:55
ROOT SHELL alun / bash -i 72 14:18
AFTER HOURS alun / vi /etc/crontab 34 02:44
LONG SESSION carl / strace -p 1 41 11:03
Sessions
Summary
Anomalies
Settings
/etc/sudo-replay/rules.yaml
● saved
+ Add rule
Risk rules
scoreconditions
90 command ∈ [cat, less, grep] AND args match /etc/shadow
edit
75 command ∈ [bash, sh, zsh, python3] AND runas = root
edit
40 hour < 6 OR hour ≥ 22
edit
35 command = strace OR command = ptrace
edit
SIEM Forwarding
enabled
forward sessions to SIEM
format
JSON ▾
endpoint
https://siem.corp.example/ingest
min risk score
0
CENTRAL SUDO BLOCK POLICY
Sessions
Summary
Settings
Blocked Users
Block message
Your sudo access has been suspended.
Contact security@example.com — Ref: SEC-4521.
/etc/sudo-logger/blocked-users.yaml · reloaded every 30 s
+ Block user
Blocked users
userhostsreasonsince
alice all hosts SEC-4521 — suspected compromise 14:02
edit
bob db-01, db-02 Policy violation 09:17
edit
carl web-01 Pending review yesterday
edit
What alice sees on her terminal
[alice@web-03 ~]$ sudo bash
[ SUDO-LOGGER: ACCESS BLOCKED BY SECURITY POLICY ]
Your sudo access has been suspended.
Contact security@example.com — Ref: SEC-4521.
sudo: session denied — command not executed
[alice@web-03 ~]$
▸ Block checked at session startup handshake — command never executes
▸ Policy enforced by log server — covers all hosts via one YAML file
▸ Changes propagate in < 30 s without service restart

SECRET
REDACTION

▸ Sensitive data masked locally — never reaches the log server
BUILT-IN PATTERNS — Gitleaks
AWS / GCP / GitHub / Stripe / Vault keys
JWT tokens (eyJhbGciOi…)
Bearer & Authorization headers
URL passwords (user:pass@host)
Key=value assignments (api_key=…)
Interactive password prompts
CONFIGURABLE
Custom regex via mask_pattern in agent.conf
Command arguments redacted in session metadata
Surgical replacement — key names preserved, only value masked
Applied to stdin, stdout, tty in/out streams
# Before redaction (stdin)
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# After redaction (stored in log)
export AWS_SECRET_ACCESS_KEY=***

# agent.conf — add custom patterns
mask_pattern = [0-9a-f]{32}
mask_pattern = (?i)acme-token-[a-z0-9]{16}

JUST-IN-TIME
APPROVAL

▸ Human-in-the-loop · time-limited windows · async notifications
FEATURES
Challenge-Response flow — user prompted for justification
Time-limited approval windows — access expires automatically
Session TTL enforcement — active sessions terminated on expiry
Pre-termination warning — amber banner shown 60s before logout
Exempt rules — whitelist root or service accounts by host
INTEGRATION
Mattermost / Slack incoming webhooks
Direct links to JIT UI in chat notifications
Result notifications sent as DMs to the user
Dynamic config stored in DB (distributed mode)
# terminal output
Reason for sudo: Jira SEC-101
Approval request 4F8F6951 submitted.
You will be notified when approved.
# chat notification (Mattermost/Slack)
🔓 Sudo approval request
User: @alun   Host: prod-db-01
Command: id
Approve or deny in sudo-logger UI

OPA
JIT POLICY

▸ Open Policy Agent · Rego rules · deny > allow > challenge
RULE CONDITIONS
User / host / command / runas — glob patterns
OS group membership — local, SSSD, LDAP, AD via NSS
Time window — hour-of-day, overnight ranges supported
Weekday restriction — Mon–Sun granularity
Local policy groups — @groupname aliases in any field
OUTCOMES
Deny — hard block, no prompt offered
Allow — session proceeds immediately
? Challenge — justification + admin approval
Hot-reload — changes apply on next sudo, no restart
Compiled Rego visible in UI for audit
# generated Rego (excerpt)
default decision := "challenge"
decision := "deny" if _any_deny
decision := "allow" if { not _any_deny; _any_allow }

# rule: SRE on-call — Mon–Fri 20:00–06:00
_any_allow if {
  _group_sre_team(input.user)
  input.weekday in {1,2,3,4,5}
  _time_ok_allow_0
}

DISTRIBUTED
STORAGE

▸ S3 · PostgreSQL · horizontal scaling · Kubernetes
LOCAL (DEFAULT)
Zero external dependencies
Sessions on local disk (/var/log/sudoreplay)
Single server, Podman / bare-metal
inotify session detection
--storage=local (default)
DISTRIBUTED
Cast files → S3 / MinIO / StorageGRID
Metadata → PostgreSQL
N log-servers + M replay-servers, no shared disk
Rolling updates, horizontal autoscaling
--storage=distributed --s3-bucket=... --db-url=...
# Migrate existing sessions once, then switch both servers to distributed
migrate-sessions \
  --logdir /var/log/sudoreplay \
  --db-url 'postgres://sudologger@pg:5432/sudologger?sslmode=require' \
  --s3-bucket sudo-logs --s3-endpoint https://minio.internal:9000 \
  --s3-path-style --workers 8
ENTERPRISE SCALE
🚀
High-Performance Pipeline
Engineered for 500+ simultaneous sessions. The log server uses an asynchronous Batch Disk Writer that bundles up to 100 chunks into a single atomic write — turning hundreds of small I/O calls into one, cutting syscall overhead by ~99%.
⚙️
Network Never Stalls on Disk
The TLS ingestion loop and the disk writer run independently. Even under heavy write load, ACKs and heartbeats are sent on time — sessions are never frozen because a disk was slow.
⚖️
Horizontal Scaling — Kubernetes Ready
Run as many log-server replicas as needed. PostgreSQL advisory locks ensure exactly one replica forwards SIEM events (no duplicates), while all replicas serve ingestion and replay traffic simultaneously.
🗂️
Stateless Storage Tier
Session recordings are stored in S3-compatible object storage (AWS S3, MinIO, NetApp). No shared filesystem needed — pods start and stop freely without data loss or coordination.
🛡️
Sandbox Scales with the Fleet
The eBPF LSM sandbox runs entirely on the client host — zero server involvement. Adding 1 000 monitored hosts does not increase server load. Sandbox policy is distributed via the same agent config as everything else.
🔑
OIDC / SSO Integration
Keycloak, Entra ID, Okta — any OIDC-compliant provider. Group claims map directly to replay-server roles. Custom React login page included. No local accounts required in enterprise deployments.
👥
Granular RBAC + GDPR
12 built-in permissions, custom roles, group→role mapping for OIDC and reverse-proxy auth. GDPR-compliant session deletion via API. Session views are recorded in a queryable access log.
▶ eBPF LSM  ·  kernel 5.7+  ·  20 hooks  ·  inode-tracked  ·  PAM-race-proof  ·  enable/disable from UI
PROCESS SANDBOX
Kernel-enforced constraints on every sudo session. Root cannot escape — not even with CAP_ALL.
BLOCKED INSIDE EVERY SUDO SESSION
rm /etc/sudoersFILE_UNLINK
echo x > /etc/shadowFILE_OPEN
mv /tmp/evil /etc/sudoersFILE_RENAME
kill -SIGKILL auditdPROCESS_KILL
truncate -s0 /var/log/audit.logFILE_TRUNCATE
chmod 777 /etc/passwdFILE_SETATTR
↓   kernel LSM returns EPERM — no userspace can override this   ↓
SANDBOX VIOLATION action=FILE_UNLINK comm="rm" pid=9182 path="/etc/sudoers" sess="bash-root" cgid=44821
SANDBOX VIOLATION action=PROCESS_KILL comm="bash" pid=9186 target="auditd" target_pid=891 sig=SIGKILL sess="bash-root" cgid=44821
WHY ROOT CANNOT BYPASS IT
LSM hooks run in the kernel, below capability checks — CAP_SYS_ADMIN and CAP_DAC_OVERRIDE don't help
20 hooks (18 LSM + 2 tracepoints) cover every attack vector: open, write, truncate, rename, unlink, setattr, mkdir, create, mknod, symlink, signal, plus bpf(), netlink sockets, ptrace, mount, capabilities, and D-Bus/systemd IPC
Inode + superblock device ID — immune to symlinks, bind-mounts, path aliasing, and Btrfs subvolumes
Dual scoping: cgroup-based + PID propagated at fork → PAM session scope migration race has no effect
inotify watcher refreshes inodes on atomic rename — protected files stay protected after every editor write
SANDBOX.YAML
protect: files: - /etc/sudoers # dirs expand recursively - /etc/shadow devices: - /dev/mem proc: - /proc/sysrq-trigger sockets: - /run/sudo-logger/agent.sock processes: - auditd - systemd
Enable: sandbox_config = /etc/sudo-logger/sandbox.yaml in agent.conf

EBPF
AGENT

▸ The kernel watches even when the plugin can't
TAMPER DETECTION
The kernel records every sudo invocation independently — the plugin cannot suppress it
If sudo ran but no session was logged, the agent raises an alert within 30 seconds
Catches disabled plugins, tampered sudo.conf, or a swapped sudo binary
Flagged sessions appear in the replay UI with a divergence badge
ALERT: divergence detected
user=alice host=prod01
comm="bash" — no plugin event
SESSIONS KEEP GOING DURING OUTAGES
If the log server goes down mid-session, the work in progress is not interrupted
Data is buffered locally and delivered in order when the connection returns
The freeze kicks in only if the buffer limit is reached — zero data loss either way
NO BLIND SPOTS — PKEXEC COVERED TOO
pkexec (PolicyKit) has no plugin API — sudo-logger records it via kernel tracepoints
Interactive shells and commands are captured the same way as sudo sessions
ONE BINARY, SIMPLE DEPLOYMENT
Session handler + kernel recorder ship as a single systemd service
Runs on older kernels without BTF — automatically falls back to plugin-only mode

READY TO
DEPLOY

▸ RPM · systemd · mTLS · browser replay
Full I/O replay — browser-based, pixel-accurate
Real-time streaming, zero local buffering
Freeze within ~1 s of network loss
Automatic recovery when network returns
GUI app freeze via SIGSTOP (pgid-aware)
RPM packages with systemd integration
Risk scoring — YAML rules, 0–100 per session
SIEM forwarding — JSON/CEF/OCSF · HTTPS · Syslog
Summary / Anomalies / Settings tabs
Browser replay — full command + args
Session terminated if agent killed (<150 ms)
Sudoers management — push rules from UI · visudo-validated · atomic apply · real-time sync badges
Central sudo block — per user/host · GUI managed · < 30 s propagation
JIT approval — challenge-response · time windows · chat bot notifications
Secret redaction — AWS keys, JWTs, Bearer tokens masked before leaving the machine
Distributed storage — S3 + PostgreSQL · horizontal scaling · Kubernetes
eBPF agent — divergence detection · outage buffering · pkexec tracking
Process sandbox — eBPF LSM · root-proof · kernel-enforced · SIEM-ready structured alerts
OIDC / SSO — Keycloak, Entra ID, Okta · group-to-role mapping
Granular RBAC — 12 permissions · custom roles · GDPR session deletion
Docker image — ghcr.io/alun-hub/sudo-logger
Helm chart for Kubernetes — one-command deploy
sudo 1.9+ · Fedora / RHEL
github.com/alun-hub/sudo-logger