Skip to content

Secrets Management

Forge integrates with HashiCorp Vault for secret storage. Secrets are never stored in PostgreSQL — only secret names travel through the scheduler and database. Agents fetch secret values from Vault at job execution time, immediately before injecting them as environment variables into the container.


How Secrets Work

Pipeline YAML:                  What actually happens at runtime:

steps:                          1. Scheduler stores step with secret_names: ["API_KEY"]
  - id: deploy                     (only the name, never the value)
    secrets: [API_KEY]
    run: curl -H "Authorization: $API_KEY" ...
                                2. Agent leases job, sees secret_names: ["API_KEY"]

                                3. Agent calls Vault:
                                   GET /v1/secret/data/forge/global/API_KEY
                                   → "my-secret-value"

                                4. Agent injects into container env:
                                   API_KEY=my-secret-value

                                5. Container runs with the secret as an env var
                                   (value is also redacted from log output)

Vault Setup

Development (Docker)

docker run -d --name vault \
  -p 8200:8200 \
  -e VAULT_DEV_ROOT_TOKEN_ID=forge-dev-token \
  -e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \
  hashicorp/vault

Set environment variables for the CLI and agent:

$env:FORGE_VAULT_ADDR  = "http://localhost:8200"
$env:FORGE_VAULT_TOKEN = "forge-dev-token"

Production

Use a properly configured Vault cluster with appropriate auth backends. The agent needs a Vault token with read access to the paths it uses. For the compose stack, Vault configuration is in compose.yml.


Secret Scoping

Secrets can be scoped to three levels. The agent walks the chain from most specific to least specific and returns the first match:

Project-scope  secret/data/forge/projects/{project_id}/{NAME}   highest priority
Org-scope      secret/data/forge/orgs/{org_id}/{NAME}
Global         secret/data/forge/global/{NAME}
Legacy         secret/data/forge/{NAME}                          backward compat

This lets you: - Store most secrets at the global scope (shared across all runs) - Override specific secrets for a whole team's projects at the org level - Override for one specific project without affecting others

Practical example

# Global: used by all runs unless overridden
forge secret set DATABASE_URL "postgres://global-db:5432/app"

# Org-level: staging org uses a different database
forge secret set DATABASE_URL "postgres://staging-db:5432/app" --org staging-org-id

# Project-level: this specific service uses yet another database
forge secret set DATABASE_URL "postgres://service-db:5432/myservice" --project proj-id

A run submitted under proj-id uses the project-level DATABASE_URL. A run under the staging org (but not proj-id) uses the org-level value. Everything else uses the global value.


CLI Usage

Setting Secrets

# Global scope (no flags)
forge secret set MY_SECRET "the-value"

# Org-scoped
forge secret set MY_SECRET "the-value" --org <org-id>

# Project-scoped
forge secret set MY_SECRET "the-value" --project <project-id>

# FORGE_ORG is used as default --org if set
$env:FORGE_ORG = "abc123"
forge secret set MY_SECRET "the-value"    # scoped to org abc123

Getting Secrets

forge secret get MY_SECRET                # from global scope
forge secret get MY_SECRET --org abc123   # from org scope

Listing Secrets

forge secret list                         # global scope
forge secret list --org abc123            # all secrets in this org
forge secret list --project xyz789        # all secrets for this project

Using Secrets in Pipelines

Declare the secret names in the step definition. The agent fetches their values from Vault at runtime:

steps:
  - id: deploy
    image: alpine:latest
    secrets: [GITHUB_TOKEN, DEPLOY_KEY, DATABASE_URL]
    run: |
      echo "Deploying..."
      curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/...

Secret values are: - Injected as environment variables with the same name as the secret - Automatically redacted from all log output (replaced with ***) - Never stored in the scheduler database - Never visible in the Web UI


Secret Rotation

To rotate a secret, overwrite it at the same scope:

forge secret set GITHUB_TOKEN "new-token-value"

Running jobs that have already fetched the secret will use the old value until they complete. New jobs will fetch the new value.


Vault Authentication in Production

For production, instead of a long-lived root token, configure Vault with a more secure auth method:

AppRole (recommended for agents):

# Create a policy
vault policy write forge-agent - <<EOF
path "secret/data/forge/*" {
  capabilities = ["read", "list"]
}
EOF

# Create an AppRole
vault auth enable approle
vault write auth/approle/role/forge-agent \
  token_policies="forge-agent" \
  token_ttl=1h \
  token_max_ttl=4h

Set FORGE_VAULT_TOKEN on agents to a token obtained via AppRole login.

Kubernetes (for agents running in Kubernetes): Use the Vault Kubernetes auth backend to grant agents service-account-based access without a static token.