5.9 KiB · text History b868907

Secrets Management & Encryption

Secrets Storage

CI/Repository Secrets

lib/git_gud/secrets/

  • RepositorySecret — Per-repository encrypted secrets
  • OrganizationSecret — Per-organization encrypted secrets
  • RepositoryVariable — Per-repository plaintext variables
  • OrganizationVariable — Per-organization plaintext variables
  • ReservedNames — Prevents overriding reserved names (e.g., GITHUB_TOKEN)

Encryption

  • AES-256-GCM for all secrets (authenticated encryption)
  • Per-row initialization vector (IV) — prevents pattern analysis
  • Per-row key ID (key_id) supports master key rotation
  • GitGud.Secrets module provides encryption/decryption wrapper

Master Key Configuration

  • lib/git_gud/secrets.ex — Encryption wrapper
  • Supports multiple master_keys for rotation
  • Primary key ID tracked per secret for automatic key identification
  • resolve_for/1 — decrypts all secrets for a given repository/organization in memory

Session Tokens

  • lib/git_gud/accounts/user_token.ex — Phoenix Token signed
  • Tokens stored in users_tokens table
  • 14-day expiry with reissue logic
  • Types: :confirm, :reset_password, update_email, :session

Personal Access Tokens

  • Stored as bcrypt hashes — cannot be recovered
  • Scope metadata stored separately from hash
  • Token comparison via Bcrypt.verify/2

Runner JWTs

  • lib/git_gud/workflows/runner_jwt.ex — HS256 signed JWTs
  • Claims: sub, jti, tid, rid, pid, perm, exp, iat, nbf
  • TTL: 24 hours (ttl_seconds: 86400 in config)
  • Grace period: 5 minutes after job completion for post-job steps
  • Verification via RunnerJwt.verify_active/1 — checks job is still running

Webhook Secret

  • 32 random bytes generated at boot
  • Can be overridden via GITGUD_HOOK_SECRET environment variable
  • Used for x-gitgud-hook-token header validation

TOTP Secrets

  • Encrypted with AES-256-GCM (same as secrets)
  • Per-user IV and key_id
  • Stored in users_two_factor table

Registry Token Signing

  • lib/git_gud/packages/token.ex — OCI registry JWT signing
  • Configurable algorithm: RS256 (recommended) or HS256
  • Private key loaded from GITGUD_REGISTRY_PRIVATE_KEY
  • Public key published for validation (/etc/zot/registry-pub.pem)

Encryption Details

AES-256-GCM Implementation

  • Used for: CI secrets, organization secrets, TOTP secrets
  • Per-row IV ensures identical plaintext produces different ciphertexts
  • Key ID enables key rotation without re-encrypting all secrets
  • Tag size: 128 bits (default for GCM)

RSA-SHA256 for ActivityPub

  • Used for: HTTP signatures in federated requests
  • Private key for signing outbound requests
  • Public key verification for inbound requests
  • Digest verification: SHA-256=<base64> for request body integrity

Bcrypt for Passwords

  • Used for: user passwords, recovery codes
  • Cost factor: default (12 rounds as of Elixir 1.15+)
  • Recovery codes: 10 per user, single-use, hashed before comparison

Security Observations

Critical

Insecure default secrets master key (config/config.exs:142-143)

config :git_gud, GitGud.Secrets,
  primary_key_id: "default",
  master_key: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
  • All zeros as default key — if production doesn’t override, all secrets are encrypted with a known key
  • Recommendation: require GITGUD_SECRETS_MASTER_KEY environment variable in production
  • Fail closed: refuse to start if master key is all zeros or empty

Insecure default runner JWT secret (config/config.exs:148)

config :git_gud, GitGud.Workflows.RunnerJwt,
  secret: "dev-runner-jwt-secret-do-not-use-in-prod"
  • Plaintext development secret — forgeable runner tokens if not overridden
  • RunnerJwt.signing_secret/0 does raise on empty secret, but only if the value is empty string, not this default
  • Recommendation: require GITGUD_RUNNER_JWT_SECRET environment variable in production
  • Fail closed: refuse to start if secret matches development default

High

HS256-only JWT signing (lib/git_gud/workflows/runner_jwt.ex:174)

  • Symmetric signing — same secret signs all tokens
  • If leaked from any runner, attacker can mint arbitrary tokens
  • No key rotation at JWT layer
  • Recommendation: consider asymmetric signing (RS256/ES256) for JWTs
  • At minimum: implement periodic secret rotation with graceful transition

Variables stored in plaintext

  • RepositoryVariable and OrganizationVariable stored without encryption
  • Acceptable by design (variables are not secrets)
  • Access controls must be strict to prevent information leakage
  • Recommendation: audit access controls on variable endpoints

No automatic key rotation

  • Master key rotation requires manual intervention
  • Old secrets must be re-encrypted with new key on access
  • Recommendation: implement background worker for key rotation
  • Rotate at least annually

Clock skew window too permissive

  • 5-minute window for ActivityPub signature verification
  • Recommendation: make configurable (max_skew_seconds: 300)
  • Default to 30 seconds for high-security deployments

Medium

Secrets decrypted in memory

  • GitGud.Secrets.resolve_for/1 decrypts all secrets for a job in memory
  • Secrets visible in memory dumps, swap, or crash dumps
  • Recommendation: clear secret values from memory after use
  • Consider :crypto.crypto.one_time with explicit memory zeroing

No audit logging for secret operations

  • Secrets can be created/updated/deleted via web UI
  • No audit trail for who accessed or modified secrets
  • Recommendation: log all secret operations (create, read, update, delete)
  • Include user ID, timestamp, and operation type

Erlang distribution cookie

  • rel/env.sh.eex generates random node name based on POD_IP
  • Cookie loaded from K8s secret (RELEASE_COOKIE)
  • If cookie shared across environments, allows node hijacking
  • Recommendation: use environment-specific cookies with strict access controls