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.
  • lash-box applies the kernel-level restrictions and then execs your command. It is a separate binary, usable from any shell.
  • 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. It runs as its own process (lash-box proxy), one per session, so a proxy fault cannot take the daemon with it.
  • 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?

These are served by lash-box, so they work from any shell too:

Terminal window
lash-box profile review ci

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

Enforcement happens in two different places, and it is worth keeping them apart:

  1. Profile capabilities — checked by the daemon in userspace, before it forks. This is what produces a readable “not permitted by active profile” error and can offer you a profile switch.
  2. Kernel enforcement — applied by lash-box, which restricts itself and then execs your command. On Linux that is Landlock for the filesystem and seccomp-BPF for syscalls; on macOS it is Seatbelt.

When a profile configures no enforcement at all — the default profile — no lash-box process is involved and commands are exec’d directly. 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