Security and Audit
Security Overview
Section titled “Security Overview”lash provides layered security through profiles, filesystem sandboxing, network policy enforcement, and tamper-evident audit logging.
- Profiles control what a session can do: process spawning, file I/O, environment, plugins, and admin actions. See Security Profiles for full details.
- Armor enforces filesystem restrictions via Landlock (Linux) and Seatbelt (macOS).
- Seccomp-BPF (Linux) restricts system calls for network and process operations.
- Network proxy provides transparent hostname-level filtering with TLS SNI verification.
- Audit log records all session activity in a tamper-evident hash chain.
Profile Switching
Section titled “Profile Switching”profile list # list all profilesprofile review ci # inspect a profile's resolved settingsprofile test ci curl # dry-run: would curl be allowed?Switching to a profile with auditAccess requires entering the audit password, verified against the stored hash in ~/.lash/audit.key.
Denied Actions
Section titled “Denied Actions”When a profile denies an action, the backend returns a Problem/Suggestion error identifying the profile and the denied capability:
Problem: This session (profile: restricted) cannot run 'curl'.Suggestion: 'curl' is not in the allowlist. Switch to a different profile.The default profile is configured in ~/.lash/config:
[settings]profile.default = defaultEnforcement Stack
Section titled “Enforcement Stack”On Linux, enforcement is layered:
- Profile capabilities — checked in the daemon before spawning
- Landlock — kernel-level filesystem sandboxing
- Seccomp-BPF — system call filtering for network and process restrictions
- Network proxy — transparent TUN-based interception with DNS and TLS SNI verification
On macOS, Landlock and seccomp are replaced by Seatbelt (sandbox-exec) profiles via the armor library.
If kernel enforcement is unavailable, the daemon logs a fallback warning and relies on daemon-level checks only.
Audit System
Section titled “Audit System”A tamper-evident audit system for logging all hook events. The audit log is a single append-only JSONL file at ~/.lash/audit.log, protected by a secret-ratcheted hash chain. Implemented as a built-in plugin (audit).
Integrity Model
Section titled “Integrity Model”salt = random 16-byte hexsecret_0 = KDF(password, salt) -- 600,000 iterations HMAC-SHA256hash_1 = HMAC-SHA256(secret_0, content_1)secret_1 = HMAC-SHA256(secret_0, hash_1) -- ratchet forwardhash_2 = HMAC-SHA256(secret_1, content_2)secret_2 = HMAC-SHA256(secret_1, hash_2)...The KDF uses 600,000 iterations (OWASP 2023 recommendation for SHA-256). Each secret ratchets forward — only the current value exists. Previous secrets are destroyed.
| Scenario | Outcome |
|---|---|
| Attacker has log only | Cannot forge any hash |
| Attacker has key file only | Can forge future, not past |
| Attacker has both | Can forge future, past is locked |
| User verifies with password | Full chain validated |
The password is never stored. A verification hash (derived from password + ":verify") is stored for password-gated profile escalation.
Key File
Section titled “Key File”~/.lash/audit.key (permissions 0600):
<salt>:<64-char hex secret>:<entry count>:<password verify hash>Updated atomically (write .tmp, then rename()). On startup, entry count is compared against actual log lines; mismatch indicates tampering.
Log Entry Format
Section titled “Log Entry Format”{"action":"pipeline.pre_execute","ts":"2026-03-06T14:30:00.123","seq":"42","sid":"s_123","command":"ls -la","cwd":"/home/user","hash":"a3f2...c9d8"}| Field | Description |
|---|---|
action | Hook event name |
ts | Timestamp (ISO 8601 extended) |
seq | Sequence number (monotonically increasing) |
sid | Session ID |
hash | HMAC-SHA256 hash from the ratcheted secret chain |
Subscribed Hooks
Section titled “Subscribed Hooks”The audit plugin subscribes as an observer to: session.connect, session.disconnect, pipeline.pre_execute, pipeline.post_execute, variable.set, variable.drop, job.background, job.foreground, error.dispatch.
Audit Commands
Section titled “Audit Commands”All audit commands require the auditAccess capability. The password is collected interactively (echo disabled) — never via CLI args or env vars.
| Command | Description |
|---|---|
audit init | Initialize audit system. Generates salt, derives secret, writes key file. Fails if already initialized. |
audit verify | Recomputes full ratchet chain, verifies every entry. Exit 0 if intact, 1 if tampered. |
audit rotate | Verifies log, deletes if valid, resets chain with new salt. |
Automatic Rotation Checks
Section titled “Automatic Rotation Checks”On startup, the audit plugin warns via stderr:
| Condition | Default Threshold |
|---|---|
| File size | 50 MB |
| File age | 7 days |
| Entry count mismatch | (always) |
Thresholds configurable in ~/.lash/config:
[settings]audit.max_size_mb = 50audit.max_age_days = 7Architecture
Section titled “Architecture”runtime/integrity.d -- HMAC-SHA256, KDF, ratchet, key file I/O ^protocol/paths.d -- auditLogPath(), auditKeyPath() ^backend/audit.d -- auditInit, auditRotate, report buildingplugins/audit/main.d -- AuditPlugin (HookDispatch), event logging ^backend/daemon/builtins.d -- 'audit' and 'profile' command routingBoundary with Session Logs
Section titled “Boundary with Session Logs”| Audit Log | Session Logs | |
|---|---|---|
| Purpose | Tamper-evident security trail | Command history/replay |
| Scope | All sessions, all hook events | One session’s commands |
| Integrity | Secret-ratcheted hash chain | Plain JSONL |
| Audience | Sysadmins, compliance | The user |
| Location | ~/.lash/audit.log | ~/.lash/sessions/*.jsonl |