lockbox.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package lockbox
  13. import (
  14. "context"
  15. "crypto/sha256"
  16. "encoding/hex"
  17. "encoding/json"
  18. "fmt"
  19. "sync"
  20. "time"
  21. "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
  22. "github.com/yandex-cloud/go-sdk/iamkey"
  23. corev1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/types"
  25. ctrl "sigs.k8s.io/controller-runtime"
  26. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  27. esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  28. "github.com/external-secrets/external-secrets/pkg/provider"
  29. "github.com/external-secrets/external-secrets/pkg/provider/schema"
  30. "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
  31. "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
  32. )
  33. const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
  34. const iamTokenCleanupDelay = 1 * time.Hour // specifies how often cleanUpIamTokenMap() is performed
  35. var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
  36. type iamTokenKey struct {
  37. authorizedKeyID string
  38. serviceAccountID string
  39. privateKeyHash string
  40. }
  41. // lockboxProvider is a provider for Yandex Lockbox.
  42. type lockboxProvider struct {
  43. yandexCloudCreator client.YandexCloudCreator
  44. lockboxClientMap map[string]client.LockboxClient // apiEndpoint -> LockboxClient
  45. lockboxClientMapMutex sync.Mutex
  46. iamTokenMap map[iamTokenKey]*client.IamToken
  47. iamTokenMapMutex sync.Mutex
  48. }
  49. func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
  50. return &lockboxProvider{
  51. yandexCloudCreator: yandexCloudCreator,
  52. lockboxClientMap: make(map[string]client.LockboxClient),
  53. iamTokenMap: make(map[iamTokenKey]*client.IamToken),
  54. }
  55. }
  56. // NewClient constructs a Yandex Lockbox Provider.
  57. func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
  58. storeSpec := store.GetSpec()
  59. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
  60. return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
  61. }
  62. storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
  63. authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
  64. if authorizedKeySecretName == "" {
  65. return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
  66. }
  67. objectKey := types.NamespacedName{
  68. Name: authorizedKeySecretName,
  69. Namespace: namespace,
  70. }
  71. // only ClusterStore is allowed to set namespace (and then it's required)
  72. if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
  73. if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
  74. return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
  75. }
  76. objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
  77. }
  78. authorizedKeySecret := &corev1.Secret{}
  79. err := kube.Get(ctx, objectKey, authorizedKeySecret)
  80. if err != nil {
  81. return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
  82. }
  83. authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
  84. if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
  85. return nil, fmt.Errorf("missing AuthorizedKey")
  86. }
  87. var authorizedKey iamkey.Key
  88. err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
  89. if err != nil {
  90. return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
  91. }
  92. lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
  93. if err != nil {
  94. return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
  95. }
  96. iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
  97. if err != nil {
  98. return nil, fmt.Errorf("failed to create IAM token: %w", err)
  99. }
  100. return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
  101. }
  102. func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
  103. p.lockboxClientMapMutex.Lock()
  104. defer p.lockboxClientMapMutex.Unlock()
  105. if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
  106. log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
  107. lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey)
  108. if err != nil {
  109. return nil, err
  110. }
  111. p.lockboxClientMap[apiEndpoint] = lockboxClient
  112. }
  113. return p.lockboxClientMap[apiEndpoint], nil
  114. }
  115. func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
  116. p.iamTokenMapMutex.Lock()
  117. defer p.iamTokenMapMutex.Unlock()
  118. iamTokenKey := buildIamTokenKey(authorizedKey)
  119. if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
  120. log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
  121. iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
  122. if err != nil {
  123. return nil, err
  124. }
  125. log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
  126. p.iamTokenMap[iamTokenKey] = iamToken
  127. }
  128. return p.iamTokenMap[iamTokenKey], nil
  129. }
  130. func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
  131. now := p.yandexCloudCreator.Now()
  132. return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
  133. }
  134. func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
  135. privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
  136. return iamTokenKey{
  137. authorizedKey.GetId(),
  138. authorizedKey.GetServiceAccountId(),
  139. hex.EncodeToString(privateKeyHash[:]),
  140. }
  141. }
  142. // Used for testing.
  143. func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
  144. p.iamTokenMapMutex.Lock()
  145. defer p.iamTokenMapMutex.Unlock()
  146. _, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
  147. return ok
  148. }
  149. func (p *lockboxProvider) cleanUpIamTokenMap() {
  150. p.iamTokenMapMutex.Lock()
  151. defer p.iamTokenMapMutex.Unlock()
  152. for key, value := range p.iamTokenMap {
  153. if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
  154. log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
  155. delete(p.iamTokenMap, key)
  156. }
  157. }
  158. }
  159. // lockboxSecretsClient is a secrets client for Yandex Lockbox.
  160. type lockboxSecretsClient struct {
  161. lockboxClient client.LockboxClient
  162. iamToken string
  163. }
  164. // GetSecret returns a single secret from the provider.
  165. func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
  166. entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
  167. if err != nil {
  168. return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
  169. }
  170. if ref.Property == "" {
  171. keyToValue := make(map[string]interface{}, len(entries))
  172. for _, entry := range entries {
  173. value, err := getValueAsIs(entry)
  174. if err != nil {
  175. return nil, err
  176. }
  177. keyToValue[entry.Key] = value
  178. }
  179. out, err := json.Marshal(keyToValue)
  180. if err != nil {
  181. return nil, fmt.Errorf("failed to marshal secret: %w", err)
  182. }
  183. return out, nil
  184. }
  185. entry, err := findEntryByKey(entries, ref.Property)
  186. if err != nil {
  187. return nil, err
  188. }
  189. return getValueAsBinary(entry)
  190. }
  191. // GetSecretMap returns multiple k/v pairs from the provider.
  192. func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  193. entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
  194. if err != nil {
  195. return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
  196. }
  197. secretMap := make(map[string][]byte, len(entries))
  198. for _, entry := range entries {
  199. value, err := getValueAsBinary(entry)
  200. if err != nil {
  201. return nil, err
  202. }
  203. secretMap[entry.Key] = value
  204. }
  205. return secretMap, nil
  206. }
  207. func (c *lockboxSecretsClient) Close(ctx context.Context) error {
  208. return nil
  209. }
  210. func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
  211. switch entry.Value.(type) {
  212. case *lockbox.Payload_Entry_TextValue:
  213. return entry.GetTextValue(), nil
  214. case *lockbox.Payload_Entry_BinaryValue:
  215. return entry.GetBinaryValue(), nil
  216. default:
  217. return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
  218. }
  219. }
  220. func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
  221. switch entry.Value.(type) {
  222. case *lockbox.Payload_Entry_TextValue:
  223. return []byte(entry.GetTextValue()), nil
  224. case *lockbox.Payload_Entry_BinaryValue:
  225. return entry.GetBinaryValue(), nil
  226. default:
  227. return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
  228. }
  229. }
  230. func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
  231. for i := range entries {
  232. if entries[i].Key == key {
  233. return entries[i], nil
  234. }
  235. }
  236. return nil, fmt.Errorf("payload entry with key '%s' not found", key)
  237. }
  238. func init() {
  239. lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
  240. go func() {
  241. for {
  242. time.Sleep(iamTokenCleanupDelay)
  243. lockboxProvider.cleanUpIamTokenMap()
  244. }
  245. }()
  246. schema.Register(
  247. lockboxProvider,
  248. &esv1alpha1.SecretStoreProvider{
  249. YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
  250. },
  251. )
  252. }