feat(agent): security hardening — systemd lockdown, rate limit, audit log
systemd unit now applies defense-in-depth: - ProtectSystem=strict + ProtectHome=read-only (rest of FS sealed) - ReadWritePaths only for ~/.claude (session JSONLs) and venv + audit log - InaccessiblePaths blocks /etc/shadow, /etc/ssh, /root, ~/.ssh, shell history - NoNewPrivileges + dropped capabilities (no setuid escalation, no caps) - PrivateTmp, PrivateDevices, ProtectKernel*, MemoryDenyWriteExecute - SystemCallFilter @system-service ~@privileged ~@debug ~@mount etc. - RestrictAddressFamilies blocks raw/packet sockets Application layer: - Per-user rate limit 60/hour (configurable via AGENT_RATE_MAX) - Per-user concurrency cap of 1 in-flight (no parallel claude burns) - JSONL audit log of every /agent/ask to /var/log/overlord-agent/audit.jsonl Logs username, message preview, result preview, timing, errors. Plus secrets migration: EnvironmentFile now prefers /etc/overlord/agent.env (root:erik 0640) over /home/erik/MosswartOverlord/.env, so even the read-only /home doesn't expose them. Falls back to old path during transition.
This commit is contained in:
parent
4ae18536be
commit
9d4c724b7f
2 changed files with 219 additions and 12 deletions
|
|
@ -11,19 +11,92 @@ Group=erik
|
|||
# - claude -p sessions land at ~/.claude/projects/-home-erik-MosswartOverlord/
|
||||
# - .mcp.json is auto-loaded
|
||||
WorkingDirectory=/home/erik/MosswartOverlord
|
||||
# Secrets moved OUT of /home/erik/ to /etc/overlord/agent.env so
|
||||
# ProtectHome=read-only blocks their read entirely. The file is
|
||||
# root-owned, mode 0640, group=erik.
|
||||
EnvironmentFile=-/etc/overlord/agent.env
|
||||
# Backwards-compat: also try the old location during transition.
|
||||
EnvironmentFile=-/home/erik/MosswartOverlord/.env
|
||||
# Run inside the venv populated by install.sh.
|
||||
ExecStart=/home/erik/MosswartOverlord/agent/.venv/bin/python -m agent.service
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
# Don't tie up the disk with stdout — let journald handle it.
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Resource hints — the service is light, but cap so a runaway can't
|
||||
# starve the host.
|
||||
# ─── Resource caps ─────────────────────────────────────────────────
|
||||
MemoryLimit=512M
|
||||
CPUQuota=200%
|
||||
TasksMax=128
|
||||
|
||||
# ─── Filesystem hardening ──────────────────────────────────────────
|
||||
# /usr, /boot, /efi become read-only; /etc + /var get a writable overlay
|
||||
# that's discarded on stop. Subprocesses inherit these protections.
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
# Allow writing only to the explicit paths claude / our service need.
|
||||
# - ~/.claude — session JSONL files
|
||||
# - .venv pycache — minor pip cache writes
|
||||
ReadWritePaths=/home/erik/.claude
|
||||
ReadWritePaths=/home/erik/MosswartOverlord/agent/.venv
|
||||
ReadWritePaths=/var/log/overlord-agent
|
||||
# Keep $HOME visible to the venv python so it can find pip cache etc.
|
||||
# (read-only via ProtectHome=read-only — this writable carve-out is
|
||||
# narrowly the .claude session dir above.)
|
||||
LogsDirectory=overlord-agent
|
||||
LogsDirectoryMode=0755
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectClock=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHostname=true
|
||||
ProtectProc=invisible
|
||||
ProcSubset=pid
|
||||
|
||||
# Hide sensitive host paths even if something in the python or claude
|
||||
# subprocess tree tries to read them.
|
||||
InaccessiblePaths=/etc/shadow
|
||||
InaccessiblePaths=/etc/gshadow
|
||||
InaccessiblePaths=/etc/ssh
|
||||
InaccessiblePaths=/root
|
||||
InaccessiblePaths=-/home/erik/.ssh
|
||||
InaccessiblePaths=-/home/erik/.bash_history
|
||||
InaccessiblePaths=-/home/erik/.zsh_history
|
||||
|
||||
# ─── Privilege & capability hardening ──────────────────────────────
|
||||
NoNewPrivileges=true
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
LockPersonality=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
RemoveIPC=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictNamespaces=true
|
||||
|
||||
# ─── Network family restriction ────────────────────────────────────
|
||||
# Block raw/packet sockets so even a kernel-LPE-class bug can't sniff
|
||||
# traffic or forge packets. We don't IPAddressAllow-restrict because
|
||||
# Anthropic's Cloudflare IPs shift and the whitelist would break claude.
|
||||
# If you need true egress filtering, run nftables scoped to this
|
||||
# service's cgroup — that's reliable in a way IPAddressAllow isn't.
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
# ─── Syscall filter ────────────────────────────────────────────────
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged
|
||||
SystemCallFilter=~@resources
|
||||
SystemCallFilter=~@debug
|
||||
SystemCallFilter=~@mount
|
||||
SystemCallFilter=~@cpu-emulation
|
||||
SystemCallFilter=~@obsolete
|
||||
SystemCallFilter=~@reboot
|
||||
SystemCallFilter=~@swap
|
||||
SystemCallFilter=~@raw-io
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue