provider.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package infisical
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "github.com/external-secrets/external-secrets/pkg/metrics"
  19. "github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
  20. infisicalSdk "github.com/infisical/go-sdk"
  21. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  22. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  23. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  25. "github.com/external-secrets/external-secrets/pkg/esutils"
  26. "github.com/external-secrets/external-secrets/pkg/esutils/resolvers"
  27. )
  28. const (
  29. machineIdentityLoginViaUniversalAuth = "MachineIdentityLoginViaUniversalAuth"
  30. machineIdentityLoginViaAzureAuth = "MachineIdentityLoginViaAzureAuth"
  31. machineIdentityLoginViaGCPIDTokenAuth = "MachineIdentityLoginViaGcpIdTokenAuth"
  32. machineIdentityLoginViaGcpServiceAccountAuth = "MachineIdentityLoginViaGcpServiceAccountAuth"
  33. machineIdentityLoginViaJwtAuth = "MachineIdentityLoginViaJwtAuth"
  34. machineIdentityLoginViaLdapAuth = "MachineIdentityLoginViaLdapAuth"
  35. machineIdentityLoginViaOciAuth = "MachineIdentityLoginViaOciAuth"
  36. machineIdentityLoginViaKubernetesAuth = "MachineIdentityLoginViaKubernetesAuth"
  37. machineIdentityLoginViaAwsAuth = "MachineIdentityLoginViaAwsAuth"
  38. machineIdentityLoginViaTokenAuth = "MachineIdentityLoginViaTokenAuth"
  39. revokeAccessToken = "RevokeAccessToken"
  40. )
  41. const errSecretDataFormat = "failed to get secret data identityId %w"
  42. // Provider implements the Infisical external secrets provider.
  43. type Provider struct {
  44. cancelSdkClient context.CancelFunc
  45. sdkClient infisicalSdk.InfisicalClientInterface
  46. apiScope *ClientScope
  47. authMethod string
  48. }
  49. // ClientScope represents the scope configuration for an Infisical client.
  50. type ClientScope struct {
  51. EnvironmentSlug string
  52. ProjectSlug string
  53. Recursive bool
  54. SecretPath string
  55. ExpandSecretReferences bool
  56. }
  57. // https://github.com/external-secrets/external-secrets/issues/644
  58. var _ esv1.SecretsClient = &Provider{}
  59. var _ esv1.Provider = &Provider{}
  60. func init() {
  61. esv1.Register(&Provider{}, &esv1.SecretStoreProvider{
  62. Infisical: &esv1.InfisicalProvider{},
  63. }, esv1.MaintenanceStatusMaintained)
  64. }
  65. // Capabilities returns the provider's supported capabilities.
  66. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  67. return esv1.SecretStoreReadOnly
  68. }
  69. func performUniversalAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  70. universalAuthCredentials := infisicalSpec.Auth.UniversalAuthCredentials
  71. clientID, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientID)
  72. if err != nil {
  73. return err
  74. }
  75. clientSecret, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientSecret)
  76. if err != nil {
  77. return err
  78. }
  79. _, err = sdkClient.Auth().UniversalAuthLogin(clientID, clientSecret)
  80. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaUniversalAuth, err)
  81. if err != nil {
  82. return fmt.Errorf("failed to authenticate via universal auth %w", err)
  83. }
  84. return nil
  85. }
  86. func performAzureAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  87. azureAuthCredentials := infisicalSpec.Auth.AzureAuthCredentials
  88. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, azureAuthCredentials.IdentityID)
  89. if err != nil {
  90. return fmt.Errorf("failed to get secret data id %w", err)
  91. }
  92. resource := ""
  93. if azureAuthCredentials.Resource.Name != "" {
  94. resource, err = GetStoreSecretData(ctx, store, kube, namespace, azureAuthCredentials.Resource)
  95. if err != nil {
  96. return fmt.Errorf("failed to get secret data resource %w", err)
  97. }
  98. }
  99. _, err = sdkClient.Auth().AzureAuthLogin(identityID, resource)
  100. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaAzureAuth, err)
  101. if err != nil {
  102. return fmt.Errorf("failed to authenticate via azure auth %w", err)
  103. }
  104. return nil
  105. }
  106. func performGcpIDTokenAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  107. gcpIDTokenAuthCredentials := infisicalSpec.Auth.GcpIDTokenAuthCredentials
  108. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, gcpIDTokenAuthCredentials.IdentityID)
  109. if err != nil {
  110. return fmt.Errorf(errSecretDataFormat, err)
  111. }
  112. _, err = sdkClient.Auth().GcpIdTokenAuthLogin(identityID)
  113. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaGCPIDTokenAuth, err)
  114. if err != nil {
  115. return fmt.Errorf("failed to authenticate via gcp id token auth %w", err)
  116. }
  117. return nil
  118. }
  119. func performGcpIamAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  120. gcpIamAuthCredentials := infisicalSpec.Auth.GcpIamAuthCredentials
  121. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, gcpIamAuthCredentials.IdentityID)
  122. if err != nil {
  123. return fmt.Errorf(errSecretDataFormat, err)
  124. }
  125. serviceAccountKeyFilePath, err := GetStoreSecretData(ctx, store, kube, namespace, gcpIamAuthCredentials.ServiceAccountKeyFilePath)
  126. if err != nil {
  127. return fmt.Errorf("failed to get secret data serviceAccountKeyFilePath %w", err)
  128. }
  129. _, err = sdkClient.Auth().GcpIamAuthLogin(identityID, serviceAccountKeyFilePath)
  130. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaGcpServiceAccountAuth, err)
  131. if err != nil {
  132. return fmt.Errorf("failed to authenticate via gcp iam auth %w", err)
  133. }
  134. return nil
  135. }
  136. func performJwtAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  137. jwtAuthCredentials := infisicalSpec.Auth.JwtAuthCredentials
  138. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, jwtAuthCredentials.IdentityID)
  139. if err != nil {
  140. return fmt.Errorf(errSecretDataFormat, err)
  141. }
  142. jwt, err := GetStoreSecretData(ctx, store, kube, namespace, jwtAuthCredentials.JWT)
  143. if err != nil {
  144. return fmt.Errorf("failed to get secret data jwt %w", err)
  145. }
  146. _, err = sdkClient.Auth().JwtAuthLogin(identityID, jwt)
  147. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaJwtAuth, err)
  148. if err != nil {
  149. return fmt.Errorf("failed to authenticate via jwt auth %w", err)
  150. }
  151. return nil
  152. }
  153. func performLdapAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  154. ldapAuthCredentials := infisicalSpec.Auth.LdapAuthCredentials
  155. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, ldapAuthCredentials.IdentityID)
  156. if err != nil {
  157. return fmt.Errorf(errSecretDataFormat, err)
  158. }
  159. ldapPassword, err := GetStoreSecretData(ctx, store, kube, namespace, ldapAuthCredentials.LDAPPassword)
  160. if err != nil {
  161. return fmt.Errorf("failed to get secret data ldapPassword %w", err)
  162. }
  163. ldapUsername, err := GetStoreSecretData(ctx, store, kube, namespace, ldapAuthCredentials.LDAPUsername)
  164. if err != nil {
  165. return fmt.Errorf("failed to get secret data ldapUsername %w", err)
  166. }
  167. _, err = sdkClient.Auth().LdapAuthLogin(identityID, ldapPassword, ldapUsername)
  168. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaLdapAuth, err)
  169. if err != nil {
  170. return fmt.Errorf("failed to authenticate via ldap auth %w", err)
  171. }
  172. return nil
  173. }
  174. func performOciAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  175. ociAuthCredentials := infisicalSpec.Auth.OciAuthCredentials
  176. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, ociAuthCredentials.IdentityID)
  177. if err != nil {
  178. return fmt.Errorf(errSecretDataFormat, err)
  179. }
  180. privateKey, err := GetStoreSecretData(ctx, store, kube, namespace, ociAuthCredentials.PrivateKey)
  181. if err != nil {
  182. return fmt.Errorf("failed to get secret data privateKey %w", err)
  183. }
  184. var privateKeyPassphrase *string
  185. if ociAuthCredentials.PrivateKeyPassphrase.Name != "" {
  186. passphrase, err := GetStoreSecretData(ctx, store, kube, namespace, ociAuthCredentials.PrivateKeyPassphrase)
  187. if err != nil {
  188. return fmt.Errorf("failed to get secret data privateKeyPassphrase %w", err)
  189. }
  190. privateKeyPassphrase = &passphrase
  191. }
  192. fingerprint, err := GetStoreSecretData(ctx, store, kube, namespace, ociAuthCredentials.Fingerprint)
  193. if err != nil {
  194. return fmt.Errorf("failed to get secret data fingerprint %w", err)
  195. }
  196. userID, err := GetStoreSecretData(ctx, store, kube, namespace, ociAuthCredentials.UserID)
  197. if err != nil {
  198. return fmt.Errorf("failed to get secret data userId %w", err)
  199. }
  200. tenancyID, err := GetStoreSecretData(ctx, store, kube, namespace, ociAuthCredentials.TenancyID)
  201. if err != nil {
  202. return fmt.Errorf("failed to get secret data tenancyId %w", err)
  203. }
  204. region, err := GetStoreSecretData(ctx, store, kube, namespace, ociAuthCredentials.Region)
  205. if err != nil {
  206. return fmt.Errorf("failed to get secret data region %w", err)
  207. }
  208. _, err = sdkClient.Auth().OciAuthLogin(infisicalSdk.OciAuthLoginOptions{
  209. IdentityID: identityID,
  210. PrivateKey: privateKey,
  211. Passphrase: privateKeyPassphrase,
  212. Fingerprint: fingerprint,
  213. UserID: userID,
  214. TenancyID: tenancyID,
  215. Region: region,
  216. })
  217. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaOciAuth, err)
  218. if err != nil {
  219. return fmt.Errorf("failed to authenticate via oci auth %w", err)
  220. }
  221. return nil
  222. }
  223. func performKubernetesAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  224. kubernetesAuthCredentials := infisicalSpec.Auth.KubernetesAuthCredentials
  225. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, kubernetesAuthCredentials.IdentityID)
  226. if err != nil {
  227. return fmt.Errorf(errSecretDataFormat, err)
  228. }
  229. serviceAccountTokenPath := ""
  230. if kubernetesAuthCredentials.ServiceAccountTokenPath.Name != "" {
  231. serviceAccountTokenPath, err = GetStoreSecretData(ctx, store, kube, namespace, kubernetesAuthCredentials.ServiceAccountTokenPath)
  232. if err != nil {
  233. return fmt.Errorf("failed to get secret data serviceAccountTokenPath %w", err)
  234. }
  235. }
  236. _, err = sdkClient.Auth().KubernetesAuthLogin(identityID, serviceAccountTokenPath)
  237. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaKubernetesAuth, err)
  238. if err != nil {
  239. return fmt.Errorf("failed to authenticate via kubernetes auth %w", err)
  240. }
  241. return nil
  242. }
  243. func performAwsAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  244. awsAuthCredentials := infisicalSpec.Auth.AwsAuthCredentials
  245. identityID, err := GetStoreSecretData(ctx, store, kube, namespace, awsAuthCredentials.IdentityID)
  246. if err != nil {
  247. return fmt.Errorf(errSecretDataFormat, err)
  248. }
  249. _, err = sdkClient.Auth().AwsIamAuthLogin(identityID)
  250. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaAwsAuth, err)
  251. if err != nil {
  252. return fmt.Errorf("failed to authenticate via aws auth %w", err)
  253. }
  254. return nil
  255. }
  256. func performTokenAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
  257. tokenAuthCredentials := infisicalSpec.Auth.TokenAuthCredentials
  258. accessToken, err := GetStoreSecretData(ctx, store, kube, namespace, tokenAuthCredentials.AccessToken)
  259. if err != nil {
  260. return fmt.Errorf(errSecretDataFormat, err)
  261. }
  262. sdkClient.Auth().SetAccessToken(accessToken)
  263. metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaTokenAuth, err)
  264. return nil
  265. }
  266. // NewClient creates a new Infisical client.
  267. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  268. storeSpec := store.GetSpec()
  269. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Infisical == nil {
  270. return nil, errors.New("invalid infisical store")
  271. }
  272. infisicalSpec := storeSpec.Provider.Infisical
  273. ctx, cancelSdkClient := context.WithCancel(ctx)
  274. sdkClient := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
  275. SiteUrl: infisicalSpec.HostAPI,
  276. })
  277. secretPath := infisicalSpec.SecretsScope.SecretsPath
  278. if secretPath == "" {
  279. secretPath = "/"
  280. }
  281. var loginFn func(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error
  282. var authMethod string
  283. switch {
  284. case infisicalSpec.Auth.UniversalAuthCredentials != nil:
  285. loginFn = performUniversalAuthLogin
  286. authMethod = machineIdentityLoginViaUniversalAuth
  287. case infisicalSpec.Auth.AzureAuthCredentials != nil:
  288. loginFn = performAzureAuthLogin
  289. authMethod = machineIdentityLoginViaAzureAuth
  290. case infisicalSpec.Auth.GcpIDTokenAuthCredentials != nil:
  291. loginFn = performGcpIDTokenAuthLogin
  292. authMethod = machineIdentityLoginViaGCPIDTokenAuth
  293. case infisicalSpec.Auth.GcpIamAuthCredentials != nil:
  294. loginFn = performGcpIamAuthLogin
  295. authMethod = machineIdentityLoginViaGcpServiceAccountAuth
  296. case infisicalSpec.Auth.JwtAuthCredentials != nil:
  297. loginFn = performJwtAuthLogin
  298. authMethod = machineIdentityLoginViaJwtAuth
  299. case infisicalSpec.Auth.LdapAuthCredentials != nil:
  300. loginFn = performLdapAuthLogin
  301. authMethod = machineIdentityLoginViaLdapAuth
  302. case infisicalSpec.Auth.OciAuthCredentials != nil:
  303. loginFn = performOciAuthLogin
  304. authMethod = machineIdentityLoginViaOciAuth
  305. case infisicalSpec.Auth.KubernetesAuthCredentials != nil:
  306. loginFn = performKubernetesAuthLogin
  307. authMethod = machineIdentityLoginViaKubernetesAuth
  308. case infisicalSpec.Auth.AwsAuthCredentials != nil:
  309. loginFn = performAwsAuthLogin
  310. authMethod = machineIdentityLoginViaAwsAuth
  311. case infisicalSpec.Auth.TokenAuthCredentials != nil:
  312. loginFn = performTokenAuthLogin
  313. authMethod = machineIdentityLoginViaTokenAuth
  314. default:
  315. cancelSdkClient()
  316. return nil, errors.New("authentication method not found")
  317. }
  318. if err := loginFn(ctx, store, infisicalSpec, sdkClient, kube, namespace); err != nil {
  319. cancelSdkClient()
  320. return nil, err
  321. }
  322. return &Provider{
  323. cancelSdkClient: cancelSdkClient,
  324. sdkClient: sdkClient,
  325. apiScope: &ClientScope{
  326. EnvironmentSlug: infisicalSpec.SecretsScope.EnvironmentSlug,
  327. ProjectSlug: infisicalSpec.SecretsScope.ProjectSlug,
  328. Recursive: infisicalSpec.SecretsScope.Recursive,
  329. SecretPath: secretPath,
  330. ExpandSecretReferences: infisicalSpec.SecretsScope.ExpandSecretReferences,
  331. },
  332. authMethod: authMethod,
  333. }, nil
  334. }
  335. // Close releases any resources used by the provider.
  336. func (p *Provider) Close(_ context.Context) error {
  337. p.cancelSdkClient()
  338. // Don't revoke token if token auth was used
  339. if p.authMethod == machineIdentityLoginViaTokenAuth {
  340. return nil
  341. }
  342. err := p.sdkClient.Auth().RevokeAccessToken()
  343. metrics.ObserveAPICall(constants.ProviderName, revokeAccessToken, err)
  344. return err
  345. }
  346. // GetStoreSecretData retrieves secret data from a Kubernetes secret using the provided reference.
  347. // It handles namespace resolution and returns the secret value as a string.
  348. func GetStoreSecretData(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string, secret esmeta.SecretKeySelector) (string, error) {
  349. secretRef := esmeta.SecretKeySelector{
  350. Name: secret.Name,
  351. Key: secret.Key,
  352. }
  353. if secret.Namespace != nil {
  354. secretRef.Namespace = secret.Namespace
  355. }
  356. secretData, err := resolvers.SecretKeyRef(ctx, kube, store.GetObjectKind().GroupVersionKind().Kind, namespace, &secretRef)
  357. if err != nil {
  358. return "", err
  359. }
  360. return secretData, nil
  361. }
  362. // ValidateStore validates the Infisical SecretStore configuration.
  363. // It checks for required fields and valid authentication settings.
  364. func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  365. storeSpec := store.GetSpec()
  366. infisicalStoreSpec := storeSpec.Provider.Infisical
  367. if infisicalStoreSpec == nil {
  368. return nil, errors.New("invalid infisical store")
  369. }
  370. if infisicalStoreSpec.SecretsScope.EnvironmentSlug == "" || infisicalStoreSpec.SecretsScope.ProjectSlug == "" {
  371. return nil, errors.New("secretsScope.projectSlug and secretsScope.environmentSlug cannot be empty")
  372. }
  373. if infisicalStoreSpec.Auth.UniversalAuthCredentials != nil {
  374. uaCredential := infisicalStoreSpec.Auth.UniversalAuthCredentials
  375. // to validate reference authentication
  376. err := esutils.ValidateReferentSecretSelector(store, uaCredential.ClientID)
  377. if err != nil {
  378. return nil, err
  379. }
  380. err = esutils.ValidateReferentSecretSelector(store, uaCredential.ClientSecret)
  381. if err != nil {
  382. return nil, err
  383. }
  384. if uaCredential.ClientID.Key == "" || uaCredential.ClientSecret.Key == "" {
  385. return nil, errors.New("universalAuthCredentials.clientId and universalAuthCredentials.clientSecret cannot be empty")
  386. }
  387. }
  388. return nil, nil
  389. }