Secrets Management & Encryption
Secrets Storage
CI/Repository Secrets
lib/git_gud/secrets/
RepositorySecret— Per-repository encrypted secretsOrganizationSecret— Per-organization encrypted secretsRepositoryVariable— Per-repository plaintext variablesOrganizationVariable— Per-organization plaintext variablesReservedNames— 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.Secretsmodule provides encryption/decryption wrapper
Master Key Configuration
lib/git_gud/secrets.ex— Encryption wrapper- Supports multiple
master_keysfor 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_tokenstable - 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: 86400in 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_SECRETenvironment variable - Used for
x-gitgud-hook-tokenheader validation
TOTP Secrets
- Encrypted with AES-256-GCM (same as secrets)
- Per-user IV and key_id
- Stored in
users_two_factortable
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_KEYenvironment 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/0does raise on empty secret, but only if the value is empty string, not this default- Recommendation: require
GITGUD_RUNNER_JWT_SECRETenvironment 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
RepositoryVariableandOrganizationVariablestored 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/1decrypts 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_timewith 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.eexgenerates random node name based onPOD_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