Secrets Management
The single reference for every secret the project uses: the Rails credential keys below, and the GitHub Actions secrets that power CI, deploys, and automation.
RAILS_MASTER_KEY
The production master key is deliberately never stored in the repo, .env, or
config/master.key — a stray copy breaks local dev and test, because Rails prefers an
ambient RAILS_MASTER_KEY (or a populated config/master.key) over the per-environment
key files, and a wrong key hard-crashes boot with
ActiveSupport::MessageEncryptor::InvalidMessage while a missing one fails gracefully.
See the comments in .env.sample and .kamal/secrets for the full history.
Where it lives:
- Source of truth: the live production web container’s environment
(
docker exec <web-container> printenv RAILS_MASTER_KEYon the Hetzner host) - Backup: Apple Passwords, under the
sean@immersive-app.comentry
Deploys resolve it automatically — bin/deploy fetches the key from the live container
over SSH (see docs/technical/production/kamal.md). Nothing needs to be exported by hand.
Local development uses the separate per-environment key files
(config/credentials/development.key), not this key.
Editing credentials
EDITOR="vim" bin/rails credentials:edit --environment=development
EDITOR="vim" bin/rails credentials:edit --environment=production
GitHub Actions secrets
All live in one place: github.com/immersive-app/immersive -> Settings -> Secrets and variables -> Actions -> Repository secrets (left sidebar: Security -> Secrets and variables). Values are write-only once saved - GitHub never shows a secret again, so rotation means pasting a fresh value.
| Secret | Used by | What it is / where the value comes from |
|---|---|---|
CLAUDE_CODE_OAUTH_TOKEN |
claude.yml (@claude on issues/PRs) |
OAuth token for the Claude subscription, generated by claude setup-token locally |
KAMAL_REGISTRY_PASSWORD |
deploy.yml |
GitHub PAT (classic) with write:packages for GHCR pushes - same value as .env locally |
POSTGRES_PASSWORD |
deploy.yml |
The production Postgres container’s internal auth - same value as .env locally (CI uses a hardcoded throwaway, not this secret) |
RAILS_MASTER_KEY |
deploy.yml |
Production credentials key - see the section above for source of truth and backup |
SSH_PRIVATE_KEY |
deploy.yml |
Private half of the dedicated deploy keypair; public half in /root/.ssh/authorized_keys on the Hetzner host |
HANDBOOK_SYNC_TOKEN |
sync-handbook.yml |
Fine-grained PAT scoped to immersive-app/handbook, Contents read/write (creation steps below). Without it every sync run is a green no-op - the handbook silently stops updating (#394) |
Local-only counterparts (.env, never Actions secrets): see
docs/technical/production/kamal.md.
Creating a fine-grained PAT (GitHub navigation)
Used for HANDBOOK_SYNC_TOKEN; the same path serves any future repo-scoped
token.
- Avatar (top right) -> Settings -> Developer settings (bottom of the left sidebar) -> Personal access tokens -> Fine-grained tokens -> Generate new token.
- Name it after the job (e.g.
handbook-sync); expiration max 1 year - calendar the renewal (re-generate and re-paste the secret). - Resource owner:
immersive-app(the org, not the personal account - otherwise org repos are not selectable). - Repository access: Only select repositories -> the target repo.
- Permissions -> Repository permissions -> only what the job needs
(
HANDBOOK_SYNC_TOKEN: Contents: Read and write; Metadata read-only is added automatically). - Generate, copy the token (shown exactly once), and paste it into a New repository secret at the Actions secrets page above.