diff --git a/deploy/crds/secretgenerator.mittwald.de_sshkeypairs_crd.yaml b/deploy/crds/secretgenerator.mittwald.de_sshkeypairs_crd.yaml index ec4d7129..4d0bb203 100644 --- a/deploy/crds/secretgenerator.mittwald.de_sshkeypairs_crd.yaml +++ b/deploy/crds/secretgenerator.mittwald.de_sshkeypairs_crd.yaml @@ -41,6 +41,12 @@ spec: type: string privateKey: type: string + privateKeyField: + description: 'Field in which to store SSH private key. ssh-privatekey is used if not specified.' + type: string + publicKeyField: + description: 'Field in which to store SSH private key. ssh-publickey is used if not specified.' + type: string type: type: string type: object diff --git a/pkg/apis/secretgenerator/v1alpha1/sshkeypair_types.go b/pkg/apis/secretgenerator/v1alpha1/sshkeypair_types.go index 8273a3f0..2d2d2313 100644 --- a/pkg/apis/secretgenerator/v1alpha1/sshkeypair_types.go +++ b/pkg/apis/secretgenerator/v1alpha1/sshkeypair_types.go @@ -15,6 +15,10 @@ type SSHKeyPairSpec struct { // +optional PrivateKey string `json:"privateKey,omitempty"` // +optional + PrivateKeyField string `json:"privateKeyField,omitempty"` + // +optional + PublicKeyField string `json:"publicKeyField,omitempty"` + // +optional Type string `json:"type,omitempty"` // +optional Data map[string]string `json:"data,omitempty"` @@ -72,6 +76,20 @@ func (in *SSHKeyPairList) SetListMeta(meta metav1.ListMeta) { in.ListMeta = meta } +func (in *SSHKeyPair) GetPrivateKeyField() string { + if in.Spec.PrivateKeyField != "" { + return in.Spec.PrivateKeyField + } + return "ssh-privatekey" +} + +func (in *SSHKeyPair) GetPublicKeyField() string { + if in.Spec.PublicKeyField != "" { + return in.Spec.PublicKeyField + } + return "ssh-publickey" +} + func (in *SSHKeyPair) GetStatus() SecretStatus { return &in.Status } diff --git a/pkg/controller/crd/sshkeypair/sshkeypair_controller.go b/pkg/controller/crd/sshkeypair/sshkeypair_controller.go index 648d850c..16f1de7f 100644 --- a/pkg/controller/crd/sshkeypair/sshkeypair_controller.go +++ b/pkg/controller/crd/sshkeypair/sshkeypair_controller.go @@ -108,19 +108,21 @@ func (r *ReconcileSSHKeyPair) updateSecret(ctx context.Context, existing *v1.Sec regenerate := instance.Spec.ForceRegenerate data := instance.Spec.Data instancePrivateKey := instance.Spec.PrivateKey + privateKeyField := instance.GetPrivateKeyField() + publicKeyField := instance.GetPublicKeyField() - existingPrivateKey := existing.Data[secret.SecretFieldPrivateKey] + existingPrivateKey := existing.Data[privateKeyField] targetSecret := existing.DeepCopy() // if regeneration is forced or existing private key is empty use private key from spec if len(instancePrivateKey) > 0 && (len(existingPrivateKey) == 0 || regenerate) { - targetSecret.Data[secret.SecretFieldPrivateKey] = []byte(instancePrivateKey) + targetSecret.Data[privateKeyField] = []byte(instancePrivateKey) } crd.UpdateData(data, targetSecret, regenerate) - err := secret.GenerateSSHKeypairData(reqLogger, length, regenerate, targetSecret.Data) + err := secret.GenerateSSHKeypairData(reqLogger, length, privateKeyField, publicKeyField, regenerate, targetSecret.Data) if err != nil { return reconcile.Result{RequeueAfter: time.Second * 30}, err } @@ -140,14 +142,16 @@ func (r *ReconcileSSHKeyPair) createNewSecret(ctx context.Context, instance *v1a length := instance.Spec.Length data := instance.Spec.Data instancePrivateKey := []byte(instance.Spec.PrivateKey) + privateKeyField := instance.GetPrivateKeyField() + publicKeyField := instance.GetPublicKeyField() for key := range data { values[key] = []byte(data[key]) } - values[secret.SecretFieldPrivateKey] = instancePrivateKey + values[privateKeyField] = instancePrivateKey - err := secret.GenerateSSHKeypairData(reqLogger, length, false, values) + err := secret.GenerateSSHKeypairData(reqLogger, length, privateKeyField, publicKeyField, false, values) if err != nil { return reconcile.Result{RequeueAfter: time.Second * 30}, err } diff --git a/pkg/controller/secret/secret_controller.go b/pkg/controller/secret/secret_controller.go index 17c34b69..e869872c 100644 --- a/pkg/controller/secret/secret_controller.go +++ b/pkg/controller/secret/secret_controller.go @@ -178,3 +178,17 @@ func getEncodingFromAnnotation(fallback string, annotations map[string]string) ( } return fallback, nil } + +func GetPrivateKeyFieldFromAnnotation(fallback string, annotations map[string]string) (string, error) { + if val, ok := annotations[AnnotationSSHPrivateKeyField]; ok { + return val, nil + } + return fallback, nil +} + +func GetPublicKeyFieldFromAnnotation(fallback string, annotations map[string]string) (string, error) { + if val, ok := annotations[AnnotationSSHPublicKeyField]; ok { + return val, nil + } + return fallback, nil +} diff --git a/pkg/controller/secret/secret_ssh.go b/pkg/controller/secret/secret_ssh.go index 1225a965..f644a8fc 100644 --- a/pkg/controller/secret/secret_ssh.go +++ b/pkg/controller/secret/secret_ssh.go @@ -16,8 +16,8 @@ import ( ) const ( - SecretFieldPublicKey = "ssh-publickey" - SecretFieldPrivateKey = "ssh-privatekey" + DefaultSecretFieldPublicKey = "ssh-publickey" + DefaultSecretFieldPrivateKey = "ssh-privatekey" ) type SSHKeypairGenerator struct { @@ -36,7 +36,17 @@ func (sg SSHKeypairGenerator) generateData(instance *corev1.Secret) (reconcile.R return reconcile.Result{}, err } - err = GenerateSSHKeypairData(sg.log, length, regenerate, instance.Data) + privateKeyField, err := GetPrivateKeyFieldFromAnnotation(DefaultSecretFieldPrivateKey, instance.Annotations) + if err != nil { + return reconcile.Result{}, err + } + + publicKeyField, err := GetPublicKeyFieldFromAnnotation(DefaultSecretFieldPublicKey, instance.Annotations) + if err != nil { + return reconcile.Result{}, err + } + + err = GenerateSSHKeypairData(sg.log, length, privateKeyField, publicKeyField, regenerate, instance.Data) if err != nil { return reconcile.Result{RequeueAfter: time.Second * 30}, err } @@ -47,12 +57,12 @@ func (sg SSHKeypairGenerator) generateData(instance *corev1.Secret) (reconcile.R // generates ssh private and public key of given length // and writes the result to data. The public key is in authorized-keys format, // the private key is PEM encoded -func GenerateSSHKeypairData(logger logr.Logger, length string, regenerate bool, data map[string][]byte) error { - privateKey := data[SecretFieldPrivateKey] - publicKey := data[SecretFieldPublicKey] +func GenerateSSHKeypairData(logger logr.Logger, length string, privateKeyField string, publicKeyField string, regenerate bool, data map[string][]byte) error { + privateKey := data[privateKeyField] + publicKey := data[publicKeyField] if len(privateKey) > 0 && !regenerate { - return CheckAndRegenPublicKey(data, publicKey, privateKey) + return CheckAndRegenPublicKey(data, publicKey, privateKey, publicKeyField) } key, err := generateNewPrivateKey(length, logger) @@ -60,7 +70,7 @@ func GenerateSSHKeypairData(logger logr.Logger, length string, regenerate bool, return err } - return generateKeysHelper(key, data) + return generateKeysHelper(key, privateKeyField, publicKeyField, data) } // generateNewPrivateKey parses the given length and generates a matching private key @@ -77,7 +87,7 @@ func generateNewPrivateKey(length string, logger logr.Logger) (*rsa.PrivateKey, } // generateKeysHelper generates the public key from the given private key and stores the result in data -func generateKeysHelper(key *rsa.PrivateKey, data map[string][]byte) error { +func generateKeysHelper(key *rsa.PrivateKey, privateKeyField string, publicKeyField string, data map[string][]byte) error { privateKeyBytes := &bytes.Buffer{} err := pem.Encode( privateKeyBytes, @@ -92,8 +102,8 @@ func generateKeysHelper(key *rsa.PrivateKey, data map[string][]byte) error { return err } - data[SecretFieldPublicKey] = publicKeyBytes - data[SecretFieldPrivateKey] = privateKeyBytes.Bytes() + data[publicKeyField] = publicKeyBytes + data[privateKeyField] = privateKeyBytes.Bytes() return nil } @@ -122,7 +132,7 @@ func SSHPublicKeyForPrivateKey(privateKey *rsa.PrivateKey) ([]byte, error) { // CheckAndRegenPublicKey checks if the specified public key has length > 0 and regenerates it from the given private key // otherwise. The result is written into data -func CheckAndRegenPublicKey(data map[string][]byte, publicKey, privateKey []byte) error { +func CheckAndRegenPublicKey(data map[string][]byte, publicKey, privateKey []byte, publicKeyField string) error { if len(publicKey) > 0 { return nil } @@ -136,7 +146,7 @@ func CheckAndRegenPublicKey(data map[string][]byte, publicKey, privateKey []byte if err != nil { return err } - data[SecretFieldPublicKey] = publicKey + data[publicKeyField] = publicKey return nil } diff --git a/pkg/controller/secret/secret_ssh_test.go b/pkg/controller/secret/secret_ssh_test.go index 7dbd3e53..836aa810 100644 --- a/pkg/controller/secret/secret_ssh_test.go +++ b/pkg/controller/secret/secret_ssh_test.go @@ -44,7 +44,7 @@ func newSSHKeypairTestSecret(t *testing.T, extraAnnotations map[string]string, i var log logr.Logger if initialized { - err := secret.GenerateSSHKeypairData(log, strconv.Itoa(secret.SSHKeyLength()), true, s.Data) + err := secret.GenerateSSHKeypairData(log, strconv.Itoa(secret.SSHKeyLength()), secret.DefaultSecretFieldPrivateKey, secret.DefaultSecretFieldPublicKey, true, s.Data) if err != nil { t.Error(err, "could not generate new ssh keypair") } @@ -64,8 +64,8 @@ func verifySSHKeypairSecret(t *testing.T, in, out *corev1.Secret) { t.Errorf("secret has no %s annotation", secret.AnnotationSecretAutoGeneratedAt) } - publicKey := out.Data[secret.SecretFieldPublicKey] - privateKey := out.Data[secret.SecretFieldPrivateKey] + publicKey := out.Data[secret.DefaultSecretFieldPublicKey] + privateKey := out.Data[secret.DefaultSecretFieldPrivateKey] if len(privateKey) == 0 || len(publicKey) == 0 { t.Errorf("publicKey(%d) or privateKey(%d) have invalid length", len(publicKey), len(privateKey)) @@ -105,11 +105,11 @@ func verifySSHKeypairRegen(t *testing.T, in, out *corev1.Secret, regenDesired bo } t.Logf("checking if keys have been regenerated") - oldPublicKey := in.Data[secret.SecretFieldPublicKey] - oldPrivateKey := in.Data[secret.SecretFieldPrivateKey] + oldPublicKey := in.Data[secret.DefaultSecretFieldPublicKey] + oldPrivateKey := in.Data[secret.DefaultSecretFieldPrivateKey] - newPublicKey := out.Data[secret.SecretFieldPublicKey] - newPrivateKey := out.Data[secret.SecretFieldPrivateKey] + newPublicKey := out.Data[secret.DefaultSecretFieldPublicKey] + newPrivateKey := out.Data[secret.DefaultSecretFieldPrivateKey] equal := bytes.Equal(oldPublicKey, newPublicKey) if equal && regenDesired { @@ -184,7 +184,7 @@ func TestSSHKeypairLengthAnnotation(t *testing.T) { Namespace: in.Namespace}, out)) verifySSHKeypairSecret(t, in, out) - key, err := secret.PrivateKeyFromPEM(out.Data[secret.SecretFieldPrivateKey]) + key, err := secret.PrivateKeyFromPEM(out.Data[secret.DefaultSecretFieldPrivateKey]) if err != nil { t.Error(err, "generated private key could not be parsed") } @@ -195,6 +195,46 @@ func TestSSHKeypairLengthAnnotation(t *testing.T) { } } +func TestSSHKeypairKeyFieldAnnotations(t *testing.T) { + in := newSSHKeypairTestSecret(t, map[string]string{ + secret.AnnotationSSHPrivateKeyField: "sshPrivateKey", + secret.AnnotationSSHPublicKeyField: "sshPublicKey", + }, true) + require.NoError(t, mgr.GetClient().Create(context.TODO(), in)) + + doReconcile(t, in, false) + + out := &corev1.Secret{} + require.NoError(t, mgr.GetClient().Get(context.TODO(), types.NamespacedName{ + Name: in.Name, + Namespace: in.Namespace}, out)) + verifySSHKeypairSecret(t, in, out) + + privateKey, ok := out.Data["sshPrivateKey"] + if !ok { + t.Error("sshPrivateKey not in data") + } + + publicKey, ok := out.Data["sshPublicKey"] + if !ok { + t.Error("sshPublicKey not in data") + } + + key, err := secret.PrivateKeyFromPEM(privateKey) + if err != nil { + t.Error(err, "generated private key could not be parsed") + } + + pub, err := secret.SSHPublicKeyForPrivateKey(key) + if err != nil { + t.Error(err, "generated public key could not be parsed") + } + + if !bytes.Equal(publicKey, pub) { + t.Error("publicKey doesn't match private key") + } +} + func TestSSHKeypairLengthDefault(t *testing.T) { in := newSSHKeypairTestSecret(t, map[string]string{ secret.AnnotationSecretRegenerate: "true", @@ -209,7 +249,7 @@ func TestSSHKeypairLengthDefault(t *testing.T) { Namespace: in.Namespace}, out)) verifySSHKeypairSecret(t, in, out) - key, err := secret.PrivateKeyFromPEM(out.Data[secret.SecretFieldPrivateKey]) + key, err := secret.PrivateKeyFromPEM(out.Data[secret.DefaultSecretFieldPrivateKey]) if err != nil { t.Error(err, "generated private key could not be parsed") } diff --git a/pkg/controller/secret/sshkeypair_controller_test.go b/pkg/controller/secret/sshkeypair_controller_test.go index 0a84b592..e3a31ca6 100644 --- a/pkg/controller/secret/sshkeypair_controller_test.go +++ b/pkg/controller/secret/sshkeypair_controller_test.go @@ -20,6 +20,11 @@ import ( "github.com/mittwald/kubernetes-secret-generator/pkg/controller/secret" ) +const ( + TestSecretFieldPrivateKey = "sshPrivateKey" + TestSecretFieldPublicKey = "sshPublicKey" +) + // newSSHKeyPairTestCR returns a SSHKeyPair custom resource. If name is set to "", a uuid will be generated func newSSHKeyPairTestCR(sshSpec v1alpha1.SSHKeyPairSpec, name string) *v1alpha1.SSHKeyPair { if name == "" { @@ -60,8 +65,8 @@ func verifySSHSecretFromCR(t *testing.T, in *v1alpha1.SSHKeyPair, out *corev1.Se t.Error("generated secret not referenced in CR status") } - publicKey := out.Data[secret.SecretFieldPublicKey] - privateKey := out.Data[secret.SecretFieldPrivateKey] + publicKey := out.Data[in.GetPublicKeyField()] + privateKey := out.Data[in.GetPrivateKeyField()] // check if keys have valid length if len(privateKey) == 0 || len(publicKey) == 0 { @@ -149,8 +154,8 @@ func TestControllerRegenerateSSHSecret(t *testing.T) { Namespace: in.Namespace}, out)) verifySSHSecretFromCR(t, in, out) - oldPrivateKey := string(out.Data[secret.SecretFieldPrivateKey]) - oldPublicKey := string(out.Data[secret.SecretFieldPublicKey]) + oldPrivateKey := string(out.Data[secret.DefaultSecretFieldPrivateKey]) + oldPublicKey := string(out.Data[secret.DefaultSecretFieldPublicKey]) in.Spec.Length = "35" @@ -161,8 +166,8 @@ func TestControllerRegenerateSSHSecret(t *testing.T) { Name: in.Name, Namespace: in.Namespace}, outNew)) - newPrivateKey := string(outNew.Data[secret.SecretFieldPrivateKey]) - newPublicKey := string(outNew.Data[secret.SecretFieldPublicKey]) + newPrivateKey := string(outNew.Data[secret.DefaultSecretFieldPrivateKey]) + newPublicKey := string(outNew.Data[secret.DefaultSecretFieldPublicKey]) if oldPrivateKey == newPrivateKey { t.Errorf("secret has not been updated") @@ -191,8 +196,8 @@ func TestControllerDoNotRegenerateSecret(t *testing.T) { Namespace: in.Namespace}, out)) verifySSHSecretFromCR(t, in, out) - oldPrivateKey := string(out.Data[secret.SecretFieldPrivateKey]) - oldPublicKey := string(out.Data[secret.SecretFieldPublicKey]) + oldPrivateKey := string(out.Data[secret.DefaultSecretFieldPrivateKey]) + oldPublicKey := string(out.Data[secret.DefaultSecretFieldPublicKey]) in.Spec.Length = "35" @@ -203,8 +208,8 @@ func TestControllerDoNotRegenerateSecret(t *testing.T) { Name: in.Name, Namespace: in.Namespace}, outNew)) - newPrivateKey := string(outNew.Data[secret.SecretFieldPrivateKey]) - newPublicKey := string(outNew.Data[secret.SecretFieldPublicKey]) + newPrivateKey := string(outNew.Data[secret.DefaultSecretFieldPrivateKey]) + newPublicKey := string(outNew.Data[secret.DefaultSecretFieldPublicKey]) if oldPrivateKey != newPrivateKey { t.Errorf("secret has been updated") @@ -233,10 +238,10 @@ func TestControllerDoNotRegenerateSSHSecretFixMissingPublicKey(t *testing.T) { Namespace: in.Namespace}, out)) verifySSHSecretFromCR(t, in, out) - oldPrivateKey := string(out.Data[secret.SecretFieldPrivateKey]) - oldPublicKey := string(out.Data[secret.SecretFieldPublicKey]) + oldPrivateKey := string(out.Data[secret.DefaultSecretFieldPrivateKey]) + oldPublicKey := string(out.Data[secret.DefaultSecretFieldPublicKey]) - out.Data[secret.SecretFieldPublicKey] = []byte{} + out.Data[secret.DefaultSecretFieldPublicKey] = []byte{} require.NoError(t, mgr.GetClient().Update(context.TODO(), out)) @@ -249,8 +254,8 @@ func TestControllerDoNotRegenerateSSHSecretFixMissingPublicKey(t *testing.T) { Name: in.Name, Namespace: in.Namespace}, outNew)) - newPrivateKey := string(outNew.Data[secret.SecretFieldPrivateKey]) - newPublicKey := string(outNew.Data[secret.SecretFieldPublicKey]) + newPrivateKey := string(outNew.Data[secret.DefaultSecretFieldPrivateKey]) + newPublicKey := string(outNew.Data[secret.DefaultSecretFieldPublicKey]) if oldPrivateKey != newPrivateKey { t.Errorf("secret has been updated") @@ -264,11 +269,13 @@ func TestControllerDoNotRegenerateSSHSecretFixMissingPublicKey(t *testing.T) { func TestControllerRegeneratePublicKey(t *testing.T) { data := make(map[string][]byte) var log logr.Logger - err := secret.GenerateSSHKeypairData(log, "40", true, data) + err := secret.GenerateSSHKeypairData(log, "40", TestSecretFieldPrivateKey, TestSecretFieldPublicKey, true, data) require.NoError(t, err) testSpec := v1alpha1.SSHKeyPairSpec{ Length: "40", - PrivateKey: string(data[secret.SecretFieldPrivateKey]), + PrivateKey: string(data[TestSecretFieldPrivateKey]), + PrivateKeyField: TestSecretFieldPrivateKey, + PublicKeyField: TestSecretFieldPublicKey, Type: string(corev1.SecretTypeOpaque), Data: map[string]string{}, ForceRegenerate: true, @@ -284,9 +291,9 @@ func TestControllerRegeneratePublicKey(t *testing.T) { Namespace: in.Namespace}, out)) verifySSHSecretFromCR(t, in, out) - privateKey := string(out.Data[secret.SecretFieldPrivateKey]) + privateKey := string(out.Data[TestSecretFieldPrivateKey]) - if privateKey != string(data[secret.SecretFieldPrivateKey]) { + if privateKey != string(data[TestSecretFieldPrivateKey]) { t.Errorf("Private key was regenerated") } } diff --git a/pkg/controller/secret/types.go b/pkg/controller/secret/types.go index 832841a5..55879954 100644 --- a/pkg/controller/secret/types.go +++ b/pkg/controller/secret/types.go @@ -19,6 +19,8 @@ const ( AnnotationSecretLength = "secret-generator.v1.mittwald.de/length" AnnotationBasicAuthUsername = "secret-generator.v1.mittwald.de/basic-auth-username" AnnotationSecretEncoding = "secret-generator.v1.mittwald.de/encoding" + AnnotationSSHPrivateKeyField = "secret-generator.v1.mittwald.de/private-key-field" + AnnotationSSHPublicKeyField = "secret-generator.v1.mittwald.de/public-key-field" ) type Type string