diff --git a/pkg/crypto/aes.go b/pkg/crypto/aes.go new file mode 100644 index 0000000000..a8c651e317 --- /dev/null +++ b/pkg/crypto/aes.go @@ -0,0 +1,83 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + + "github.com/pkg/errors" +) + +type AESCipher struct { + key []byte + cipher cipher.AEAD + nonce []byte +} + +const keyLength = 24 // 192 bit + +func NewAESCypher() (*AESCipher, error) { + key := make([]byte, keyLength) + if _, err := rand.Read(key); err != nil { + return nil, errors.Wrap(err, "failed to read key") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, errors.Wrap(err, "failed ro create new cipher") + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, errors.Wrap(err, "failed to wrap cipher gcm") + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, errors.Wrap(err, "failed to read nonce") + } + + return &AESCipher{ + key: key, + cipher: gcm, + nonce: nonce, + }, nil +} + +func AESCipherFromString(data string) (*AESCipher, error) { + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return nil, errors.Wrap(err, "failed to decode string") + } + + if len(decoded) < keyLength { + return nil, errors.Errorf("cipher key is invalid: len=%d, expected %d", len(decoded), keyLength) + } + + key := decoded[:keyLength] + block, err := aes.NewCipher(key) + if err != nil { + return nil, errors.Wrap(err, "failed to create cipher from data") + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, errors.Wrap(err, "failed to wrap cipher gcm") + } + + nonceLen := len(decoded) - keyLength + if nonceLen < gcm.NonceSize() { + return nil, errors.Errorf("cipher nonce is invalid: len=%d, expected %d", nonceLen, gcm.NonceSize()) + } + + return &AESCipher{ + key: key, + cipher: gcm, + nonce: decoded[keyLength:], + }, nil +} + +func (c *AESCipher) ToString() string { + return base64.StdEncoding.EncodeToString(append(c.key, c.nonce...)) +} diff --git a/pkg/kotsadm/kotsadm.go b/pkg/kotsadm/kotsadm.go index 13535f7075..4565188243 100644 --- a/pkg/kotsadm/kotsadm.go +++ b/pkg/kotsadm/kotsadm.go @@ -26,6 +26,7 @@ type DeployOptions struct { S3SecretKey string JWT string PostgresPassword string + APIEncryptionKey string ServiceType string NodePort int32 Hostname string @@ -304,5 +305,17 @@ func readDeployOptionsFromCluster(namespace string, kubeconfig string, clientset } } + // API encryption key, read from the secret or create new password + encyptionSecret, err := getAPIEncryptionSecret(namespace, clientset) + if err != nil { + return nil, errors.Wrap(err, "failed to get postgres secret") + } + if encyptionSecret != nil { + key, ok := encyptionSecret.Data["encryptionKey"] + if ok { + deployOptions.APIEncryptionKey = string(key) + } + } + return &deployOptions, nil } diff --git a/pkg/kotsadm/secrets.go b/pkg/kotsadm/secrets.go index 2012ea92f2..8533d74438 100644 --- a/pkg/kotsadm/secrets.go +++ b/pkg/kotsadm/secrets.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/manifoldco/promptui" "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/crypto" "golang.org/x/crypto/bcrypt" corev1 "k8s.io/api/core/v1" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" @@ -45,6 +46,19 @@ func getSecretsYAML(deployOptions *DeployOptions) (map[string][]byte, error) { } docs["secret-shared-password.yaml"] = sharedPassword.Bytes() + if deployOptions.APIEncryptionKey == "" { + cipher, err := crypto.NewAESCypher() + if err != nil { + return nil, errors.Wrap(err, "failed to create new API encryption key") + } + deployOptions.APIEncryptionKey = cipher.ToString() + } + var apiEncryptionBuffer bytes.Buffer + if err := s.Encode(apiEncryptionKeySecret(deployOptions.Namespace, deployOptions.APIEncryptionKey), &apiEncryptionBuffer); err != nil { + return nil, errors.Wrap(err, "failed to marshal shared password secret") + } + docs["secret-api-encryption.yaml"] = apiEncryptionBuffer.Bytes() + var s3 bytes.Buffer if deployOptions.S3SecretKey == "" { deployOptions.S3SecretKey = uuid.New().String() @@ -79,6 +93,10 @@ func ensureSecrets(deployOptions *DeployOptions, clientset *kubernetes.Clientset return errors.Wrap(err, "failed to ensure s3 secret") } + if err := ensureAPIEncryptionSecret(deployOptions, clientset); err != nil { + return errors.Wrap(err, "failed to ensure s3 secret") + } + return nil } @@ -247,3 +265,44 @@ func promptForSharedPassword() (string, error) { } } + +func ensureAPIEncryptionSecret(deployOptions *DeployOptions, clientset *kubernetes.Clientset) error { + secret, err := getAPIEncryptionSecret(deployOptions.Namespace, clientset) + if err != nil { + return errors.Wrap(err, "failed to check for existing postgres secret") + } + + if secret != nil { + if key, _ := secret.Data["encryptionKey"]; len(key) > 0 { + return nil + } + } + + if deployOptions.APIEncryptionKey == "" { + cipher, err := crypto.NewAESCypher() + if err != nil { + return errors.Wrap(err, "failed to create new AES cipher") + } + deployOptions.APIEncryptionKey = cipher.ToString() + } + + _, err = clientset.CoreV1().Secrets(deployOptions.Namespace).Create(apiEncryptionKeySecret(deployOptions.Namespace, deployOptions.APIEncryptionKey)) + if err != nil { + return errors.Wrap(err, "failed to create API encryption secret") + } + + return nil +} + +func getAPIEncryptionSecret(namespace string, clientset *kubernetes.Clientset) (*corev1.Secret, error) { + apiSecret, err := clientset.CoreV1().Secrets(namespace).Get("kotsadm-encryption", metav1.GetOptions{}) + if err != nil { + if kuberneteserrors.IsNotFound(err) { + return nil, nil + } + + return nil, errors.Wrap(err, "failed to get postgres secret from cluster") + } + + return apiSecret, nil +} diff --git a/pkg/kotsadm/secrets_objects.go b/pkg/kotsadm/secrets_objects.go index b8c8240380..6fa7d0e1f4 100644 --- a/pkg/kotsadm/secrets_objects.go +++ b/pkg/kotsadm/secrets_objects.go @@ -89,3 +89,21 @@ func s3Secret(namespace string, accessKey string, secretKey string) *corev1.Secr return secret } + +func apiEncryptionKeySecret(namespace string, key string) *corev1.Secret { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "kotsadm-encryption", + Namespace: namespace, + }, + Data: map[string][]byte{ + "encryptionKey": []byte(key), + }, + } + + return secret +} diff --git a/pkg/upstream/admin-console.go b/pkg/upstream/admin-console.go index 2fc8455e5c..27ee5e3822 100644 --- a/pkg/upstream/admin-console.go +++ b/pkg/upstream/admin-console.go @@ -79,11 +79,17 @@ func findFileAndReadSecret(secretName string, key string, renderDir string, file for _, fi := range files { data, err := ioutil.ReadFile(path.Join(renderDir, "admin-console", fi.Name())) if err != nil { - return "", errors.Wrap(err, "failed to read file") + // TODO: log errors.Wrap(err, "failed to read file") + continue } decode := scheme.Codecs.UniversalDeserializer().Decode obj, gvk, err := decode(data, nil, nil) + if err != nil { + // TODO: log err + continue + } + if gvk.Group != "" || gvk.Version != "v1" || gvk.Kind != "Secret" { continue } diff --git a/pkg/upstream/write.go b/pkg/upstream/write.go index e4a1dcb7d6..6d850c0413 100644 --- a/pkg/upstream/write.go +++ b/pkg/upstream/write.go @@ -2,10 +2,6 @@ package upstream import ( "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" "io/ioutil" "os" "path" @@ -13,6 +9,7 @@ import ( "github.com/pkg/errors" kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1" kotsscheme "github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme" + "github.com/replicatedhq/kots/pkg/crypto" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/client-go/kubernetes/scheme" @@ -154,29 +151,12 @@ func (u *Upstream) GetBaseDir(options WriteOptions) string { func getEncryptionKey(previousInstallationContent []byte) (string, error) { if previousInstallationContent == nil { - key := make([]byte, 24) // 192 bit - if _, err := rand.Read(key); err != nil { - return "", errors.Wrap(err, "failed to read key") - } - - block, err := aes.NewCipher(key) - if err != nil { - return "", errors.Wrap(err, "failed ro create new cipher") - } - - gcm, err := cipher.NewGCM(block) + cipher, err := crypto.NewAESCypher() if err != nil { - return "", errors.Wrap(err, "failed to wrap cipher gcm") - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err := rand.Read(nonce); err != nil { - return "", errors.Wrap(err, "failed to read nonce") + return "", errors.Wrap(err, "failed to create new AES cipher") } - newKey := base64.StdEncoding.EncodeToString(append(key, nonce...)) - - return newKey, nil + return cipher.ToString(), nil } kotsscheme.AddToScheme(scheme.Scheme)