renar v0.1.0
the reference

From artifact
to hardened install.

Everything an operator needs: verify a signed release, place the binary, compose the systemd unit, learn the vocabulary — and then every door the engine answers: the browser, the JSON twins, the WebDAV mount, the share link, the ledger's own format.

§1

Install

verify · place · unit

Releases ship as signed artifacts for x86-64 and arm64, in both editions, with minisign-shaped Ed25519 signature files whose trusted comment names edition and version — an OSS signature cannot be swapped onto an Enterprise build. No Rust toolchain is needed on the target: the shipped binary carries the verifier.

console — verify before install$ chmod +x renar-0.1.0-x86_64-unknown-linux-gnu
$ ./renar-0.1.0-x86_64-unknown-linux-gnu verify-artifact \
      renar-0.1.0-x86_64-unknown-linux-gnu \
      renar-0.1.0-x86_64-unknown-linux-gnu.sig \
      "$PUBLISHED_KEY"
verified …: signature matches …
trusted comment: renar 0.1.0 oss x86_64-unknown-linux-gnu

The deployment shape is one binary, one config, one state directory, one systemd unit:

console# user and directories — dedicated system user, no login shell
$ sudo useradd -r -M -s /usr/sbin/nologin renar
$ sudo install -m 0755 renar /usr/local/bin/renar
$ sudo mkdir -p /etc/renar /var/lib/renar /srv/notes

# config
$ sudo tee /etc/renar/config.toml <<'EOF'
schema = 1

[[roots]]
path = "/srv/notes"

[stores]
dir = "/var/lib/renar"
EOF
$ sudo chown renar:renar /var/lib/renar

# first admin (first-run only; password on stdin, 8+ chars)
$ printf 'a-long-password' | sudo -u renar \
      /usr/local/bin/renar bootstrap-admin op \
      --config /etc/renar/config.toml --password-stdin

# unit — composed for this install, hardened
$ sudo /usr/local/bin/renar install --service \
      --config /etc/renar/config.toml \
      --output /etc/systemd/system/renar.service
$ sudo systemctl daemon-reload && systemctl enable --now renar

install --service pins the binary and config path and turns every root and the stores dir into ReadWritePaths — review those lines before enabling, and delete the ones for roots you serve read-only. Verify with systemctl status, then the engine's own self-declaration at /status and the full knob table at /posture.

§2

First steps

cli · discovery · state
usagerenar [DIR] [--host ADDR] [--port N] [--config PATH]
  • There is no unauthenticated mode. The auth spine always starts. Before the first account exists, the engine serves only the bootstrap notice.
  • State lives under ~/.local/share/renar unless a config sets [stores]. bootstrap-admin with no --config targets the same default state as a bare renar DIR.
  • A config at /etc/renar/config.toml or ~/.config/renar/config.toml is picked up automatically — and its roots then win over the CLI path argument. A deliberate deployment declares a config.
  • Text renders up to 1 MB (buffered); CSV tables cap at 5 000 rows, JSON panels at 1 000 rows / 16 columns — the complete source always rides below a panel. It is a control room, not a bulk transfer server.
  • The engine holds accounts, tokens, grants and sessions in memory, loaded at boot: CLI-side writes (token mint, share create, bootstrap-admin) and grants.json edits are invisible to a running engine until it restarts. Sessions live in memory too — a restart signs everyone out, by design.
§3

Concepts

