The frontend should read, editors should write, the import job should touch a single collection — and nobody outside should see drafts. RustyCMS handles that with two mechanisms sharing the same limits: API keys for machines, sessions for people.
One key, one job
RUSTYCMS_API_KEYS_JSON='[
{"key":"prod-frontend","role":"read","collections":["page","post"],"environments":["prod"]},
{"key":"editorial","role":"read_write","collections":["page","post","tag"]},
{"key":"ops","role":"admin"}
]'
Three roles: read, read_write, admin. Per key, an allow-list for collections and spaces — omit it and the key sees everything, admin bypasses both. The frontend key can only read, and only page and post in the prod space; if it leaks, that is the full extent of the damage. Pass the key as Authorization: Bearer or X-API-Key. If you don’t need scopes, the shorter RUSTYCMS_API_KEYS=alice:read_write,bob:read or a single RUSTYCMS_API_KEY will do.
Users with the same limits
Editorial accounts carry the same role and the same allow-lists. The admin UI shows only what the account may see: the sidebar lists permitted collections, list endpoints filter, every write is checked against collection and space. The batch endpoint checks each sub-request too — bundling requests does not sidestep a scope. Accounts, password resets and scopes are managed in the settings, no config file needed.
Sessions you can’t forge
POST /api/auth/login sets a signed cookie. The signing key comes from RUSTYCMS_SESSION_SECRET, at least 64 bytes, mandatory in release builds. RUSTYCMS_SESSION_MAX_AGE sets the lifetime in days; unset means a session cookie, gone when the browser closes. RUSTYCMS_COOKIE_SECURE=true restricts the cookie to HTTPS.
When a write arrives with a cookie, the API checks the Origin header against its own base URL, the CORS origins and optionally RUSTYCMS_CSRF_ALLOWED_ORIGINS. A foreign page cannot use a logged-in browser as its tool. Key auth via header is exempt — no browser sends that on its own.
Login is throttled: five attempts per minute per IP and username, then 429. An attacker iterating usernames hits the per-IP limit; distributed credential stuffing hits the per-username limit.
CORS and what anonymous callers see
RUSTYCMS_CORS_ORIGIN names the permitted frontend origin; * is the dev default. The X-Total-Count header is exposed so paginating clients can read it cross-origin.
Without a key or session the API returns published entries only. Drafts and archived entries stay invisible and _status=draft is ignored — unless the request carries a valid, signed preview token.
Deliberately left out
No per-field permissions, no per-entry permissions. The boundary is the collection and the space. Three roles and two allow-lists can be checked on a single screen — a permission tree cannot.