Browse Source

SSH keyring backed by k8s secret resource

Adam Harrison 2 years ago
parent
commit
84cdfb902b
2 changed files with 134 additions and 0 deletions
  1. 125
    0
      cluster/kubernetes/sshkeyring.go
  2. 9
    0
      ssh/keyring.go

+ 125
- 0
cluster/kubernetes/sshkeyring.go View File

@@ -0,0 +1,125 @@
1
+package kubernetes
2
+
3
+import (
4
+	"encoding/base64"
5
+	"encoding/json"
6
+	"os"
7
+	"path"
8
+	"sync"
9
+
10
+	"k8s.io/client-go/1.5/kubernetes/typed/core/v1"
11
+	"k8s.io/client-go/1.5/pkg/api"
12
+
13
+	"github.com/weaveworks/flux/ssh"
14
+)
15
+
16
+const (
17
+	// The private key file must have these permissions, or ssh will refuse to
18
+	// use it
19
+	privateKeyFileMode = os.FileMode(0400)
20
+)
21
+
22
+// SSHKeyRingConfig is used to configure the keyring with key generation
23
+// options and the parameters of its backing kubernetes secret resource.
24
+// SecretVolumeMountPath must be mounted RW for regenerate() to work, and to
25
+// set the privateKeyFileMode on the identity secret file.
26
+type SSHKeyRingConfig struct {
27
+	SecretAPI             v1.SecretInterface
28
+	SecretName            string
29
+	SecretVolumeMountPath string // e.g. "/etc/fluxd/ssh"
30
+	SecretDataKey         string // e.g. "identity"
31
+	KeyBits               ssh.OptionalValue
32
+	KeyType               ssh.OptionalValue
33
+}
34
+
35
+type sshKeyRing struct {
36
+	sync.RWMutex
37
+	SSHKeyRingConfig
38
+	publicKey      ssh.PublicKey
39
+	privateKeyPath string
40
+}
41
+
42
+// NewSSHKeyRing constructs an sshKeyRing backed by a kubernetes secret
43
+// resource. The keyring is initialised with the key that was previously stored
44
+// in the secret (either by regenerate() or an administrator), or a freshly
45
+// generated key if none was found.
46
+func NewSSHKeyRing(config SSHKeyRingConfig) (*sshKeyRing, error) {
47
+	skr := &sshKeyRing{SSHKeyRingConfig: config}
48
+	privateKeyPath := path.Join(skr.SecretVolumeMountPath, skr.SecretDataKey)
49
+
50
+	fileInfo, err := os.Stat(privateKeyPath)
51
+	switch {
52
+	case os.IsNotExist(err):
53
+		if err := skr.Regenerate(); err != nil {
54
+			return nil, err
55
+		}
56
+	case err != nil:
57
+		return nil, err
58
+	case fileInfo.Mode() != privateKeyFileMode:
59
+		if err := os.Chmod(privateKeyPath, privateKeyFileMode); err != nil {
60
+			return nil, err
61
+		}
62
+		fallthrough
63
+	default:
64
+		publicKey, err := ssh.ExtractPublicKey(privateKeyPath)
65
+		if err != nil {
66
+			return nil, err
67
+		}
68
+		skr.privateKeyPath = privateKeyPath
69
+		skr.publicKey = publicKey
70
+	}
71
+
72
+	return skr, nil
73
+}
74
+
75
+// KeyPair returns the current public key and the path to its corresponding
76
+// private key. The private key file is guaranteed to exist for the lifetime of
77
+// the process, however as the returned pair can be discarded from the keyring
78
+// at any time by use of the regenerate() method it is inadvisable to cache the
79
+// results for long periods; instead request the key pair from the ring
80
+// immediately prior to each use.
81
+func (skr *sshKeyRing) KeyPair() (publicKey ssh.PublicKey, privateKeyPath string) {
82
+	skr.RLock()
83
+	defer skr.RUnlock()
84
+	return skr.publicKey, skr.privateKeyPath
85
+}
86
+
87
+// regenerate creates a new keypair in the configured SecretVolumeMountPath and
88
+// updates the kubernetes secret resource with the private key so that it will
89
+// be available to the keyring after restart. If this operation is successful
90
+// the keyPair() method will return the new pair; if it fails for any reason,
91
+// keyPair() will continue to return the existing pair.
92
+//
93
+// BUG(awh) Updating the kubernetes secret should be done via an ephemeral
94
+// external executable invoked with coredumps disabled and using
95
+// syscall.Mlockall(MCL_FUTURE) in conjunction with an appropriate ulimit to
96
+// ensure the private key isn't unintentionally written to persistent storage.
97
+func (skr *sshKeyRing) Regenerate() error {
98
+	privateKeyPath, privateKey, publicKey, err := ssh.KeyGen(skr.KeyBits, skr.KeyType, skr.SecretVolumeMountPath)
99
+	if err != nil {
100
+		return err
101
+	}
102
+
103
+	patch := map[string]map[string]string{
104
+		"data": map[string]string{
105
+			"identity": base64.StdEncoding.EncodeToString(privateKey),
106
+		},
107
+	}
108
+
109
+	jsonPatch, err := json.Marshal(patch)
110
+	if err != nil {
111
+		return err
112
+	}
113
+
114
+	_, err = skr.SecretAPI.Patch(skr.SecretName, api.StrategicMergePatchType, jsonPatch)
115
+	if err != nil {
116
+		return err
117
+	}
118
+
119
+	skr.Lock()
120
+	skr.privateKeyPath = privateKeyPath
121
+	skr.publicKey = publicKey
122
+	skr.Unlock()
123
+
124
+	return nil
125
+}

+ 9
- 0
ssh/keyring.go View File

@@ -0,0 +1,9 @@
1
+package ssh
2
+
3
+// KeyRing is an abstraction providing access to a managed SSH key pair. Whilst
4
+// the public half is available in byte form, the private half is left on the
5
+// filesystem to avoid memory management issues.
6
+type KeyRing interface {
7
+	KeyPair() (publicKey PublicKey, privateKeyPath string)
8
+	Regenerate() error
9
+}

Loading…
Cancel
Save