Skip to content

Commit ffac2ad

Browse files
committed
storageconfig: encryption YAML support and validation
Support yaml for the encryption options and add validation. Epic: none Release note: None
1 parent 99b2e09 commit ffac2ad

File tree

6 files changed

+204
-67
lines changed

6 files changed

+204
-67
lines changed

pkg/cli/flags_test.go

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,55 +1751,60 @@ func TestParseEncryptionSpec(t *testing.T) {
17511751
expected storeEncryptionSpec
17521752
}{
17531753
// path
1754-
{",", "no path specified", storeEncryptionSpec{}},
1755-
{"", "no path specified", storeEncryptionSpec{}},
1756-
{"/mnt/hda1", "field not in the form <key>=<value>: /mnt/hda1", storeEncryptionSpec{}},
1757-
{"path=", "no value specified for path", storeEncryptionSpec{}},
1758-
{"path=~/data", "path cannot start with '~': ~/data", storeEncryptionSpec{}},
1759-
{"path=data,path=data2", "path field was used twice in encryption definition", storeEncryptionSpec{}},
1754+
{value: ",", expectedErr: "no path specified"},
1755+
{expectedErr: "no path specified"},
1756+
{value: "/mnt/hda1", expectedErr: "field not in the form <key>=<value>: /mnt/hda1"},
1757+
{value: "path=", expectedErr: "no value specified for path"},
1758+
{value: "path=~/data", expectedErr: "path cannot start with '~': ~/data"},
1759+
{value: "path=data,path=data2", expectedErr: "path field was used twice in encryption definition"},
17601760

17611761
// The same logic applies to key and old-key, don't repeat everything.
1762-
{"path=data", "no key specified", storeEncryptionSpec{}},
1763-
{"path=data,key=new.key", "no old-key specified", storeEncryptionSpec{}},
1762+
{value: "path=data", expectedErr: "no key specified"},
1763+
{value: "path=data,key=new.key", expectedErr: "no old key specified"},
17641764

17651765
// Rotation period.
1766-
{"path=data,key=new.key,old-key=old.key,rotation-period", "field not in the form <key>=<value>: rotation-period", storeEncryptionSpec{}},
1767-
{"path=data,key=new.key,old-key=old.key,rotation-period=", "no value specified for rotation-period", storeEncryptionSpec{}},
1768-
{"path=data,key=new.key,old-key=old.key,rotation-period=1", `could not parse rotation-duration value: 1: time: missing unit in duration "1"`, storeEncryptionSpec{}},
1769-
{"path=data,key=new.key,old-key=old.key,rotation-period=1d", `could not parse rotation-duration value: 1d: time: unknown unit "d" in duration "1d"`, storeEncryptionSpec{}},
1766+
{value: "path=data,key=new.key,old-key=old.key,rotation-period", expectedErr: "field not in the form <key>=<value>: rotation-period"},
1767+
{value: "path=data,key=new.key,old-key=old.key,rotation-period=", expectedErr: "no value specified for rotation-period"},
1768+
{value: "path=data,key=new.key,old-key=old.key,rotation-period=1", expectedErr: `could not parse rotation-duration value: 1: time: missing unit in duration "1"`},
1769+
{value: "path=data,key=new.key,old-key=old.key,rotation-period=1d", expectedErr: `could not parse rotation-duration value: 1d: time: unknown unit "d" in duration "1d"`},
1770+
{value: "path=data,key=new.key,old-key=old.key,rotation-period=1ms", expectedErr: `invalid rotation period 1ms`},
17701771

17711772
// Good values. Note that paths get absolutized so we start most of them
17721773
// with / so we can used fixed expected values.
17731774
{
1774-
"path=/data,key=/new.key,old-key=/old.key", "",
1775-
storeEncryptionSpec{Path: "/data",
1775+
value: "path=/data,key=/new.key,old-key=/old.key",
1776+
expected: storeEncryptionSpec{
1777+
Path: "/data",
17761778
Options: storageconfig.EncryptionOptions{
17771779
KeyFiles: &storageconfig.EncryptionKeyFiles{CurrentKey: "/new.key", OldKey: "/old.key"},
17781780
RotationPeriod: storageconfig.DefaultRotationPeriod,
17791781
},
17801782
},
17811783
},
17821784
{
1783-
"path=/data,key=/new.key,old-key=/old.key,rotation-period=1h", "",
1784-
storeEncryptionSpec{Path: "/data",
1785+
value: "path=/data,key=/new.key,old-key=/old.key,rotation-period=1h",
1786+
expected: storeEncryptionSpec{
1787+
Path: "/data",
17851788
Options: storageconfig.EncryptionOptions{
17861789
KeyFiles: &storageconfig.EncryptionKeyFiles{CurrentKey: "/new.key", OldKey: "/old.key"},
17871790
RotationPeriod: time.Hour,
17881791
},
17891792
},
17901793
},
17911794
{
1792-
"path=/data,key=plain,old-key=/old.key,rotation-period=1h", "",
1793-
storeEncryptionSpec{Path: "/data",
1795+
value: "path=/data,key=plain,old-key=/old.key,rotation-period=1h",
1796+
expected: storeEncryptionSpec{
1797+
Path: "/data",
17941798
Options: storageconfig.EncryptionOptions{
17951799
KeyFiles: &storageconfig.EncryptionKeyFiles{CurrentKey: "plain", OldKey: "/old.key"},
17961800
RotationPeriod: time.Hour,
17971801
},
17981802
},
17991803
},
18001804
{
1801-
"path=/data,key=/new.key,old-key=plain,rotation-period=1h", "",
1802-
storeEncryptionSpec{Path: "/data",
1805+
value: "path=/data,key=/new.key,old-key=plain,rotation-period=1h",
1806+
expected: storeEncryptionSpec{
1807+
Path: "/data",
18031808
Options: storageconfig.EncryptionOptions{
18041809
KeyFiles: &storageconfig.EncryptionKeyFiles{CurrentKey: "/new.key", OldKey: "plain"},
18051810
RotationPeriod: time.Hour,
@@ -1809,8 +1814,9 @@ func TestParseEncryptionSpec(t *testing.T) {
18091814

18101815
// One relative path to test absolutization.
18111816
{
1812-
"path=data,key=/new.key,old-key=/old.key", "",
1813-
storeEncryptionSpec{Path: absDataPath,
1817+
value: "path=data,key=/new.key,old-key=/old.key",
1818+
expected: storeEncryptionSpec{
1819+
Path: absDataPath,
18141820
Options: storageconfig.EncryptionOptions{
18151821
KeyFiles: &storageconfig.EncryptionKeyFiles{CurrentKey: "/new.key", OldKey: "/old.key"},
18161822
RotationPeriod: storageconfig.DefaultRotationPeriod,
@@ -1820,8 +1826,9 @@ func TestParseEncryptionSpec(t *testing.T) {
18201826

18211827
// Special path * is not absolutized.
18221828
{
1823-
"path=*,key=/new.key,old-key=/old.key", "",
1824-
storeEncryptionSpec{Path: "*",
1829+
value: "path=*,key=/new.key,old-key=/old.key",
1830+
expected: storeEncryptionSpec{
1831+
Path: "*",
18251832
Options: storageconfig.EncryptionOptions{
18261833
KeyFiles: &storageconfig.EncryptionKeyFiles{CurrentKey: "/new.key", OldKey: "/old.key"},
18271834
RotationPeriod: storageconfig.DefaultRotationPeriod,

pkg/cli/flags_util.go

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -530,22 +530,13 @@ func (es storeEncryptionSpec) PathMatches(path string) bool {
530530
return es.Path == path || es.Path == "*"
531531
}
532532

533-
// Special value of key paths to mean "no encryption". We do not accept empty fields.
534-
const plaintextFieldValue = "plain"
535-
536533
// parseStoreEncryptionSpec parses the string passed in and returns a new
537534
// storeEncryptionSpec if parsing succeeds.
538535
// TODO(mberhault): we should share the parsing code with the StoreSpec.
539536
func parseStoreEncryptionSpec(value string) (storeEncryptionSpec, error) {
540537
const pathField = "path"
541-
es := storeEncryptionSpec{
542-
Path: "",
543-
Options: storageconfig.EncryptionOptions{
544-
KeySource: storageconfig.EncryptionKeyFromFiles,
545-
KeyFiles: &storageconfig.EncryptionKeyFiles{},
546-
RotationPeriod: storageconfig.DefaultRotationPeriod,
547-
},
548-
}
538+
var es storeEncryptionSpec
539+
es.Options.KeyFiles = &storageconfig.EncryptionKeyFiles{}
549540

550541
used := make(map[string]struct{})
551542
for _, split := range strings.Split(value, ",") {
@@ -582,25 +573,9 @@ func parseStoreEncryptionSpec(value string) (storeEncryptionSpec, error) {
582573
}
583574
}
584575
case "key":
585-
if value == plaintextFieldValue {
586-
es.Options.KeyFiles.CurrentKey = plaintextFieldValue
587-
} else {
588-
var err error
589-
es.Options.KeyFiles.CurrentKey, err = getAbsoluteFSPath("key", value)
590-
if err != nil {
591-
return storeEncryptionSpec{}, err
592-
}
593-
}
576+
es.Options.KeyFiles.CurrentKey = value
594577
case "old-key":
595-
if value == plaintextFieldValue {
596-
es.Options.KeyFiles.OldKey = plaintextFieldValue
597-
} else {
598-
var err error
599-
es.Options.KeyFiles.OldKey, err = getAbsoluteFSPath("old-key", value)
600-
if err != nil {
601-
return storeEncryptionSpec{}, err
602-
}
603-
}
578+
es.Options.KeyFiles.OldKey = value
604579
case "rotation-period":
605580
dur, err := time.ParseDuration(value)
606581
if err != nil {
@@ -616,11 +591,8 @@ func parseStoreEncryptionSpec(value string) (storeEncryptionSpec, error) {
616591
if es.Path == "" {
617592
return storeEncryptionSpec{}, fmt.Errorf("no path specified")
618593
}
619-
if es.Options.KeyFiles.CurrentKey == "" {
620-
return storeEncryptionSpec{}, fmt.Errorf("no key specified")
621-
}
622-
if es.Options.KeyFiles.OldKey == "" {
623-
return storeEncryptionSpec{}, fmt.Errorf("no old-key specified")
594+
if err := es.Options.Validate(); err != nil {
595+
return storeEncryptionSpec{}, err
624596
}
625597

626598
return es, nil

pkg/storage/storageconfig/encryption_spec.go

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,31 @@
55

66
package storageconfig
77

8-
import "time"
8+
import (
9+
"path/filepath"
10+
"strings"
11+
"time"
12+
13+
"github.com/cockroachdb/errors"
14+
"gopkg.in/yaml.v3"
15+
)
916

1017
// EncryptionOptions defines the per-store encryption options.
1118
type EncryptionOptions struct {
1219
// The store key source. Defines which fields are useful.
13-
KeySource EncryptionKeySource
14-
// Set if key_source == KeyFiles.
15-
KeyFiles *EncryptionKeyFiles
16-
// Data key rotation period.
17-
RotationPeriod time.Duration
20+
KeySource EncryptionKeySource `yaml:"key-source,omitempty"`
21+
// Set if KeySource == EncryptionKeyFiles.
22+
KeyFiles *EncryptionKeyFiles `yaml:",inline"`
23+
// Data key rotation period. Defaults to 7 days if not specified.
24+
RotationPeriod time.Duration `yaml:"rotation-period,omitempty"`
1825
}
1926

2027
// EncryptionKeyFiles is used when plain key files are passed.
2128
type EncryptionKeyFiles struct {
22-
CurrentKey string
23-
OldKey string
29+
// Path to the current encryption key file, or "plain" for no encryption.
30+
CurrentKey string `yaml:"key"`
31+
// Path to the previous encryption key file, or "plain" for no encryption.
32+
OldKey string `yaml:"old-key"`
2433
}
2534

2635
// EncryptionKeySource is an enum identifying the source of the encryption key.
@@ -30,5 +39,67 @@ const (
3039
EncryptionKeyFromFiles EncryptionKeySource = 0
3140
)
3241

42+
var _ yaml.IsZeroer = EncryptionKeyFromFiles
43+
44+
func (e EncryptionKeySource) IsZero() bool {
45+
return e == 0
46+
}
47+
3348
// DefaultRotationPeriod is the rotation period used if not specified.
3449
const DefaultRotationPeriod = time.Hour * 24 * 7 // 1 week, give or take time changes.
50+
51+
// PlaintextKeyValue is the value of a key field that indicates no encryption.
52+
const PlaintextKeyValue = "plain"
53+
54+
// Validate checks all fields, possibly normalizing them and filling in
55+
// defaults.
56+
func (e *EncryptionOptions) Validate() error {
57+
if e.RotationPeriod == 0 {
58+
e.RotationPeriod = DefaultRotationPeriod
59+
} else if e.RotationPeriod < time.Second {
60+
return errors.Newf("invalid rotation period %s", e.RotationPeriod)
61+
}
62+
switch e.KeySource {
63+
case EncryptionKeyFromFiles:
64+
if e.KeyFiles == nil {
65+
return errors.New("no key files specified")
66+
}
67+
currentKey, err := getAbsoluteFSPath("key", e.KeyFiles.CurrentKey)
68+
if err != nil {
69+
return err
70+
}
71+
oldKey, err := getAbsoluteFSPath("old key", e.KeyFiles.OldKey)
72+
if err != nil {
73+
return err
74+
}
75+
e.KeyFiles.CurrentKey = currentKey
76+
e.KeyFiles.OldKey = oldKey
77+
return nil
78+
79+
default:
80+
return errors.Newf("unknown key source %d", e.KeySource)
81+
}
82+
}
83+
84+
// getAbsoluteFSPath takes a (possibly relative) path to an encryption key and
85+
// returns the absolute path. Handles PlaintextKeyValue as a special case.
86+
//
87+
// Returns an error if the path begins with '~' or Abs fails.
88+
// 'fieldName' is used in error strings.
89+
func getAbsoluteFSPath(fieldName string, p string) (string, error) {
90+
if p == "" {
91+
return "", errors.Newf("no %s specified", fieldName)
92+
}
93+
if p == PlaintextKeyValue {
94+
return p, nil
95+
}
96+
if strings.HasPrefix(p, "~") {
97+
return "", errors.Newf("%s %q cannot start with '~'", fieldName, p)
98+
}
99+
100+
ret, err := filepath.Abs(p)
101+
if err != nil {
102+
return "", errors.Wrapf(err, "could not find absolute path for %s %q", fieldName, p)
103+
}
104+
return ret, nil
105+
}

pkg/storage/storageconfig/store.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ func (s *Store) IsEncrypted() bool {
5757
// hard coded to 640MiB.
5858
const MinimumStoreSize = 10 * 64 << 20
5959

60+
// Validate checks all fields, possibly normalizing them and filling in
61+
// defaults.
6062
func (s *Store) Validate() error {
6163
if s.Size.IsBytes() && s.Size.Bytes() < MinimumStoreSize {
6264
return errors.Newf("store size (%s) must be at least %s",
@@ -88,6 +90,11 @@ func (s *Store) Validate() error {
8890
return errors.Newf("on-disk store cannot use a sticky VFS ID")
8991
}
9092
}
93+
if s.EncryptionOptions != nil {
94+
if err := s.EncryptionOptions.Validate(); err != nil {
95+
return errors.Wrap(err, "invalid encryption options")
96+
}
97+
}
9198

9299
return nil
93100
}

pkg/storage/storageconfig/store_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"math"
1111
"math/rand/v2"
1212
"testing"
13+
"time"
1314

1415
"github.com/cockroachdb/cockroach/pkg/testutils/datapathutils"
1516
"github.com/cockroachdb/cockroach/pkg/util/yamlutil"
@@ -35,6 +36,14 @@ func TestStoreYAMLOutput(t *testing.T) {
3536
BallastSize: PercentSize(10),
3637
Attributes: []string{"foo", "bar"},
3738
ProvisionedRate: ProvisionedRate{ProvisionedBandwidth: 1000 * 1024},
39+
EncryptionOptions: &EncryptionOptions{
40+
KeySource: EncryptionKeyFromFiles,
41+
KeyFiles: &EncryptionKeyFiles{
42+
CurrentKey: "/path/to/current-key",
43+
OldKey: "/path/to/old-key",
44+
},
45+
RotationPeriod: time.Hour,
46+
},
3847
},
3948
"pebble-opts": {
4049
Path: "/mnt/data1",
@@ -73,7 +82,9 @@ l0_stop_writes_threshold=10`,
7382
if err := s.Validate(); err != nil {
7483
return err.Error()
7584
}
76-
return "ok"
85+
out, err := yaml.Marshal(s)
86+
require.NoError(t, err)
87+
return string(out)
7788

7889
default:
7990
t.Fatalf("unknown command %q", td.Cmd)

0 commit comments

Comments
 (0)