|
| 1 | +# Database Encryption |
| 2 | + |
| 3 | +By default, Coder stores external user tokens in plaintext in the database. This |
| 4 | +is undesirable in high-security environments, as an attacker with access to the |
| 5 | +database can use these tokens to impersonate users. Database Encryption allows |
| 6 | +Coder administrators to encrypt these tokens at-rest, preventing attackers from |
| 7 | +using them. |
| 8 | + |
| 9 | +## How it works |
| 10 | + |
| 11 | +Coder allows administrators to specify up to two |
| 12 | +[external token encryption keys](../cli/server.md#external-token-encryption-keys). |
| 13 | +If configured, Coder will use these keys to encrypt external user tokens before |
| 14 | +storing them in the database. The encryption algorithm used is AES-256-GCM with |
| 15 | +a 32-byte key length. |
| 16 | + |
| 17 | +Coder will use the first key provided for both encryption and decryption. If a |
| 18 | +second key is provided, Coder will use it for decryption only. This allows |
| 19 | +administrators to rotate encryption keys without invalidating existing tokens. |
| 20 | + |
| 21 | +The following database fields are currently encrypted: |
| 22 | + |
| 23 | +- `user_links.oauth_access_token` |
| 24 | +- `user_links.oauth_refresh_token` |
| 25 | +- `git_auth_links.oauth_access_token` |
| 26 | +- `git_auth_links.oauth_refresh_token` |
| 27 | + |
| 28 | +Additional database fields may be encrypted in the future. |
| 29 | + |
| 30 | +> Implementation note: there is an additional encrypted database field |
| 31 | +> `dbcrypt_sentinel.value`. This field is used to verify that the encryption |
| 32 | +> keys are valid for the configured database. It is not used to encrypt any user |
| 33 | +> data. |
| 34 | +
|
| 35 | +Encrypted data is stored in the following format: |
| 36 | + |
| 37 | +- `encrypted_data = dbcrypt-<b64data>` |
| 38 | +- `b64data = <cipher checksum>-<ciphertext>` |
| 39 | + |
| 40 | +All encrypted data is prefixed with the string `dbcrypt-`. The cipher checksum |
| 41 | +is the first 7 bytes of the SHA256 hex digest of the encryption key used to |
| 42 | +encrypt the data. |
| 43 | + |
| 44 | +## Enabling encryption |
| 45 | + |
| 46 | +1. Ensure you have a valid backup of your database. **Do not skip this step.** |
| 47 | + If you are using the built-in PostgreSQL database, you can run |
| 48 | + [`coder server postgres-builtin-url`](../cli/server_postgres-builtin-url.md) |
| 49 | + to get the connection URL. |
| 50 | + |
| 51 | +1. Generate a 32-byte random key and base64-encode it. For example: |
| 52 | + |
| 53 | +```shell |
| 54 | +dd if=/dev/urandom bs=32 count=1 | base64 |
| 55 | +``` |
| 56 | + |
| 57 | +1. Store this key in a secure location (for example, a Kubernetes secret): |
| 58 | + |
| 59 | +```shell |
| 60 | +kubectl create secret generate coder-external-token-encryption-keys --from-literal=keys=<key> |
| 61 | +``` |
| 62 | + |
| 63 | +1. In your Coder configuration set the `external_token_encryption_keys` field to |
| 64 | + a comma-separated list of base64-encoded keys. For example, in your Helm |
| 65 | + `values.yaml`: |
| 66 | + |
| 67 | +```yaml |
| 68 | +coder: |
| 69 | + env: |
| 70 | + [...] |
| 71 | + - name: CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS |
| 72 | + valueFrom: |
| 73 | + secretKeyRef: |
| 74 | + name: coder-external-token-encryption-keys |
| 75 | + key: keys |
| 76 | +``` |
| 77 | +
|
| 78 | +## Rotating keys |
| 79 | +
|
| 80 | +We recommend only having one active encryption key at a time normally. However, |
| 81 | +if you need to rotate keys, you can perform the following procedure: |
| 82 | +
|
| 83 | +1. Ensure you have a valid backup of your database. **Do not skip this step.** |
| 84 | +
|
| 85 | +1. Generate a new encryption key following the same procedure as above. |
| 86 | +
|
| 87 | +1. Add the above key to the list of |
| 88 | + [external token encryption keys](../cli/server.md#external-token-encryption-keys). |
| 89 | + **The new key must appear first in the list**. For example, in the Kubernetes |
| 90 | + secret created above: |
| 91 | +
|
| 92 | +```yaml |
| 93 | +apiVersion: v1 |
| 94 | +kind: Secret |
| 95 | +type: Opaque |
| 96 | +metadata: |
| 97 | + name: coder-external-token-encryption-keys |
| 98 | + namespace: coder-namespace |
| 99 | +data: |
| 100 | + keys: <new-key>,<old-key> |
| 101 | +``` |
| 102 | +
|
| 103 | +1. After updating the configuration, restart the Coder server. The server will |
| 104 | + now encrypt all new data with the new key, but will be able to decrypt tokens |
| 105 | + encrypted with the old key. |
| 106 | +
|
| 107 | +1. To re-encrypt all encrypted database fields with the new key, run |
| 108 | + [`coder dbcrypt-rotate`](../cli/dbcrypt-rotate.md). This command will |
| 109 | + re-encrypt all tokens with the first key in the list of external token |
| 110 | + encryption keys. We recommend performing this action during a maintenance |
| 111 | + window. |
| 112 | + |
| 113 | + > Note: this command requires direct access to the database. If you are using |
| 114 | + > the built-in PostgreSQL database, you can run |
| 115 | + > [`coder server postgres-builtin-url`](../cli/server_postgres-builtin-url.md) |
| 116 | + > to get the connection URL. |
| 117 | + |
| 118 | +1. Once the above command completes successfully, remove the old encryption key |
| 119 | + from Coder's configuration and restart Coder once more. You can now safely |
| 120 | + delete the old key from your secret store. |
| 121 | + |
| 122 | +## Disabling encryption |
| 123 | + |
| 124 | +Automatically disabling encryption is currently not supported. Encryption can be |
| 125 | +disabled by removing the encrypted data manually from the database: |
| 126 | + |
| 127 | +```sql |
| 128 | +DELETE FROM user_links WHERE oauth_access_token LIKE 'dbcrypt-%'; |
| 129 | +DELETE FROM user_links WHERE oauth_refresh_token LIKE 'dbcrypt-%'; |
| 130 | +DELETE FROM git_auth_links WHERE oauth_access_token LIKE 'dbcrypt-%'; |
| 131 | +DELETE FROM git_auth_links WHERE oauth_refresh_token LIKE 'dbcrypt-%'; |
| 132 | +DELETE FROM dbcrypt_sentinel WHERE value LIKE 'dbcrypt-%'; |
| 133 | +``` |
| 134 | + |
| 135 | +Users will then need to re-authenticate with external authentication providers. |
| 136 | + |
| 137 | +## Troubleshooting |
| 138 | + |
| 139 | +- If Coder detects that the data stored in the database under |
| 140 | + `dbcrypt_sentinel.value` was not encrypted with a known key, it will refuse to |
| 141 | + start. If you are seeing this behaviour, ensure that the encryption keys |
| 142 | + provided are correct. |
| 143 | +- If Coder is unable to decrypt a token, it will be treated as if the data were |
| 144 | + not present. This means that the user will be prompted to re-authenticate with |
| 145 | + the external provider. If you are seeing this behaviour consistently, ensure |
| 146 | + that the encryption keys are correct. |
0 commit comments