Use
Vault and secrets
API tokens live in an age-encrypted file and are only ever decrypted into locked memory, by an operator, after the hub starts.
What goes where
| secret | where | why |
|---|---|---|
| agent token | environmentFile |
agents need it unattended, and it only reads host facts |
| heartbeat URL | environmentFile |
the hub needs it from startup |
| Linode token | vault | reads billing and account details |
| Cloudflare token | vault | reads DNS records and token expiry |
| Cert Spotter key | vault, optional | raises Cert Spotter’s rate limit |
| admin password hash | vault | protects the detail view |
The file
/var/lib/sitescope/vault.age is encrypted with age in passphrase mode. scrypt
uses work factor 2^15, which needs 32 MiB during unlock; age’s default
of 2^18 would need 256 MiB, four times the hub’s memory limit. Writes go
to a temporary file with mode 0600, are synced, and then renamed over
the old one. The plaintext never touches the disk. The vault holds at
most 64 KiB.
Back it up. Without it you re-enter the tokens; history is optional.
Unlocking
The hub always starts locked, and says so by email. Until it is
unlocked, checks that need a secret report locked (which
doesn’t count against any light), and the detail view answers 503.
Everything else runs. If it is still locked after
hub.lockedAfter (15 minutes), hub.vault
warns.
sitescope unlock # passphrase from the terminal, over the control socket
sitescope lockThe control socket is mode 0660, owned by the admin group, and the hub logs each caller’s uid and pid. There is no way to unlock from the web.
After vault set, the running hub keeps the old contents
until the next sitescope unlock.
In memory
- The decrypted vault lives in one buffer allocated with
mmap, outside the Go heap,mlocked (never swapped) and markedMADV_DONTDUMP. - It is zeroed and unmapped on
lock, on SIGTERM and on exit. - The process is not dumpable (
PR_SET_DUMPABLE=0) and has no core limit, so other processes of the same user can’t read its memory. - Secrets are never logged, and never part of an error.
Two copies are unavoidable: a token sits on the Go heap for the
moment it is put in an Authorization header, and age
decrypts through its own heap buffer. Both are short-lived, but not
zeroed.
Token scopes
Give each token the least it needs.
| name | minimum scope |
|---|---|
linode_token |
Personal access token with Account: Read Only, Events: Read Only, Linodes: Read Only; everything else No Access |
cloudflare_token |
Zone → DNS → Read, for the one zone. Add Zone → Zone → Read only if
cloudflare.zoneId is not set. To watch other tokens add
User → API Tokens → Read (Account → Account API Tokens → Read for
account-owned tokens); it shows names and expiry, never token
values |
certspotter_token |
optional: a Cert Spotter API key. Without it the CT checks run unauthenticated, 10 requests an hour |
The secret names can be changed with linode.tokenSecret,
cloudflare.tokenSecret and ct.tokenSecret.