provider_support.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /*
  2. Copyright © The ESO Authors
  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 aws
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "os"
  19. "strings"
  20. "sync"
  21. "time"
  22. "github.com/aws/aws-sdk-go-v2/aws"
  23. "github.com/aws/aws-sdk-go-v2/config"
  24. "github.com/aws/aws-sdk-go-v2/credentials"
  25. awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
  26. secretsmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
  27. "github.com/aws/aws-sdk-go-v2/service/sts"
  28. ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
  29. . "github.com/onsi/ginkgo/v2"
  30. . "github.com/onsi/gomega"
  31. corev1 "k8s.io/api/core/v1"
  32. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  33. "github.com/external-secrets/external-secrets-e2e/framework"
  34. "github.com/external-secrets/external-secrets-e2e/framework/log"
  35. frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
  36. awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
  37. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  38. esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
  39. awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
  40. )
  41. const (
  42. awsProviderAPIVersion = "provider.external-secrets.io/v2alpha1"
  43. defaultV2WaitTimeout = 60 * time.Second
  44. defaultV2PollInterval = 2 * time.Second
  45. )
  46. const (
  47. assumeRoleSessionName = "eso-e2e-probe"
  48. )
  49. type awsAuthProfile string
  50. const (
  51. awsAuthProfileStatic awsAuthProfile = "static"
  52. awsAuthProfileExternalID awsAuthProfile = "external-id"
  53. awsAuthProfileSessionTags awsAuthProfile = "session-tags"
  54. awsAuthProfileReferencedIRSA awsAuthProfile = "referenced-irsa"
  55. awsAuthProfileMountedIRSA awsAuthProfile = "mounted-irsa"
  56. )
  57. type awsAccessConfig struct {
  58. KID string
  59. SAK string
  60. ST string
  61. Region string
  62. Role string
  63. SAName string
  64. SANamespace string
  65. }
  66. type secretsManagerBackend struct {
  67. access awsAccessConfig
  68. client *awssm.Client
  69. clientErr error
  70. clientOnce sync.Once
  71. framework *framework.Framework
  72. }
  73. type stsAssumeRoleClient interface {
  74. AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
  75. }
  76. type assumeRoleProbeKey struct {
  77. access awsAccessConfig
  78. profile awsAuthProfile
  79. }
  80. type assumeRoleProbeResult struct {
  81. err error
  82. }
  83. var assumeRoleProbeCache sync.Map
  84. func loadAWSAccessConfigFromEnv() awsAccessConfig {
  85. return awsAccessConfig{
  86. KID: os.Getenv("AWS_ACCESS_KEY_ID"),
  87. SAK: os.Getenv("AWS_SECRET_ACCESS_KEY"),
  88. ST: os.Getenv("AWS_SESSION_TOKEN"),
  89. Region: os.Getenv("AWS_REGION"),
  90. SAName: os.Getenv("AWS_SA_NAME"),
  91. SANamespace: os.Getenv("AWS_SA_NAMESPACE"),
  92. }
  93. }
  94. func newBackendFromEnv(f *framework.Framework) *secretsManagerBackend {
  95. return newSecretsManagerBackend(f, loadAWSAccessConfigFromEnv())
  96. }
  97. func newSecretsManagerBackend(f *framework.Framework, access awsAccessConfig) *secretsManagerBackend {
  98. return &secretsManagerBackend{
  99. access: access,
  100. framework: f,
  101. }
  102. }
  103. func (c awsAccessConfig) missingStaticCredentials() []string {
  104. var missing []string
  105. if c.KID == "" {
  106. missing = append(missing, "AWS_ACCESS_KEY_ID")
  107. }
  108. if c.SAK == "" {
  109. missing = append(missing, "AWS_SECRET_ACCESS_KEY")
  110. }
  111. if c.Region == "" {
  112. missing = append(missing, "AWS_REGION")
  113. }
  114. return missing
  115. }
  116. func skipIfAWSStaticCredentialsMissing(access awsAccessConfig) {
  117. if missing := access.missingStaticCredentials(); len(missing) > 0 {
  118. Skip("missing AWS e2e credentials: " + strings.Join(missing, ", "))
  119. }
  120. }
  121. func skipIfAWSManagedIRSAEnvMissing(access awsAccessConfig) {
  122. var missing []string
  123. if access.Region == "" {
  124. missing = append(missing, "AWS_REGION")
  125. }
  126. if access.SAName == "" {
  127. missing = append(missing, "AWS_SA_NAME")
  128. }
  129. if access.SANamespace == "" {
  130. missing = append(missing, "AWS_SA_NAMESPACE")
  131. }
  132. if len(missing) > 0 {
  133. Skip("missing AWS managed IRSA environment: " + strings.Join(missing, ", "))
  134. }
  135. }
  136. func skipIfAWSAssumeRoleProbeDenied(access awsAccessConfig, profile awsAuthProfile) {
  137. if profile != awsAuthProfileExternalID && profile != awsAuthProfileSessionTags {
  138. return
  139. }
  140. cacheKey := assumeRoleProbeKey{
  141. access: access,
  142. profile: profile,
  143. }
  144. if cached, ok := assumeRoleProbeCache.Load(cacheKey); ok {
  145. handleAssumeRoleProbeResult(access, profile, cached.(assumeRoleProbeResult).err)
  146. return
  147. }
  148. cfg, err := loadAWSConfig(access)
  149. Expect(err).NotTo(HaveOccurred())
  150. err = probeAssumeRoleAccess(context.Background(), sts.NewFromConfig(cfg), access, profile)
  151. assumeRoleProbeCache.Store(cacheKey, assumeRoleProbeResult{err: err})
  152. handleAssumeRoleProbeResult(access, profile, err)
  153. }
  154. func handleAssumeRoleProbeResult(access awsAccessConfig, profile awsAuthProfile, err error) {
  155. if err == nil {
  156. return
  157. }
  158. if isAssumeRoleAccessDenied(err) {
  159. Skip(fmt.Sprintf("skipping AWS %s auth e2e: %s is not authorized to assume role %q with the current credentials", profile, assumeRoleAction(profile), roleARNForProfile(access, profile)))
  160. }
  161. Expect(err).NotTo(HaveOccurred())
  162. }
  163. func assumeRoleAction(profile awsAuthProfile) string {
  164. if profile == awsAuthProfileSessionTags {
  165. return "sts:TagSession"
  166. }
  167. return "sts:AssumeRole"
  168. }
  169. func staticAWSAuth(secretName string) esv1.AWSAuth {
  170. return esv1.AWSAuth{
  171. SecretRef: &esv1.AWSAuthSecretRef{
  172. AccessKeyID: esmetav1.SecretKeySelector{
  173. Name: secretName,
  174. Key: awscommon.StaticAccessKeyIDKey,
  175. },
  176. SecretAccessKey: esmetav1.SecretKeySelector{
  177. Name: secretName,
  178. Key: awscommon.StaticSecretAccessKeyKey,
  179. },
  180. SessionToken: &esmetav1.SecretKeySelector{
  181. Name: secretName,
  182. Key: awscommon.StaticSessionTokenKey,
  183. },
  184. },
  185. }
  186. }
  187. func newStaticCredentialsSecret(namespace, name string, access awsAccessConfig) *corev1.Secret {
  188. return &corev1.Secret{
  189. ObjectMeta: metav1.ObjectMeta{
  190. Name: name,
  191. Namespace: namespace,
  192. },
  193. StringData: awscommon.StaticCredentialsSecretData(access.KID, access.SAK, access.ST),
  194. }
  195. }
  196. func createStaticCredentialsSecret(f *framework.Framework, namespace, name string, access awsAccessConfig) {
  197. Expect(f.CRClient.Create(GinkgoT().Context(), newStaticCredentialsSecret(namespace, name, access))).To(Succeed())
  198. }
  199. func roleARNForProfile(access awsAccessConfig, profile awsAuthProfile) string {
  200. if access.Role != "" {
  201. return access.Role
  202. }
  203. switch profile {
  204. case awsAuthProfileExternalID:
  205. return awscommon.IAMRoleExternalID
  206. case awsAuthProfileSessionTags:
  207. return awscommon.IAMRoleSessionTags
  208. default:
  209. return ""
  210. }
  211. }
  212. func sessionTagsForProfile(profile awsAuthProfile) []ststypes.Tag {
  213. if profile != awsAuthProfileSessionTags {
  214. return nil
  215. }
  216. return []ststypes.Tag{{
  217. Key: aws.String("namespace"),
  218. Value: aws.String("e2e-test"),
  219. }}
  220. }
  221. func probeAssumeRoleAccess(ctx context.Context, client stsAssumeRoleClient, access awsAccessConfig, profile awsAuthProfile) error {
  222. if profile != awsAuthProfileExternalID && profile != awsAuthProfileSessionTags {
  223. return nil
  224. }
  225. input := &sts.AssumeRoleInput{
  226. RoleArn: aws.String(roleARNForProfile(access, profile)),
  227. RoleSessionName: aws.String(assumeRoleSessionName),
  228. Tags: sessionTagsForProfile(profile),
  229. }
  230. if profile == awsAuthProfileExternalID {
  231. input.ExternalId = aws.String(awscommon.IAMTrustedExternalID)
  232. }
  233. _, err := client.AssumeRole(ctx, input)
  234. return err
  235. }
  236. func isAssumeRoleAccessDenied(err error) bool {
  237. if err == nil {
  238. return false
  239. }
  240. msg := strings.ToLower(err.Error())
  241. if !strings.Contains(msg, "accessdenied") {
  242. return false
  243. }
  244. return strings.Contains(msg, "sts:assumerole") || strings.Contains(msg, "sts:tagsession")
  245. }
  246. func newSecretsManagerV2Config(namespace, name string, access awsAccessConfig, profile awsAuthProfile) *awsv2alpha1.SecretsManager {
  247. cfg := &awsv2alpha1.SecretsManager{
  248. TypeMeta: metav1.TypeMeta{
  249. APIVersion: awsv2alpha1.GroupVersion.String(),
  250. Kind: awsv2alpha1.SecretsManagerKind,
  251. },
  252. ObjectMeta: metav1.ObjectMeta{
  253. Name: name,
  254. Namespace: namespace,
  255. },
  256. Spec: awsv2alpha1.SecretsManagerSpec{
  257. Region: access.Region,
  258. },
  259. }
  260. switch profile {
  261. case awsAuthProfileStatic:
  262. cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
  263. case awsAuthProfileExternalID:
  264. cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
  265. cfg.Spec.Role = access.Role
  266. if cfg.Spec.Role == "" {
  267. cfg.Spec.Role = awscommon.IAMRoleExternalID
  268. }
  269. cfg.Spec.ExternalID = awscommon.IAMTrustedExternalID
  270. case awsAuthProfileSessionTags:
  271. cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
  272. cfg.Spec.Role = access.Role
  273. if cfg.Spec.Role == "" {
  274. cfg.Spec.Role = awscommon.IAMRoleSessionTags
  275. }
  276. cfg.Spec.SessionTags = []*esv1.Tag{{
  277. Key: "namespace",
  278. Value: "e2e-test",
  279. }}
  280. case awsAuthProfileReferencedIRSA:
  281. cfg.Spec.Auth = esv1.AWSAuth{
  282. JWTAuth: &esv1.AWSJWTAuth{
  283. ServiceAccountRef: &esmetav1.ServiceAccountSelector{
  284. Name: access.SAName,
  285. Namespace: &access.SANamespace,
  286. },
  287. },
  288. }
  289. case awsAuthProfileMountedIRSA:
  290. cfg.Spec.Auth = esv1.AWSAuth{}
  291. default:
  292. cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
  293. }
  294. return cfg
  295. }
  296. func createSecretsManagerV2Config(f *framework.Framework, namespace, name string, access awsAccessConfig, profile awsAuthProfile) *awsv2alpha1.SecretsManager {
  297. if profile == awsAuthProfileStatic || profile == awsAuthProfileExternalID || profile == awsAuthProfileSessionTags {
  298. createStaticCredentialsSecret(f, namespace, awscommon.CredentialsSecretName(name), access)
  299. }
  300. cfg := newSecretsManagerV2Config(namespace, name, access, profile)
  301. Expect(f.CRClient.Create(GinkgoT().Context(), cfg)).To(Succeed())
  302. return cfg
  303. }
  304. func createSecretsManagerV2ProviderConnection(f *framework.Framework, namespace, name, providerName, providerNamespace string) {
  305. frameworkv2.CreateProviderConnection(
  306. f,
  307. namespace,
  308. name,
  309. frameworkv2.ProviderAddress("aws"),
  310. awsProviderAPIVersion,
  311. awsv2alpha1.SecretsManagerKind,
  312. providerName,
  313. providerNamespace,
  314. )
  315. }
  316. func loadAWSConfig(access awsAccessConfig) (aws.Config, error) {
  317. loadOptions := []func(*config.LoadOptions) error{
  318. config.WithRegion(access.Region),
  319. }
  320. if access.KID != "" || access.SAK != "" || access.ST != "" {
  321. loadOptions = append(loadOptions, config.WithCredentialsProvider(
  322. credentials.NewStaticCredentialsProvider(access.KID, access.SAK, access.ST),
  323. ))
  324. }
  325. return config.LoadDefaultConfig(context.Background(), loadOptions...)
  326. }
  327. func (b *secretsManagerBackend) ensureClient() {
  328. b.clientOnce.Do(func() {
  329. cfg, err := loadAWSConfig(b.access)
  330. if err != nil {
  331. b.clientErr = err
  332. return
  333. }
  334. b.client = awssm.NewFromConfig(cfg)
  335. })
  336. Expect(b.clientErr).ToNot(HaveOccurred())
  337. Expect(b.client).NotTo(BeNil())
  338. }
  339. func (b *secretsManagerBackend) CreateSecret(key string, val framework.SecretEntry) {
  340. b.ensureClient()
  341. smTags := make([]secretsmanagertypes.Tag, 0, len(val.Tags))
  342. for tagKey, tagValue := range val.Tags {
  343. smTags = append(smTags, secretsmanagertypes.Tag{
  344. Key: aws.String(tagKey),
  345. Value: aws.String(tagValue),
  346. })
  347. }
  348. attempts := 20
  349. for {
  350. log.Logf("creating secret %s / attempts left: %d", key, attempts)
  351. _, err := b.client.CreateSecret(GinkgoT().Context(), &awssm.CreateSecretInput{
  352. Name: aws.String(key),
  353. SecretString: aws.String(val.Value),
  354. Tags: smTags,
  355. })
  356. if err == nil {
  357. return
  358. }
  359. attempts--
  360. if attempts < 0 {
  361. Fail("unable to create secret: " + err.Error())
  362. }
  363. <-time.After(5 * time.Second)
  364. }
  365. }
  366. func (b *secretsManagerBackend) DeleteSecret(key string) {
  367. b.ensureClient()
  368. log.Logf("deleting secret %s", key)
  369. _, err := b.client.DeleteSecret(GinkgoT().Context(), &awssm.DeleteSecretInput{
  370. SecretId: aws.String(key),
  371. ForceDeleteWithoutRecovery: aws.Bool(true),
  372. })
  373. var notFound *secretsmanagertypes.ResourceNotFoundException
  374. if errors.As(err, &notFound) {
  375. return
  376. }
  377. Expect(err).ToNot(HaveOccurred())
  378. }
  379. func (b *secretsManagerBackend) WaitForSecretValue(name, expectedValue string) {
  380. b.ensureClient()
  381. Eventually(func(g Gomega) {
  382. out, err := b.client.GetSecretValue(GinkgoT().Context(), &awssm.GetSecretValueInput{
  383. SecretId: aws.String(name),
  384. })
  385. g.Expect(err).NotTo(HaveOccurred())
  386. g.Expect(secretValueString(out)).To(Equal(expectedValue))
  387. }, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
  388. }
  389. func (b *secretsManagerBackend) ExpectSecretAbsent(name string) {
  390. b.ensureClient()
  391. Eventually(func() bool {
  392. _, err := b.client.GetSecretValue(GinkgoT().Context(), &awssm.GetSecretValueInput{
  393. SecretId: aws.String(name),
  394. })
  395. return secretReadErrorIndicatesAbsence(err)
  396. }, defaultV2WaitTimeout, defaultV2PollInterval).Should(BeTrue(), fmt.Sprintf("expected AWS secret %q to be absent", name))
  397. }
  398. func secretValueString(out *awssm.GetSecretValueOutput) string {
  399. if out == nil {
  400. return ""
  401. }
  402. if out.SecretString != nil {
  403. return aws.ToString(out.SecretString)
  404. }
  405. if len(out.SecretBinary) > 0 {
  406. return string(out.SecretBinary)
  407. }
  408. return ""
  409. }
  410. func secretReadErrorIndicatesAbsence(err error) bool {
  411. if err == nil {
  412. return false
  413. }
  414. var notFound *secretsmanagertypes.ResourceNotFoundException
  415. if errors.As(err, &notFound) {
  416. return true
  417. }
  418. msg := strings.ToLower(err.Error())
  419. return strings.Contains(msg, "marked for deletion") || strings.Contains(msg, "scheduled for deletion")
  420. }