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 1 commit
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
Prev Previous commit
Next Next commit
beef up unit tests, refactor cli
  • Loading branch information
johnstcn committed Sep 6, 2023
commit 2de6cc3cb334dffb51c78faa3ba4963998b4fea3
259 changes: 180 additions & 79 deletions enterprise/cli/server_dbcrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
package cli

import (
"bytes"
"context"
"encoding/base64"
"fmt"
"strings"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/dbcrypt"

"golang.org/x/xerrors"
Expand All @@ -35,20 +35,10 @@ func (r *RootCmd) dbcryptCmd() *clibase.Cmd {
}

func (*RootCmd) dbcryptRotateCmd() *clibase.Cmd {
var (
vals = new(codersdk.DeploymentValues)
opts = vals.Options()
)
var flags rotateFlags
cmd := &clibase.Cmd{
Use: "rotate",
Short: "Rotate database encryption keys.",
Options: clibase.OptionSet{
*opts.ByName("Postgres Connection URL"),
*opts.ByName("External Token Encryption Keys"),
},
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
),
Handler: func(inv *clibase.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
Expand All @@ -57,38 +47,47 @@ func (*RootCmd) dbcryptRotateCmd() *clibase.Cmd {
logger = logger.Leveled(slog.LevelDebug)
}

if vals.PostgresURL == "" {
return xerrors.Errorf("no database configured")
if err := flags.valid(); err != nil {
return err
}

switch len(vals.ExternalTokenEncryptionKeys) {
case 0:
return xerrors.Errorf("no external token encryption keys provided")
case 1:
logger.Info(ctx, "only one key provided, data will be re-encrypted with the same key")
ks := [][]byte{}
dk, err := base64.StdEncoding.DecodeString(flags.New)
if err != nil {
return xerrors.Errorf("decode new key: %w", err)
}
ks = append(ks, dk)

keys := make([][]byte, 0, len(vals.ExternalTokenEncryptionKeys))
var newKey []byte
for idx, ek := range vals.ExternalTokenEncryptionKeys {
dk, err := base64.StdEncoding.DecodeString(ek)
for _, k := range flags.Old {
dk, err := base64.StdEncoding.DecodeString(k)
if err != nil {
return xerrors.Errorf("key must be base64-encoded")
}
if idx == 0 {
newKey = dk
} else if bytes.Equal(dk, newKey) {
return xerrors.Errorf("old key at index %d is the same as the new key", idx)
return xerrors.Errorf("decode old key: %w", err)
}
keys = append(keys, dk)
ks = append(ks, dk)
}

ciphers, err := dbcrypt.NewCiphers(keys...)
ciphers, err := dbcrypt.NewCiphers(ks...)
if err != nil {
return xerrors.Errorf("create ciphers: %w", err)
}

sqlDB, err := cli.ConnectToPostgres(inv.Context(), logger, "postgres", vals.PostgresURL.Value())
newDigest := ciphers[0].HexDigest()
oldDigests := make([]string, 0, len(ciphers)-1)
for _, c := range ciphers[1:] {
oldDigests = append(oldDigests, c.HexDigest())
}
if len(oldDigests) == 0 {
oldDigests = append(oldDigests, "none")
}
msg := fmt.Sprintf(`Rotate external token encryptions keys?\n- New key: %s\n- Old keys: %s`,
newDigest,
strings.Join(oldDigests, ", "),
)
if _, err := cliui.Prompt(inv, cliui.PromptOptions{Text: msg, IsConfirm: true}); err != nil {
return err
}

sqlDB, err := cli.ConnectToPostgres(inv.Context(), logger, "postgres", flags.PostgresURL)
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
Expand All @@ -103,24 +102,15 @@ func (*RootCmd) dbcryptRotateCmd() *clibase.Cmd {
return nil
},
}
flags.attach(&cmd.Options)
return cmd
}

func (*RootCmd) dbcryptDecryptCmd() *clibase.Cmd {
var (
vals = new(codersdk.DeploymentValues)
opts = vals.Options()
)
var flags decryptFlags
cmd := &clibase.Cmd{
Use: "decrypt",
Short: "Decrypt a previously encrypted database.",
Options: clibase.OptionSet{
*opts.ByName("Postgres Connection URL"),
*opts.ByName("External Token Encryption Keys"),
},
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
),
Handler: func(inv *clibase.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
Expand All @@ -129,38 +119,32 @@ func (*RootCmd) dbcryptDecryptCmd() *clibase.Cmd {
logger = logger.Leveled(slog.LevelDebug)
}

if vals.PostgresURL == "" {
return xerrors.Errorf("no database configured")
}

switch len(vals.ExternalTokenEncryptionKeys) {
case 0:
return xerrors.Errorf("no external token encryption keys provided")
case 1:
logger.Info(ctx, "only one key provided, data will be re-encrypted with the same key")
if err := flags.valid(); err != nil {
return err
}

keys := make([][]byte, 0, len(vals.ExternalTokenEncryptionKeys))
var newKey []byte
for idx, ek := range vals.ExternalTokenEncryptionKeys {
dk, err := base64.StdEncoding.DecodeString(ek)
ks := make([][]byte, 0, len(flags.Keys))
for _, k := range flags.Keys {
dk, err := base64.StdEncoding.DecodeString(k)
if err != nil {
return xerrors.Errorf("key must be base64-encoded")
return xerrors.Errorf("decode key: %w", err)
}
if idx == 0 {
newKey = dk
} else if bytes.Equal(dk, newKey) {
return xerrors.Errorf("old key at index %d is the same as the new key", idx)
}
keys = append(keys, dk)
ks = append(ks, dk)
}

ciphers, err := dbcrypt.NewCiphers(keys...)
ciphers, err := dbcrypt.NewCiphers(ks...)
if err != nil {
return xerrors.Errorf("create ciphers: %w", err)
}

sqlDB, err := cli.ConnectToPostgres(inv.Context(), logger, "postgres", vals.PostgresURL.Value())
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "This will decrypt all encrypted data in the database. Are you sure you want to continue?",
IsConfirm: true,
}); err != nil {
return err
}

sqlDB, err := cli.ConnectToPostgres(inv.Context(), logger, "postgres", flags.PostgresURL)
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
Expand All @@ -175,23 +159,15 @@ func (*RootCmd) dbcryptDecryptCmd() *clibase.Cmd {
return nil
},
}
flags.attach(&cmd.Options)
return cmd
}

func (*RootCmd) dbcryptDeleteCmd() *clibase.Cmd {
var (
vals = new(codersdk.DeploymentValues)
opts = vals.Options()
)
var flags deleteFlags
cmd := &clibase.Cmd{
Use: "delete",
Short: "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.",
Options: clibase.OptionSet{
*opts.ByName("Postgres Connection URL"),
},
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
),
Handler: func(inv *clibase.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
Expand All @@ -200,8 +176,8 @@ func (*RootCmd) dbcryptDeleteCmd() *clibase.Cmd {
logger = logger.Leveled(slog.LevelDebug)
}

if vals.PostgresURL == "" {
return xerrors.Errorf("no database configured")
if err := flags.valid(); err != nil {
return err
}

if _, err := cliui.Prompt(inv, cliui.PromptOptions{
Expand All @@ -211,7 +187,7 @@ func (*RootCmd) dbcryptDeleteCmd() *clibase.Cmd {
return err
}

sqlDB, err := cli.ConnectToPostgres(inv.Context(), logger, "postgres", vals.PostgresURL.Value())
sqlDB, err := cli.ConnectToPostgres(inv.Context(), logger, "postgres", flags.PostgresURL)
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
Expand All @@ -226,5 +202,130 @@ func (*RootCmd) dbcryptDeleteCmd() *clibase.Cmd {
return nil
},
}
flags.attach(&cmd.Options)
return cmd
}

type rotateFlags struct {
PostgresURL string
New string
Old []string
}

func (f *rotateFlags) attach(opts *clibase.OptionSet) {
*opts = append(
*opts,
clibase.Option{
Flag: "postgres-url",
Env: "CODER_PG_CONNECTION_URL",
Description: "The connection URL for the Postgres database.",
Value: clibase.StringOf(&f.PostgresURL),
},
clibase.Option{
Flag: "new-key",
Env: "CODER_EXTERNAL_TOKEN_ENCRYPTION_ENCRYPT_NEW_KEY",
Description: "The new external token encryption key. Must be base64-encoded.",
Value: clibase.StringOf(&f.New),
},
clibase.Option{
Flag: "old-keys",
Env: "CODER_EXTERNAL_TOKEN_ENCRYPTION_ENCRYPT_OLD_KEYS",
Description: "The old external token encryption keys. Must be a comma-separated list of base64-encoded keys.",
Value: clibase.StringArrayOf(&f.Old),
},
cliui.SkipPromptOption(),
)
}

func (f *rotateFlags) valid() error {
if f.New == "" {
return xerrors.Errorf("no new key provided")
}

if val, err := base64.StdEncoding.DecodeString(f.New); err != nil {
return xerrors.Errorf("new key must be base64-encoded")
} else if len(val) != 32 {
return xerrors.Errorf("new key must be exactly 32 bytes in length")
}

for i, k := range f.Old {
if val, err := base64.StdEncoding.DecodeString(k); err != nil {
return xerrors.Errorf("old key at index %d must be base64-encoded", i)
} else if len(val) != 32 {
return xerrors.Errorf("old key at index %d must be exactly 32 bytes in length", i)
}

// Pedantic, but typos here will ruin your day.
if k == f.New {
return xerrors.Errorf("old key at index %d is the same as the new key", i)
}
}

return nil
}

type decryptFlags struct {
PostgresURL string
Keys []string
}

func (f *decryptFlags) attach(opts *clibase.OptionSet) {
*opts = append(
*opts,
clibase.Option{
Flag: "postgres-url",
Env: "CODER_PG_CONNECTION_URL",
Description: "The connection URL for the Postgres database.",
Value: clibase.StringOf(&f.PostgresURL),
},
clibase.Option{
Flag: "keys",
Env: "CODER_EXTERNAL_TOKEN_ENCRYPTION_DECRYPT_KEYS",
Description: "Keys required to decrypt existing data. Must be a comma-separated list of base64-encoded keys.",
Value: clibase.StringArrayOf(&f.Keys),
},
cliui.SkipPromptOption(),
)
}

func (f *decryptFlags) valid() error {
if len(f.Keys) == 0 {
return xerrors.Errorf("no keys provided")
}

for i, k := range f.Keys {
if val, err := base64.StdEncoding.DecodeString(k); err != nil {
return xerrors.Errorf("key at index %d must be base64-encoded", i)
} else if len(val) != 32 {
return xerrors.Errorf("key at index %d must be exactly 32 bytes in length", i)
}
}

return nil
}

type deleteFlags struct {
PostgresURL string
Confirm bool
}

func (f *deleteFlags) attach(opts *clibase.OptionSet) {
*opts = append(
*opts,
clibase.Option{
Flag: "postgres-url",
Env: "CODER_EXTERNAL_TOKEN_ENCRYPTION_POSTGRES_URL",
Description: "The connection URL for the Postgres database.",
Value: clibase.StringOf(&f.PostgresURL),
},
cliui.SkipPromptOption(),
)
}

func (f *deleteFlags) valid() error {
if f.PostgresURL == "" {
return xerrors.Errorf("no database configured")
}

return nil
}
Loading