Skip to content

Secrets Management

Secure handling of sensitive configuration data across all Jubiloop environments.

Overview

Jubiloop uses a multi-layered approach to secrets management:

  1. GitHub Secrets - Primary storage for CI/CD secrets
  2. Environment Variables - Runtime injection of secrets
  3. 1Password - Team access and secret sharing
  4. Automated Discovery - Scripts to identify required secrets

Secret Storage

GitHub Secrets

All deployment secrets are stored in GitHub:

  • Repository-level secrets
  • Environment-specific secrets
  • Encrypted at rest
  • Access controlled by GitHub permissions

Adding Secrets:

bash
# Via GitHub UI
Settings Secrets and Variables Actions New repository secret

# Secret naming convention
{ENVIRONMENT}_{SERVICE}_{KEY_NAME}

1Password Vault

Team secrets are documented in 1Password:

  • Jubiloop vault for shared access
  • Environment-specific sections
  • Secure notes for context
  • API keys and credentials

Secret Types

Application Secrets

Database Credentials

  • PostgreSQL passwords
  • Redis passwords
  • Connection strings

API Keys

  • Third-party service keys
  • OAuth credentials
  • Service-specific signing secrets

Signing And Session Secrets

  • Application and Better Auth secrets
  • Encryption passwords

Infrastructure Secrets

Cloud Provider

  • DigitalOcean API tokens
  • Cloudflare API keys
  • Terraform state credentials

Deployment

  • SSH private keys
  • Container registry tokens

Secret Patterns

Auto-Generated Names

Using from_secrets in configuration:

yaml
DEV:
  server:
    APP_KEY: 'from_secrets'
    # Generates: DEV_SERVER_APP_KEY

Explicit References

For shared secrets between services:

yaml
DEV:
  server:
    DB_PASSWORD: '${{ secrets.DEV_POSTGRES_PASSWORD }}'
  postgres:
    POSTGRES_PASSWORD: '${{ secrets.DEV_POSTGRES_PASSWORD }}'

Secret Discovery

List Required Secrets

bash
cd infra/deploy

# Deployment configuration secrets from env.deploy.yml
node scripts/extract-config.js list-secrets

list-secrets reports secrets referenced by env.deploy.yml, including generated names. It does not include CI-only workflow secrets and does not support environment filtering.

Verify Secret Configuration

bash
# Check missing secrets
node scripts/generate-env-files.js DEV --list-secrets

# Generate documentation
node scripts/extract-config.js documentation

Security Best Practices

Secret Generation

Strong Passwords:

bash
# Generate secure password
openssl rand -base64 32

# Generate hex string
openssl rand -hex 32

API Key Format:

  • Minimum 32 characters
  • Include special characters
  • Avoid predictable patterns

Access Control

Principle of Least Privilege:

  • Only necessary team members
  • Environment-specific access
  • Regular access reviews

Secret Sharing:

  • Never share via chat/email
  • Use 1Password for sharing
  • Document access in team wiki

Audit and Compliance

Secret Usage Tracking:

  • GitHub audit logs
  • Deployment logs
  • Access monitoring

Regular Audits:

  • Quarterly secret review
  • Access permission audit
  • Unused secret cleanup

Common Secret Patterns

Database URLs

yaml
# Pattern
DATABASE_URL: 'postgresql://user:${{ secrets.DB_PASS }}@host:5432/db'

# Separate components
DB_HOST: 'localhost'
DB_USER: 'app_user'
DB_PASSWORD: '${{ secrets.DEV_DB_PASSWORD }}'
DB_NAME: 'jubiloop_dev'

Sanity Credentials

Marketing and Studio configuration currently require SANITY_STUDIO_API_TOKEN. Sanity makes SANITY_STUDIO_* variables available to the Studio browser bundle, so use a read-only token with the minimum project and dataset access needed for content queries. Never use an administrator, editor, or deploy token for this value. Store environment values in GitHub Secrets and 1Password.

The marketing workflow uses a separate environment-specific Studio deploy token after the Worker deploy succeeds:

EnvironmentToken to use
DevelopmentDevelopment Sanity Studio deploy token
QAQA Sanity Studio deploy token
ProductionProduction Sanity Studio deploy token
bash
cd apps/marketing
SANITY_AUTH_TOKEN=<token-from-1password> \
  pnpm exec sanity deploy -y

Use the command only as an approved fallback. The deploy token is a CI/CLI credential, not a marketing runtime credential. Never assign it to a SANITY_STUDIO_* variable. Before deploying, set the environment-specific project ID, dataset, and Studio host. Do not place either token in a tracked file. Verify the hosted Studio after automatic or fallback deployment.

Discord Notification Credentials

Store each channel's incoming webhook URL as a repository secret:

SecretDiscord channel
DISCORD_DEV_NOTIFICATIONS_WEBHOOK_URL🛠️・ops-dev
DISCORD_QA_DEPLOYMENTS_WEBHOOK_URL🧪・ops-qa
DISCORD_PROD_DEPLOYMENTS_WEBHOOK_URL🚀・ops-production

GitHub Actions injects each secret into its notification action step through the DISCORD_WEBHOOK_URL environment variable. Webhook URLs aren't passed as action inputs and aren't application runtime credentials, so don't add them to infra/deploy/env.deploy.yml or an app's environment configuration.

Discord role IDs aren't secrets. Store the development reviewers role ID in DISCORD_DEV_REVIEWERS_ROLE_ID and the operations maintainers role ID in DISCORD_PROD_MAINTAINERS_ROLE_ID as repository variables. The operations role receives separate failure alerts in both QA and production channels, so it needs access to both. Store the development event and mention switches as repository variables too; unset switches are false. See the CI/CD pipeline guide for the full variable list and behavior.

If a webhook URL is exposed, delete the webhook in Discord, create a replacement in the same channel, and update only the matching GitHub secret.

Troubleshooting

Secret Not Found

  1. Check exact secret name (case-sensitive)
  2. Verify secret exists in GitHub
  3. Check environment spelling
  4. Review deployment logs

Invalid Secret Format

  1. Check for special characters
  2. Verify encoding (base64 if needed)
  3. Check length requirements
  4. Test locally with dry-run

Access Denied

  1. Verify GitHub permissions
  2. Check environment access
  3. Contact team admin
  4. Review audit logs

Rotation

  1. Create a replacement at the provider without revoking the active credential when overlap is supported.
  2. Update 1Password and every matching GitHub secret.
  3. Redeploy each consumer and verify it uses the replacement.
  4. Revoke the old credential and record the rotation date and owner in 1Password.

For signing or session secrets that invalidate active sessions, schedule the change and communicate the user impact before redeployment.

Compromised Secret

  1. Revoke or disable the exposed credential immediately.
  2. Identify every environment, repository setting, 1Password item, host, and dependent credential that contains or trusts it.
  3. Generate replacements and update GitHub and 1Password without writing values to logs or tracked files.
  4. Redeploy every consumer and verify service health.
  5. Review provider, GitHub, deployment, and application logs for misuse.
  6. Record the exposure window, affected systems, response, and follow-up work through the team's incident process.

Documentation

Required Documentation

For each secret:

  • Purpose and usage
  • Format requirements
  • Source (if external service)
  • Dependencies

Secret Inventory

Maintain inventory in 1Password:

  • Service name
  • Environment
  • Last rotation date
  • Owner/contact

Built with ❤️ by the Jubiloop team