Features / Deliver

Access and keys

API keys with a role, collection and space scope. Users with the same limits, signed session cookies, origin-checked CSRF, login throttle. Anonymous callers only get what's published.

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.

Want to see this on your own project?

Tell me briefly what you're planning. I'll show the admin UI on a real installation and say honestly whether it fits.

Request early access