Skip to content

Commit 2762759

Browse files
authored
Merge pull request #17 from arangodb/rocksdb-encryption
Adding rocksdb encryption key support
2 parents 91c65dc + 078fe09 commit 2762759

File tree

10 files changed

+208
-24
lines changed

10 files changed

+208
-24
lines changed

Jenkinsfile.groovy

+11-7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pipeline {
2727
parameters {
2828
string(name: 'KUBECONFIG', defaultValue: '/home/jenkins/.kube/scw-183a3b', description: 'KUBECONFIG controls which k8s cluster is used', )
2929
string(name: 'TESTNAMESPACE', defaultValue: 'jenkins', description: 'TESTNAMESPACE sets the kubernetes namespace to ru tests in (this must be short!!)', )
30+
string(name: 'ENTERPRISEIMAGE', defaultValue: '', description: 'ENTERPRISEIMAGE sets the docker image used for enterprise tests)', )
3031
}
3132
stages {
3233
stage('Build') {
@@ -44,13 +45,16 @@ pipeline {
4445
steps {
4546
timestamps {
4647
lock("${params.TESTNAMESPACE}-${env.GIT_COMMIT}") {
47-
withEnv([
48-
"KUBECONFIG=${params.KUBECONFIG}",
49-
"TESTNAMESPACE=${params.TESTNAMESPACE}-${env.GIT_COMMIT}",
50-
"IMAGETAG=${env.GIT_COMMIT}",
51-
"PUSHIMAGES=1",
52-
]) {
53-
sh "make run-tests"
48+
withCredentials([string(credentialsId: 'ENTERPRISEIMAGE', variable: 'DEFAULTENTERPRISEIMAGE')]) {
49+
withEnv([
50+
"KUBECONFIG=${params.KUBECONFIG}",
51+
"TESTNAMESPACE=${params.TESTNAMESPACE}-${env.GIT_COMMIT}",
52+
"IMAGETAG=${env.GIT_COMMIT}",
53+
"PUSHIMAGES=1",
54+
"ENTERPRISEIMAGE=${params.ENTERPRISEIMAGE}",
55+
]) {
56+
sh "make run-tests"
57+
}
5458
}
5559
}
5660
}

Makefile

+10-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ endif
4040
ifndef TESTIMAGE
4141
TESTIMAGE := $(DOCKERNAMESPACE)/arangodb-operator-test$(IMAGESUFFIX)
4242
endif
43+
ifndef ENTERPRISEIMAGE
44+
ENTERPRISEIMAGE := $(DEFAULTENTERPRISEIMAGE)
45+
endif
4346

4447
BINNAME := $(PROJECT)
4548
BIN := $(BINDIR)/$(BINNAME)
@@ -170,7 +173,13 @@ endif
170173
kubectl create namespace $(TESTNAMESPACE)
171174
$(ROOTDIR)/examples/setup-rbac.sh --namespace=$(TESTNAMESPACE)
172175
$(ROOTDIR)/scripts/kube_create_operator.sh $(TESTNAMESPACE) $(OPERATORIMAGE)
173-
kubectl --namespace $(TESTNAMESPACE) run arangodb-operator-test -i --rm --quiet --restart=Never --image=$(TESTIMAGE) --env="TEST_NAMESPACE=$(TESTNAMESPACE)" -- -test.v
176+
kubectl --namespace $(TESTNAMESPACE) \
177+
run arangodb-operator-test -i --rm --quiet --restart=Never \
178+
--image=$(TESTIMAGE) \
179+
--env="ENTERPRISEIMAGE=$(ENTERPRISEIMAGE)" \
180+
--env="TEST_NAMESPACE=$(TESTNAMESPACE)" \
181+
-- \
182+
-test.v
174183
kubectl delete namespace $(TESTNAMESPACE) --ignore-not-found --now
175184

176185
# Release building

docs/user/custom_resource.md

+6
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,19 @@ This requires the Enterprise version.
118118

119119
The encryption key cannot be changed after the cluster has been created.
120120

121+
The secret specified by this setting, must have a data field named 'key' containing
122+
an encryption key that is exactly 32 bytes long.
123+
121124
### `spec.auth.jwtSecretName: string`
122125

123126
This setting specifies the name of a kubernetes `Secret` that contains
124127
the JWT token used for accessing all ArangoDB servers.
125128
When no name is specified, it defaults to `<deployment-name>-jwt`.
126129
To disable authentication, set this value to `None`.
127130

131+
If you specify a name of a `Secret`, that secret must have the token
132+
in a data field named `token`.
133+
128134
If you specify a name of a `Secret` that does not exist, a random token is created
129135
and stored in a `Secret` with given name.
130136

pkg/apis/arangodb/v1alpha/deployment_spec.go

+22
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ type RocksDBSpec struct {
142142
} `json:"encryption"`
143143
}
144144

145+
// IsEncrypted returns true when an encryption key secret name is provided,
146+
// false otherwise.
147+
func (s RocksDBSpec) IsEncrypted() bool {
148+
return s.Encryption.KeySecretName != ""
149+
}
150+
145151
// Validate the given spec
146152
func (s RocksDBSpec) Validate() error {
147153
if err := k8sutil.ValidateOptionalResourceName(s.Encryption.KeySecretName); err != nil {
@@ -155,6 +161,19 @@ func (s *RocksDBSpec) SetDefaults() {
155161
// Nothing needed
156162
}
157163

164+
// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec.
165+
// It returns a list of fields that have been reset.
166+
// Field names are relative to given field prefix.
167+
func (s RocksDBSpec) ResetImmutableFields(fieldPrefix string, target *RocksDBSpec) []string {
168+
var resetFields []string
169+
if s.IsEncrypted() != target.IsEncrypted() {
170+
// Note: You can change the name, but not from empty to non-empty (or reverse).
171+
target.Encryption.KeySecretName = s.Encryption.KeySecretName
172+
resetFields = append(resetFields, fieldPrefix+".encryption.keySecretName")
173+
}
174+
return resetFields
175+
}
176+
158177
// AuthenticationSpec holds authentication specific configuration settings
159178
type AuthenticationSpec struct {
160179
JWTSecretName string `json:"jwtSecretName,omitempty"`
@@ -519,6 +538,9 @@ func (s DeploymentSpec) ResetImmutableFields(target *DeploymentSpec) []string {
519538
target.StorageEngine = s.StorageEngine
520539
resetFields = append(resetFields, "storageEngine")
521540
}
541+
if l := s.RocksDB.ResetImmutableFields("rocksdb", &target.RocksDB); l != nil {
542+
resetFields = append(resetFields, l...)
543+
}
522544
if l := s.Single.ResetImmutableFields(ServerGroupSingle, "single", &target.Single); l != nil {
523545
resetFields = append(resetFields, l...)
524546
}

pkg/deployment/pod_creator.go

+15-11
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ package deployment
2525
import (
2626
"fmt"
2727
"net"
28+
"path/filepath"
2829
"strconv"
2930

3031
api "github.com/arangodb/k8s-operator/pkg/apis/arangodb/v1alpha"
3132
"github.com/arangodb/k8s-operator/pkg/util/arangod"
3233
"github.com/arangodb/k8s-operator/pkg/util/constants"
3334
"github.com/arangodb/k8s-operator/pkg/util/k8sutil"
35+
"github.com/pkg/errors"
3436
)
3537

3638
type optionPair struct {
@@ -92,17 +94,11 @@ func (d *Deployment) createArangodArgs(apiObject *api.ArangoDeployment, group ap
9294
}*/
9395

9496
// RocksDB
95-
if apiObject.Spec.RocksDB.Encryption.KeySecretName != "" {
96-
/*args = append(args,
97-
fmt.Sprintf("--rocksdb.encryption-keyfile=%s", apiObject.Spec.StorageEngine),
97+
if apiObject.Spec.RocksDB.IsEncrypted() {
98+
keyPath := filepath.Join(k8sutil.RocksDBEncryptionVolumeMountDir, "key")
99+
options = append(options,
100+
optionPair{"--rocksdb.encryption-keyfile", keyPath},
98101
)
99-
rocksdbSection := &configSection{
100-
Name: "rocksdb",
101-
Settings: map[string]string{
102-
"encryption-keyfile": bsCfg.RocksDBEncryptionKeyFile,
103-
},
104-
}
105-
config = append(config, rocksdbSection)*/
106102
}
107103

108104
options = append(options,
@@ -285,6 +281,7 @@ func (d *Deployment) createReadinessProbe(apiObject *api.ArangoDeployment, group
285281
// ensurePods creates all Pods listed in member status
286282
func (d *Deployment) ensurePods(apiObject *api.ArangoDeployment) error {
287283
kubecli := d.deps.KubeCli
284+
ns := apiObject.GetNamespace()
288285

289286
if err := apiObject.ForeachServerGroup(func(group api.ServerGroup, spec api.ServerGroupSpec, status *api.MemberStatusList) error {
290287
for _, m := range *status {
@@ -304,13 +301,20 @@ func (d *Deployment) ensurePods(apiObject *api.ArangoDeployment) error {
304301
if err != nil {
305302
return maskAny(err)
306303
}
304+
rocksdbEncryptionSecretName := ""
305+
if apiObject.Spec.RocksDB.IsEncrypted() {
306+
rocksdbEncryptionSecretName = apiObject.Spec.RocksDB.Encryption.KeySecretName
307+
if err := k8sutil.ValidateEncryptionKeySecret(kubecli, rocksdbEncryptionSecretName, ns); err != nil {
308+
return maskAny(errors.Wrapf(err, "RocksDB encryption key secret validation failed"))
309+
}
310+
}
307311
if apiObject.Spec.IsAuthenticated() {
308312
env[constants.EnvArangodJWTSecret] = k8sutil.EnvValue{
309313
SecretName: apiObject.Spec.Authentication.JWTSecretName,
310314
SecretKey: constants.SecretKeyJWT,
311315
}
312316
}
313-
if err := k8sutil.CreateArangodPod(kubecli, apiObject.Spec.IsDevelopment(), apiObject, role, m.ID, m.PersistentVolumeClaimName, apiObject.Spec.Image, apiObject.Spec.ImagePullPolicy, args, env, livenessProbe, readinessProbe); err != nil {
317+
if err := k8sutil.CreateArangodPod(kubecli, apiObject.Spec.IsDevelopment(), apiObject, role, m.ID, m.PersistentVolumeClaimName, apiObject.Spec.Image, apiObject.Spec.ImagePullPolicy, args, env, livenessProbe, readinessProbe, rocksdbEncryptionSecretName); err != nil {
314318
return maskAny(err)
315319
}
316320
} else if group.IsArangosync() {

pkg/util/constants/constants.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ const (
2929
EnvArangodJWTSecret = "ARANGOD_JWT_SECRET" // Contains JWT secret for the ArangoDB cluster
3030
EnvArangoSyncJWTSecret = "ARANGOSYNC_JWT_SECRET" // Contains JWT secret for the ArangoSync masters
3131

32-
SecretKeyJWT = "token" // Key inside a Secret used to hold a JW token
32+
SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key
33+
SecretKeyJWT = "token" // Key inside a Secret used to hold a JW token
3334
)

pkg/util/k8sutil/pods.go

+35-4
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import (
2929
)
3030

3131
const (
32-
arangodVolumeName = "arangod-data"
33-
ArangodVolumeMountDir = "/data"
32+
arangodVolumeName = "arangod-data"
33+
rocksdbEncryptionVolumeName = "rocksdb-encryption"
34+
ArangodVolumeMountDir = "/data"
35+
RocksDBEncryptionVolumeMountDir = "/secrets/rocksdb/encryption"
3436
)
3537

3638
// EnvValue is a helper structure for environment variable sources.
@@ -103,6 +105,16 @@ func arangodVolumeMounts() []v1.VolumeMount {
103105
}
104106
}
105107

108+
// arangodVolumeMounts creates a volume mount structure for arangod.
109+
func rocksdbEncryptionVolumeMounts() []v1.VolumeMount {
110+
return []v1.VolumeMount{
111+
{
112+
Name: rocksdbEncryptionVolumeName,
113+
MountPath: RocksDBEncryptionVolumeMountDir,
114+
},
115+
}
116+
}
117+
106118
// arangodContainer creates a container configured to run `arangod`.
107119
func arangodContainer(name string, image string, imagePullPolicy v1.PullPolicy, args []string, env map[string]EnvValue, livenessProbe *HTTPProbeConfig, readinessProbe *HTTPProbeConfig) v1.Container {
108120
c := v1.Container{
@@ -177,13 +189,19 @@ func newPod(deploymentName, ns, role, id string) v1.Pod {
177189
// CreateArangodPod creates a Pod that runs `arangod`.
178190
// If the pod already exists, nil is returned.
179191
// If another error occurs, that error is returned.
180-
func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deployment APIObject, role, id, pvcName, image string, imagePullPolicy v1.PullPolicy,
181-
args []string, env map[string]EnvValue, livenessProbe *HTTPProbeConfig, readinessProbe *HTTPProbeConfig) error {
192+
func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deployment APIObject,
193+
role, id, pvcName, image string, imagePullPolicy v1.PullPolicy,
194+
args []string, env map[string]EnvValue,
195+
livenessProbe *HTTPProbeConfig, readinessProbe *HTTPProbeConfig,
196+
rocksdbEncryptionSecretName string) error {
182197
// Prepare basic pod
183198
p := newPod(deployment.GetName(), deployment.GetNamespace(), role, id)
184199

185200
// Add arangod container
186201
c := arangodContainer(p.GetName(), image, imagePullPolicy, args, env, livenessProbe, readinessProbe)
202+
if rocksdbEncryptionSecretName != "" {
203+
c.VolumeMounts = append(c.VolumeMounts, rocksdbEncryptionVolumeMounts()...)
204+
}
187205
p.Spec.Containers = append(p.Spec.Containers, c)
188206

189207
// Add volume
@@ -209,6 +227,19 @@ func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deploy
209227
p.Spec.Volumes = append(p.Spec.Volumes, vol)
210228
}
211229

230+
// RocksDB encryption secret mount (if any)
231+
if rocksdbEncryptionSecretName != "" {
232+
vol := v1.Volume{
233+
Name: rocksdbEncryptionVolumeName,
234+
VolumeSource: v1.VolumeSource{
235+
Secret: &v1.SecretVolumeSource{
236+
SecretName: rocksdbEncryptionSecretName,
237+
},
238+
},
239+
}
240+
p.Spec.Volumes = append(p.Spec.Volumes, vol)
241+
}
242+
212243
// Add (anti-)affinity
213244
p.Spec.Affinity = createAffinity(deployment.GetName(), role, !developmentMode, "")
214245

pkg/util/k8sutil/secrets.go

+36
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,42 @@ import (
3232
"github.com/arangodb/k8s-operator/pkg/util/constants"
3333
)
3434

35+
// ValidateEncryptionKeySecret checks that a secret with given name in given namespace
36+
// exists and it contains a 'key' data field of exactly 32 bytes.
37+
func ValidateEncryptionKeySecret(kubecli kubernetes.Interface, secretName, namespace string) error {
38+
s, err := kubecli.CoreV1().Secrets(namespace).Get(secretName, metav1.GetOptions{})
39+
if err != nil {
40+
return maskAny(err)
41+
}
42+
// Check `key` field
43+
keyData, found := s.Data[constants.SecretEncryptionKey]
44+
if !found {
45+
return maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretEncryptionKey, secretName))
46+
}
47+
if len(keyData) != 32 {
48+
return maskAny(fmt.Errorf("'%s' in secret '%s' is expected to be 32 bytes long, found %d", constants.SecretEncryptionKey, secretName, len(keyData)))
49+
}
50+
return nil
51+
}
52+
53+
// CreateEncryptionKeySecret creates a secret used to store a RocksDB encryption key.
54+
func CreateEncryptionKeySecret(kubecli kubernetes.Interface, secretName, namespace string, key []byte) error {
55+
// Create secret
56+
secret := &v1.Secret{
57+
ObjectMeta: metav1.ObjectMeta{
58+
Name: secretName,
59+
},
60+
Data: map[string][]byte{
61+
constants.SecretEncryptionKey: key,
62+
},
63+
}
64+
if _, err := kubecli.CoreV1().Secrets(namespace).Create(secret); err != nil {
65+
// Failed to create secret
66+
return maskAny(err)
67+
}
68+
return nil
69+
}
70+
3571
// GetJWTSecret loads the JWT secret from a Secret with given name.
3672
func GetJWTSecret(kubecli kubernetes.Interface, secretName, namespace string) (string, error) {
3773
s, err := kubecli.CoreV1().Secrets(namespace).Get(secretName, metav1.GetOptions{})

tests/rocksdb_encryption_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package tests
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"strings"
7+
"testing"
8+
9+
"github.com/dchest/uniuri"
10+
11+
api "github.com/arangodb/k8s-operator/pkg/apis/arangodb/v1alpha"
12+
"github.com/arangodb/k8s-operator/pkg/client"
13+
"github.com/arangodb/k8s-operator/pkg/util/k8sutil"
14+
)
15+
16+
// TestRocksDBEncryptionSingle tests the creating of a single server deployment
17+
// with RocksDB & Encryption.
18+
func TestRocksDBEncryptionSingle(t *testing.T) {
19+
image := getEnterpriseImageOrSkip(t)
20+
c := client.MustNewInCluster()
21+
kubecli := mustNewKubeClient(t)
22+
ns := getNamespace(t)
23+
24+
// Prepare deployment config
25+
depl := newDeployment("test-rocksdb-enc-sng-" + uniuri.NewLen(4))
26+
depl.Spec.Image = image
27+
depl.Spec.StorageEngine = api.StorageEngineRocksDB
28+
depl.Spec.RocksDB.Encryption.KeySecretName = strings.ToLower(uniuri.New())
29+
30+
// Create encryption key secret
31+
key := make([]byte, 32)
32+
rand.Read(key)
33+
if err := k8sutil.CreateEncryptionKeySecret(kubecli, depl.Spec.RocksDB.Encryption.KeySecretName, ns, key); err != nil {
34+
t.Fatalf("Create encryption key secret failed: %v", err)
35+
}
36+
37+
// Create deployment
38+
_, err := c.DatabaseV1alpha().ArangoDeployments(ns).Create(depl)
39+
if err != nil {
40+
t.Fatalf("Create deployment failed: %v", err)
41+
}
42+
43+
// Wait for deployment to be ready
44+
apiObject, err := waitUntilDeployment(c, depl.GetName(), ns, deploymentHasState(api.DeploymentStateRunning))
45+
if err != nil {
46+
t.Errorf("Deployment not running in time: %v", err)
47+
}
48+
49+
// Create database client
50+
ctx := context.Background()
51+
client := mustNewArangodDatabaseClient(ctx, kubecli, apiObject, t)
52+
53+
// Wait for single server available
54+
if err := waitUntilVersionUp(client); err != nil {
55+
t.Fatalf("Single server not running returning version in time: %v", err)
56+
}
57+
58+
// Cleanup
59+
removeDeployment(c, depl.GetName(), ns)
60+
removeSecret(kubecli, depl.Spec.RocksDB.Encryption.KeySecretName, ns)
61+
}

tests/test_util.go

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ var (
5252
maskAny = errors.WithStack
5353
)
5454

55+
// getEnterpriseImageOrSkip returns the docker image used for enterprise
56+
// tests. If empty, enterprise tests are skipped.
57+
func getEnterpriseImageOrSkip(t *testing.T) string {
58+
image := os.Getenv("ENTERPRISEIMAGE")
59+
if image == "" {
60+
t.Skip("Skipping test because ENTERPRISEIMAGE is not set")
61+
}
62+
return image
63+
}
64+
5565
// mustNewKubeClient creates a kubernetes client
5666
// failing the test on errors.
5767
func mustNewKubeClient(t *testing.T) kubernetes.Interface {

0 commit comments

Comments
 (0)