the vocabulary the ui speaks
Root
A served directory — the unit of authorization as well as of serving. Each root carries its own grants and readonly flag.
Grant
Account × Root × capability (read / write / exec / pty / shell). Evaluated on every request; the ledger records the verdicts that matter.
Account
A person or role in the engine's own store — OS-mapped (PAM) or virtual.
Credential
How an account proves itself: passkey, password + TOTP, or API token.
Verifier
Delegation of proof to an external authority: PAM locally; LDAP, Kerberos, OIDC, SAML, or a trusted reverse proxy's header on licensed plans.
Session
HMAC-signed sliding cookie — 12 h idle, 7 day absolute — with step-up re-proof on the dangerous rungs.
Share
An expiring, read-only bearer link to a subtree: /s/<256-bit-secret>/…, minted by CLI, optionally locked behind an unlock password. The format's write bit is reserved by decision — no edition mints a write-share.
WebDAV door
/dav/<label>/ — Class 1 plus exclusive LOCK, riding the same grants as the browser surface, so every mounted editor's writes land in the same trash/history/ledger as the UI's.
API token
A credential for scripts and fleet peers; acts under an account's live grants, can never satisfy step-up. SHA-256 at rest; the secret prints once, at mint.
Execution identity
The named operator an exec run is allowed to invoke — an allowlist, not a shell.
Ledger
The append-only, hash-chained audit log. Scope is a posture parameter stamped into the chain itself.
Tier · Posture
The declared environment (T0–T4). Posture presets every parameter; Surface is the subset of capability a posture actually exposes.
Client mTLS
Transport-layer proof: declare client_ca under [server.tls] and the TLS handshake itself refuses clients without a certificate validating to your anchors (Team-and-above).
§4

Configuration

schema 1 · toml

One file, discovered at /etc/renar/config.toml or ~/.config/renar/config.toml, or passed with --config. The load-bearing sections:

config.tomlschema = 1

[[roots]]                     # licensed plans: more than one; OSS: the one
path = "/srv/notes"
label = "notes"
readonly = false

[stores]
dir = "/var/lib/renar"   # accounts, ledger, grants, trash, history

[server]
host = "0.0.0.0"
port = 8080
upload_max_mb = 64
history_keep = 20             # shadow-copy versions per file
editor = "full"               # minimal | advanced | full
trusted_proxy = ["10.0.0.2"]   # whose X-Forwarded-For to believe

[server.tls]                  # opt-in; hybrid ML-KEM kex
cert = "/etc/renar/cert.pem"
key  = "/etc/renar/key.pem"
# client_ca = "/etc/renar/client-ca.pem"   # mTLS (Team+): handshake
#                                          # refuses unanchored clients

[posture]
tier = "t2"                    # presets every knob; see /posture
ledger_scope = "mutations"      # or "full" (licensed)

[license]                     # Enterprise builds only
path = "/etc/renar/license.json"
# issuer_public_key = "…"       # on-prem self-issuance

[terminal]
enabled = true
record = false                # asciinema casts (licensed)

[plugins]
enabled = true
render_timeout_ms = 500
# signer_public_key = "…"       # verify every package before parse

[fleet]                        # licensed
poll_secs = 60
[[fleet.peers]]
name = "backup"
origin = "https://backup.tailnet:8443"
token = "…"

[verifiers.oidc]               # licensed (Pro+)
issuer = "https://idp.example"
client_id = "renar"

[verifiers.proxy]              # licensed (Pro+) — ADR 0008
header = "Remote-User"          # default; 1..=64 chars [A-Za-z0-9_-]
trusted = ["10.0.0.2"]          # exact socket peers, no ranges

Surfaces are independently switchable — [search], [terminal], [events], [du], [plugins] each carry enabled — and JSON panel specs are declared as [[panels]] blocks. Unknown or malformed config is a startup death, not a silent default.

The proxy verifier's trust rule: only the socket peer may assert the header — never a forwarded value. A connection that is not a trusted proxy arriving with the header set is refused with 403 and a proxy.spoof ledger row, loudly, because a spoofable identity header is worse than no verifier. Client names are JIT-provisioned as proxy:<name> virtual accounts with zero grants — the operator grants exactly what the proxy's users should see, in grants.json, and the seat ceiling applies to them like anyone else.
§5

Policy files

.renar.toml — the auditable .htaccess

A subtree can declare its own rules: read-only for a role, no exec, hidden after 22:00. Policies are data — reviewable in git, rendered by the engine itself, and evaluated with grants on every request.

  • A policy write through the UI is validated before it lands; an invalid policy is refused, and one that would break the tree is reported, not swallowed.
  • A policy change takes effect on the next request — access control moves at file-write speed, not restart speed.
  • Policies can only restrict within a granted capability set — never grant what no operator granted. The deepest policy on a path wins, and the engine refuses to serve the policy files themselves.
