95 lines
4.5 KiB
Markdown
95 lines
4.5 KiB
Markdown
# 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
|
|
|
|
1. In `my.inference.coop` → **Settings → API Tokens**, create a **"Read and Write"** token.
|
|
2. Set it as the portal's `CLOUDRON_TOKEN` env var (custom apps have no env-var
|
|
UI — use the Cloudron CLI or the API; see the next section).
|
|
3. Verify: the portal can reach `GET /api/v1/users` with the new token (HTTP 200).
|
|
|
|
### Agent token
|
|
|
|
1. Create a fresh token in `Settings → API Tokens`.
|
|
2. Put it in the agent's `.env` as `CLOUDRON_TOKEN`.
|
|
3. 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.gz` containing `config.json`, whose `.env` field holds the original map).
|
|
|
|
## Reference
|
|
|
|
- Cloudron CLI: `npm install -g cloudron` (install on your workstation, not the server), then `cloudron 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 `contributorEmail` column 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.
|