- Notifications
You must be signed in to change notification settings - Fork935
feat(coderd): connect dbcrypt package implementation#9523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes from1 commit
fb953e4
3b8140b
55b93e7
f340cba
feae634
381f078
c42e6a6
6a50a43
46b1ff4
9c18168
6c28ce5
c54b64a
5959b34
b1546b1
3859e03
a4f93c5
55a0fd0
cce0244
d51ec66
aa39fcc
e69e3ef
ebf4eef
2de6cc3
7774811
35ca78f
3a92a7d
8b1f43c
270cdc1
2514ffe
cd351af
2f5c112
2450d13
e56b639
441fcbf
ba14128
2ae45c6
13451f0
b3ff024
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
This builds upon a previous PR. It is recommended to read that first.- Adds a command dbcrypt-rotate to re-enncrypt encrypted data- Plumbs through dbcrypt in enterprise/coderd (including unit tests)- Enables database encryption in develop.sh by default- Adds documentation in admin/encryption.md
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -691,7 +691,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. | ||
options.Database = dbfake.New() | ||
options.Pubsub = pubsub.NewInMemory() | ||
} else { | ||
sqlDB, err :=ConnectToPostgres(ctx, logger, sqlDriver, vals.PostgresURL.String()) | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
if err != nil { | ||
return xerrors.Errorf("connect to postgres: %w", err) | ||
} | ||
@@ -1953,7 +1953,7 @@ func BuildLogger(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (slog. | ||
}, nil | ||
} | ||
funcConnectToPostgres(ctx context.Context, logger slog.Logger, driver string, dbURL string) (*sql.DB, error) { | ||
logger.Debug(ctx, "connecting to postgresql") | ||
// Try to connect for 30 seconds. | ||
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -248,6 +248,12 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon | ||
UserID: key.UserID, | ||
LoginType: key.LoginType, | ||
}) | ||
if errors.Is(err, sql.ErrNoRows) { | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
return optionalWrite(http.StatusUnauthorized, codersdk.Response{ | ||
Message: SignedOutErrorMessage, | ||
Detail: "You must re-authenticate with the login provider.", | ||
}) | ||
} | ||
if err != nil { | ||
return write(http.StatusInternalServerError, codersdk.Response{ | ||
Message: "A database error occurred", | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -46,8 +46,9 @@ const ( | ||
FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons" | ||
FeatureAppearance FeatureName = "appearance" | ||
FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling" | ||
FeatureWorkspaceProxy FeatureName = "workspace_proxy" | ||
FeatureExternalTokenEncryption FeatureName = "external_token_encryption" | ||
FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement" | ||
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions" | ||
) | ||
@@ -65,6 +66,8 @@ var FeatureNames = []FeatureName{ | ||
FeatureAdvancedTemplateScheduling, | ||
FeatureWorkspaceProxy, | ||
FeatureUserRoleManagement, | ||
FeatureExternalTokenEncryption, | ||
FeatureTemplateAutostopRequirement, | ||
FeatureWorkspaceBatchActions, | ||
} | ||
@@ -154,6 +157,7 @@ type DeploymentValues struct { | ||
AgentFallbackTroubleshootingURL clibase.URL `json:"agent_fallback_troubleshooting_url,omitempty" typescript:",notnull"` | ||
BrowserOnly clibase.Bool `json:"browser_only,omitempty" typescript:",notnull"` | ||
SCIMAPIKey clibase.String `json:"scim_api_key,omitempty" typescript:",notnull"` | ||
ExternalTokenEncryptionKeys clibase.StringArray `json:"external_token_encryption_keys,omitempty" typescript:",notnull"` | ||
Provisioner ProvisionerConfig `json:"provisioner,omitempty" typescript:",notnull"` | ||
RateLimit RateLimitConfig `json:"rate_limit,omitempty" typescript:",notnull"` | ||
Experiments clibase.StringArray `json:"experiments,omitempty" typescript:",notnull"` | ||
@@ -1605,7 +1609,14 @@ when required by your organization's security policy.`, | ||
Annotations: clibase.Annotations{}.Mark(annotationEnterpriseKey, "true").Mark(annotationSecretKey, "true"), | ||
Value: &c.SCIMAPIKey, | ||
}, | ||
{ | ||
Name: "External Token Encryption Keys", | ||
Description: "Encrypt OIDC and Git authentication tokens with AES-256-GCM in the database. The value must be a comma-separated list of base64-encoded keys. Each key, when base64-decoded, must be exactly 32 bytes in length. The first key will be used to encrypt new values. Subsequent keys will be used as a fallback when decrypting. During normal operation it is recommended to only set one key.", | ||
mtojek marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page.
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
Flag: "external-token-encryption-keys", | ||
Env: "CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS", | ||
Annotations: clibase.Annotations{}.Mark(annotationEnterpriseKey, "true").Mark(annotationSecretKey, "true"), | ||
Value: &c.ExternalTokenEncryptionKeys, | ||
}, | ||
{ | ||
Name: "Disable Path Apps", | ||
Description: "Disable workspace apps that are not served from subdomains. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. This is recommended for security purposes if a --wildcard-access-url is configured.", | ||
@@ -1783,7 +1794,7 @@ func (c *DeploymentValues) WithoutSecrets() (*DeploymentValues, error) { | ||
// This only works with string values for now. | ||
switch v := opt.Value.(type) { | ||
case *clibase.String, *clibase.StringArray: | ||
err := v.Set("") | ||
if err != nil { | ||
panic(err) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
# Database Encryption | ||
By default, Coder stores external user tokens in plaintext in the database. | ||
Database Encryption allows Coder administrators to encrypt these tokens at-rest, | ||
preventing attackers with database access from using them to impersonate users. | ||
## How it works | ||
Coder allows administrators to specify up to two | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
[external token encryption keys](../cli/server.md#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 a | ||
second key is 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` | ||
- `git_auth_links.oauth_access_token` | ||
- `git_auth_links.oauth_refresh_token` | ||
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. A key cannot be revoked until all rows referring to | ||
> it have been re-encrypted with a different key. | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
## Enabling encryption | ||
1. 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`](../cli/server_postgres-builtin-url.md) | ||
to get the connection URL. | ||
1. Generate a 32-byte random key and base64-encode it. For example: | ||
```shell | ||
dd if=/dev/urandom bs=32 count=1 | base64 | ||
``` | ||
1. Store this key in a secure location (for example, a Kubernetes secret): | ||
```shell | ||
kubectl create secret generic coder-external-token-encryption-keys --from-literal=keys=<key> | ||
``` | ||
1. 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`: | ||
```yaml | ||
coder: | ||
env: | ||
[...] | ||
- name: CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS | ||
valueFrom: | ||
secretKeyRef: | ||
name: coder-external-token-encryption-keys | ||
key: keys | ||
``` | ||
## Rotating keys | ||
We recommend only having one active encryption key at a time normally. However, | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
if you need to rotate keys, you can perform the following procedure: | ||
1. Ensure you have a valid backup of your database. **Do not skip this step.** | ||
1. Generate a new encryption key following the same procedure as above. | ||
1. Add the above key to the list of | ||
[external token encryption keys](../cli/server.md#external-token-encryption-keys). | ||
**The new key must appear first in the list**. For example, in the Kubernetes | ||
secret created above: | ||
```yaml | ||
apiVersion: v1 | ||
kind: Secret | ||
type: Opaque | ||
metadata: | ||
name: coder-external-token-encryption-keys | ||
namespace: coder-namespace | ||
data: | ||
keys: <new-key>,<old-key1>,<old-key2>,... | ||
``` | ||
1. 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). | ||
1. To re-encrypt all encrypted database fields with the new key, run | ||
[`coder dbcrypt-rotate`](../cli/dbcrypt-rotate.md). This command will | ||
re-encrypt all tokens with the first key in the list of external token | ||
encryption keys. We recommend performing this action during a maintenance | ||
window. | ||
> Note: 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`](../cli/server_postgres-builtin-url.md) | ||
> to get the connection URL. | ||
1. 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 | ||
Disabling encryption is currently not supported. | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page.
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
## 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 behaviour, | ||
ensure that the encryption keys provided are correct. |
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.