Adding encryption config to airshipctl config

Design document: https://docs.google.com/document/d/1EjiCuXoiy8DEEXe15KxVJ4iWrwogCyG113_0LdzcWzQ/edit?usp=drive_web&ouid=102644738301620637153

This patchset comprises of:
- airship config now supports encryption configs to store encryption and decryption keys
from local file system or the kubernetes api server that will be used to encrypt
and decrypt secrets in a future patchset

This is the first of multiple patchsets to support encryption and decryption in airshipctl

Complete feature: https://review.opendev.org/#/c/742695/

Change-Id: I195e8e254b7cc6b3e04e45d67e0a0e3797183816
This commit is contained in:
uday.ruddarraju 2020-09-04 00:45:09 -07:00
parent 3601c2b59c
commit f328c43295
13 changed files with 478 additions and 14 deletions

View File

@ -62,6 +62,9 @@ type Config struct {
// Manifests is a map of referenceable names to documents
Manifests map[string]*Manifest `json:"manifests"`
// EncryptionConfigs is a map of referenceable names to encryption configs
EncryptionConfigs map[string]*EncryptionConfig `json:"encryptionConfigs"`
// CurrentContext is the name of the context that you would like to use by default
CurrentContext string `json:"currentContext"`
@ -732,6 +735,9 @@ func (c *Config) ModifyContext(context *Context, theContext *ContextOptions) {
if theContext.Manifest != "" {
context.Manifest = theContext.Manifest
}
if theContext.EncryptionConfig != "" {
context.EncryptionConfig = theContext.EncryptionConfig
}
if theContext.Namespace != "" {
kubeContext.Namespace = theContext.Namespace
}
@ -1105,6 +1111,57 @@ func (c *Config) ModifyRepository(repository *Repository, theManifest *ManifestO
return nil
}
// GetEncryptionConfigs returns all the encryption configs associated with the Config sorted by name
func (c *Config) GetEncryptionConfigs() []*EncryptionConfig {
keys := make([]string, 0, len(c.EncryptionConfigs))
for name := range c.EncryptionConfigs {
keys = append(keys, name)
}
sort.Strings(keys)
encryptionConfigs := make([]*EncryptionConfig, 0, len(c.EncryptionConfigs))
for _, name := range keys {
encryptionConfigs = append(encryptionConfigs, c.EncryptionConfigs[name])
}
return encryptionConfigs
}
// AddEncryptionConfig creates a new encryption config
func (c *Config) AddEncryptionConfig(options *EncryptionConfigOptions) *EncryptionConfig {
encryptionConfig := &EncryptionConfig{
EncryptionKeyFileSource: EncryptionKeyFileSource{
EncryptionKeyPath: options.EncryptionKeyPath,
DecryptionKeyPath: options.DecryptionKeyPath,
},
EncryptionKeySecretSource: EncryptionKeySecretSource{
KeySecretName: options.KeySecretName,
KeySecretNamespace: options.KeySecretNamespace,
},
}
if c.EncryptionConfigs == nil {
c.EncryptionConfigs = make(map[string]*EncryptionConfig)
}
c.EncryptionConfigs[options.Name] = encryptionConfig
return encryptionConfig
}
// ModifyEncryptionConfig sets existing values to existing encryption config
func (c *Config) ModifyEncryptionConfig(encryptionConfig *EncryptionConfig, options *EncryptionConfigOptions) {
if options.EncryptionKeyPath != "" {
encryptionConfig.EncryptionKeyPath = options.EncryptionKeyPath
}
if options.DecryptionKeyPath != "" {
encryptionConfig.DecryptionKeyPath = options.DecryptionKeyPath
}
if options.KeySecretName != "" {
encryptionConfig.KeySecretName = options.KeySecretName
}
if options.KeySecretNamespace != "" {
encryptionConfig.KeySecretNamespace = options.KeySecretNamespace
}
return
}
// CurrentContextManagementConfig returns the management options for the current context
func (c *Config) CurrentContextManagementConfig() (*ManagementConfiguration, error) {
currentCluster, err := c.CurrentContextCluster()

View File

@ -197,3 +197,34 @@ func RunSetManifest(o *ManifestOptions, airconfig *Config, writeToStorage bool)
return modified, nil
}
// RunSetEncryptionConfig validates the given command line options
// and invokes AddEncryptionConfig/ModifyEncryptionConfig
func RunSetEncryptionConfig(o *EncryptionConfigOptions, airconfig *Config, writeToStorage bool) (bool, error) {
modified := false
err := o.Validate()
if err != nil {
return modified, err
}
encryptionConfig, exists := airconfig.EncryptionConfigs[o.Name]
if !exists {
// encryption config didn't exist, create it
// ignoring the returned added encryption config
airconfig.AddEncryptionConfig(o)
modified = true
} else {
// encryption config exists, lets update
airconfig.ModifyEncryptionConfig(encryptionConfig, o)
modified = true
}
// Update configuration file just in time persistence approach
if writeToStorage {
if err := airconfig.PersistConfig(false); err != nil {
// Error that it didnt persist the changes
return modified, ErrConfigFailed{}
}
}
return modified, nil
}

View File

@ -140,3 +140,59 @@ func TestRunSetManifest(t *testing.T) {
assert.Equal(t, "/tmp/default", conf.Manifests["dummy_manifest"].TargetPath)
})
}
func TestRunSetEncryptionConfigLocalFile(t *testing.T) {
t.Run("testAddEncryptionConfig", func(t *testing.T) {
conf := testutil.DummyConfig()
dummyEncryptionConfig := testutil.DummyEncryptionConfigOptions()
dummyEncryptionConfig.Name = "test_encryption_config"
modified, err := config.RunSetEncryptionConfig(dummyEncryptionConfig, conf, false)
assert.Error(t, err)
assert.False(t, modified)
})
t.Run("testModifyEncryptionConfig", func(t *testing.T) {
conf := testutil.DummyConfig()
dummyEncryptionConfigOptions := &config.EncryptionConfigOptions{
Name: "testModifyEncryptionConfig",
EncryptionKeyPath: "testdata/ca.crt",
DecryptionKeyPath: "testdata/test-key.pem",
}
modified, err := config.RunSetEncryptionConfig(dummyEncryptionConfigOptions, conf, false)
assert.NoError(t, err)
assert.True(t, modified)
assert.Equal(t, "testdata/ca.crt", conf.EncryptionConfigs["testModifyEncryptionConfig"].EncryptionKeyPath)
assert.Equal(t, "testdata/test-key.pem", conf.EncryptionConfigs["testModifyEncryptionConfig"].DecryptionKeyPath)
})
}
func TestRunSetEncryptionConfigAPIBackend(t *testing.T) {
t.Run("testAddEncryptionConfig", func(t *testing.T) {
conf := testutil.DummyConfig()
dummyEncryptionConfig := testutil.DummyEncryptionConfigOptions()
dummyEncryptionConfig.Name = "test_encryption_config"
modified, err := config.RunSetEncryptionConfig(dummyEncryptionConfig, conf, false)
assert.Error(t, err)
assert.False(t, modified)
})
t.Run("testModifyEncryptionConfig", func(t *testing.T) {
conf := testutil.DummyConfig()
dummyEncryptionConfigOptions := &config.EncryptionConfigOptions{
Name: "testModifyEncryptionConfig",
KeySecretName: "dummySecret",
KeySecretNamespace: "dummyNamespace",
EncryptionKeyPath: "",
DecryptionKeyPath: "",
}
modified, err := config.RunSetEncryptionConfig(dummyEncryptionConfigOptions, conf, false)
assert.NoError(t, err)
assert.True(t, modified)
assert.Equal(t, "dummySecret", conf.EncryptionConfigs["testModifyEncryptionConfig"].KeySecretName)
assert.Equal(t, "dummyNamespace", conf.EncryptionConfigs["testModifyEncryptionConfig"].KeySecretNamespace)
})
}

