Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to set SSH private/public key field #102

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions deploy/crds/secretgenerator.mittwald.de_sshkeypairs_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions pkg/apis/secretgenerator/v1alpha1/sshkeypair_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}
Expand Down
14 changes: 9 additions & 5 deletions pkg/controller/crd/sshkeypair/sshkeypair_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
14 changes: 14 additions & 0 deletions pkg/controller/secret/secret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
36 changes: 23 additions & 13 deletions pkg/controller/secret/secret_ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import (
)

const (
SecretFieldPublicKey = "ssh-publickey"
SecretFieldPrivateKey = "ssh-privatekey"
DefaultSecretFieldPublicKey = "ssh-publickey"
DefaultSecretFieldPrivateKey = "ssh-privatekey"
)

type SSHKeypairGenerator struct {
Expand All @@ -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
}
Expand All @@ -47,20 +57,20 @@ 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)
if err != nil {
return err
}

return generateKeysHelper(key, data)
return generateKeysHelper(key, privateKeyField, publicKeyField, data)
}

// generateNewPrivateKey parses the given length and generates a matching private key
Expand All @@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
58 changes: 49 additions & 9 deletions pkg/controller/secret/secret_ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
Expand All @@ -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",
Expand All @@ -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")
}
Expand Down
Loading