Skip to content

Security and Audit

Each frontend connection is assigned a role. Five built-in roles control what actions a session can perform:

RoleDescription
adminFull access. Config, other sessions, audit log.
userStandard interactive use. Own processes, plugins, jobs.
script(default) Like user without job control, plugins, or admin.
ciRestricted spawn via allowlist. Full redirect and env.
restrictedMinimal. No arbitrary processes, no redirects, no env mutation.
CapabilityDescription
spawnAnyAllow spawning any process from PATH.
spawnAllowlistComma-separated permitted commands (when spawnAny is false).
fileRedirectAllow >, >> file writes.
fileSandboxRestrict writes to this directory. Textual path normalization only — symlink traversal is not resolved and may bypass the sandbox.
envMutationAllow modifying env vars.
envSafelistComma-separated env vars that can be modified.
pluginAccessAllow calling plugin methods.
pluginAllowlistComma-separated permitted plugins.
configModifyAllow modifying config at runtime.
sessionManageAllow managing other sessions (list, kill).
jobControlOwnAllow fg/bg/jobs for own jobs.
jobControlOthersAllow managing other sessions’ jobs.
auditAccessAllow audit commands and log access.

Defined in ~/.lash/roles.conf:

[role:ci]
spawnAny = false
spawnAllowlist = make, cmake, ninja, gcc, g++, clang, git, cargo, dub
fileRedirect = true
fileSandbox = /home/ci/builds
envMutation = true
envSafelist = PATH, HOME, CC, CXX
pluginAccess = false
[role:monitoring]
spawnAny = false
spawnAllowlist = ps, top, df, free, uptime
fileRedirect = false
auditAccess = true

Custom roles override built-in defaults of the same name.

role # prints current role (no password needed)
role user # switch (no password needed)
role admin # REQUIRES audit password

Switching to a role with auditAccess requires entering the audit password, verified against the stored hash in ~/.lash/audit.key.

When a role denies an action, the backend returns a Problem/Suggestion error identifying the role and the denied capability:

Problem: This session (role: restricted) cannot run 'curl'.
Suggestion: 'curl' is not in the allowlist. Switch to a 'user' session.

The default role is configured in ~/.lash/config:

[settings]
role.default = script

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 role 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 'role' 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