Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Credential denylist reference

Reference for read_deny_roots, the one subtractive term in ProcessSandboxPolicy, and for the data file that seeds its defaults.

For how the process sandbox works overall, see Process sandboxing.

read_deny_roots#

TypeVec<String>
JSON fieldprocess_sandbox.read_deny_roots
Defaultthe twelve home-relative paths below, resolved against $HOME
Scopechild processes, and Harn's own file builtins via check_fs_path_scope
Nestingunions; every other field of the policy intersects

Each entry is an absolute path or a $HOME-relative path. A path is denied when it equals an entry or sits underneath one; there is no globbing and no pattern syntax.

Two properties distinguish it from every other field:

  • It beats every grant. The denial is checked before any preset, any read_roots entry, and any workspace_roots entry. PackageManagerConfig grants ~/.config, ~/.cache, and ~/.netrc wholesale, so a term that merely competed with presets would never fire on the paths it exists for.
  • It unions as policies nest. CapabilityPolicy::intersect narrows presets and roots to their common set. Narrowing a denial would widen authority, so a nested policy may add a denial and can never drop one.

Default data file#

crates/harn-vm/src/orchestration/policy/read_deny_defaults.toml, embedded with include_str! and parsed once into default_read_deny_home_paths(). Parse failure or an empty list panics at first use rather than silently producing an empty denylist.

[defaults]
home_relative = [".ssh", ".aws", "..."]

[reason]
".ssh" = "private keys and known_hosts"
KeyTypeRequiredMeaning
defaults.home_relativearray of stringsyes, non-emptypaths denied under $HOME
reason.<path>stringnowhy the entry is on the list, for review

Keys in [reason] that do not appear in home_relative are ignored, and an entry with no reason is not an error. The table is documentation kept next to the data instead of in a comment that drifts from it.

Current defaults#

.ssh, .aws, .gnupg, .netrc, .docker/config.json, .config/gh/hosts.yml, .config/gcloud, .kube/config, .npmrc, .pypirc, .cargo/credentials, .cargo/credentials.toml.

A host may add to this list through read_deny_roots. It cannot remove from it.

Per-backend enforcement#

BackendMechanismApplied
macOS(deny file-read* (subpath …)) emitted after every allowyes
LinuxLandlock grants the siblings that do not lead to the denialyes
WindowsAppContainernot yet
OpenBSDunveilnot yet

Windows and OpenBSD do not refuse the spawn either. The default denylist is never empty, so refusing on an unsupporting backend would refuse every spawn on that platform. The term is simply unenforced there, and saying so is worth more than a claim the code does not honor.

sandbox-exec is last-match-wins, so on macOS the deny rules' position in the generated profile is the enforcement.

Linux has no deny rule. Landlock is allow-only, so expand_around_denied expresses a denial by not granting: it walks from the granted root toward each denied path and substitutes, at every level, the sibling entries that do not lead to the denial.

  • An ancestor that cannot be enumerated ends the walk, granting nothing beneath it and never the ancestor itself. That covers both a missing directory (nothing below it to subtract) and an unreadable one (its children cannot be granted, so granting none of them is exactly right). Ending early is strictly narrower than continuing, so it is the safe direction on an allow-only backend; refusing the spawn instead bought no authority and took down every run whose $HOME the runtime could not list.
  • Any other enumeration error still refuses the spawn, so the relaxation cannot widen into "any error means nothing to exclude".
  • Expansion is capped at MAX_DENY_EXPANSION_RULES (4096). Measured cost for the twelve defaults on a real home directory is 187 to 190 rules in 10 to 45 ms; report_default_denylist_expansion_cost prints the number for the host you are on.

Refusal event#

A refused child emits harn.process.sandbox_refusal.v1 through log_warn_meta("process_sandbox_refusal", …).

FieldTypeMeaning
schemastringharn.process.sandbox_refusal.v1
commandarray of stringsargv of the refused child
cwdstringworking directory of the spawn
refused_pathsarray of stringspaths implicated, when they can be determined
observabilitystringhow the refusal was detected; currently only inferred
stderr_excerptstringfirst 512 bytes of the child's stderr
countintegerrefusals coalesced into this record

observability: "inferred" is the honest label for what the OS gives us today: Landlock and seatbelt refuse in-kernel without notifying the supervisor, so the refusal is reconstructed from the child's exit and stderr rather than observed directly. The enum is #[non_exhaustive]; a backend that can report a refusal directly will add a variant rather than change the meaning of this one.