Skip to content

Commit e6f9d4f

Browse files
committed
MB-62186: Introduce key management for encryption-at-rest
* Add support for receiving encryption-at-rest key notifications from cbauth * cbauth will push key information for each "key data type". Query will receive key information for the "logs", "other", and per-bucket keys. * Implement a node-level key manager that maintains a cache/store of key information for fast access * Update the key store only when received key info from cbauth differs from the cached info for a given data type. * On Query service startup, "prime" the key store with key info for all known data types ("logs", "other", and all existing buckets) before starting listeners. * Priming means proactively loading the key store with key info from cbauth, so that manager's key stor eis ready before any Query requests are handled. * Requests to the key store for unloaded key types can trigger calls to cbauth to fetch key information. To prevent this potential flood of fetch requests, prime the key store on startup. * During priming, Query waits up to 5 seconds for each key fetch from cbauth to avoid indefinite blocking. If fetching fails or times out, priming for that type is skipped. * Priming failures will not result in the Query service startup failing and exiting. Or that key info is permanently missing from Query's key manager. Query service can function and serve requests even when priming fails. This is because, missing key info entries are loaded into the key store by future refresh callbacks or by lazy loading the key information when the key information is requested from the key store for the first time. * Key management is only implemented for EE version Note: * For now, The code to create the encryption manager and register the encryption callbacks with cbauth is currently commented out. And instead, dummy callbacks are registered with cbauth. This is because currently, ns-server does not push key information to the Query service. And registering the actual callbacks will result in errors. Change-Id: Ib1bf0664272f2ef3a502917023fc2b7fcbfd3ce8 Reviewed-on: https://review.couchbase.org/c/query/+/242165 Reviewed-by: Sitaram Vemulapalli <sitaram.vemulapalli@couchbase.com> Reviewed-by: Bingjie Miao <bingjie.miao@couchbase.com> Tested-by: Dhanya Gowrish <dhanya.gowrish@couchbase.com>
1 parent 2f7b1dd commit e6f9d4f

16 files changed

Lines changed: 739 additions & 11 deletions

File tree

