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.
Install
verify · place · unitReleases 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.
First steps
cli · discovery · stateusagerenar [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/renarunless a config sets[stores].bootstrap-adminwith no--configtargets the same default state as a barerenar DIR. - A config at
/etc/renar/config.tomlor~/.config/renar/config.tomlis 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.
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_caunder[server.tls]and the TLS handshake itself refuses clients without a certificate validating to your anchors (Team-and-above).
Configuration
schema 1 · tomlOne 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.
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.Policy files
.renar.toml — the auditable .htaccessA 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.
Tokens & shares
credentials that are not peopleAn 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
--passwordthe 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
/sharespage 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.
--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.WebDAV
/dav/<label>/ — the mount doorThe 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.
| method | behavior |
|---|---|
| OPTIONS | advertises DAV: 1, 2, the allowed methods, and Class 2 compliance |
| PROPFIND | Depth 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 / HEAD | raw bytes with ETag; the renderers are browser-only — a mount gets the file, not the HTML |
| PUT | write handler: grants, conflict detection (If-Match), atomic replace, trash on overwrite where applicable, ledger row |
| MKCOL · DELETE · MOVE | the write handlers; DELETE trash-moves; MOVE rewrites the Destination header into the root and refuses a cross-root move with 400 |
| LOCK · UNLOCK | exclusive 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 |
| PROPPATCH | honest 207: renar stores no dead properties, so every prop answers 403 rather than pretending |
| COPY | 405 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.
Terminal & exec
the guarded capability- Exec is argv-only, against an operator allowlist.
The engine never constructs a shell;
shexists 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.
Plugins
out-of-process · signed · cagedA 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+ Ed25519SIGNATUREpair, and the engine verifies it before the manifest is even parsed — a rewritten manifest, an added file, a swapped binary are each aplugin.refusedledger row and a package that does not load. - Distribution is one
.renarpfile, length-prefixed so the publisher's signature survives transit;plugin-pack installverifies against the key on the command line before moving anything into place.
Fleet
one engine is a room; several are a fleetEach 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
/fleetpage 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;
/metricsfeeds Prometheus.
Verifiers & client mTLS
proof, delegated — on your termsEvery edition carries local accounts (passkey, password + TOTP) and PAM mapping. Licensed plans can additionally delegate proof to your authority. The whole set:
| verifier | wire name | plan | shape |
|---|---|---|---|
| Local accounts | — | every | passkey, password + TOTP, PAM import — the floor everyone gets |
| OIDC | verifier_oidc | Pro+ | your IdP's login button beside the local form; JIT account mapping |
| Trusted proxy header | verifier_proxy | Pro+ | an authenticating edge asserts Remote-User; socket-peer trust only (ADR 0008) |
| LDAP | verifier_ldap | Team+ | directory bind + lookup against your LDAP server |
| SAML | verifier_saml | Ent | assertion-consumer login against your IdP |
| Kerberos | verifier_kerberos | Ent | negotiate against your realm |
| Client certificates | mtls | Team+ | 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.
Grants
grants.json — the operator's ledger-side twinGrants 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, andshell— 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.
HTTP reference
every door · every twinThe 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.
| route | what | twin |
|---|---|---|
| / · /<path> | listings and rendered files; ?raw=1 for the bytes, ?download to keep them | JSON |
| /login | the only unauthenticated page; every credential ladder rung starts here | — |
| /account | the session's factors: passkeys, password + TOTP, tokens, the way out | JSON |
| /status | edition, licensing state, live degradations, ceilings, effective posture | json · fleet |
| /posture | every posture knob beside its tier preset and the bound that judges it | JSON |
| /search · /search/content | filenames, then content hits with context | JSON |
| /du | disk-usage scans and the dry-run-first cleaner | JSON |
| /trash | what DELETE moved out, with restore | JSON |
| /shares | live shares: expiry, creator, revoke | JSON |
| /fleet | live peer cards — up or down, with reasons | JSON |
| /events | live refresh over SSE — listing deltas and file tails (?type=tail&path=…), under an admission budget | SSE |
| /metrics | Prometheus text format | text |
| /dav/<label>/ | the WebDAV door (§7) — mounts and DAV clients | DAV |
| /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.
The ledger
hash-chained · append-only · self-verifyingOne 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
mutationsledger says so in its own first lines; thefullscope (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.
CLI reference
one binary · every role| command | what it does |
|---|---|
| renar [DIR] [options] | serve — --host, --port, --config PATH; TLS per [server.tls] |
| renar bootstrap-admin NAME | first-run admin with full grants; --password-stdin (8+ chars); refuses a non-empty store |
| renar token create NAME | mint an API token — --expires-days N; secret prints once |
| renar token list · revoke ID | inventory and revoke; both are ledger rows |
| renar share create NAME PATH | mint a share — --root LABEL, --days N, --password (unlock passphrase via stdin, never argv) |
| renar share list · revoke ID | live shares and revocation; both are ledger rows |
| renar verify-artifact FILE SIG KEY | the artifact verifier every binary carries — edition and version from the trusted comment |
| renar install --service | compose 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.
Licensing & the key ceremony
signed envelopes · offline verificationA 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_keyto your own key — a deliberate, operator-managed trust decision. - A license naming features above its plan is refused at
presentation —
mtlson a Pro envelope is a malformed license, not an overdelivery. The only reserved wire name still in the schema isshare_write_bit: parseable, unbuyable in any edition, by decision. - Licensing state self-declares on
/status; boot verdicts land in the ledger aslicense.ok,license.refused,license.ignored, plus one degradation note each.
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, orfullon 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.
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-adminis 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.spoofrow 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 fromtrusted, 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_caconfigured, the handshake dies before HTTP for any client without a valid certificate. That is the point — if it is not what you wanted, remove theclient_caline and restart. - PROPFIND shows fewer entries than the listing
- Large listings truncate with a
urn:renar:davmarker 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.
FAQ
asked and answeredCan it run a shell?
Why your own TLS instead of rustls?
Can I hand someone a link instead of an account?
/shares page. The
secret prints once at mint; the store keeps only its hash.Does it work offline?
What license is the code under?
How do I try the paid surface?
/status. On-prem installs that want independence can
issue their own licenses against their own key.