§6

Tokens & shares

credentials that are not people

An API token is the credential for scripts, backup jobs and fleet peers:

console$ renar token create op --config /etc/renar.toml --expires-days 30
token 2a042f9f96de2482 created for op
secret (shown once): c7SAnbiQb4idMIXxP_LF7v_nLyWEDLMJ0czMukXP1yg
use: Authorization: Bearer c7SAnbiQb4idMIXxP_LF7v_nLyWEDLMJ0czMukXP1yg

$ renar token list --config /etc/renar.toml
$ renar token revoke 2a042f9f96de2482 --config /etc/renar.toml

Presented as Authorization: Bearer: a wrong token gets the JSON 401 a script needs; only browsers get the login redirect. Step-up is permanently owed — a token can never re-prove, so exec, shell and delete refuse it, always. Mint and revoke are ledger rows; a peer polling every minute does not drown the ledger.

A Share is the same idea for people who should not have an account: an expiring, read-only link to a subtree (ADR 0006) — shipped in every edition, OSS included.

console$ printf 'unlock-passphrase' | renar share create ops /srv/notes/q3 \
      --root notes --days 7 --password --config /etc/renar/config.toml
share 8c31 created for ops (root notes, path q3, 7 days)
link (shown once): /s/9vXk4QhTzW8mR2yJ5cAeN7uLdP3sF6gH1iB0oK9rVq4w/

$ renar share list --config /etc/renar/config.toml
$ renar share revoke 8c31 --config /etc/renar/config.toml
  • The secret is the capability. 256 bits, shown exactly once at mint; the store keeps only its SHA-256. Guessing is not a strategy and neither is brute force.
  • Read-only by construction — a share rides the granting account's read grant on the subtree and nothing else. The format reserves a write bit; no edition mints it, by decision.
  • Optional unlock password. With --password the link asks once, sets a scoped unlock cookie (Path=/s/, HttpOnly, SameSite=Lax), and the password itself never appears in any URL.
  • Expiry is enforced, not advisory — an expired share is a miss, and misses are ledger rows (share.miss), as are mints, unlocks, revocations and denials.
  • Managed where you manage everything else — the /shares page is the minting surface too: create with expiry and optional unlock password there (mint and revoke POSTs owe step-up), list live shares with their expiry and creator, revoke with one form POST.
Operator discipline: a share link in a chat log outlives your intent. Prefer short --days, prefer the unlock password for anything sensitive, and revoke on principle when the reason for the link has passed — expiry is the backstop, not the plan.
§7

WebDAV

/dav/<label>/ — the mount door

The engine answers WebDAV at /dav/<label>/ for every served root: Class 1 (GET, PUT, MKCOL, DELETE, MOVE) plus exclusive LOCK — advertised as DAV: 1, 2. Every edition, OSS included, because the door is not a feature bolted on: it routes mutations through the exact same write handlers as the browser — grants, trash, shadow history, ledger, step-up semantics unchanged.

Authentication is the normal ladder, tried in order: session cookie, then Bearer token, then HTTP Basic. Basic is re-verified on every request — with the same lockout discipline as the login form — and deliberately refused for accounts whose credentials cannot be checked safely that way: TOTP accounts presenting an empty code, passkey-only accounts, and break-glass accounts. DAV clients that can hold cookies (most can) get the sliding session; everything else should use Basic over TLS or a token.

methodbehavior
OPTIONSadvertises DAV: 1, 2, the allowed methods, and Class 2 compliance
PROPFINDDepth 0 and 1; fixed honest schema (name, type, size, MIME, mtime, ETag, lock state); Depth infinity is treated as 1; large listings truncate with a marker comment, never lie
GET / HEADraw bytes with ETag; the renderers are browser-only — a mount gets the file, not the HTML
PUTwrite handler: grants, conflict detection (If-Match), atomic replace, trash on overwrite where applicable, ledger row
MKCOL · DELETE · MOVEthe write handlers; DELETE trash-moves; MOVE rewrites the Destination header into the root and refuses a cross-root move with 400
LOCK · UNLOCKexclusive locks (default 600 s, max 3600 s, refreshable) keyed on the canonical path; submission via If or Lock-Token; a second account's conflicting write reads 423 — the holder's own account is never locked out
PROPPATCHhonest 207: renar stores no dead properties, so every prop answers 403 rather than pretending
COPY405 with an Allow header — recursive copy belongs to the write path, not the door

