Upgrades
This guide covers upgrading Rack Gateway to new versions, including planning, execution, and rollback procedures.
Before Upgrading
Section titled “Before Upgrading”Check Release Notes
Section titled “Check Release Notes”Always review the release notes for breaking changes:
- Visit the GitHub Releases
- Read all releases between your current and target version
- Note any breaking changes or migration requirements
Verify Current Version
Section titled “Verify Current Version”# The CLI's version and the gateway'srack-gateway versionrack-gateway version asks the gateway for its version (GET /api/v1/info), so it needs a login.
Backup Database
Section titled “Backup Database”Create a backup before upgrading:
# RDS snapshot (AWS)aws rds create-db-snapshot \ --db-instance-identifier rack-gateway-prod \ --db-snapshot-identifier rack-gateway-pre-upgrade-$(date +%Y%m%d)
# Or manual backup, with the admin connectionpg_dump "$ADMIN_DATABASE_URL" > backup_pre_upgrade.sqlUpgrade Procedures
Section titled “Upgrade Procedures”Convox Deployment
Section titled “Convox Deployment”-
Update the image tag
Edit
convox.yml:services:gateway:image: docker.io/docspringcom/rack-gateway:v1.2.0 -
Review environment changes
Check if new environment variables are required:
Terminal window # Compare current env with docsconvox env -a rack-gateway -
Build, migrate, then promote
The gateway doesn’t run migrations on startup in production, so run them against the new release before promoting it:
Terminal window convox build -a rack-gateway # note the release ID it printsconvox run admin -a rack-gateway --release RELEASE_ID ./rack-gateway-api migrateconvox releases promote RELEASE_ID -a rack-gateway --waitscripts/deploy.sh <rack>does these three steps through the gateway, andscripts/deploy_all.shruns them for every rack in turn. -
Verify deployment
Terminal window # Check healthcurl https://gateway.example.com/api/v1/health# Check logs for errorsconvox logs -a rack-gateway --since 5m
Docker Deployment
Section titled “Docker Deployment”-
Pull new image
Terminal window docker pull docker.io/docspringcom/rack-gateway:v1.2.0 -
Stop current container
Terminal window docker compose down -
Update compose.yaml
services:gateway:image: docker.io/docspringcom/rack-gateway:v1.2.0 -
Run migrations (as the admin database user)
Terminal window docker compose run --rm gateway ./rack-gateway-api migrate -
Start new version
Terminal window docker compose up -d -
Verify
Terminal window curl http://localhost:8080/api/v1/healthdocker compose logs -f
Zero-Downtime Upgrades
Section titled “Zero-Downtime Upgrades”On Convox, promoting a release is a rolling deploy: with the default deployment settings Convox starts the new process, waits for its health check, then stops the old one, so even a single instance upgrades without downtime. Migrations run before the promote, while the old release is still serving, so they must work with the old release; the release notes below say when one doesn’t.
Database Migrations
Section titled “Database Migrations”Migrations run automatically on startup only in development builds with DEV_MODE=true; the release image never
runs them on startup. In production, run them against the new release before promoting it:
convox run admin -a rack-gateway --release RELEASE_ID ./rack-gateway-api migrateMigrations are idempotent; running them again is safe.
Rollback Migrations
Section titled “Rollback Migrations”- Restore from backup if needed
- Deploy the previous version
- Migrations are forward-only. Most leave the schema usable by the previous version, but not all: a release note below says when the previous version can’t run (or can’t fully run) against the new schema.
Rollback Procedures
Section titled “Rollback Procedures”Quick Rollback (Convox)
Section titled “Quick Rollback (Convox)”# List previous releasesconvox releases -a rack-gateway
# Rollback to previous releaseconvox releases rollback RELEASE_ID -a rack-gatewayImage Rollback
Section titled “Image Rollback”The image tag is pinned in convox.yml. To go back to an earlier version, deploy the convox.yml from that
version’s tag (git checkout v1.1.0 -- convox.yml, then scripts/deploy.sh <rack>), or roll back to a release
that already runs it. Migrations aren’t undone, so check the release notes below before rolling back past one.
Database Rollback
Section titled “Database Rollback”If a migration caused issues:
- Stop the gateway
- Restore database from backup
- Deploy previous version
# Stop gatewayconvox scale gateway --count 0 -a rack-gateway
# Restore database (example for RDS)aws rds restore-db-instance-from-db-snapshot \ --db-instance-identifier rack-gateway-restored \ --db-snapshot-identifier rack-gateway-pre-upgrade-20240115
# Update DATABASE_URL and ADMIN_DATABASE_URL to point to restored instance# Deploy previous versionA restore to before the upgrade loses the audit events written since; see Restore from Backup.
Release Notes That Need Action
Section titled “Release Notes That Need Action”Security audit fixes (v0.1.15)
Section titled “Security audit fixes (v0.1.15)”This release fixes the findings of a security audit. Most changes need nothing from you; check these before you deploy.
Before deploying:
- The gateway verifies its database’s certificate.
convox.ymlsetsPGSSLMODE=verify-fullwith Amazon RDS’s CA bundle, which the image ships.DATABASE_URL(andADMIN_DATABASE_URL) must name the database by its RDS hostname: an IP address or a CNAME fails the hostname check. For a database that isn’t on RDS, setPGSSLROOTCERTto its CA. - Service restarts need
convox:app:restart(they tookconvox:process:start). Give it to any API token that restarts services. - CircleCI auto-approval needs the app’s
vcs_repo. Without it, approving a request no longer queues the CircleCI approval job. The job also checks the pipeline number and the repository’s host, owner and name. - Deploys from the repository’s default branch are refused when
allow_deploy_from_default_branchis off, whateverdefault_branchsays: the gateway now reads the default branch from GitHub too. - Rate limits on failures and approvals. Each client IP gets
AUTH_FAILURE_LIMIT_BURST(30) refused sign-ins, refilled atAUTH_FAILURE_LIMIT_PER_MINUTE(30). A session or API token that signed in within the last 24 hours, or a session just created, still gets through when its IP is refused, so clients sharing an address aren’t locked out by one of them. It has the same limit of its own, which caps how many requests with it are checked at once. Each user can createDEPLOY_APPROVAL_LIMIT_BURST(20) deploy approval requests, across all their API tokens, refilled atDEPLOY_APPROVAL_LIMIT_PER_HOUR(60). Raise that if CI creates more requests than that. A request that repeats an open one gets409with that request and doesn’t count, but every attempt, repeats and refusals included, counts against a limit three times as large. - Settings with a stored value of the wrong type are errors. The MFA settings refuse to load one, rather than
use a default, and approving or extending a deploy approval request fails while
deploy_approval_window_minutescan’t be read. If a settings error shows up after migrating, set that setting again.
What changes for users and CI:
- More env vars are secrets: names containing
PASSWORD,PASSWD,PASSPHRASEorCREDENTIAL, or ending in_PASS. A value with credentials in it (a URL with a user or password,password=) is masked for callers withoutconvox:secret:read, whatever its name. - Creating a user whose email exists answers
409instead of replacing their roles. Change roles with an update. An update whose roles were decided against roles that changed since it read them (another admin changed them) also gets409: reload the user and make the change again. - Changing a user’s email deletes their API tokens and ends their sessions. Issue new tokens.
- An archive’s object key can’t be uploaded twice, and a concurrent env change waits up to 15 seconds for the
app’s env lock, then gets
409. - A promote whose outcome the gateway couldn’t see keeps its approval’s claim: CI needs a new approval to promote again.
- A build, object or process the rack created but the approval couldn’t record gets
503, naming what to do again (create the build again, upload to a new key, start a new process). So does an upload the rack says it stored at a different key. - Exec and log sessions are rechecked every minute and end when the user, session, token, permission or approval
is gone, or when a recheck can’t finish within 30 seconds. A session lasts at most 12 hours, so a long
convox logs -freconnects. One a deploy approval allowed lasts at most an hour, whether or not the approval’s window ends first. - Web logout is a POST with the session’s CSRF token; a GET no longer signs out.
Logs and audit:
- An exec’s command is audited with the credentials a command line commonly carries redacted: a URL’s password, the
value of a secret-named assignment or flag (
DB_PASSWORD=...,--token ...), the value of a credential header (Authorization,Cookie,X-API-Key: ...), aBearervalue, and the credentials common programs take with short flags (mysql -p...,redis-cli -a,sshpass -p,curl -u user:...). A deploy approval request’s record of the commands run under it, and the refusal of aconvox runcommand it doesn’t approve, are redacted the same way. Redaction can’t recognize every credential, and the audit log is permanent: pass credentials to commands through the app’s env, never on the command line. - A request’s query string is logged and audited with the value of every parameter named for a credential redacted
(
api_key,client_secret,password, any*token*,code,state, …), not only the login parameters. - Request log lines say
rbac_decisiondenyfor every 401, 403 and 429,errorwhen the permission check failed, andnot_evaluatedwhen no check ran. Update CloudWatch filters that relied on the old values. - A deploy approval request’s approve, reject and extend entries use the resource type
deploy_approval_request(it wasdeploy-approval-request), like its create and expire entries, and all of them setdeploy_approval_request_id. - With
AUDIT_HASH_V2_FROMpinned, a chain that has no hash v2 events fails verification. An anchor over 16 KiB fails verification. When the chain has legacy (v1) events,verify-audit’s result says their content can’t be verified.
users.suspended dropped (v0.1.12)
Section titled “users.suspended dropped (v0.1.12)”This release’s migration drops the unused users.suspended column. v0.1.10 and earlier still read it, so they
can’t load users once you’ve migrated.
- Upgrade through v0.1.11 first. Migrations run before the promote, while the old release is still serving (see Zero-Downtime Upgrades), and only v0.1.11 or later keeps working against the new schema.
- Don’t roll back past v0.1.11 after migrating.
Anchor bucket retention checked at startup (v0.1.11)
Section titled “Anchor bucket retention checked at startup (v0.1.11)”AUDIT_ANCHOR_RETENTION_DAYS is now checked: the gateway reads the anchor bucket’s Object Lock configuration at
startup and in every audit verification, and refuses to start (verification fails and alerts) unless the bucket’s
default retention is COMPLIANCE mode for at least that many days.
- Give the gateway’s role
s3:GetBucketObjectLockConfigurationon the anchor bucket before deploying (see IAM Role for the Gateway), and the roleverify-auditruns with elsewhere. Without it the new release doesn’t start. - Check
AUDIT_ANCHOR_RETENTION_DAYSmatches the bucket’s retention (worm_retention_days). It defaults to 400.
Audit hash v2 and verify-audit
Section titled “Audit hash v2 and verify-audit”This release changes how audit events are hashed (hash v2: every stored column, recomputable from the row) and
adds rack-gateway-api verify-audit, which checks the chain and the S3 anchors. The gateway also runs those checks
itself every hour and alerts when they fail.
-
The previous version can’t write audit events to the database once you’ve migrated. The migration replaces
audit.append_audit_event. Frommigrateuntil the new release is promoted, the running release’s audit writes fail; requests aren’t blocked, the failures are logged, and every event still goes to stdout (and so to CloudWatch). Promote straight after migrating (scripts/deploy.shdoes). The same applies if you roll back: the earlier release can’t write audit events to the database until you roll forward again. Events from those windows are only in the logs, never in the chain. -
Existing events become legacy (v1) events.
verify-auditcan’t recompute their HMAC (it covered values the database doesn’t store exactly), so it only checks theirprevious_hashlinks. Events written from now on are hash v2 and fully checked. -
Pin where hash v2 starts. Right after promoting, run
verify-audit(see below). It printshash v2 from event Nor, before the new release has written an event, a failure naming the event that will be the first. SetAUDIT_HASH_V2_FROM=Nin the gateway’s environment and wherever elseverify-auditruns. With the pin set, verification fails until that first hash v2 event exists (any request writes one). Without it, verification fails while legacy events exist, until an anchor written by this release (within the hour) confirms the start, and always without anchors. See Pinning where hash v2 starts. -
Give the gateway’s role
s3:ListBucketVersionsands3:GetObjectVersionon the anchor bucket (see IAM Role for the Gateway). The hourly verification reads every version of each anchor; without them every run fails and alerts. To runverify-auditelsewhere, it needsAUDIT_HMAC_SECRET, a database role that can readaudit.audit_event(audit_readeris enough), and for the anchor checksAUDIT_ANCHOR_BUCKET,AUDIT_ANCHOR_CHAIN_IDand AWS credentials alloweds3:ListBucket,s3:ListBucketVersions,s3:GetObjectands3:GetObjectVersionon the anchor bucket andkms:Decrypton its key. -
Run it once, and route its alerts to Slack.
Terminal window convox run gateway -a rack-gateway ./rack-gateway-api verify-auditFrom then on the gateway runs the same checks every hour, sends failures to Sentry and Slack, and shows the latest result on the Audit Logs page (Hourly verification in the gateway). Slack integrations connected from this release on send the alerts (actions
audit_log.verification_failedandaudit_log.verification_recovered) to#security; for an existing integration, addaudit_log.verification_*to a channel’s action patterns on the Integrations page. The same goes for failed MFA verifications (mfa.verify_failed). Verification also fails when no anchor has been written for 3 hours. -
Expect findings in anchors from earlier versions. Anchors written before this release have no
format. Theirprev_anchor_hashstopped naming the previous anchor once the bucket held 1000 anchors; those are counted, not failures. Earlier releases didn’t serialise anchor writes, so concurrent runs could write an hour’s key twice, whichverify-auditreports as an overwritten anchor; check the versions’ timestamps and contents to confirm they’re from the same hour’s job. Earlier releases also retried a failing anchor job for weeks; an anchor without aformatstored more than 6 hours after itsts_utcgets a warning. Builds from before v0.0.1 wrote per-second keys (anchor-YYYYMMDDTHHMMSSZ.json, reported as not matchingts_utc) and.sha256files (ignored, with a warning). Locked anchors can’t be changed, so these findings repeat on every run, and the hourly verification keeps failing (with a daily reminder), until the anchors’ retention ends and they’re deleted.
Column encryption key required (v0.1.7)
Section titled “Column encryption key required (v0.1.7)”TOTP seeds, YubiKey public IDs and the Slack bot token are now stored encrypted with a dedicated key. See Column Encryption.
- Set
COLUMN_ENCRYPTION_KEYbefore deploying (openssl rand -base64 32) and back it up. Every command that opens the database, includingmigrate, refuses to run without it, so a deploy without the key fails before the new release is promoted. - Existing secrets are encrypted when the new release starts (v0.1.7 to v0.1.10 only), before it serves requests. Releases after v0.1.10 don’t encrypt existing secrets, so upgrade a gateway from before v0.1.7 to v0.1.10 first.
- Expect a short gap for TOTP and YubiKey OTP. Once the new release has upgraded the data, any process still running the previous release can’t read the encrypted values, so its TOTP and YubiKey OTP checks and Slack notifications fail until it is replaced. WebAuthn and backup codes keep working.
- Rolling back to a release from before column encryption has the same effect until you deploy this release again.
Test bypasses compiled out of the release image
Section titled “Test bypasses compiled out of the release image”The published image no longer contains the development and test bypasses. It refuses to start, migrate,
reset-db or write-anchor while any of these is set, naming each one:
DEV_MODEorE2E_TEST_MODEset to a true value (DEV_MODE=falseis fine)COOKIE_SECURE=false(session cookies always haveSecure, so serve the gateway over HTTPS)AWS_ENDPOINT_URL,AWS_ENDPOINT_URL_S3,AWS_ENDPOINT_URL_STS,AWS_ENDPOINT_URL_KMSorPOSTMARK_API_BASE- a
GOOGLE_OAUTH_BASE_URLthat isn’thttps://
Before upgrading, remove them from the gateway’s environment. rack-gateway-api build-info shows whether an image
has the bypasses compiled in.
A database the gateway first started with DEV_MODE on is marked development, and stays marked that way. Check
SELECT environment FROM rgw_internal_metadata on production databases; a production
gateway should report production.
Secrets and env var lists checked at startup (v0.1.7)
Section titled “Secrets and env var lists checked at startup (v0.1.7)”Check these before upgrading:
- Secrets:
APP_SECRET_KEYandAUDIT_HMAC_SECRETmust each be at least 32 bytes, and neither can be the development value from this repository.migraterefuses weak ones too, so a deploy stops before it promotes. Generate new ones withopenssl rand -base64 32. ChangingAUDIT_HMAC_SECRETbreaks verification of earlier audit events, so only replace it if it’s actually weak. - Env var list variables:
CONVOX_SECRET_ENV_VARSand theprotected_env_vars,secret_env_varsandaudit_visible_env_varssetting variables must be a JSON array of names or a comma-separated list of names. A quoted value ('"A,B"', asjsonencodeof a string writes) or an invalid name stops the gateway from starting. - Stored lists and image patterns: a stored env var list with an invalid name (for example
STRIPE_*, which never matched anything) makes that app’s env count as all secret and protected. Its env pages return 500 until you fix the list under Apps →{app}→ Settings. A storedservice_image_patternsvalue that isn’t an object of valid patterns refuses the app’s builds. - Promotes are checked against image patterns: for an app with
service_image_patterns, a promote now loads the release’s manifest with the release’s env, as the rack does, and is refused (403) if an init container image doesn’t match the patterns, with{{GIT_COMMIT}}as the commit the release was built from, or a resource sets its ownimage. Check this before rolling back to an older release. See Promotes are checked too.
Deploy approvals bound to commits (v0.1.3)
Section titled “Deploy approvals bound to commits (v0.1.3)”Approval-bound CI builds now have to use pre-built images tagged with the approved commit, from a repository
pinned by service_image_patterns. Builds are refused until the patterns are set, so plan the rollout:
- Set
service_image_patternsright after the new version is live, not before. Earlier versions require the build’sgit-shaonce patterns are configured, and a CI build from a directory that isn’t a git checkout sends none, so setting them first breaks deploys on the old version. Avoid running CI deploys until the patterns are set: a build refused in that window has already used up its approval’s archive upload, so the request has to be rejected (or left to expire) and requested again. - Check
approved_deploy_commandsis a JSON array of strings for every app. An empty list (or a value of any other shape) now allows no commands, where it used to allow every command. The settings API refuses other shapes from this version on. - Check
vcs_repoisowner/repofor apps using CircleCI auto-approval. Hold jobs are only approved when the workflow’s pipeline builds the approved commit of that repository. - CircleCI approval jobs queued by the previous version are cancelled rather than run; approve those holds in CircleCI by hand.
- Earlier versions stored
CIRCLECI_TOKENandGITHUB_TOKENin background job arguments. Once the old version is fully drained, remove them fromriver_job.argsand rotate both tokens.
Version Compatibility
Section titled “Version Compatibility”Rack Gateway is pre-1.0: any release can change the API, the CLI or the schema. Keep the CLI and every gateway on
the same release, and read the release notes for each version you skip. rack-gateway version shows the CLI’s
version and the gateway’s; a CLI that’s too old or too new for the gateway’s login flow says so when you log in.
Migrations are forward-only. Rolling back the gateway works when the previous version can use the new schema (the release notes say when it can’t); otherwise restore from backup.
Upgrade Checklist
Section titled “Upgrade Checklist”Before upgrading:
- Read release notes for all versions
- Backup database
- Note any new required environment variables
- Schedule maintenance window (if not zero-downtime)
- Notify users of potential brief interruption
After upgrading:
- Verify the health endpoint
- Check application logs for errors
- Test critical functionality (login, deploy)
- Run
verify-audit, or check the next hourly verification on the Audit Logs page - Monitor error rates for 24 hours
- Upgrade the CLI to the same release
Troubleshooting Upgrades
Section titled “Troubleshooting Upgrades”Upgrade Stuck
Section titled “Upgrade Stuck”# Check pod/container statusconvox ps -a rack-gateway
# Check eventsconvox logs -a rack-gateway --since 5mHealth Check Failing
Section titled “Health Check Failing”# Test health endpoint inside a running process (PID from convox ps)convox exec <pid> -a rack-gateway wget -q -O - http://localhost:8080/api/v1/health
# Check startup logsconvox logs -a rack-gateway | grep -i "start\|error"Database Migration Error
Section titled “Database Migration Error”# Run the migrations again; the error names the failing migrationconvox run admin ./rack-gateway-api migrate -a rack-gateway-- Applied migrationsSELECT version FROM schema_migrations ORDER BY version DESC LIMIT 10;Each migration runs in its own transaction, so a failed one leaves nothing behind.
Further Reading
Section titled “Further Reading”- Database Maintenance - Backup and restore procedures
- Monitoring - Verify upgrade success
- Troubleshooting - Resolve upgrade issues