lockbox.go 11 KB

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