Mount it with anything that speaks DAV — the sandbox's tree, for instance:

console — rclone against a renar door$ rclone lsd :webdav,url=https://host/dav/notes/,vendor=other \
      :webdav: -vv   # auth: the same Basic/Bearer as any client

# gnome files / windows explorer:
#   davs://host/dav/notes/     (add the account in the dialog)

Locks live in engine memory — restart clears them, which is the honest trade for a single-binary door with no lock database to corrupt. DAV rows land in the ledger as dav.auth, dav.auth.denied, dav.lock, dav.unlock, dav.lock.denied — a mounted editor is as accountable as a logged-in browser.

§8

Terminal & exec

the guarded capability
  • Exec is argv-only, against an operator allowlist. The engine never constructs a shell; sh exists only as a separately named grant you can simply not give. Children run env-cleared, cwd-pinned, in their own process group; the deadline kills the group — a spawner's grandchildren included — and output is capped with a truncation flag, never silently dropped.
  • Terminal sessions are tmux-backed — they survive a closed browser and reattach from another device. Idle timeouts apply; recording (asciinema format, licensed plans) covers the session when the posture demands it.
  • Step-up gates the rungs: exec and shell demand fresh credential proof, a stale proof is refused, and no operators configured means no exec — full stop.
§9

Plugins

out-of-process · signed · caged

A plugin is a separate OS process — any language — that the engine spawns, sockets, and supervises. Edition-independent: an OSS install runs them exactly as a licensed install does.

  • Default-deny scopes. Filesystem reads, writes and exec are each declared in the manifest and denied unless listed; render and answer budgets cap time and bytes.
  • Kernel cage. Landlock confines the process to its package and loader roots — writes denied everywhere, ptrace, mount and module-loading blocked — installed between fork and exec and inherited by anything it spawns. No Landlock on the kernel means the package does not run unsandboxed.
  • Signed packages. With a publisher key configured, every package must carry a DIGESTS.json + Ed25519 SIGNATURE pair, and the engine verifies it before the manifest is even parsed — a rewritten manifest, an added file, a swapped binary are each a plugin.refused ledger row and a package that does not load.
  • Distribution is one .renarp file, length-prefixed so the publisher's signature survives transit; plugin-pack install verifies against the key on the command line before moving anything into place.
Placement rule: the packages dir must live outside every served root — a plugin package is executable code, and a write grant must never be able to swap it. Config validation enforces this.
§10

Fleet

one engine is a room; several are a fleet

Each engine owns an Ed25519 instance key, generated once at first boot, owner-only, kept for the life of the install — and refused, not silently replaced, if the file ever stops matching. A peer scrape fetches /status?format=fleet: the status snapshot wrapped in a signed envelope, so the watching engine can prove who answered and that nothing changed in flight.

  • The /fleet page shows live cards — up with reasons, or down with reasons: wrong key, bad status, stale.
  • Transitions land in the ledger; quiet rounds deliberately do not.
  • Scrapes are bearer-authed with an API token minted for the peer.
  • A health badge rides the top bar; /metrics feeds Prometheus.
§11

Verifiers & client mTLS

proof, delegated — on your terms

Every edition carries local accounts (passkey, password + TOTP) and PAM mapping. Licensed plans can additionally delegate proof to your authority. The whole set:

verifierwire nameplanshape
Local accountseverypasskey, password + TOTP, PAM import — the floor everyone gets
OIDCverifier_oidcPro+your IdP's login button beside the local form; JIT account mapping
Trusted proxy headerverifier_proxyPro+an authenticating edge asserts Remote-User; socket-peer trust only (ADR 0008)
LDAPverifier_ldapTeam+directory bind + lookup against your LDAP server
SAMLverifier_samlEntassertion-consumer login against your IdP
Kerberosverifier_kerberosEntnegotiate against your realm
Client certificatesmtlsTeam+not a login form — the TLS handshake refuses unproven clients before any HTTP happens

