Who can reach the server, who can read which ZIMs, who can create, and how the very first admin is set safely.
Public-access modes. The whole instance runs in one of three modes:
- open — anyone can read everything without logging in.
- limited — anonymous visitors see only an allowlisted subset of ZIMs; named accounts see what their own allowlist grants.
- private — nothing is readable until you log in.
The mode lives in Zimi's state and is set from Manage or by ZIMI_PUBLIC_ACCESS (open | limited | private), which overrides the stored value.
Named accounts & per-ZIM allowlists. Beyond the single admin, Zimi supports multiple user accounts (zimi/users.py), each with a per-ZIM allowlist so different people see different slices of the library. Sessions are cookie-based.
Creator role. An account can carry can_create, letting it drive the web Create page's URL modes (single page, --site, video) without full admin credentials. The server-disk modes stay with the primary admin: a folder or a web archive is packaged from the CLI, or on the Create page from the Folder tree or the Import picker over the create directory (see Creating ZIMs).
Secure first-run bootstrap (GHSA-5mw2-53vv-9pw6). Setting the first admin password used to be "any private-tier client sets it" — on a LAN, a Docker bridge, or a tailnet, too many hands: an adjacent device could race the owner to claim admin. The fix splits the bootstrap door in two:
- Loopback (the host itself) needs no secret — set the first password freely.
- A device on your home network (10.x, 192.168.x, IPv6 ULA, reaching Zimi directly) answers a fresh install's setup page without a key for the first hour after it first starts, the way other self-hosted apps onboard: a fresh install does nothing until that page is answered, so it is never left claimable for long. Not from the server's own container networks (Docker Desktop's are 192.168.x), not from Tailscale's IPv6 range, and never through a proxy: any forwarding header (
X-Forwarded-*,Forwarded,Via,X-Real-IP, Cloudflare's, Tailscale's) makes it a remote client. A proxy in front of a fresh install must addX-Forwarded-For, or the internet looks like your network for that hour. The machine running Zimi has the same hour on a fresh install (a header-less proxy on the same host makes the internet look like loopback); after it, the host reads the key from its own log. Under Kubernetes no network counts as home: a NodePort can make the internet look like 10.x. Virtual bridges (Docker, libvirt, LXD, Podman, CNI) never count as home either. - Any other remote client (a Docker bridge, a tailnet, a proxied request, or any device on an install that ran before 1.13.1 and so never shows the page) must present a one-time setup key the server generates on first start and prints to its own log. Without it they get the same locked response a public client does.
The key is a CSPRNG value shaped like 7Q2K-9F4M-XR8T, stored 0600 in ZIMI_DATA_DIR/setup-key, sent as Authorization: Bearer <key> or the X-Zimi-Setup-Key header, and constant-time compared. It persists across restarts until spent, and is cleared the moment a password is set — its whole life is the bootstrap window.
Who can manage Zimi. One switch: Require a password to manage. On, whoever has the password manages Zimi, from anywhere. Off, anyone who can reach Zimi manages it. Zimi does not try to tell your network from the internet for this: behind a reverse proxy it cannot, and guessing kept owners from turning their own password off. A new self-hosted install opens on this one question before anything else, with the password as the suggested answer. With a password you also choose a username, which password managers save and the admin can edit later under Your account in Settings > Access; left empty it is admin. The same page is the desktop app's second welcome page when you open Zimi to other devices, and its switch stays in Settings > Access.
The page is the first-run bootstrap, so the rules above hold: the machine running Zimi and devices on your home network answer it freely, any other device brings the setup key, and a visitor from the internet sees only that Zimi is being set up. Until it is answered, a fresh install serves other devices nothing but the page; the host's own API keeps working. An install that ran before 1.13.1 never waits. Environment variables that already answer the question (ZIMI_MANAGE_PASSWORD, ZIMI_LAN_ADMIN, ZIMI_MANAGE_OPEN) skip the page.
Turning the password off. It takes a press of Turn off password under a line that says what it allows, and the current password when there is one. Nothing is deleted: the password, API token and accounts stay, and turning the switch back on returns to the same password. While it is off, the switch's line says who can manage Zimi, the API token row is hidden (programs need no token then, and the same one works again when the password returns), captures of private addresses are refused, and a BitTorrent client connection can only come from the environment, since anyone could otherwise point Zimi's requests at your network. Nothing that would outlive it can be made: no API token, account or new password while a password is kept. ZIMI_MANAGE_OPEN, when set, decides instead and locks the switch.
Kept from before. An install that chose "anyone on my network" on an earlier version, or sets ZIMI_LAN_ADMIN=1, keeps it: the switch shows off, with "Anyone on your network can manage Zimi", until its owner sets a password, so no passwordless install widens on upgrade. A saved "manage only from inside my network" is no longer read: with a password, settings are reachable from anywhere and the password is the wall. ZIMI_MANAGE_EXTERNAL=0 still keeps settings inside the private network, decided from the client's resolved address, and refuses an outside client before the password is checked.
| Setting | Where | Default | Effect |
|---|---|---|---|
ZIMI_PUBLIC_ACCESS |
env | stored value | open / limited / private; overrides the stored mode |
ZIMI_MANAGE |
env / config manage |
1 |
0 disables the /manage/* endpoints entirely |
ZIMI_MANAGE_USER |
env / config | password file | Admin username (else read from the password file) |
ZIMI_MANAGE_PASSWORD |
env / config | password file | Admin password (else the password file) |
ZIMI_API_TOKEN |
env / config | token file | Bearer token for programmatic access (else the generated token file) |
ZIMI_MANAGE_OPEN |
env | 0 |
1 turns management authentication off entirely: no password, no setup key. Only for a network you control; it warns at every boot. |
ZIMI_LAN_ADMIN |
env | 0 |
1 makes any direct private-network client the admin while no password is set, as before 1.9.0. Not through a proxy. |
ZIMI_MANAGE_EXTERNAL |
env / config manage_external |
on | Deprecated, and will be removed. 0 keeps settings inside the private network even with the password, judged by where the visitor seems to be, which a reverse proxy can hide. Keep /manage/ off the internet at your proxy instead. |
| setup key | ZIMI_DATA_DIR/setup-key |
auto-generated | One-time remote bootstrap secret; printed to the server log |
- Every page says
setup_pending(503): a fresh install waiting for its setup page. Open Zimi on the machine running it or on a device on your home network (or anywhere else with the setup key from the log), and answer it. Or setZIMI_MANAGE_PASSWORD/ZIMI_LAN_ADMINand restart. - Signing in from outside says settings stay inside this network:
ZIMI_MANAGE_EXTERNAL=0is set. Remove it, or manage from inside the network. - Remote first-run says
needs_setup_key— by design. Read the setup key from the server's own log and present it asX-Zimi-Setup-Key(or a Bearer token). Or set the first password from a loopback shell on the host, which needs no key. - Lost the setup key — it's in the server log, and in
ZIMI_DATA_DIR/setup-keywhile unspent. If a password is already set, the key is gone on purpose; reset via the password file /ZIMI_MANAGE_PASSWORD. - Can't generate an API token — you must set an admin password first; a passwordless instance refuses. If token generation returns a 500, the data dir isn't writable.
- A user can't see a ZIM they should — check the mode (private/limited) and that ZIM's per-user allowlist.
- A creator account can't use folder/import — expected. Those are primary-admin only (folder from the CLI, import from the CLI or the Import picker). Creators get the URL capture modes.
- Management endpoints 404 —
ZIMI_MANAGE=0disables them. Set it back to1.
Trusted-header single sign-on via Cloudflare Access. Experimental, off by default. If you don't run Cloudflare Access, ignore this — the normal password/token flow is untouched.
Cloudflare Access authenticates at the edge and forwards the result to the origin as an RS256-signed JWT in the Cf-Access-Jwt-Assertion header. Zimi does real signature verification in stdlib (verify-only RSA — no key material to protect, every compared value public), plus JWKS fetch/cache/rotation, strict alg pinning, and audience/issuer/expiry checks. The security contract:
- Off unless configured. Both the team domain (
ZIMI_SSO_TEAM) and the Access application audience tag (ZIMI_SSO_AUD) must be set, or the header is not read at all. A bare install auto-trusting an identity header anyone can send would be a login bypass. - Only from the proxy. The header is honored only when the direct socket peer is the tunnel — not a forwarded IP, which is the same untrusted class of input. Default trust: any private/loopback peer (cloudflared beside the app or in a sibling container). Narrow it with
ZIMI_SSO_PROXY. A header from anywhere else is ignored entirely. - Fail closed, three ways. No header → anonymous (password/token flow untouched). Header from an untrusted peer → ignored. Header from the proxy that does not verify → 401, never a fall-through to the claimed identity or another credential.
- RS256 only.
alg: noneand HS256 downgrade are rejected before any key lookup.
JWKS is cached in memory and on disk (<data-dir>/sso_jwks.json) and honored until it expires, so a restart or network blip doesn't break logins. When a refresh can't happen (offline, endpoint down) the cached keys keep verifying for a bounded staleness window, then tokens are rejected. No failure mode makes an invalid token valid, and none of it touches the password/API-token paths — an operator is never locked out of an instance they can reach directly.
| Setting | Where | Default | Effect |
|---|---|---|---|
ZIMI_SSO_TEAM |
env / config sso_team |
unset (SSO off) | Access team domain (must be https://…). Required to enable SSO. |
ZIMI_SSO_AUD |
env / config sso_aud |
unset (SSO off) | Access application AUD tag. Required to enable SSO. |
ZIMI_SSO_PROXY |
env / config sso_proxy |
private networks | CSV of peer addresses/CIDRs allowed to present the header |
ZIMI_SSO_ROLE |
env / config sso_role |
user |
Role granted to SSO-authenticated identities |
Neither ZIMI_SSO_TEAM nor ZIMI_SSO_AUD is a secret — they identify the org and app, and zimi config shows them unmasked because a hidden value would hide the most common misconfiguration.
- SSO doesn't engage — both
ZIMI_SSO_TEAMandZIMI_SSO_AUDmust be set; with either missing the header is ignored. Confirm withzimi config. - Authenticated at the edge but Zimi 401s — the direct socket peer isn't trusted. Ensure cloudflared reaches Zimi from a private/loopback address, or add its address to
ZIMI_SSO_PROXY. ZIMI_SSO_TEAMignored with a warning — it must behttps://. A non-https value is rejected.- Logins fail after going offline for a long time — the cached JWKS aged past its staleness limit. Restore network so the certs can refresh; cached keys cover restarts and short blips only.
- Locked out entirely — you can't be via SSO alone: reach the instance directly (LAN/loopback) and use the password/API-token path, which SSO never touches.