4.5 KiB
Operator runbook — Cloudron tokens
How the co-op's Cloudron credentials are organized, and how to rotate them without breaking the stack.
The two-token split
There are two separate Cloudron API tokens, deliberately decoupled so rotating one doesn't break the other:
| Token | Purpose | Where it lives |
|---|---|---|
| Agent token | Inferencebot's own Cloudron access | The agent's private .env (gitignored) |
| Portal token | The member portal's Cloudron access (create users, set groups, invite links) | Cloudron's env store, under the portal app's CLOUDRON_TOKEN |
The portal needs only a handful of Cloudron capabilities (user/group management, invite links). It is the most exposed app (public webhook + broker endpoints), so it must not hold the agent's token — a portal compromise shouldn't reach the agent's wider Cloudron access.
Rotating a token
Portal token
- In
my.inference.coop→ Settings → API Tokens, create a "Read and Write" token. - Set it as the portal's
CLOUDRON_TOKENenv var (custom apps have no env-var UI — use the Cloudron CLI or the API; see the next section). - Verify: the portal can reach
GET /api/v1/userswith the new token (HTTP 200).
Agent token
- Create a fresh token in
Settings → API Tokens. - Put it in the agent's
.envasCLOUDRON_TOKEN. - Verify against
/api/v1/cloudron/status.
The trap this split exists to prevent: the portal keeps its own copy of a
Cloudron token. If the agent's token is rotated without also updating the portal
copy, the portal's Cloudron calls start returning 401 — which breaks
provisioning (new members can't be created) and the member dashboard (its
usage/keys data is resolved through the portal's Cloudron calls).
Custom apps have no env-var UI
The member portal is a custom app (packaged by us), not an App Store app. Custom apps do not expose environment variables in the Cloudron dashboard. Env vars for custom apps are set via:
- Cloudron CLI (on your workstation, not the server):
cloudron env set --app portal.inference.coop CLOUDRON_TOKEN=... - Cloudron API:
POST /apps/:appId/configure/env
configure/env is a full replace
POST /apps/:appId/configure/env with body {"env": {KEY: value, ...}}
replaces the entire environment — it does not merge. Sending a partial map
(e.g. a single test variable) wipes every other variable.
- To change one variable, read the current env first (
GET /api/v1/apps/:id→.env), merge in memory, and POST the full map back. - Never probe the endpoint with a throwaway body against a live app.
- If the env is wiped, it is recoverable from a Cloudron app backup:
GET /api/v1/apps/:appId/backups→ newest entry →GET .../backups/:backupId/download(a.tar.gzcontainingconfig.json, whose.envfield holds the original map).
Reference
- Cloudron CLI:
npm install -g cloudron(install on your workstation, not the server), thencloudron login my.inference.coop. - Cloudron API docs:
docs.cloudron.io/api/.
Open Collective: the bot token needs the email scope (Sept 2026)
The portal identifies members by email, read via the bot's OC personal token
(OC_PERSONAL_TOKEN) as collective.members.nodes[].account.email with the
... on Individual { email } inline fragment.
The email scope is load-bearing. The bot's personal token must be minted
with the email scope. Without it, the GraphQL email field returns null for
every member — including the bot's own me { email } — even though:
- the bot still authenticates and reports
isAdmin: true, and - the OC admin CSV export still shows emails (it's a separate, non-GraphQL
path; its
contributorEmailcolumn is not a GraphQL field).
Symptoms when the scope is missing: fetch_members() filters out empty
emails and returns [], so the webhook can't provision anyone by email and the
nightly sweep goes into its empty-list fail-safe (skips, doesn't deactivate).
A new member's webhook fires cleanly but no "Sent email to …" log line follows,
and no Cloudron user is created. (This is easy to misread as a "guest with no
email" problem — it is not; verify the scope first.)
Diagnosis: run me { email } with the bot token. null here while
isAdmin: true → the token is missing the email scope (or the scope was
revoked). Fix: re-add the email scope to the token on Open Collective
(Settings → Developers → Personal Tokens). No token rotation, no re-granting of
admin — just the scope.