The proxy verifier, precisely. Configure [verifiers.proxy] with the exact IPs of your trusted edge proxies. A request from a trusted peer carrying the header is that named identity — JIT-provisioned as proxy:<name> with zero grants, so the operator decides what proxy users may read, root by root, in grants.json. A request from anyone else carrying the header is refused: 403 and a proxy.spoof ledger row. A proxy-asserted identity can never satisfy step-up — the proxy asserts who, never a fresh Credential proof — so exec, shell and delete stay out of reach no matter what the edge says. The verifier needs posture acknowledgement ([posture.verifiers] proxy = true): a T3 install declares that it knows an edge speaks for it.

Client mTLS, precisely. Add client_ca = "/etc/renar/client-ca.pem" under [server.tls] and handshakes start requiring a client certificate that validates to those anchors — no certificate, no wrong-CA certificate: the connection dies in the handshake, it does not complete-and-403. Anchors are read strictly separately from the server's own chain: a client CA answers "which clients may connect", never "who issued us". On a plan without the capability the field is ignored and the degradation says so on /status — the engine never silently enforces or silently ignores a trust decision.

§12

Grants

grants.json — the operator's ledger-side twin

Grants are declarative operator config: a JSON file in the stores dir, read at boot, mapping each account to its capability set per root. The engine never widens it from a request — policy files and shares only narrow.

stores/grants.json{
  "<account-id>": {            // the id /account shows, hex
    "notes": ["read", "write"],
    "archive": ["read"]      // a root not listed: nothing, not read
  },
  "<ops-account-id>": {
    "notes": ["read", "write", "exec", "pty", "shell"]
  }
}
  • Capabilities: read, write, exec, pty, and shell — the last one deliberately separate, because a shell subsumes the others and deserves its own decision.
  • Edits take effect at restart — the file is loaded at boot alongside the store. That is the honest shape for a capability ceiling: it changes when an operator acts, visibly, not mid-request.
  • bootstrap-admin writes the first full-grant entry; everything after that is this file, reviewed in git like any other infrastructure truth.
§13

HTTP reference

every door · every twin

The browser surface is server-rendered HTML; nearly every page keeps a JSON twin for scripts. Doors that are not the browser: the WebDAV mount (§7), the share link (§6), the metrics and SSE endpoints below.

routewhattwin
/ · /<path>listings and rendered files; ?raw=1 for the bytes, ?download to keep themJSON
/loginthe only unauthenticated page; every credential ladder rung starts here
/accountthe session's factors: passkeys, password + TOTP, tokens, the way outJSON
/statusedition, licensing state, live degradations, ceilings, effective posturejson · fleet
/postureevery posture knob beside its tier preset and the bound that judges itJSON
/search · /search/contentfilenames, then content hits with contextJSON
/dudisk-usage scans and the dry-run-first cleanerJSON
/trashwhat DELETE moved out, with restoreJSON
/shareslive shares: expiry, creator, revokeJSON
/fleetlive peer cards — up or down, with reasonsJSON
/eventslive refresh over SSE — listing deltas and file tails (?type=tail&path=…), under an admission budgetSSE
/metricsPrometheus text formattext
/dav/<label>/the WebDAV door (§7) — mounts and DAV clientsDAV
/s/<secret>/…the share door (§6) — public, read-only, expiring, optionally password-unlocked

Status codes are the contract: browsers get 302 to /login when unauthenticated; scripts bearing a bad token get a JSON 401; denied grants get 403 with the reason in the page and a ledger row; conflicts surface 412 (If-Match); DAV lock conflicts 423; cross-root DAV moves 400. The .rnar- prefix is reserved end-to-end — engine files (policy, markers) may live in your tree, but they are never served, never writable through the UI, and your own files may not take the prefix.

§14

The ledger

hash-chained · append-only · self-verifying

One JSONL file under the stores dir. Each line is an event; each event carries its sequence number and the hash of the previous line, so the whole file is a chain: editing a line breaks every hash after it, and deleting a tail is caught by the sequence.