View File

@ -84,6 +84,10 @@ func TestString(t *testing.T) {
name: "managementconfiguration",
stringer: testutil.DummyManagementConfiguration(),
},
{
name: "encryption-config",
stringer: testutil.DummyEncryptionConfig(),
},
}
for _, tt := range tests {
@ -223,6 +227,7 @@ func TestEnsureComplete(t *testing.T) {
AuthInfos: map[string]*config.AuthInfo{"testAuthInfo": {}},
Contexts: map[string]*config.Context{"testContext": {Manifest: "testManifest"}},
Manifests: map[string]*config.Manifest{"testManifest": {}},
EncryptionConfigs: map[string]*config.EncryptionConfig{"testEncryptionConfig": {}},
CurrentContext: "testContext",
},
expectedErr: nil,
@ -844,3 +849,39 @@ func TestModifyManifests(t *testing.T) {
err = conf.ModifyManifest(manifest, mo)
require.Error(t, err)
}
func TestGetDefaultEncryptionConfigs(t *testing.T) {
conf, cleanup := testutil.InitConfig(t)
defer cleanup(t)
encryptionConfigs := conf.GetEncryptionConfigs()
require.NotNil(t, encryptionConfigs)
// by default, we dont expect any encryption configs
assert.Equal(t, 0, len(encryptionConfigs))
}
func TestModifyEncryptionConfigs(t *testing.T) {
conf, cleanup := testutil.InitConfig(t)
defer cleanup(t)
eco := testutil.DummyEncryptionConfigOptions()
encryptionConfig := conf.AddEncryptionConfig(eco)
require.NotNil(t, encryptionConfig)
eco.KeySecretName += stringDelta
conf.ModifyEncryptionConfig(encryptionConfig, eco)
modifiedConfig := conf.EncryptionConfigs[eco.Name]
assert.Equal(t, eco.KeySecretName, modifiedConfig.KeySecretName)
eco.KeySecretNamespace += stringDelta
conf.ModifyEncryptionConfig(encryptionConfig, eco)
assert.Equal(t, eco.KeySecretNamespace, modifiedConfig.KeySecretNamespace)
eco.EncryptionKeyPath += stringDelta
conf.ModifyEncryptionConfig(encryptionConfig, eco)
assert.Equal(t, eco.EncryptionKeyPath, modifiedConfig.EncryptionKeyPath)
eco.DecryptionKeyPath += stringDelta
conf.ModifyEncryptionConfig(encryptionConfig, eco)
assert.Equal(t, eco.DecryptionKeyPath, modifiedConfig.DecryptionKeyPath)
}

View File

@ -29,10 +29,14 @@ type Context struct {
// NameInKubeconf is the Context name in kubeconf
NameInKubeconf string `json:"contextKubeconf"`
// Manifest is the default manifest to be use with this context
// Manifest is the default manifest to be used with this context
// +optional
Manifest string `json:"manifest,omitempty"`
// EncryptionConfig is the default encryption config to be used with this context
// +optional
EncryptionConfig string `json:"encryptionConfig,omitempty"`
// KubeConfig Context Object
context *api.Context
}

View File

@ -0,0 +1,49 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import "sigs.k8s.io/yaml"
// EncryptionConfig holds the public and private key information
// used to encrypt and decrypt secrets
type EncryptionConfig struct {
EncryptionKeyFileSource `json:",inline"`
EncryptionKeySecretSource `json:",inline"`
}
// EncryptionKeyFileSource hold the local file information for the public and private
// keys used for encryption and decryption
type EncryptionKeyFileSource struct {
EncryptionKeyPath string `json:"encryptionKeyPath,omitempty"`
DecryptionKeyPath string `json:"decryptionKeyPath,omitempty"`
}
// EncryptionKeySecretSource holds the secret information for the public and private
// keys used for encryption and decryption
type EncryptionKeySecretSource struct {
KeySecretName string `json:"keySecretName,omitempty"`
KeySecretNamespace string `json:"keySecretNamespace,omitempty"`
}
// String returns the encryption config in yaml format
func (ec *EncryptionConfig) String() string {
yamlData, err := yaml.Marshal(&ec)
if err != nil {
return ""
}
return string(yamlData)
}

View File

@ -0,0 +1,93 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncryptionConfigOutputString(t *testing.T) {
expectedEncryptionConfigYaml := `decryptionKeyPath: /tmp/decryption.pub
encryptionKeyPath: /tmp/encryption.key
`
encryptionConfig := &EncryptionConfig{
EncryptionKeyFileSource: EncryptionKeyFileSource{
EncryptionKeyPath: "/tmp/encryption.key",
DecryptionKeyPath: "/tmp/decryption.pub",
},
}
assert.Equal(t, expectedEncryptionConfigYaml, encryptionConfig.String())
}
func TestValidateEncryptionConfigInvalid(t *testing.T) {
encryptionConfig := &EncryptionConfigOptions{}
err := encryptionConfig.Validate()
assert.Error(t, err)
}
func TestValidateEncryptionConfigInvalidOnlyEncKey(t *testing.T) {
encryptionConfig := &EncryptionConfigOptions{
EncryptionKeyPath: "/tmp/encryption.key",
}
err := encryptionConfig.Validate()
assert.Error(t, err)
}
func TestValidateEncryptionConfigInvalidOnlyDecKey(t *testing.T) {
encryptionConfig := &EncryptionConfigOptions{
DecryptionKeyPath: "/tmp/decryption.pub",
}
err := encryptionConfig.Validate()
assert.Error(t, err)
}
func TestValidateEncryptionConfigInvalidOnlySecretName(t *testing.T) {
encryptionConfig := &EncryptionConfigOptions{
KeySecretName: "secretName",
}
err := encryptionConfig.Validate()
assert.Error(t, err)
}
func TestValidateEncryptionConfigInvalidOnlySecretNamespace(t *testing.T) {
encryptionConfig := &EncryptionConfigOptions{
KeySecretNamespace: "secretNamespace",
}
err := encryptionConfig.Validate()
assert.Error(t, err)
}
func TestValidateEncryptionConfigValidWithSecret(t *testing.T) {
encryptionConfig := &EncryptionConfigOptions{
KeySecretName: "secretName",
KeySecretNamespace: "secretNamespace",
}
err := encryptionConfig.Validate()
assert.Error(t, err)
}
func TestValidateEncryptionConfigValidWithFile(t *testing.T) {
encryptionConfig := &EncryptionConfigOptions{
EncryptionKeyPath: "/tmp/encryption.key",
DecryptionKeyPath: "/tmp/decryption.pub",
}
err := encryptionConfig.Validate()
assert.Error(t, err)
}

View File

@ -143,6 +143,16 @@ func (e ErrManagementConfigurationNotFound) Error() string {
return fmt.Sprintf("Unknown management configuration '%s'.", e.Name)
}
// ErrEncryptionConfigurationNotFound describes a situation in which a user has attempted to reference an encryption
// configuration that cannot be referenced.
type ErrEncryptionConfigurationNotFound struct {
Name string
}
func (e ErrEncryptionConfigurationNotFound) Error() string {
return fmt.Sprintf("Unknown encryption configuration '%s'.", e.Name)
}
// ErrMissingCurrentContext returned in case --current used without setting current-context
type ErrMissingCurrentContext struct {
}
@ -248,6 +258,42 @@ func (e ErrMissingManifestName) Error() string {
return "missing manifest name"
}
// ErrMissingEncryptionConfigName is returned when encryption config name is empty
type ErrMissingEncryptionConfigName struct {
}
func (e ErrMissingEncryptionConfigName) Error() string {
return "missing encryption config name"
}
// ErrMutuallyExclusiveEncryptionConfigType is returned when encryption config specifies both
// local key files and secret information for keys stored as secrets in the apiserver
type ErrMutuallyExclusiveEncryptionConfigType struct {
}
func (e ErrMutuallyExclusiveEncryptionConfigType) Error() string {
return "Specify mutually exclusive encryption config sources, use either: " +
"--decryption-key-path/--decryption-key-path or --secret-name/--secret-namespace."
}
// ErrInvalidEncryptionKeyPath is returned when encryption config specifies only one of
// encryption and decryption keys
type ErrInvalidEncryptionKeyPath struct {
}
func (e ErrInvalidEncryptionKeyPath) Error() string {
return "Specify both encryption and decryption keys when setting encryption config"
}
// ErrInvalidEncryptionKey is returned when encryption config specifies only one of
// encryption keys secret name and namespace
type ErrInvalidEncryptionKey struct {
}
func (e ErrInvalidEncryptionKey) Error() string {
return "Specify both secret name and namespace when setting encryption config"
}
// ErrMissingFlag is returned when flag is not provided
type ErrMissingFlag struct {
FlagName string

View File

@ -42,6 +42,7 @@ type ContextOptions struct {
Cluster string
AuthInfo string
Manifest string
EncryptionConfig string
Namespace string
Current bool
}
@ -71,6 +72,15 @@ type ManifestOptions struct {
TargetPath string
}
// EncryptionConfigOptions holds all configurable options for encryption configuration
type EncryptionConfigOptions struct {
Name string
EncryptionKeyPath string
DecryptionKeyPath string
KeySecretName string
KeySecretNamespace string
}
// TODO(howell): The following functions are tightly coupled with flags passed
// on the command line. We should find a way to remove this coupling, since it
// is possible to create (and validate) these objects without using the command
@ -195,3 +205,45 @@ func (o *ManifestOptions) Validate() error {
}
return nil
}
// Validate checks for the possible errors with encryption config
// Error when invalid value, incompatible choice of values given or if the
// key file paths do not exist in the file system
func (o *EncryptionConfigOptions) Validate() error {
switch {
case o.Name == "":
return ErrMissingEncryptionConfigName{}
case o.backedByFileSystem() == o.backedByAPIServer():
return ErrMutuallyExclusiveEncryptionConfigType{}
case o.backedByFileSystem():
if o.EncryptionKeyPath == "" || o.DecryptionKeyPath == "" {
return ErrInvalidEncryptionKeyPath{}
}
case o.backedByAPIServer():
if o.KeySecretName == "" || o.KeySecretNamespace == "" {
return ErrInvalidEncryptionKey{}
}
}
if o.backedByFileSystem() {
if err := checkExists("encryption-key-path", o.EncryptionKeyPath); err != nil {
return err
}
if err := checkExists("decryption-key-path", o.EncryptionKeyPath); err != nil {
return err
}
}
return nil
}
func (o EncryptionConfigOptions) backedByFileSystem() bool {
return o.EncryptionKeyPath != "" || o.DecryptionKeyPath != ""
}
func (o EncryptionConfigOptions) backedByAPIServer() bool {
return o.KeySecretName != "" || o.KeySecretNamespace != ""
}

View File

@ -11,8 +11,13 @@ clusters:
contexts:
dummy_context:
contextKubeconf: dummy_cluster_ephemeral
encryptionConfig: dummy_encryption_config
manifest: dummy_manifest
currentContext: dummy_context
encryptionConfigs:
dummy_encryption_config:
decryptionKeyPath: /tmp/decryption.pub
encryptionKeyPath: /tmp/encryption.key
kind: Config
managementConfiguration:
dummy_management_config:

View File

@ -1,4 +1,5 @@
contextKubeconf: dummy_cluster_ephemeral
encryptionConfig: dummy_encryption_config
manifest: dummy_manifest
LocationOfOrigin: ""

View File

@ -0,0 +1,2 @@
decryptionKeyPath: /tmp/decryption.pub
encryptionKeyPath: /tmp/encryption.key

View File

@ -55,6 +55,9 @@ func DummyConfig() *config.Config {
ManagementConfiguration: map[string]*config.ManagementConfiguration{
"dummy_management_config": DummyManagementConfiguration(),
},
EncryptionConfigs: map[string]*config.EncryptionConfig{
"dummy_encryption_config": DummyEncryptionConfig(),
},
CurrentContext: "dummy_context",
}
conf.SetKubeConfig(kubeconfig.NewConfig())
@ -74,6 +77,7 @@ func DummyContext() *config.Context {
context.Namespace = "dummy_namespace"
context.AuthInfo = "dummy_user"
context.Cluster = "dummy_cluster_ephemeral"
c.EncryptionConfig = "dummy_encryption_config"
c.SetKubeContext(context)
return c
@ -207,6 +211,7 @@ func DummyContextOptions() *config.ContextOptions {
co.AuthInfo = "dummy_user"
co.CurrentContext = false
co.Namespace = "dummy_namespace"
co.EncryptionConfig = "dummy_encryption_config"
return co
}
@ -223,6 +228,27 @@ func DummyAuthInfoOptions() *config.AuthInfoOptions {
return authinfo
}
// DummyEncryptionConfig creates EncryptionConfigOptions object
// for unit testing
func DummyEncryptionConfig() *config.EncryptionConfig {
return &config.EncryptionConfig{
EncryptionKeyFileSource: config.EncryptionKeyFileSource{
EncryptionKeyPath: "/tmp/encryption.key",
DecryptionKeyPath: "/tmp/decryption.pub",
},
}
}
// DummyEncryptionConfigOptions creates ManifestOptions config object
// for unit testing
func DummyEncryptionConfigOptions() *config.EncryptionConfigOptions {
return &config.EncryptionConfigOptions{
Name: "dummy_encryption_config",
EncryptionKeyPath: "/tmp/encryption.key",
DecryptionKeyPath: "/tmp/decryption.pub",
}
}
// DummyManagementConfiguration creates a management configuration for unit testing
func DummyManagementConfiguration() *config.ManagementConfiguration {
return &config.ManagementConfiguration{
@ -281,6 +307,7 @@ contexts:
contextKubeconf: def_target
onlyink:
contextKubeconf: onlyinkubeconf_target
encryptionConfigs: {}
currentContext: ""
kind: Config
manifests: {}