Skip to content

Environment Variables

This page provides a complete reference of Rack Gateway environment variables. For a shorter overview, see Configuration.

A few settings switch off or redirect a security control so the gateway can run against local mocks in development and in the end-to-end tests:

VariableWhat it does
DEV_MODE=trueRelaxes cookies, CSP, origin checks and secret requirements, and runs migrations on startup
E2E_TEST_MODE=trueSkips WebAuthn assertion checks
COOKIE_SECURE=falseLeaves the Secure attribute off cookies
AWS_ENDPOINT_URLSends every AWS call (S3 audit anchors, STS, KMS) to another endpoint
AWS_ENDPOINT_URL_S3Sends audit anchors to another S3 endpoint
AWS_ENDPOINT_URL_STS, AWS_ENDPOINT_URL_KMSSends AWS credential exchange or key operations to another endpoint
POSTMARK_API_BASESends email and the Postmark token to another server
GOOGLE_OAUTH_BASE_URL without https://Accepts identity tokens over plain HTTP
COLUMN_ENCRYPTION_KEY set to the development keyThe key is published in the repository (mise.toml), so anyone could decrypt stored secrets
APP_SECRET_KEY or AUDIT_HMAC_SECRET set to a development value, or shorter than 32 bytesThe development values are published in the repository, so anyone could forge sessions or recompute audit hashes, and a short secret can be guessed. The server refuses them when it starts

They only work in a build compiled with the testbypass Go build tag, which the development and E2E stacks use. The published Docker image is built without it, so it can’t honour these settings: a release build refuses to start while any of them is set, and the error names each one. Remove them from the environment and restart. rack-gateway-api build-info shows whether a binary has the bypasses compiled in.

A testbypass build refuses them too when its database is marked production. The first time a gateway starts against a database, it marks the database as production, or development in a testbypass build with DEV_MODE=true.

DOMAIN, APP_SECRET_KEY, GOOGLE_ALLOWED_DOMAIN and AUDIT_HMAC_SECRET are required whenever DEV_MODE is off.

