Skip to content

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fb953e4
feat(coderd): add dbcrypt package
johnstcn Sep 4, 2023
3b8140b
feat(coderd): plumb through dbcrypt package
johnstcn Sep 4, 2023
55b93e7
fix indentation
johnstcn Sep 5, 2023
f340cba
fixup! fix indentation
johnstcn Sep 5, 2023
feae634
check for primary key revocation on startup
johnstcn Sep 5, 2023
381f078
retry insert active key on tx serialization failure
johnstcn Sep 5, 2023
c42e6a6
fixup! retry insert active key on tx serialization failure
johnstcn Sep 5, 2023
6a50a43
use database.IsSerializedError
johnstcn Sep 5, 2023
46b1ff4
encryptFields: check for nil field or digest
johnstcn Sep 5, 2023
9c18168
rm insertDBCryptKeyNoLock
johnstcn Sep 5, 2023
6c28ce5
Merge branch 'cj/dbcrypt_redux_1' into cj/dbcrypt_redux_2
johnstcn Sep 5, 2023
c54b64a
Update enterprise/cli/dbcrypt_rotate.go
johnstcn Sep 5, 2023
5959b34
Update enterprise/coderd/coderd.go
johnstcn Sep 5, 2023
b1546b1
add unit test for ExtractAPIKeyMW
johnstcn Sep 5, 2023
3859e03
add unit test for cli.ConnectToPostgres
johnstcn Sep 5, 2023
a4f93c5
Merge remote-tracking branch 'origin/main' into cj/dbcrypt_redux_2
johnstcn Sep 6, 2023
55a0fd0
DON'T PANIC
johnstcn Sep 6, 2023
cce0244
debug log user_ids
johnstcn Sep 6, 2023
d51ec66
dbcrypt-rotate -> server dbcrypt rotate
johnstcn Sep 6, 2023
aa39fcc
refactor: move rotate logic into dbcrypt
johnstcn Sep 6, 2023
e69e3ef
add decrypt/delete commands
johnstcn Sep 6, 2023
ebf4eef
fixup! add decrypt/delete commands
johnstcn Sep 6, 2023
2de6cc3
beef up unit tests, refactor cli
johnstcn Sep 6, 2023
7774811
update golden files
johnstcn Sep 6, 2023
35ca78f
Update codersdk/deployment.go
johnstcn Sep 6, 2023
3a92a7d
Merge remote-tracking branch 'origin/main' into cj/dbcrypt_redux_2
johnstcn Sep 7, 2023
8b1f43c
revoke all active keys on dbcrypt delete
johnstcn Sep 7, 2023
270cdc1
fixup! Merge remote-tracking branch 'origin/main' into cj/dbcrypt_red…
johnstcn Sep 7, 2023
2514ffe
update docs
johnstcn Sep 7, 2023
cd351af
fixup! update docs
johnstcn Sep 7, 2023
2f5c112
fixup! update docs
johnstcn Sep 7, 2023
2450d13
soft-enforce dbcrypt in license
johnstcn Sep 7, 2023
e56b639
do not add external token encryption keys by default (as it will alwa…
johnstcn Sep 7, 2023
441fcbf
update golden files
johnstcn Sep 7, 2023
ba14128
log encryption status on startup
johnstcn Sep 7, 2023
2ae45c6
modify CLI output
johnstcn Sep 7, 2023
13451f0
Merge remote-tracking branch 'origin/main' into cj/dbcrypt_redux_2
johnstcn Sep 7, 2023
b3ff024
rm unused golden file
johnstcn Sep 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
sqlDB, err := ConnectToPostgres(ctx, logger, sqlDriver, vals.PostgresURL.String())
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
Expand Down Expand Up @@ -1953,7 +1953,7 @@ func BuildLogger(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (slog.
}, nil
}

func connectToPostgres(ctx context.Context, logger slog.Logger, driver string, dbURL string) (*sql.DB, error) {
func ConnectToPostgres(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.
Expand Down
2 changes: 1 addition & 1 deletion cli/server_createadminuser.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *clibase.Cmd {
newUserDBURL = url
}

sqlDB, err := connectToPostgres(ctx, logger, "postgres", newUserDBURL)
sqlDB, err := ConnectToPostgres(ctx, logger, "postgres", newUserDBURL)
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
Expand Down
26 changes: 26 additions & 0 deletions cli/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ import (
"go.uber.org/goleak"
"gopkg.in/yaml.v3"

"cdr.dev/slog/sloggers/slogtest"

"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/postgres"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/codersdk"
Expand Down Expand Up @@ -1657,3 +1660,26 @@ func TestServerYAMLConfig(t *testing.T) {

require.Equal(t, string(wantByt), string(got))
}

func TestConnectToPostgres(t *testing.T) {
t.Parallel()

if !dbtestutil.WillUsePostgres() {
t.Skip("this test does not make sense without postgres")
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)

log := slogtest.Make(t, nil)

dbURL, closeFunc, err := postgres.Open()
require.NoError(t, err)
t.Cleanup(closeFunc)

sqlDB, err := cli.ConnectToPostgres(ctx, log, "postgres", dbURL)
require.NoError(t, err)
t.Cleanup(func() {
_ = sqlDB.Close()
})
require.NoError(t, sqlDB.PingContext(ctx))
}
10 changes: 10 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,16 @@ These options are only available in the Enterprise Edition.
An HTTP URL that is accessible by other replicas to relay DERP
traffic. Required for high availability.

--external-token-encryption-keys string-array, $CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS
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 unless you are in the
process of rotating keys with the `coder server dbcrypt rotate`
command.

--scim-auth-header string, $CODER_SCIM_AUTH_HEADER
Enables SCIM and sets the authentication header for the built-in SCIM
server. New users are automatically created with OIDC authentication.
Expand Down
6 changes: 6 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coderd/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestDeploymentValues(t *testing.T) {
cfg.OIDC.EmailField.Set("some_random_field_you_never_expected")
cfg.PostgresURL.Set(hi)
cfg.SCIMAPIKey.Set(hi)
cfg.ExternalTokenEncryptionKeys.Set("the_random_key_we_never_expected,an_other_key_we_never_unexpected")

client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: cfg,
Expand All @@ -44,6 +45,7 @@ func TestDeploymentValues(t *testing.T) {
require.Empty(t, scrubbed.Values.OIDC.ClientSecret.Value())
require.Empty(t, scrubbed.Values.PostgresURL.Value())
require.Empty(t, scrubbed.Values.SCIMAPIKey.Value())
require.Empty(t, scrubbed.Values.ExternalTokenEncryptionKeys.Value())
}

func TestDeploymentStats(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions coderd/httpmw/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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",
Expand Down
28 changes: 28 additions & 0 deletions coderd/httpmw/apikey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,34 @@ func TestAPIKey(t *testing.T) {
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
})

t.Run("UserLinkNotFound", func(t *testing.T) {
t.Parallel()
var (
db = dbfake.New()
r = httptest.NewRequest("GET", "/", nil)
rw = httptest.NewRecorder()
user = dbgen.User(t, db, database.User{
LoginType: database.LoginTypeGithub,
})
// Intentionally not inserting any user link
_, token = dbgen.APIKey(t, db, database.APIKey{
UserID: user.ID,
LoginType: user.LoginType,
})
)
r.Header.Set(codersdk.SessionTokenHeader, token)
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
DB: db,
RedirectToLogin: false,
})(successHandler).ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
var resp codersdk.Response
require.NoError(t, json.NewDecoder(res.Body).Decode(&resp))
require.Equal(t, resp.Message, httpmw.SignedOutErrorMessage)
})

t.Run("InvalidSecret", func(t *testing.T) {
t.Parallel()
var (
Expand Down
17 changes: 14 additions & 3 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ const (
FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons"
FeatureAppearance FeatureName = "appearance"
FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling"
FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement"
FeatureWorkspaceProxy FeatureName = "workspace_proxy"
FeatureExternalTokenEncryption FeatureName = "external_token_encryption"
FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement"
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
)

Expand All @@ -65,6 +66,8 @@ var FeatureNames = []FeatureName{
FeatureAdvancedTemplateScheduling,
FeatureWorkspaceProxy,
FeatureUserRoleManagement,
FeatureExternalTokenEncryption,
FeatureTemplateAutostopRequirement,
FeatureWorkspaceBatchActions,
}

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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 unless you are in the process of rotating keys with the `coder server dbcrypt rotate` command.",
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.",
Expand Down Expand Up @@ -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:
case *clibase.String, *clibase.StringArray:
err := v.Set("")
if err != nil {
panic(err)
Expand Down
3 changes: 3 additions & 0 deletions codersdk/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func TestDeploymentValues_HighlyConfigurable(t *testing.T) {
"SCIM API Key": {
yaml: true,
},
"External Token Encryption Keys": {
yaml: true,
},
// These complex objects should be configured through YAML.
"Support Links": {
flag: true,
Expand Down
Loading