Files
coder/docs/admin/security/database-encryption.md
T
Nick Vigilante bc77c40296 docs: add front-matter titles to mechanical and no-H1 pages (Phase 3) (#28030)
## Summary

**Batch A of Phase 3** of the H1 → front-matter migration (`DOCS-484`;
parent `DOCS-477`). Adds a front-matter `title` to every navigable docs
page whose title can be migrated **mechanically**, with no editorial
judgment.

This is the content step that Phase 1 (renderers prefer front-matter
title, `DOCS-482`) and Phase 2 (tooling + generators front-matter-aware,
`DOCS-483`) unblocked. Both are merged; coder.com #964/#974 are merged
and live.

**Rendered no-op.** The renderers already resolve the page title from
the manifest and hide the leading body H1 (Phase 1), so no page changes
visually. This just moves the title into front matter where Fumadocs and
the migrated tooling can read it.

## What's in this batch

Dry-run on `main` (464 navigable pages) splits into:

| category | count | this PR |
|----------|-------|---------|
| already has front-matter title (Reference, from Phase 2 generators) |
196 | skipped (idempotent) |
| **mechanical** — leading body H1 equals the manifest label | 138 | ✅
add front-matter `title`, drop the duplicate H1 |
| **no body H1** — renders under the manifest label only | 4 | ✅ add
front-matter `title` only |
| **mismatch** — body H1 differs from the manifest label | 126 | ⏭️
deferred (needs an editorial decision, see below) |

142 files changed, all under `docs/`.

## Deliberately out of scope: the 126 mismatches

Pages where the body H1 is richer than the short sidebar label (e.g.
label **Modules** / H1 *Contributing modules*, label **Install** / H1
*Installing Coder*) need a canonical-title decision, not a script. A few
even look like the body H1 is the redundant one (`install/cli.md` and
`install/index.md` both carry the H1 *Installing Coder*). These will
land in follow-up batches **by nav section** once the policy is set, so
each gets real review.

## Verification

AI was the primary author of this PR (see disclosure below); per the [AI
Contribution
Guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
here is the manual verification.

- Every added front-matter block parses as YAML and its `title`
round-trips to the manifest label (checked programmatically across all
142 files).
- Every removed line is a leading `# H1` that equalled the manifest
title; no body prose was reflowed. Front matter is correctly hoisted
above pre-existing `<!-- markdownlint-disable -->` comments on the two
pages that had them.
- `pnpm check-docs` (markdownlint-cli2 + table formatter) passes on the
changed set: `Summary: 0 error(s)`. `MD041` stays off (re-enabled in
Phase 4); `MD025` is not tripped because the duplicate body H1s are
removed.

```
$ pnpm exec markdownlint-cli2 $(git diff --name-only origin/main)
Linting: 142 file(s)
Summary: 0 error(s)
```

Linear: DOCS-484

> This PR was created with AI assistance (Coder Agents).
2026-08-27 12:34:13 -04:00

7.2 KiB

title
title
Database Encryption

By default, Coder stores external user tokens and other sensitive values in plaintext in the database. Database Encryption allows Coder administrators to encrypt these values at-rest, preventing attackers with database access from reading or misusing them.

How it works

Coder allows administrators to specify external token encryption keys. If configured, Coder will use these keys to encrypt external user tokens before storing them in the database. The encryption algorithm used is AES-256-GCM with a 32-byte key length.

Coder will use the first key provided for both encryption and decryption. If additional keys are provided, Coder will use it for decryption only. This allows administrators to rotate encryption keys without invalidating existing tokens.

The following database fields are currently encrypted:

  • user_links.oauth_access_token
  • user_links.oauth_refresh_token
  • external_auth_links.oauth_access_token
  • external_auth_links.oauth_refresh_token
  • crypto_keys.secret
  • user_secrets.value
  • gitsshkeys.private_key

Additional database fields may be encrypted in the future.

Implementation notes

Each encrypted database column $C has a corresponding $C_key_id column. This column is used to determine which encryption key was used to encrypt the data. This allows Coder to rotate encryption keys without invalidating existing tokens, and provides referential integrity for encrypted data.

The $C_key_id column stores the first 7 bytes of the SHA-256 hash of the encryption key used to encrypt the data.

Encryption keys in use are stored in dbcrypt_keys. This table stores a record of all encryption keys that have been used to encrypt data. Active keys have a null revoked_key_id column, and revoked keys have a non-null revoked_key_id column. You cannot revoke a key until you have rotated all values using that key to a new key.

Enabling encryption

Note

Enabling encryption does not encrypt all existing data. To encrypt existing data, see rotating keys below.

  • Ensure you have a valid backup of your database. Do not skip this step. If you are using the built-in PostgreSQL database, you can run coder server postgres-builtin-url to get the connection URL.

  • Generate a 32-byte random key and base64-encode it. For example:

dd if=/dev/urandom bs=32 count=1 | base64
  • Store this key in a secure location (for example, a Kubernetes secret):
kubectl create secret generic coder-external-token-encryption-keys --from-literal=keys=<key>
  • In your Coder configuration set CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS to a comma-separated list of base64-encoded keys. For example, in your Helm values.yaml:
coder:
  env:
    [...]
    - name: CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS
      valueFrom:
        secretKeyRef:
          name: coder-external-token-encryption-keys
          key: keys
  • Restart the Coder server. The server will now encrypt all new data with the provided key.

Rotating keys

We recommend only having one active encryption key at a time normally. However, if you need to rotate keys, you can perform the following procedure:

  • Ensure you have a valid backup of your database. Do not skip this step.

  • Generate a new encryption key following the same procedure as above.

  • Add the above key to the list of external token encryption keys. The new key must appear first in the list. For example, in the Kubernetes secret created above:

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: coder-external-token-encryption-keys
  namespace: coder-namespace
data:
  keys: <new-key>,<old-key1>,<old-key2>,...
  • After updating the configuration, restart the Coder server. The server will now encrypt all new data with the new key, but will be able to decrypt tokens encrypted with the old key(s).

  • To re-encrypt all encrypted database fields with the new key, run coder server dbcrypt rotate. This command will re-encrypt all tokens with the specified new encryption key. We recommend performing this action during a maintenance window.

    This command requires direct access to the database. If you are using the built-in PostgreSQL database, you can run coder server postgres-builtin-url to get the connection URL.

  • Once the above command completes successfully, remove the old encryption key from Coder's configuration and restart Coder once more. You can now safely delete the old key from your secret store.

Disabling encryption

To disable encryption, perform the following actions:

  • Ensure you have a valid backup of your database. Do not skip this step.

  • Stop all active coderd instances. This will prevent new encrypted data from being written, which may cause the next step to fail.

  • Run coder server dbcrypt decrypt. This command will decrypt all encrypted user tokens and revoke all active encryption keys.

    Note

    for decrypt command, the equivalent environment variable for --keys is CODER_EXTERNAL_TOKEN_ENCRYPTION_DECRYPT_KEYS and not CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS. This is explicitly named differently to help prevent accidentally decrypting data.

  • Remove all external token encryption keys from Coder's configuration.

  • Start coderd. You can now safely delete the encryption keys from your secret store.

Deleting Encrypted Data

Caution

This is a destructive operation.

To delete all encrypted data from your database, perform the following actions:

  • Ensure you have a valid backup of your database. Do not skip this step.

  • Stop all active coderd instances. This will prevent new encrypted data from being written.

  • Run coder server dbcrypt delete. This command will delete all encrypted user tokens and revoke all active encryption keys.

  • Remove all external token encryption keys from Coder's configuration.

  • Start coderd. You can now safely delete the encryption keys from your secret store.

Troubleshooting

  • If Coder detects that the data stored in the database was not encrypted with any known keys, it will refuse to start. If you are seeing this behavior, ensure that the encryption keys provided are correct.
  • If Coder detects that the data stored in the database was encrypted with a key that is no longer active, it will refuse to start. If you are seeing this behavior, ensure that the encryption keys provided are correct and that you have not revoked any keys that are still in use.
  • Decryption may fail if newly encrypted data is written while decryption is in progress. If this happens, ensure that all active coder instances are stopped, and retry.

Next steps