VariableDefaultDescription
PORT8080TCP port the API listens on
DOMAIN(required)The gateway’s host name, e.g. gateway.example.com. It sets the public URL (https://<DOMAIN>) used for OAuth redirects, links in emails and Slack, and the WebAuthn origin, and requests for any other Host or Origin are refused
DEV_MODEfalseDevelopment mode (testbypass builds only, see Test Bypasses)
APP_SECRET_KEY(required)Secret for sessions and CSRF: at least 32 bytes (openssl rand -base64 32)
COLUMN_ENCRYPTION_KEY(required)Key that encrypts stored secrets: standard base64 of exactly 32 random bytes (openssl rand -base64 32). See Column Encryption
COOKIE_SECUREtruefalse leaves the Secure attribute off cookies, for plain-HTTP stacks (testbypass builds only). Release builds always set it
TRUSTED_PROXY_CIDRSComma-separated CIDRs of the reverse proxies in front of the gateway (e.g. an ingress). X-Forwarded-For is only believed from these, and the client IP is the right-most address that isn’t one of them. Leave empty when clients connect directly; set it whenever there is a proxy, or audit logs, rate limits and security emails all see the proxy’s address
LOG_LEVELinfoLog level (debug, info, warn, error)
DEBUG_TOPICSComma-separated debug topics
RATE_LIMIT_RPS10Requests per second allowed from one client IP on the sign-in, MFA and API token routes. An IPv6 client is limited by its /64. The gateway keeps a limit for up to 50,000 clients at once; past that, new clients share one
RATE_LIMIT_BURST20Burst size for the per-IP rate limit
AUTH_FAILURE_LIMIT_BURST30Failed sign-ins (refused session or API tokens) one client IP can make before its requests are refused with 429 until some come back. Requests without credentials don’t count. A session or API token that signed in from anywhere in the last 24 hours, or a session just created, isn’t refused for its IP’s failures: someone else behind the same NAT or proxy may have made them. It has the same limit of its own instead, which also caps how many requests with it are checked at once
AUTH_FAILURE_LIMIT_PER_MINUTE30How many failed attempts come back each minute
DEPLOY_APPROVAL_LIMIT_BURST20Deploy approval requests one user can create, themselves or with any of their API tokens, before they’re refused with 429 until some come back. Each can cost several GitHub API calls on GITHUB_TOKEN. A request that repeats an open one is answered with it, and doesn’t count. Every attempt, repeats and refusals included, counts against a limit three times as large, checked before anything else
DEPLOY_APPROVAL_LIMIT_PER_HOUR60How many deploy approval requests come back each hour

COLUMN_ENCRYPTION_KEY encrypts the secrets the gateway stores in its database: TOTP seeds and YubiKey public IDs (mfa_methods.secret) and the Slack bot token. Values are encrypted with AES-256-GCM and bound to where they belong (the user and MFA method type, or the Slack workspace), so a value copied to another user’s row, or to a method of another type, can’t be decrypted there. A plaintext value is never used: reading one is an error, as for a value that doesn’t decrypt.

  • Generate it once with openssl rand -base64 32 and store it with your other secrets. Use a different key per environment. It is separate from APP_SECRET_KEY, so rotating the session secret doesn’t affect stored secrets.
  • It is required in every mode. The server and every maintenance command that opens the database (migrate, reset-db, write-anchor) refuse to run without a valid key, so a missing key fails a deploy before the new release is promoted. Development setups use a published key from mise.toml and docker-compose.yml. The published image refuses to start with that key, and so does any build against a production database.
  • Back it up. If the key is lost or changed, stored TOTP seeds, YubiKey public IDs and the Slack token can’t be read. Those MFA methods stop working (WebAuthn security keys and backup codes still work): users sign in with another factor or a backup code, remove the broken method and enroll again. Codes from an authenticator app that can’t be used get a message saying so, and don’t count toward the automatic account lock. Slack must be reconnected. The gateway logs (and reports to Sentry, if configured) every value it fails to decrypt when it tries to use it.
  • Upgrading from before v0.1.7 (plaintext TOTP seeds, base64 Slack tokens): deploy a release from v0.1.7 to v0.1.10 first. Those releases encrypt the existing secrets when they start; later releases don’t, and can’t read secrets stored before column encryption.
  • Rotation isn’t supported yet. Every encrypted value records which key encrypted it, so rotation can be added later without changing the stored format.
VariableDefaultDescription
GOOGLE_CLIENT_ID(required)OAuth client ID
GOOGLE_CLIENT_SECRET(required)OAuth client secret
GOOGLE_ALLOWED_DOMAIN(required)Allowed email domain
GOOGLE_OAUTH_BASE_URLhttps://accounts.google.comOIDC issuer URL. Must be https:// outside testbypass builds
VariableDescription
ADMIN_USERSComma-separated admin emails
VIEWER_USERSComma-separated viewer emails
DEPLOYER_USERSComma-separated deployer emails
OPERATIONS_USERSComma-separated ops emails

Every start creates the listed users that don’t exist yet, with the role of each list that names them. Existing users are never changed, so a role you change in the web UI stays changed; to take a seeded user’s access away, lock or delete them in the web UI and remove them from the list. Each user it creates is recorded in the audit log as user.create by system, in the same transaction, and the gateway doesn’t start if that record can’t be written.

VariableDefaultDescription
RACK_HOSTConvox rack API URL. Inside a Convox v3 rack it can be left unset: the gateway then uses https://api.<RACK>-system.svc.cluster.local:5443
RACK_TOKENConvox rack API token
RACK_USERNAMEconvoxBasic auth username
RACKdefaultRack name (internal identifier). Convox sets it for apps running on a rack
RACK_ALIASRACKShort alias (user-facing). Authenticator apps show TOTP entries as Rack Gateway (<RACK_ALIAS>)
RACK_DISPLAY_NAMERACK_ALIASHuman-friendly display name; every email subject and Slack message starts with Rack Gateway (<name>)
ENABLE_RACK_TLS_PINNINGfalsePin the rack API’s self-signed certificate on first use. Off by default: the Convox rack API generates a new certificate every time it restarts, so a pinned certificate stops matching after each rack update
VariableDescription
RGW_DATABASE_URLPreferred Postgres URL
GATEWAY_DATABASE_URLAlternate Postgres URL
DATABASE_URLAlternate Postgres URL
ADMIN_DATABASE_URLAdmin URL for migrate and reset-db (falls back to the URLs above)
PGHOST / PGPORT / PGUSER / PGPASSWORD / PGDATABASElibpq connection fields
PGSSLMODESSL mode. It replaces the URL’s sslmode only when it’s stricter (verify-full over require, never the reverse). convox.yml sets verify-full
PGSSLROOTCERTCA bundle that verifies the database’s certificate. The image ships Amazon RDS’s at /app/certs/rds-global-bundle.pem
VariableDefaultDescription
DB_MAX_OPEN_CONNS10Max open connections per pool. Each process has two pools, so it can open twice this many
DB_MAX_IDLE_CONNS3Max idle connections per pool (the job queue’s pool keeps this many open)
DB_CONN_MAX_LIFETIME30mConnection lifetime
DB_CONN_MAX_IDLE_TIME10mIdle timeout

See Connection Pool for sizing.

VariableDescription
TEST_DATABASE_URLOverride database URL for tests
VariableDescription
YUBICO_CLIENT_IDYubiKey OTP client ID
YUBICO_SECRET_KEYYubiKey OTP secret
WEBAUTHN_RP_IDWebAuthn relying party ID (default: DOMAIN)
WEBAUTHN_ORIGINWebAuthn origin (default: the gateway’s public URL, https://<DOMAIN>)
VariableDefaultDescription
DEPLOY_APPROVAL_PENDING_MAX_AGE168hHow long a request can stay pending before it expires (a Go duration; 0 keeps pending requests forever). Each expiry is audit-logged.

Settings resolve in this order: env var → DB value → default.

An environment variable pins its setting: the web UI shows it read-only, and the API refuses to change it (409 Conflict). Change it in the deployment config instead. Values are parsed strictly by the setting’s type:

  • Booleans: true or false
  • Numbers: whole numbers from 1 to the setting’s maximum: 1440 (a day) for session_timeout_minutes and mfa_step_up_window_minutes, 365 for mfa_trusted_device_ttl_days, and 10080 (a week) for deploy_approval_window_minutes
  • Lists: a JSON array of strings, or a comma-separated list (use the JSON form when a value contains a comma, e.g. an approved deploy command). A quoted value such as "A,B" (what jsonencode writes for a string) is refused: use jsonencode on a list. The env var lists (protected_env_vars, secret_env_vars, audit_visible_env_vars) must hold valid env var names, here and when saved through the API.
  • Maps (service_image_patterns): a JSON object of strings
  • Strings: the plain value, or a JSON string ("main", as Terraform’s jsonencode writes it). Other quoting is refused. verify_git_commit_mode must be latest or branch.

Every value can be JSON-encoded, so a deployment tool can write jsonencode(value) for all of them.

The API checks the values it saves the same way, by JSON type: a boolean sent as the string "true", or a number that’s fractional or out of range, is refused with 400 Bad Request. A string setting also takes null, which reads as its default. Each settings group (a card in the web UI) is saved in one transaction, so a save changes all of its settings or none. A database value saved before these checks that doesn’t fit its setting is an error, not some other value: the gateway refuses to start with an invalid MFA setting, and the other settings fail safe (the default session timeout, deploy approvals required, destructive actions refused). Save a new value, delete it, or set its environment variable.

The gateway refuses to start when a RGW_SETTING_* or RGW_APP_*_SETTING_* variable names an unknown setting, isn’t spelled the way the gateway reads it (upper case, with dashes in app names written as underscores), or has a value that doesn’t fit its type. A database value that an environment variable overrides has no effect: the startup log lists them, and the settings page marks them and lets an admin clear them.

VariableDescription
RGW_SETTING_SESSION_TIMEOUT_MINUTESBrowser session inactivity timeout
RGW_SETTING_MFA_REQUIRE_ALL_USERSRequire MFA for all users
RGW_SETTING_MFA_STEP_UP_WINDOW_MINUTESStep-up window
RGW_SETTING_MFA_TRUSTED_DEVICE_TTL_DAYSTrusted device TTL
RGW_SETTING_DEPLOY_APPROVALS_ENABLEDEnable deploy approvals
RGW_SETTING_DEPLOY_APPROVAL_WINDOW_MINUTESApproval window
RGW_SETTING_DEPLOY_APPROVAL_REQUIRE_DIFFERENT_APPROVERRequire a different approver
RGW_SETTING_ALLOW_DESTRUCTIVE_ACTIONSAllow destructive actions
RGW_SETTING_DEFAULT_CI_PROVIDERDefault CI provider
RGW_SETTING_DEFAULT_VCS_PROVIDERDefault VCS provider

Per-app overrides use:

RGW_APP_<APP>_SETTING_<KEY>=value

<APP> is the app name in upper case, with dashes replaced by underscores. Examples:

RGW_APP_MY_APP_SETTING_CI_PROVIDER=circleci
RGW_APP_MY_APP_SETTING_PROTECTED_ENV_VARS=DATABASE_URL,SECRET_KEY_BASE
RGW_APP_MY_APP_SETTING_AUDIT_VISIBLE_ENV_VARS=PORT,RAILS_ENV
RGW_APP_MY_APP_SETTING_SERVICE_IMAGE_PATTERNS={"*":"registry\\.example\\.com/my-app:{{GIT_COMMIT}}"}

See Deploy Approvals for common keys.

VariableDefaultDescription
AUDIT_HMAC_SECRET(required in prod)HMAC chain secret: at least 32 bytes (openssl rand -base64 32). Without it (DEV_MODE only) the gateway doesn’t verify its audit log.
AUDIT_ANCHOR_BUCKETS3 bucket for anchors. Unset, no anchors are written
AUDIT_ANCHOR_CHAIN_IDChain identifier, the anchors’ key prefix. Required with AUDIT_ANCHOR_BUCKET
AUDIT_ANCHOR_RETENTION_DAYS400The least the anchor bucket’s default Object Lock retention may be. The bucket’s rule locks the anchors; the gateway checks it at startup and in every verification, and refuses to start (verification fails) unless the bucket locks new objects in COMPLIANCE mode for at least this many days. Set it to the bucket’s retention. Needs s3:GetBucketObjectLockConfiguration
AUDIT_ANCHOR_INTERVAL_MINUTES60Anchor interval
AUDIT_HASH_V2_FROMChain index where hash v2 starts, recorded after upgrading, for the hourly verification and verify-audit (see Verification). The gateway refuses to start when it isn’t a chain index.
AWS_ENDPOINT_URL_S3S3 endpoint override, e.g. MinIO (testbypass builds only)

The anchor writer uses the AWS SDK’s default credential chain: on Convox, the app’s IAM role (the Terraform module creates it), or AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION elsewhere.

Heartbeat monitors (Cronitor, Healthchecks.io and the like) alert when the gateway stops requesting a URL on schedule. See Heartbeats. The URLs must be https; they usually carry the monitoring service’s key, so set them as secrets. An invalid value stops the gateway from starting.

VariableDefaultDescription
UPTIME_HEARTBEAT_URLRequested when the gateway starts and every UPTIME_HEARTBEAT_INTERVAL_MINUTES while it’s healthy
UPTIME_HEARTBEAT_INTERVAL_MINUTES15How often the uptime heartbeat is requested
AUDIT_VERIFY_HEARTBEAT_URLRequested after every hourly audit verification run (half past the hour, UTC), whatever it found
VariableDefaultDescription
POSTMARK_API_TOKENPostmark API token
POSTMARK_FROMno-reply@<GOOGLE_ALLOWED_DOMAIN>Sender
POSTMARK_STREAMoutboundMessage stream
POSTMARK_API_BASEhttps://api.postmarkapp.comAPI base override (testbypass builds only)
DEV_EMAIL_LOGfalsetrue logs emails instead of dropping them when POSTMARK_API_TOKEN isn’t set (always on with DEV_MODE)
VariableDescription
SLACK_CLIENT_IDSlack OAuth client ID
SLACK_CLIENT_SECRETSlack OAuth client secret
GITHUB_TOKENGitHub token for deploy approvals. With github_verification on (the default), requests are refused without it
GITHUB_API_URLGitHub REST API root, e.g. for GitHub Enterprise (default https://api.github.com)
CIRCLECI_TOKENCircleCI token for deploy approvals
VariableDefaultDescription
SENTRY_DSNBackend DSN
SENTRY_ENVIRONMENTEnvironment name
SENTRY_RELEASERELEASERelease tag (Convox sets RELEASE to the release ID)
SENTRY_JS_DSNFrontend DSN
SENTRY_JS_TRACES_SAMPLE_RATE0Frontend perf rate
ENABLE_SENTRY_TEST_BUTTONSfalseShow test UI
VariableDescription
RACK_GATEWAY_RACKDefault rack name
RACK_GATEWAY_URLGateway URL for RACK_GATEWAY_API_TOKEN; without one it must match the rack’s configured URL. Not allowed with several racks
RACK_GATEWAY_API_TOKENAPI token for automation
GATEWAY_CLI_CONFIG_DIROverride CLI config dir (default ~/.config/rack-gateway)
CONVOX_APPDefault app for commands that take --app (after --app and before .convox/app)
VariableDescription
CONVOX_SECRET_ENV_VARSComma-separated env vars treated as secrets in every app: masked, changed only with convox:secret:set, and never shown in audit logs
RGW_APP_<APP>_SETTING_AUDIT_VISIBLE_ENV_VARSThe app’s env vars whose values audit logs and Slack show (audit_visible_env_vars). Every other value is [REDACTED], and secret and protected keys always are

See Secret Redaction.

VariableDescription
RESET_RACK_GATEWAY_DATABASEMust be DELETE_ALL_DATA for rack-gateway-api reset-db to run
DISABLE_DATABASE_ENVIRONMENT_CHECKLets reset-db reset a database marked production (or unmarked, outside DEV_MODE). A database marked development can always be reset
VariableDescription
FORCE_CSP_IN_DEVtrue sends the Content Security Policy in DEV_MODE too (the E2E and preview stacks set it)
WEB_DEV_SERVER_URLVite dev server the gateway proxies the web UI to in development

Ports are defined in mise.toml:

VariableDefaultDescription
GATEWAY_PORT8447Gateway API port (dev)
WEB_PORT5223Web dev server
MOCK_OAUTH_PORT3345Mock OAuth
MOCK_CONVOX_PORT5443Mock Convox
TEST_GATEWAY_PORT9447Test gateway
TEST_MOCK_OAUTH_PORT9345Test OAuth
TEST_MOCK_CONVOX_PORT6443Test Convox