GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sshkeyring.go 5.0KB

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