stores/ledger.jsonl{"seq":41,"ts":1759000000,"event":"write.put","actor":"op",
 "path":"/notes/q3/report.md","bytes":2049,"prev":"57b8f98e…",
 "hash":"9c04d1aa…"}}

The event families a 0.1.0 install writes today — bootstrap.admin; login.* (ok, denied, locked); read / read.denied (full scope); write.* (put, mkcol, delete, move, upload, restore, precondition, stepup_required, denied); exec.* (run, denied, error, stepup_required); trash.denied, search.denied; token.minted / token.revoked; share.mint / share.unlock / share.deny / share.miss / share.revoke; dav.auth, dav.auth.denied, dav.lock, dav.lock.denied, dav.unlock; proxy.spoof, account.provisioned, account.ceiling; license.ok / license.refused / license.ignored / license.degraded; plus plugin, posture and fleet verdicts.

  • Scope is stamped into the chain — a mutations ledger says so in its own first lines; the full scope (licensed) additionally records reads: who read what, when.
  • Tampering is detected at boot, loudly: the engine refuses to start over a broken chain — including the two-writers case, which is exactly what running two engines against one stores dir looks like. That refusal is the detector working, not a bug.
  • Quiet is deliberate: polls, misses on unchanged state and healthy fleet rounds do not spam the chain; transitions and verdicts do.
§15

CLI reference

one binary · every role
commandwhat it does
renar [DIR] [options]serve — --host, --port, --config PATH; TLS per [server.tls]
renar bootstrap-admin NAMEfirst-run admin with full grants; --password-stdin (8+ chars); refuses a non-empty store
renar token create NAMEmint an API token — --expires-days N; secret prints once
renar token list · revoke IDinventory and revoke; both are ledger rows
renar share create NAME PATHmint a share — --root LABEL, --days N, --password (unlock passphrase via stdin, never argv)
renar share list · revoke IDlive shares and revocation; both are ledger rows
renar verify-artifact FILE SIG KEYthe artifact verifier every binary carries — edition and version from the trusted comment
renar install --servicecompose the hardened systemd unit for this install; review the ReadWritePaths lines
renar plugin-pack …build and install signed .renarp packages; verifies before anything moves

Every subcommand takes --config PATH and opens the same stores the engine will — which is why their writes want a restart to be seen by a running engine, and why two writers on one stores dir is a tampering event, not a sync.

§16

Licensing & the key ceremony

signed envelopes · offline verification

A license is a JSON envelope whose payload is Ed25519-signed by the issuer; the signature covers the canonical payload (fixed key order, no whitespace), so formatting differences change nothing and any signed difference changes everything.

license.json{
  "payload": {
    "schema": 2,
    "customer": "Example Industries GmbH",
    "edition": "enterprise",
    "plan": "team",
    "tier_cap": "t3",
    "features": ["fleet", "ledger_full_scope", "verifier_ldap", "mtls"],
    "issued_at": 1750000000,
    "expires_at": 1893456000,
    "max_accounts": 250,
    "max_roots": 12
  },
  "signature": "base64url(64-byte ed25519 signature)"
}
  • The issuer's public key is baked at build time. The secret seed lives on an offline issuer machine and never touches a build machine — the ceremony runbook covers generation, baking, registry and rotation.
  • An unbaked Enterprise build carries an all-zeros placeholder — zeros are not a valid Ed25519 point, so it cannot accidentally trust any license and runs degraded at free capabilities.
  • On-prem installs may self-issue: set [license] issuer_public_key to your own key — a deliberate, operator-managed trust decision.
  • A license naming features above its plan is refused at presentation — mtls on a Pro envelope is a malformed license, not an overdelivery. The only reserved wire name still in the schema is share_write_bit: parseable, unbuyable in any edition, by decision.
  • Licensing state self-declares on /status; boot verdicts land in the ledger as license.ok, license.refused, license.ignored, plus one degradation note each.
§17

Operations

