Skip to content

Security and Audit

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 list # list all profiles
profile review ci # inspect a profile's resolved settings
profile 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.

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 = default

On Linux, enforcement is layered:

  1. Profile capabilities — checked in the daemon before spawning
  2. Landlock — kernel-level filesystem sandboxing
  3. Seccomp-BPF — system call filtering for network and process restrictions
  4. 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.

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).

salt = random 16-byte hex
secret_0 = KDF(password, salt) -- 600,000 iterations HMAC-SHA256
hash_1 = HMAC-SHA256(secret_0, content_1)
secret_1 = HMAC-SHA256(secret_0, hash_1) -- ratchet forward
hash_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.

ScenarioOutcome
Attacker has log onlyCannot forge any hash
Attacker has key file onlyCan forge future, not past
Attacker has bothCan forge future, past is locked
User verifies with passwordFull chain validated

The password is never stored. A verification hash (derived from password + ":verify") is stored for password-gated profile escalation.

~/.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.

{"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"}
FieldDescription
actionHook event name
tsTimestamp (ISO 8601 extended)
seqSequence number (monotonically increasing)
sidSession ID
hashHMAC-SHA256 hash from the ratcheted secret chain

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.

All audit commands require the auditAccess capability. The password is collected interactively (echo disabled) — never via CLI args or env vars.

CommandDescription
audit initInitialize audit system. Generates salt, derives secret, writes key file. Fails if already initialized.
audit verifyRecomputes full ratchet chain, verifies every entry. Exit 0 if intact, 1 if tampered.
audit rotateVerifies log, deletes if valid, resets chain with new salt.

On startup, the audit plugin warns via stderr:

ConditionDefault Threshold
File size50 MB
File age7 days
Entry count mismatch(always)

Thresholds configurable in ~/.lash/config:

[settings]
audit.max_size_mb = 50
audit.max_age_days = 7
runtime/integrity.d -- HMAC-SHA256, KDF, ratchet, key file I/O
^
protocol/paths.d -- auditLogPath(), auditKeyPath()
^
backend/audit.d -- auditInit, auditRotate, report building
plugins/audit/main.d -- AuditPlugin (HookDispatch), event logging
^
backend/daemon/builtins.d -- 'audit' and 'profile' command routing
Audit LogSession Logs
PurposeTamper-evident security trailCommand history/replay
ScopeAll sessions, all hook eventsOne session’s commands
IntegritySecret-ratcheted hash chainPlain JSONL
AudienceSysadmins, complianceThe user
Location~/.lash/audit.log~/.lash/sessions/*.jsonl