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_KEY on the Hetzner host)
  • Backup: Apple Passwords, under the sean@immersive-app.com entry

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.

  1. Avatar (top right) -> Settings -> Developer settings (bottom of the left sidebar) -> Personal access tokens -> Fine-grained tokens -> Generate new token.
  2. Name it after the job (e.g. handbook-sync); expiration max 1 year - calendar the renewal (re-generate and re-paste the secret).
  3. Resource owner: immersive-app (the org, not the personal account - otherwise org repos are not selectable).
  4. Repository access: Only select repositories -> the target repo.
  5. Permissions -> Repository permissions -> only what the job needs (HANDBOOK_SYNC_TOKEN: Contents: Read and write; Metadata read-only is added automatically).
  6. Generate, copy the token (shown exactly once), and paste it into a New repository secret at the Actions secrets page above.

References


This site uses Just the Docs, a documentation theme for Jekyll.