feat(coderd): connect dbcrypt package implementation (#9523)

See also: https://github.com/coder/coder/pull/9522

- Adds commands `server dbcrypt {rotate,decrypt,delete}` to re-encrypt, decrypt, or delete encrypted data, respectively.
- Plumbs through dbcrypt in enterprise/coderd (including unit tests).
- Adds documentation in admin/encryption.md.

This enables dbcrypt by default, but the feature is soft-enforced on supplying external token encryption keys. Without specifying any keys, encryption/decryption is a no-op.
This commit is contained in:
Cian Johnston
2023-09-07 15:49:49 +01:00
committed by GitHub
parent ed7f682fd1
commit 7d7c84bb4d
36 changed files with 1600 additions and 36 deletions

View File

@ -33,6 +33,7 @@ import (
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/enterprise/coderd/proxyhealth"
"github.com/coder/coder/v2/enterprise/coderd/schedule"
"github.com/coder/coder/v2/enterprise/dbcrypt"
"github.com/coder/coder/v2/enterprise/derpmesh"
"github.com/coder/coder/v2/enterprise/replicasync"
"github.com/coder/coder/v2/enterprise/tailnet"
@ -47,8 +48,8 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
if options.EntitlementsUpdateInterval == 0 {
options.EntitlementsUpdateInterval = 10 * time.Minute
}
if options.Keys == nil {
options.Keys = Keys
if options.LicenseKeys == nil {
options.LicenseKeys = Keys
}
if options.Options == nil {
options.Options = &coderd.Options{}
@ -61,10 +62,38 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
}
ctx, cancelFunc := context.WithCancel(ctx)
api := &API{
ctx: ctx,
cancel: cancelFunc,
if options.ExternalTokenEncryption == nil {
options.ExternalTokenEncryption = make([]dbcrypt.Cipher, 0)
}
// Database encryption is an enterprise feature, but as checking license entitlements
// depends on the database, we end up in a chicken-and-egg situation. To avoid this,
// we always enable it but only soft-enforce it.
if len(options.ExternalTokenEncryption) > 0 {
var keyDigests []string
for _, cipher := range options.ExternalTokenEncryption {
keyDigests = append(keyDigests, cipher.HexDigest())
}
options.Logger.Info(ctx, "database encryption enabled", slog.F("keys", keyDigests))
}
cryptDB, err := dbcrypt.New(ctx, options.Database, options.ExternalTokenEncryption...)
if err != nil {
cancelFunc()
// If we fail to initialize the database, it's likely that the
// database is encrypted with an unknown external token encryption key.
// This is a fatal error.
var derr *dbcrypt.DecryptFailedError
if xerrors.As(err, &derr) {
return nil, xerrors.Errorf("database encrypted with unknown key, either add the key or see https://coder.com/docs/v2/latest/admin/encryption#disabling-encryption: %w", derr)
}
return nil, xerrors.Errorf("init database encryption: %w", err)
}
options.Database = cryptDB
api := &API{
ctx: ctx,
cancel: cancelFunc,
AGPL: coderd.New(options.Options),
Options: options,
provisionerDaemonAuth: &provisionerDaemonAuth{
@ -364,6 +393,8 @@ type Options struct {
BrowserOnly bool
SCIMAPIKey []byte
ExternalTokenEncryption []dbcrypt.Cipher
// Used for high availability.
ReplicaSyncUpdateInterval time.Duration
DERPServerRelayAddress string
@ -374,7 +405,7 @@ type Options struct {
EntitlementsUpdateInterval time.Duration
ProxyHealthInterval time.Duration
Keys map[string]ed25519.PublicKey
LicenseKeys map[string]ed25519.PublicKey
// optional pre-shared key for authentication of external provisioner daemons
ProvisionerDaemonPSK string
@ -429,13 +460,14 @@ func (api *API) updateEntitlements(ctx context.Context) error {
entitlements, err := license.Entitlements(
ctx, api.Database,
api.Logger, len(api.replicaManager.AllPrimary()), len(api.GitAuthConfigs), api.Keys, map[codersdk.FeatureName]bool{
api.Logger, len(api.replicaManager.AllPrimary()), len(api.GitAuthConfigs), api.LicenseKeys, map[codersdk.FeatureName]bool{
codersdk.FeatureAuditLog: api.AuditLogging,
codersdk.FeatureBrowserOnly: api.BrowserOnly,
codersdk.FeatureSCIM: len(api.SCIMAPIKey) != 0,
codersdk.FeatureHighAvailability: api.DERPServerRelayAddress != "",
codersdk.FeatureMultipleGitAuth: len(api.GitAuthConfigs) > 1,
codersdk.FeatureTemplateRBAC: api.RBAC,
codersdk.FeatureExternalTokenEncryption: len(api.ExternalTokenEncryption) > 0,
codersdk.FeatureExternalProvisionerDaemons: true,
codersdk.FeatureAdvancedTemplateScheduling: true,
// FeatureTemplateAutostopRequirement depends on
@ -615,6 +647,16 @@ func (api *API) updateEntitlements(ctx context.Context) error {
}
}
// External token encryption is soft-enforced
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
if featureExternalTokenEncryption.Enabled && featureExternalTokenEncryption.Entitlement != codersdk.EntitlementEntitled {
msg := fmt.Sprintf("%s is enabled (due to setting external token encryption keys) but your license is not entitled to this feature.", codersdk.FeatureExternalTokenEncryption.Humanize())
api.Logger.Warn(ctx, msg)
entitlements.Warnings = append(entitlements.Warnings, msg)
}
entitlements.Features[codersdk.FeatureExternalTokenEncryption] = featureExternalTokenEncryption
api.entitlementsMu.Lock()
defer api.entitlementsMu.Unlock()
api.entitlements = entitlements