status · bundles · restarts
  • /status — edition, licensing state, effective posture, which surfaces are actually running, live degradations and ceilings. The page an operator shows an auditor.
  • /posture — every posture parameter beside its tier preset and the bound that judges it. The page an auditor reads before believing either.
  • Support bundles — redacted by construction: paths and facts, secrets as markers, license state included. Safe to attach to a ticket.
  • The ledger — hash-chained JSONL under the stores dir; scope is a posture parameter (mutations, or full on a licensed plan — who read what, when).
  • Upgrades — a new signed artifact verified the same way, then the unit restarted; state and stores carry forward. A restart re-reads grants and CLI-written state, and signs sessions out — schedule it like the deliberate act it is.
§18

Troubleshooting

the messages, decoded
"ledger tampering detected at line N — sequence number broken"
The chain is inconsistent. The common cause is not an attacker: it is two engines running against one stores dir (a forgotten instance, a duplicate unit), each writing its own sequence. Find the second writer, stop it, and restore the stores dir from backup — the engine refusing to boot here is the tamper detector doing its job.
A minted token or share "does not work" on a running engine
CLI subcommands open the stores directly; a running engine loaded its copy at boot. Restart the engine to pick up tokens, shares, and grants.json edits — nothing is lost, sessions aside.
"refusing: the Account store is not empty"
bootstrap-admin is first-run-only. The store already has an admin; manage accounts from there. Pointing bootstrap-admin at a used stores dir by accident is exactly what the refusal prevents.
Login locks out after DAV/client failures
Basic auth on the DAV door rides the same lockout ladder as the login form — a misconfigured mount retrying a bad password locks the account exactly like a person mistyping. Fix the client's stored credential, then let the lockout window pass or have an admin clear it.
A passkey/TOTP account cannot use the DAV door
By design: Basic cannot safely prove those credentials (a TOTP account would be handing over password-only; a passkey account has no password at all; break-glass accounts are refused everywhere but the console). Use a session-capable client or an API token for that account.
403 with a proxy.spoof row in the ledger
A connection that is not in [verifiers.proxy] trusted arrived carrying the identity header. Either the edge proxy's IP is missing from trusted, or something is impersonating your edge — the refusal is loud on purpose. Check which before adding anything to the list.
Clients without certificates cannot connect at all
That is mTLS enforcing, not failing: with client_ca configured, the handshake dies before HTTP for any client without a valid certificate. That is the point — if it is not what you wanted, remove the client_ca line and restart.
PROPFIND shows fewer entries than the listing
Large listings truncate with a urn:renar:dav marker comment rather than lying or stalling — the browser listing pages are paginated for the same reason. Narrow the mount's depth or raise the ceiling in posture.
§19

FAQ

asked and answered
Can it run a shell?
Yes — as a named grant, never as plumbing. Every exec is argv-only against an operator allowlist; a shell is a separately granted operator you can decline to configure, and when you do grant it, sessions can be recorded and are killed — descendants included — at their deadline.
Why your own TLS instead of rustls?
One crypto vendor with one dependency story. The in-house RUSSL tree is pure Rust with no C in the path, and the TLS acceptor carries hybrid ML-KEM post-quantum key exchange with CI gates that re-prove interop against a reference OpenSSL client and a real Chromium on every push. TLS itself stays opt-in — over a tailnet it would be double encryption.
Can I hand someone a link instead of an account?
Yes — that is a Share (§6): read-only, expiring, optionally password-unlocked, revoked from the /shares page. The secret prints once at mint; the store keeps only its hash.
Does it work offline?
Completely. CSS, JS, fonts, editors and syntax colors are embedded — pages work with no internet access, releases install without a toolchain, and licenses verify offline. There is no telemetry and there are no CDN assets, ever.
What license is the code under?
The whole tree is AGPL-3.0-only — both editions, one codebase. The free build is fully usable for the single-box case; the paid plans buy signed builds, the update channel, support, and the multi-box surface. Anyone may build the Enterprise edition and self-issue against their own key.
How do I try the paid surface?
An Enterprise build without a license is the trial: it runs degraded at exactly the free capability set and says so on /status. On-prem installs that want independence can issue their own licenses against their own key.