Skip to content

Latest commit

 

History

History
248 lines (188 loc) · 8.28 KB

File metadata and controls

248 lines (188 loc) · 8.28 KB

Secrets Management

Overview

This project uses dotconfig to manage environment configuration. dotconfig assembles a single .env file from layered source files — public config, SOPS-encrypted secrets, and per-developer local overrides. Secrets are encrypted at rest with SOPS + age.

config/
  sops.yaml              ← SOPS encryption rules
  dev/
    public.env            ← non-secret dev vars (committed)
    secrets.env           ← SOPS-encrypted dev secrets (committed)
    secrets.env.example   ← plaintext template showing required vars
  prod/
    public.env            ← non-secret prod vars (committed)
    secrets.env           ← SOPS-encrypted prod secrets (committed)
    secrets.env.example   ← plaintext template showing required vars
  local/
    <username>/
      public.env          ← developer-specific overrides (gitignored)
      secrets.env         ← optional encrypted local secrets (gitignored)

dotconfig load decrypts and merges these into a single .env file. dotconfig save round-trips edits from .env back to the source files.

In production, secrets also flow through Docker Swarm:

SOPS + age (at rest, in repo)
  → dotconfig load / sops decrypt
    → Docker Swarm secrets (runtime, file-mounted)
      → entrypoint.sh
        → environment variables (application reads process.env)

Application code never reads files from /run/secrets/ directly. The docker/entrypoint.sh script handles that.

File Inventory

File Committed Purpose
config/sops.yaml Yes Lists authorized age public keys
config/dev/public.env Yes Non-secret dev env vars (plaintext)
config/dev/secrets.env Yes Encrypted development secrets
config/dev/secrets.env.example Yes Plaintext template (shows required vars)
config/prod/public.env Yes Non-secret prod env vars (plaintext)
config/prod/secrets.env Yes Encrypted production secrets
config/prod/secrets.env.example Yes Plaintext template (shows required vars)
config/local/<username>/ No (gitignored) Developer-specific overrides
.env No (gitignored) Generated by dotconfig load — do not edit directly
*.agekey No (gitignored) Private keys

Legacy: The secrets/ directory and .env.old still exist for reference but are superseded by config/. They will be removed in a future sprint.

Required Secrets

Secret Used By Description
session_secret server Express session signing key
github_client_id server GitHub OAuth app client ID
github_client_secret server GitHub OAuth app client secret
google_client_id server Google OAuth client ID
google_client_secret server Google OAuth client secret
pike13_access_token server Pike 13 API access token (pre-obtained)
mcp_default_token server Bearer token for MCP server authentication
anthropic_api_key server Claude API key for AI features
openai_api_key server OpenAI API key (optional, for future use)
admin_password server Admin dashboard login password

All OAuth secrets are optional. The app starts cleanly without them — unconfigured integrations return 501 with setup instructions. See docs/api-integrations.md for credential setup.

Daily Development

Loading config

# Load dev environment with your local overrides
dotconfig load dev <username>

# Load prod environment (no local overrides)
dotconfig load prod

Editing a value

Edit .env directly, then save changes back to the source files:

$EDITOR .env
dotconfig save

dotconfig save re-encrypts secrets sections with SOPS automatically.

Editing encrypted secrets directly

SOPS_CONFIG=config/sops.yaml sops config/dev/secrets.env
SOPS_CONFIG=config/sops.yaml sops config/prod/secrets.env

Codespaces Key Setup

Codespaces environments are ephemeral — your age private key is not present by default. Store it as a GitHub Codespaces secret and the devcontainer will install it automatically on every new Codespace.

1. Find your age private key

If you don't have a keypair yet, generate one first (see Onboarding a New Developer), then have a teammate add your public key to config/sops.yaml and re-encrypt the secrets.

On your local machine:

cat ~/.config/sops/age/keys.txt

Copy the full output (the comment lines and the AGE-SECRET-KEY-1... line).

2. Store it as a Codespaces secret

  1. Go to GitHub → Settings → Codespaces → New secret
  2. Name: AGE_PRIVATE_KEY
  3. Value: paste the full key contents
  4. Under Repository access, authorize this repository

3. Use the key in a new Codespace

When you next create (or rebuild) a Codespace, post-create.sh reads $AGE_PRIVATE_KEY and writes it to ~/.config/sops/age/keys.txt automatically. dotconfig load dev <username> will work immediately.

Note: The secret is user-scoped. Each developer must add their own AGE_PRIVATE_KEY and have their public key onboarded to config/sops.yaml.


Onboarding a New Developer

Prerequisites: sops, age, and dotconfig must be installed.

  • Codespaces: sops and age are installed automatically by post-create.sh. Install dotconfig: pipx install dotconfig
  • macOS (local): brew install sops age && pipx install dotconfig
  • Linux (local): see SOPS releases and age releases, then pipx install dotconfig

1. New developer: generate an age keypair

Run this on your own machine:

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
cat ~/.config/sops/age/keys.txt

This prints your public key (starts with age1...). Share it with the team.

2. Teammate with access: add the key and re-encrypt

Run the interactive script — it handles both steps:

npm run secrets:add-key

It will prompt for the new age public key, append it to config/sops.yaml, and run sops updatekeys on every encrypted file in config/.

Commit and push the updated config/sops.yaml and re-encrypted files.

3. New developer: create local overrides and load config

mkdir -p config/local/<yourname>
cp config/local/eric/public.env config/local/<yourname>/public.env
# Edit with your values (Docker context, key path, etc.)
$EDITOR config/local/<yourname>/public.env

# Load dev config
dotconfig load dev <yourname>

Adding a New Secret

  1. Add the key to config/dev/secrets.env.example and config/prod/secrets.env.example
  2. Edit the encrypted files:
    SOPS_CONFIG=config/sops.yaml sops config/dev/secrets.env
    SOPS_CONFIG=config/sops.yaml sops config/prod/secrets.env
  3. Reload locally: dotconfig load dev <username>
  4. If the secret is used in production, add it to the secrets: block in docker-compose.prod.yml:
    secrets:
      db_password:
        external: true
      new_secret_name:
        external: true
  5. Reference it in the server's secrets: list in the same file
  6. Load it to the swarm: npm run secrets:prod:rm && npm run secrets:prod
  7. Re-deploy: npm run deploy:prod

The docker/entrypoint.sh script automatically converts any file under /run/secrets/ to an uppercase environment variable. No code changes needed for the entrypoint.

Loading Secrets to Docker Swarm

# Create secrets (first time)
npm run secrets:prod

# Update secrets (remove old, create new)
npm run secrets:prod:rm
npm run secrets:prod

These scripts use scripts/load-secrets.sh which:

  • Decrypts config/prod/secrets.env via SOPS
  • Creates each KEY=value as a lowercase Docker Swarm secret
  • Uses the production Docker context from .env

Security Rules

  • Never hardcode secrets in source code
  • Never commit .env (it's gitignored)
  • Never commit *.agekey private keys (gitignored)
  • Secrets flow through entrypoint.sh — app code reads process.env
  • Use dotconfig save or sops to edit encrypted files — never decrypt to a file other than .env