Security and credential storage
What each deployment secret protects and how to change it, how a self-hosted Gitterm encrypts and hands out credentials, and how to rotate the encryption key.
This page is for whoever runs a self-hosted Gitterm deployment. It covers what each deployment secret protects and how to change it, how stored credentials are encrypted, what reaches a workspace, how to rotate the master key, and what revocation does. Read it before you put real credentials into the deployment. Where each variable is set is on Configuration.
Secrets
Generate every secret with openssl rand -hex 32, and use a different value for each. Keep them in your deployment's secret manager or a file only you can read (chmod 600 .env.selfhost), never in the database or source control.
The boundary
An agent can read any credential its own process can use. File permissions and hidden paths prevent accidental leaks; they don't hide a usable credential from the agent. So Gitterm limits how long credentials last, what they can reach and where they are kept, rather than claiming a workspace can't see them.
What reaches a workspace
How runtime values reach the machine depends on the workspace provider. On Railway they stay encrypted in Gitterm and the workspace fetches them at boot with a revocable token. On AWS they live in a Secrets Manager secret per workspace. Sandbox providers receive them through their authenticated API.
Gitterm records when credentials are issued, without token values, headers or secret contents.
Encryption
Each encrypted value gets its own random 256-bit data key. That key is wrapped with the deployment's AES-256-GCM master key, and the ciphertext records the key id. No KMS is needed.
ENCRYPTION_MASTER_KEY=<64 hex characters>
ENCRYPTION_MASTER_KEY_ID=primarySet the same keyring on server, listener, and any job that runs migrations or the security scripts. Without a master key, saving or reading a credential fails; production never falls back to a default key.
Hardened writes
Two settings control the format of new writes. Reads accept every format.
A fresh install can use the hardened values from the start. An existing install switches in this order:
-
Back up the database. Don't rotate any secret during the switch.
-
Deploy the current release to every service with the compatibility values, and wait for old processes to stop.
-
Set the hardened values and restart the services.
-
Run the dry runs, then apply, then dry-run again:
Shell docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml exec server bun run /app/dist/scripts/security-rewrap.mjs docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml exec server bun run /app/dist/scripts/security-oauth.mjs docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml exec server bun run /app/dist/scripts/security-rewrap.mjs --apply docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml exec server bun run /app/dist/scripts/security-oauth.mjs --applyOutside Compose, run
bun run /app/dist/scripts/security-rewrap.mjsandsecurity-oauth.mjsin the server container. Each run prints one JSON line. Run both dry runs again at the end: you should see"candidates":0and"conflicts":0from each. Repeat the apply step until you do.
Once hardened writes are on, only roll back to a release that can read them. Turning them off again doesn't decrypt anything and allows weaker writes.
Rotate the master key
-
Move the current key into
ENCRYPTION_MASTER_KEYSunder its existing id. -
Set a new
ENCRYPTION_MASTER_KEYand a newENCRYPTION_MASTER_KEY_ID:INI ENCRYPTION_MASTER_KEY_ID=2026-09 ENCRYPTION_MASTER_KEY=<new key> ENCRYPTION_MASTER_KEYS={"primary":"<previous key>"} -
Deploy to
serverandlistener. New writes use the new key; reads accept both. -
Run
security-rewrap.mjsas above to see how many records still use the old key, then run it with--apply. -
Repeat the dry run until it prints
"candidates":0and"conflicts":0. Then remove the old key from the live services, and keep it separately until old backups expire.
The rewrap covers model credentials, provider configs, workspace passwords, route headers, runtime bundles, environment sets, MCP auth, SSH revocation tokens, Google issuer keys, and GitHub App keys, webhook secrets and tokens. A failed or interrupted run is safe to repeat. Don't remove a key after a run that didn't finish cleanly.
Revocation
- Paused workspaces can't fetch runtime values or get new GitHub, Google or model credentials. Terminated workspaces and deleted integrations can't get any.
- Revoking or deleting a model credential stops every workspace from getting new access tokens for it. Tokens already issued last until they expire.
- Removing an MCP connection stops future attachments only. Workspaces that already have it keep it; revoke the credential at the MCP provider.
- Runtime values are deleted when provisioning fails and when a workspace is terminated.
- Each workspace token is checked against the workspace's state and version on every request. Bumping the version revokes all its tokens, and the workspace then has to be recreated.
What stays your responsibility
Database TLS, disk and backup encryption, access to the servers, and retention policies are up to your deployment. Application encryption doesn't protect against someone who controls the running application.