lockbox.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  28. "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
  29. "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
  30. )
  31. const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
  32. const iamTokenCleanupDelay = 1 * time.Hour // specifies how often cleanUpIamTokenMap() is performed
  33. var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
  34. type iamTokenKey struct {
  35. authorizedKeyID string
  36. serviceAccountID string
  37. privateKeyHash string
  38. }
  39. // lockboxProvider is a provider for Yandex Lockbox.
  40. type lockboxProvider struct {
  41. yandexCloudCreator client.YandexCloudCreator
  42. lockboxClientMap map[string]client.LockboxClient // apiEndpoint -> LockboxClient
  43. lockboxClientMapMutex sync.Mutex
  44. iamTokenMap map[iamTokenKey]*client.IamToken
  45. iamTokenMapMutex sync.Mutex
  46. }
  47. func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
  48. return &lockboxProvider{
  49. yandexCloudCreator: yandexCloudCreator,
  50. lockboxClientMap: make(map[string]client.LockboxClient),
  51. iamTokenMap: make(map[iamTokenKey]*client.IamToken),
  52. }
  53. }
  54. // NewClient constructs a Yandex Lockbox Provider.
  55. func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
  56. storeSpec := store.GetSpec()
  57. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
  58. return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
  59. }
  60. storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
  61. authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
  62. if authorizedKeySecretName == "" {
  63. return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
  64. }
  65. objectKey := types.NamespacedName{
  66. Name: authorizedKeySecretName,
  67. Namespace: namespace,
  68. }
  69. // only ClusterStore is allowed to set namespace (and then it's required)
  70. if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
  71. if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
  72. return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
  73. }
  74. objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
  75. }
  76. authorizedKeySecret := &corev1.Secret{}
  77. err := kube.Get(ctx, objectKey, authorizedKeySecret)
  78. if err != nil {
  79. return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
  80. }
  81. authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
  82. if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
  83. return nil, fmt.Errorf("missing AuthorizedKey")
  84. }
  85. var authorizedKey iamkey.Key
  86. err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
  87. if err != nil {
  88. return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
  89. }
  90. var caCertificateData []byte
  91. if storeSpecYandexLockbox.CAProvider != nil {
  92. certObjectKey := types.NamespacedName{
  93. Name: storeSpecYandexLockbox.CAProvider.Certificate.Name,
  94. Namespace: namespace,
  95. }
  96. if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
  97. if storeSpecYandexLockbox.CAProvider.Certificate.Namespace == nil {
  98. return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
  99. }
  100. certObjectKey.Namespace = *storeSpecYandexLockbox.CAProvider.Certificate.Namespace
  101. }
  102. caCertificateSecret := &corev1.Secret{}
  103. err := kube.Get(ctx, certObjectKey, caCertificateSecret)
  104. if err != nil {
  105. return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
  106. }
  107. caCertificateData = caCertificateSecret.Data[storeSpecYandexLockbox.CAProvider.Certificate.Key]
  108. if (caCertificateData == nil) || (len(caCertificateData) == 0) {
  109. return nil, fmt.Errorf("missing CA Certificate")
  110. }
  111. }
  112. lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey, caCertificateData)
  113. if err != nil {
  114. return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
  115. }
  116. iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
  117. if err != nil {
  118. return nil, fmt.Errorf("failed to create IAM token: %w", err)
  119. }
  120. return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
  121. }
  122. func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
  123. p.lockboxClientMapMutex.Lock()
  124. defer p.lockboxClientMapMutex.Unlock()
  125. if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
  126. log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
  127. lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey, caCertificate)
  128. if err != nil {
  129. return nil, err
  130. }
  131. p.lockboxClientMap[apiEndpoint] = lockboxClient
  132. }
  133. return p.lockboxClientMap[apiEndpoint], nil
  134. }
  135. func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
  136. p.iamTokenMapMutex.Lock()
  137. defer p.iamTokenMapMutex.Unlock()
  138. iamTokenKey := buildIamTokenKey(authorizedKey)
  139. if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
  140. log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
  141. iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
  142. if err != nil {
  143. return nil, err
  144. }
  145. log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
  146. p.iamTokenMap[iamTokenKey] = iamToken
  147. }
  148. return p.iamTokenMap[iamTokenKey], nil
  149. }
  150. func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
  151. now := p.yandexCloudCreator.Now()
  152. return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
  153. }
  154. func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
  155. privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
  156. return iamTokenKey{
  157. authorizedKey.GetId(),
  158. authorizedKey.GetServiceAccountId(),
  159. hex.EncodeToString(privateKeyHash[:]),
  160. }
  161. }
  162. // Used for testing.
  163. func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
  164. p.iamTokenMapMutex.Lock()
  165. defer p.iamTokenMapMutex.Unlock()
  166. _, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
  167. return ok
  168. }
  169. func (p *lockboxProvider) cleanUpIamTokenMap() {
  170. p.iamTokenMapMutex.Lock()
  171. defer p.iamTokenMapMutex.Unlock()
  172. for key, value := range p.iamTokenMap {
  173. if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
  174. log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
  175. delete(p.iamTokenMap, key)
  176. }
  177. }
  178. }
  179. func (p *lockboxProvider) ValidateStore(store esv1beta1.GenericStore) error {
  180. return nil
  181. }
  182. // lockboxSecretsClient is a secrets client for Yandex Lockbox.
  183. type lockboxSecretsClient struct {
  184. lockboxClient client.LockboxClient
  185. iamToken string
  186. }
  187. // Empty GetAllSecrets.
  188. func (c *lockboxSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  189. // TO be implemented
  190. return nil, fmt.Errorf("GetAllSecrets not implemented")
  191. }
  192. // GetSecret returns a single secret from the provider.
  193. func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  194. entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
  195. if err != nil {
  196. return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
  197. }
  198. if ref.Property == "" {
  199. keyToValue := make(map[string]interface{}, len(entries))
  200. for _, entry := range entries {
  201. value, err := getValueAsIs(entry)
  202. if err != nil {
  203. return nil, err
  204. }
  205. keyToValue[entry.Key] = value
  206. }
  207. out, err := json.Marshal(keyToValue)
  208. if err != nil {
  209. return nil, fmt.Errorf("failed to marshal secret: %w", err)
  210. }
  211. return out, nil
  212. }
  213. entry, err := findEntryByKey(entries, ref.Property)
  214. if err != nil {
  215. return nil, err
  216. }
  217. return getValueAsBinary(entry)
  218. }
  219. // GetSecretMap returns multiple k/v pairs from the provider.
  220. func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  221. entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
  222. if err != nil {
  223. return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
  224. }
  225. secretMap := make(map[string][]byte, len(entries))
  226. for _, entry := range entries {
  227. value, err := getValueAsBinary(entry)
  228. if err != nil {
  229. return nil, err
  230. }
  231. secretMap[entry.Key] = value
  232. }
  233. return secretMap, nil
  234. }
  235. func (c *lockboxSecretsClient) Close(ctx context.Context) error {
  236. return nil
  237. }
  238. func (c *lockboxSecretsClient) Validate() error {
  239. return nil
  240. }
  241. func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
  242. switch entry.Value.(type) {
  243. case *lockbox.Payload_Entry_TextValue:
  244. return entry.GetTextValue(), nil
  245. case *lockbox.Payload_Entry_BinaryValue:
  246. return entry.GetBinaryValue(), nil
  247. default:
  248. return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
  249. }
  250. }
  251. func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
  252. switch entry.Value.(type) {
  253. case *lockbox.Payload_Entry_TextValue:
  254. return []byte(entry.GetTextValue()), nil
  255. case *lockbox.Payload_Entry_BinaryValue:
  256. return entry.GetBinaryValue(), nil
  257. default:
  258. return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
  259. }
  260. }
  261. func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
  262. for i := range entries {
  263. if entries[i].Key == key {
  264. return entries[i], nil
  265. }
  266. }
  267. return nil, fmt.Errorf("payload entry with key '%s' not found", key)
  268. }
  269. func init() {
  270. lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
  271. go func() {
  272. for {
  273. time.Sleep(iamTokenCleanupDelay)
  274. lockboxProvider.cleanUpIamTokenMap()
  275. }
  276. }()
  277. esv1beta1.Register(
  278. lockboxProvider,
  279. &esv1beta1.SecretStoreProvider{
  280. YandexLockbox: &esv1beta1.YandexLockboxProvider{},
  281. },
  282. )
  283. }