datastore/couchbase/couchbase.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,16 @@ func SetDeploymentModel(deploymentModel string) {
162162

163163
// store is the root for the couchbase datastore
164164
type store struct {
165-
client cb.Client // instance of primitives/couchbase client
166-
gcClient *gcagent.Client
167-
namespaceCache map[string]*namespace // map of pool-names and IDs
168-
CbAuthInit bool // whether cbAuth is initialized
169-
inferencer datastore.Inferencer // what we use to infer schemas
170-
statUpdater datastore.StatUpdater // what we use to update statistics
171-
connectionUrl string // where to contact ns_server
172-
connSecConfig *datastore.ConnectionSecurityConfig
173-
nslock sync.RWMutex
165+
client cb.Client // instance of primitives/couchbase client
166+
gcClient *gcagent.Client
167+
namespaceCache map[string]*namespace // map of pool-names and IDs
168+
CbAuthInit bool // whether cbAuth is initialized
169+
inferencer datastore.Inferencer // what we use to infer schemas
170+
statUpdater datastore.StatUpdater // what we use to update statistics
171+
connectionUrl string // where to contact ns_server
172+
connSecConfig *datastore.ConnectionSecurityConfig
173+
nslock sync.RWMutex
174+
encryptionProvider datastore.EncryptionProvider
174175
}
175176

176177
func (s *store) Id() string {
@@ -1072,6 +1073,8 @@ func NewDatastore(u string) (s datastore.Datastore, e errors.Error) {
10721073
return nil, er
10731074
}
10741075

1076+
store.encryptionProvider = datastore.NoopEncryptionProviderInstance
1077+
10751078
// initialize the default pool.
10761079
// TODO can couchbase server contain more than one pool ?
10771080

@@ -3908,3 +3911,11 @@ func (s *store) CheckSystemCollection(bucketName, requestId string, forceIndex b
39083911

39093912
return empty, nil
39103913
}
3914+
3915+
func (s *store) EncryptionProvider() (datastore.EncryptionProvider, errors.Error) {
3916+
return s.encryptionProvider, nil
3917+
}
3918+
3919+
func (s *store) SetEncryptionProvider(encProvider datastore.EncryptionProvider) {
3920+
s.encryptionProvider = encProvider
3921+
}

datastore/datastore.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ type Datastore interface {
110110
RollbackTransaction(stmtAtomicity bool, context QueryContext, sname string) errors.Error
111111
SetSavepoint(stmtAtomicity bool, context QueryContext, sname string) errors.Error
112112
TransactionDeltaKeyScan(keyspace string, conn *IndexConnection) // Keys of Delta keyspace
113+
114+
EncryptionProvider() (EncryptionProvider, errors.Error)
115+
SetEncryptionProvider(encryptionProvider EncryptionProvider)
113116
}
114117

115118
type Systemstore interface {

datastore/encryption_provider.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2026-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
package datastore
10+
11+
import (
12+
"github.com/couchbase/query/encryption"
13+
"github.com/couchbase/query/errors"
14+
)
15+
16+
var NoopEncryptionProviderInstance = &NoopEncryptionProvider{}
17+
18+
type EncryptionProvider interface {
19+
GetActiveKey(dt encryption.KeyDataType) (*encryption.EaRKey, errors.Error)
20+
}
21+
22+
type NoopEncryptionProvider struct{}
23+
24+
func (NoopEncryptionProvider) GetActiveKey(dt encryption.KeyDataType) (*encryption.EaRKey, errors.Error) {
25+
return nil, nil
26+
}

datastore/file/file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,14 @@ func (s *store) TransactionDeltaKeyScan(keyspace string, conn *datastore.IndexCo
281281
defer conn.Sender().Close()
282282
}
283283

284+
func (s *store) EncryptionProvider() (datastore.EncryptionProvider, errors.Error) {
285+
return datastore.NoopEncryptionProviderInstance, nil
286+
}
287+
288+
func (s *store) SetEncryptionProvider(datastore.EncryptionProvider) {
289+
return
290+
}
291+
284292
// NewStore creates a new file-based store for the given filepath.
285293
func NewDatastore(path string) (s datastore.Datastore, e errors.Error) {
286294
path, er := filepath.Abs(path)

datastore/mock/mock.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ func (s *store) TransactionDeltaKeyScan(keyspace string, conn *datastore.IndexCo
267267
defer conn.Sender().Close()
268268
}
269269

270+
func (s *store) EncryptionProvider() (datastore.EncryptionProvider, errors.Error) {
271+
return datastore.NoopEncryptionProviderInstance, nil
272+
}
273+
274+
func (s *store) SetEncryptionProvider(datastore.EncryptionProvider) {
275+
return
276+
}
277+
270278
// namespace represents a mock-based Namespace.
271279
type namespace struct {
272280
store *store

datastore/system/system.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,14 @@ func (s *store) TransactionDeltaKeyScan(keyspace string, conn *datastore.IndexCo
381381
defer conn.Sender().Close()
382382
}
383383

384+
func (s *store) EncryptionProvider() (datastore.EncryptionProvider, errors.Error) {
385+
return s.actualStore.EncryptionProvider()
386+
}
387+
388+
func (s *store) SetEncryptionProvider(datastore.EncryptionProvider) {
389+
return
390+
}
391+
384392
func NewDatastore(actualStore datastore.Datastore, acctStore accounting.AccountingStore,
385393
enterprise bool) (datastore.Systemstore, errors.Error) {
386394
s := &store{actualStore: actualStore, acctStore: acctStore, enterprise: enterprise}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2026-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
//go:build !enterprise
10+
11+
package keymgmt
12+
13+
import (
14+
"github.com/couchbase/cbauth"
15+
"github.com/couchbase/query/encryption"
16+
"github.com/couchbase/query/errors"
17+
)
18+
19+
func NewEncryptionManager() EncryptionManager {
20+
return &NoopEncryptionManager{}
21+
}
22+
23+
type NoopEncryptionManager struct{}
24+
25+
func (this *NoopEncryptionManager) GetActiveKey(dt encryption.KeyDataType) (*encryption.EaRKey, errors.Error) {
26+
return nil, nil
27+
}
28+
29+
func (this *NoopEncryptionManager) PrimeKeys(keyDataTypes []encryption.KeyDataType) errors.Error {
30+
return nil
31+
}
32+
33+
func (this *NoopEncryptionManager) UpdateKeys(dataType cbauth.KeyDataType, newInfo *cbauth.EncrKeysInfo, prime bool) errors.Error {
34+
return nil
35+
}
36+
37+
func (this *NoopEncryptionManager) RegisterCbauthEncryptionCallbacks() {
38+
}
39+
40+
func (this *NoopEncryptionManager) GetInUseKeysCallback(dt cbauth.KeyDataType) ([]string, error) {
41+
return nil, nil
42+
}
43+
44+
func (this *NoopEncryptionManager) DropKeysCallback(dt cbauth.KeyDataType, KeyIdsToDrop []string) {
45+
}
46+
47+
func (this *NoopEncryptionManager) SynchronizeKeyFilesCallback(dt cbauth.KeyDataType) error {
48+
return nil
49+
}
50+
51+
func (this *NoopEncryptionManager) RefreshKeysCallback(dt cbauth.KeyDataType) error {
52+
return nil
53+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2026-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
//go:build enterprise
10+
11+
package keymgmt
12+
13+
func NewEncryptionManager() EncryptionManager {
14+
return NewNodeEncryptionManager()
15+
}

encryption/keymgmt/mgmt.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2026-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
package keymgmt
10+
11+
import (
12+
"github.com/couchbase/cbauth"
13+
"github.com/couchbase/query/encryption"
14+
"github.com/couchbase/query/errors"
15+
)
16+
17+
type EncryptionManager interface {
18+
GetActiveKey(dt encryption.KeyDataType) (*encryption.EaRKey, errors.Error)
19+
PrimeKeys(keyDataTypes []encryption.KeyDataType) errors.Error
20+
UpdateKeys(dataType cbauth.KeyDataType, newInfo *cbauth.EncrKeysInfo, prime bool) errors.Error
21+
RegisterCbauthEncryptionCallbacks()
22+
GetInUseKeysCallback(dt cbauth.KeyDataType) ([]string, error)
23+
DropKeysCallback(dt cbauth.KeyDataType, KeyIdsToDrop []string)
24+
SynchronizeKeyFilesCallback(dt cbauth.KeyDataType) error
25+
RefreshKeysCallback(dt cbauth.KeyDataType) error
26+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright 2026-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
//go:build enterprise
10+
11+
package keymgmt
12+
13+
import (
14+
"fmt"
15+
16+
"github.com/couchbase/cbauth"
17+
"github.com/couchbase/query/encryption"
18+
"github.com/couchbase/query/errors"
19+
"github.com/couchbase/query/logging"
20+
)
21+
22+
type NodeEncryptionManager struct {
23+
keyStore *nodeKeyStore
24+
}
25+
26+
func NewNodeEncryptionManager() *NodeEncryptionManager {
27+
return &NodeEncryptionManager{
28+
keyStore: newNodeKeyStore(),
29+
}
30+
}
31+
32+
// Performs initialization of the manager with info about the provided key datatypes
33+
// Typically invoked during service startup to initialize the manager with required key data.
34+
func (this *NodeEncryptionManager) PrimeKeys(keyDataTypes []encryption.KeyDataType) errors.Error {
35+
logging.Infof("Priming encryption-at-rest manager with keys for all available key data types")
36+
this.keyStore.PrimeKeys(keyDataTypes)
37+
logging.Infof("Finished priming operation of encryption-at-rest manager with keys for all available key data types")
38+
return nil
39+
}
40+
41+
func (this *NodeEncryptionManager) UpdateKeys(dataType cbauth.KeyDataType, newInfo *cbauth.EncrKeysInfo, prime bool) errors.Error {
42+
return this.keyStore.UpdateKeys(dataType, newInfo, prime)
43+
}
44+
45+
func (this *NodeEncryptionManager) GetActiveKey(dt encryption.KeyDataType) (*encryption.EaRKey, errors.Error) {
46+
return this.keyStore.GetActiveKey(dt)
47+
}
48+
49+
func (this *NodeEncryptionManager) RegisterCbauthEncryptionCallbacks() {
50+
cbauth.RegisterEncryptionKeysCallbacks(this.RefreshKeysCallback, this.GetInUseKeysCallback, this.DropKeysCallback,
51+
this.SynchronizeKeyFilesCallback)
52+
}
53+
54+
func (this *NodeEncryptionManager) RefreshKeysCallback(dt cbauth.KeyDataType) error {
55+
// Key info will always be present in cbauth when RefreshKeysCallback is called.
56+
// Thus call cbauth.GetEncryptionKeys() instead of GetEncryptionKeysBlocking().
57+
newKeys, cbErr := cbauth.GetEncryptionKeys(dt)
58+
59+
// In RefreshKeysCallback, any error returned by GetEncryptionKeys() is a hard error including cbauth.ErrKeysNotAvailable.
60+
// This is because key info should always be available when RefreshKeysCallback is called by cbauth.
61+
if cbErr != nil {
62+
logging.Errorf(
63+
"Error refreshing encryption-at-rest configuration for key data type %s. Failed to fetch configuration from cbauth: %v",
64+
cbauthTypeToDataType(dt).String(), cbErr)
65+
return cbErr
66+
}
67+
68+
err := this.keyStore.UpdateKeys(dt, newKeys, false)
69+
if err != nil && err.Code() != errors.E_INVALID_ENCRYPTION_KEY_DATATYPE {
70+
logging.Errorf(
71+
"Error refreshing encryption-at-rest configuration for key data type %s. Failed to update local encryption manager: %v",
72+
cbauthTypeToDataType(dt).String(), err)
73+
}
74+
75+
return nil
76+
}
77+
78+
func (this *NodeEncryptionManager) GetInUseKeysCallback(dt cbauth.KeyDataType) ([]string, error) {
79+
// EAR TODO
80+
return nil, nil
81+
}
82+
83+
func (this *NodeEncryptionManager) DropKeysCallback(dt cbauth.KeyDataType, KeyIdsToDrop []string) {
84+
// EAR TODO
85+
}
86+
87+
func (this *NodeEncryptionManager) SynchronizeKeyFilesCallback(dt cbauth.KeyDataType) error {
88+
// Query has no requirement for this as of now
89+
return nil
90+
}
91+
92+
func validateKeyDataType(dt cbauth.KeyDataType) (encryption.KeyDataType, errors.Error) {
93+
kdt := encryption.KeyDataType{
94+
TypeName: dt.TypeName,
95+
BucketUUID: dt.BucketUUID,
96+
}
97+
98+
if dt.BucketUUID != "" && dt.TypeName != encryption.BUCKET_KEY_DATATYPE {
99+
return encryption.KeyDataType{}, errors.NewEncryptionError(errors.E_INVALID_ENCRYPTION_KEY_DATATYPE,
100+
fmt.Errorf("bucketUUID is only valid when typeName is service_bucket"), kdt.String())
101+
}
102+
103+
if dt.TypeName == encryption.BUCKET_KEY_DATATYPE && dt.BucketUUID == "" {
104+
return encryption.KeyDataType{}, errors.NewEncryptionError(errors.E_INVALID_ENCRYPTION_KEY_DATATYPE,
105+
fmt.Errorf("bucketUUID is required when typeName is service_bucket"), kdt.String())
106+
}
107+
108+
switch dt.TypeName {
109+
case encryption.BUCKET_KEY_DATATYPE:
110+
if dt.BucketUUID == "" {
111+
return encryption.KeyDataType{}, errors.NewEncryptionError(errors.E_INVALID_ENCRYPTION_KEY_DATATYPE,
112+
fmt.Errorf("bucketUUID is required when typeName is service_bucket"), kdt.String())
113+
}
114+
case encryption.LOG_KEY_DATATYPE, encryption.OTHER_KEY_DATATYPE:
115+
default:
116+
return encryption.KeyDataType{}, errors.NewEncryptionError(errors.E_INVALID_ENCRYPTION_KEY_DATATYPE,
117+
fmt.Errorf("unsupported key data type"), kdt.String())
118+
}
119+
120+
return kdt, nil
121+
}
122+
123+
func dataTypeToCbauthType(dt encryption.KeyDataType) cbauth.KeyDataType {
124+
return cbauth.KeyDataType{
125+
TypeName: dt.TypeName,
126+
BucketUUID: dt.BucketUUID,
127+
}
128+
}
129+
130+
func cbauthTypeToDataType(dt cbauth.KeyDataType) encryption.KeyDataType {
131+
return encryption.KeyDataType{
132+
TypeName: dt.TypeName,
133+
BucketUUID: dt.BucketUUID,
134+
}
135+
}

0 commit comments

Comments
 (0)