diff --git a/README.md b/README.md index 7b507315..9668a566 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,24 @@ volumes: secretName: cert ``` +#### Cross Account Assume Role + +You can configure the AWS Private CA issuer to assume an IAM role before making AWS Private CA API calls. This method provides flexibility for various authentication scenarios and is an alternative to AWS Resource Access Manager (RAM) sharing for cross-account use cases. + +When you specify a role in the `role` field, the issuer assumes that role using AWS Security Token Service (STS) before making AWS Private CA API calls. + +Example: +``` +apiVersion: awspca.cert-manager.io/v1beta1 +kind: AWSPCAClusterIssuer +metadata: + name: example +spec: + arn: + role: + region: +``` + ## Supported workflows AWS Private Certificate Authority(PCA) Issuer Plugin supports the following integrations and use cases: diff --git a/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaclusterissuers.yaml b/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaclusterissuers.yaml index 53806eae..814fe4b1 100644 --- a/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaclusterissuers.yaml +++ b/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaclusterissuers.yaml @@ -46,6 +46,9 @@ spec: region: description: Should contain the AWS region if it cannot be inferred type: string + role: + description: Specifies the ARN of role to assume when issuing certificates. + type: string secretRef: description: Needs to be specified if you want to authorize with AWS using an access and secret key diff --git a/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaissuers.yaml b/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaissuers.yaml index 31ab5e4e..ed145113 100644 --- a/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaissuers.yaml +++ b/charts/aws-pca-issuer/crds/awspca.cert-manager.io_awspcaissuers.yaml @@ -45,6 +45,9 @@ spec: region: description: Should contain the AWS region if it cannot be inferred type: string + role: + description: Specifies the ARN of role to assume when issuing certificates. + type: string secretRef: description: Needs to be specified if you want to authorize with AWS using an access and secret key diff --git a/config/crd/bases/awspca.cert-manager.io_awspcaclusterissuers.yaml b/config/crd/bases/awspca.cert-manager.io_awspcaclusterissuers.yaml index 53806eae..814fe4b1 100644 --- a/config/crd/bases/awspca.cert-manager.io_awspcaclusterissuers.yaml +++ b/config/crd/bases/awspca.cert-manager.io_awspcaclusterissuers.yaml @@ -46,6 +46,9 @@ spec: region: description: Should contain the AWS region if it cannot be inferred type: string + role: + description: Specifies the ARN of role to assume when issuing certificates. + type: string secretRef: description: Needs to be specified if you want to authorize with AWS using an access and secret key diff --git a/config/crd/bases/awspca.cert-manager.io_awspcaissuers.yaml b/config/crd/bases/awspca.cert-manager.io_awspcaissuers.yaml index 31ab5e4e..ed145113 100644 --- a/config/crd/bases/awspca.cert-manager.io_awspcaissuers.yaml +++ b/config/crd/bases/awspca.cert-manager.io_awspcaissuers.yaml @@ -45,6 +45,9 @@ spec: region: description: Should contain the AWS region if it cannot be inferred type: string + role: + description: Specifies the ARN of role to assume when issuing certificates. + type: string secretRef: description: Needs to be specified if you want to authorize with AWS using an access and secret key diff --git a/config/examples/cluster-issuer-with-assumption-role/issuer.yaml b/config/examples/cluster-issuer-with-assumption-role/issuer.yaml new file mode 100644 index 00000000..d0aedeac --- /dev/null +++ b/config/examples/cluster-issuer-with-assumption-role/issuer.yaml @@ -0,0 +1,8 @@ +apiVersion: awspca.cert-manager.io/v1beta1 +kind: AWSPCAClusterIssuer +metadata: + name: example +spec: + arn: + role: + region: us-west-2 diff --git a/e2e/aws_helpers.go b/e2e/aws_helpers.go index 959618b5..8868b396 100644 --- a/e2e/aws_helpers.go +++ b/e2e/aws_helpers.go @@ -8,7 +8,6 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/acmpca" @@ -296,7 +295,7 @@ func getAccountID(ctx context.Context, cfg aws.Config) string { return *callerID.Account } -func getPartition(ctx context.Context, cfg aws.Config) string { +func getCallerIdentity(ctx context.Context, cfg aws.Config) *sts.GetCallerIdentityOutput { stsClient := sts.NewFromConfig(cfg) callerID, callerErr := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) @@ -305,12 +304,7 @@ func getPartition(ctx context.Context, cfg aws.Config) string { panic(callerErr.Error()) } - parsedArn, parseErr := arn.Parse(*callerID.Arn) - if parseErr != nil { - return "aws" - } - - return parsedArn.Partition + return callerID } func assumeRole(ctx context.Context, cfg aws.Config, roleName string, region string) aws.Config { diff --git a/e2e/awspcaissuer_test.go b/e2e/awspcaissuer_test.go index 0b3cf182..6fc50afc 100644 --- a/e2e/awspcaissuer_test.go +++ b/e2e/awspcaissuer_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go-v2/config" "github.com/cert-manager/aws-privateca-issuer/pkg/api/v1beta1" clientV1beta1 "github.com/cert-manager/aws-privateca-issuer/pkg/clientset/v1beta1" @@ -31,7 +32,7 @@ type TestContext struct { xaCfg aws.Config caArns map[string]string - region, partition, accessKey, secretKey, endEntityResourceShareArn, subordinateCaResourceShareArn, userName, policyArn string + region, partition, accessKey, secretKey, endEntityResourceShareArn, subordinateCaResourceShareArn, userName, policyArn, roleToAssume string } // These are variables specific to each test @@ -111,7 +112,19 @@ func InitializeTestSuite(suiteCtx *godog.TestSuiteContext) { panic(cfgErr.Error()) } - testContext.partition = getPartition(ctx, cfg) + callerID := getCallerIdentity(ctx, cfg) + + parsedArn, parseErr := arn.Parse(*callerID.Arn) + if parseErr != nil { + panic("Failed to parse caller identity ARN: " + parseErr.Error()) + } + + testContext.partition = parsedArn.Partition + + testContext.roleToAssume = fmt.Sprintf("arn:%s:iam::%s:role/IssuerTestRole-test-us-east-1", testContext.partition, *callerID.Account) + if roleToAssumeOverride, exists := os.LookupEnv("ROLE_TO_ASSUME_OVERRIDE"); exists { + testContext.roleToAssume = roleToAssumeOverride + } testContext.iclient, err = clientV1beta1.NewForConfig(clientConfig) @@ -217,8 +230,10 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Step(`^I create a namespace`, issuerContext.createNamespace) ctx.Step(`^I create a Secret with keys ([A-Za-z_]+) and ([A-Za-z_]+) for my AWS credentials$`, issuerContext.createSecret) ctx.Step(`^I create an AWSPCAClusterIssuer using a (RSA|ECDSA|XA) CA$`, issuerContext.createClusterIssuer) + ctx.Step(`^I create an AWSPCAClusterIssuer with role assumption$`, issuerContext.createClusterIssuerWithRole) ctx.Step(`^I delete the AWSPCAClusterIssuer$`, issuerContext.deleteClusterIssuer) ctx.Step(`^I create an AWSPCAIssuer using a (RSA|ECDSA|XA) CA$`, issuerContext.createNamespaceIssuer) + ctx.Step(`^I create an AWSPCAIssuer with role assumption$`, issuerContext.createNamespaceIssuerWithRole) ctx.Step(`^I issue a (SHORT_VALIDITY|RSA|ECDSA|CA) certificate$`, issuerContext.issueCertificate) ctx.Step(`^the certificate should be issued successfully$`, issuerContext.verifyCertificateIssued) ctx.Step(`^the certificate request has been created$`, issuerContext.verifyCertificateRequestIsCreated) diff --git a/e2e/clusterissuer_steps.go b/e2e/clusterissuer_steps.go index 682a7164..06cc8f94 100644 --- a/e2e/clusterissuer_steps.go +++ b/e2e/clusterissuer_steps.go @@ -13,13 +13,22 @@ import ( ) func (issCtx *IssuerContext) createClusterIssuer(ctx context.Context, caType string) error { + return issCtx.createClusterIssuerWithSpec(ctx, caType, getIssuerSpec(caType)) +} + +func (issCtx *IssuerContext) createClusterIssuerWithRole(ctx context.Context) error { + return issCtx.createClusterIssuerWithSpec(ctx, "RSA", getIssuerSpecWithRole("RSA")) +} + +func (issCtx *IssuerContext) createClusterIssuerWithSpec(ctx context.Context, caType string, spec v1beta1.AWSPCAIssuerSpec) error { if issCtx.issuerName == "" { issCtx.issuerName = uuid.New().String() + "--cluster-issuer--" + strings.ToLower(caType) } + issCtx.issuerType = "AWSPCAClusterIssuer" issSpec := v1beta1.AWSPCAClusterIssuer{ ObjectMeta: metav1.ObjectMeta{Name: issCtx.issuerName}, - Spec: getIssuerSpec(caType), + Spec: spec, } if issCtx.secretRef != (v1beta1.AWSCredentialsSecretReference{}) { diff --git a/e2e/common_steps.go b/e2e/common_steps.go index 098b4269..d9e39754 100644 --- a/e2e/common_steps.go +++ b/e2e/common_steps.go @@ -34,6 +34,12 @@ func getIssuerSpec(caType string) v1beta1.AWSPCAIssuerSpec { } } +func getIssuerSpecWithRole(caType string) v1beta1.AWSPCAIssuerSpec { + spec := getIssuerSpec(caType) + spec.Role = testContext.roleToAssume + return spec +} + func (issCtx *IssuerContext) createNamespace(ctx context.Context) error { namespaceName := "pca-issuer-ns-" + uuid.New().String() namespace := v1.Namespace{ diff --git a/e2e/features/role_assumption.feature b/e2e/features/role_assumption.feature new file mode 100644 index 00000000..f68671e4 --- /dev/null +++ b/e2e/features/role_assumption.feature @@ -0,0 +1,15 @@ +@RoleAssumption +Feature: Issue certificates using role assumption + As a user of the aws-privateca-issuer + I need to be able to issue certificates using role assumption + + Scenario: Issue a certificate with a ClusterIssuer using role assumption + Given I create an AWSPCAClusterIssuer with role assumption + When I issue a RSA certificate + Then the certificate should be issued successfully + + Scenario: Issue a certificate with a namespaced Issuer using role assumption + Given I create a namespace + And I create an AWSPCAIssuer with role assumption + When I issue a RSA certificate + Then the certificate should be issued successfully diff --git a/e2e/namespaceissuer_steps.go b/e2e/namespaceissuer_steps.go index 13c9f3a0..2b782fc8 100644 --- a/e2e/namespaceissuer_steps.go +++ b/e2e/namespaceissuer_steps.go @@ -13,11 +13,19 @@ import ( ) func (issCtx *IssuerContext) createNamespaceIssuer(ctx context.Context, caType string) error { + return issCtx.createNamespaceIssuerWithSpec(ctx, caType, getIssuerSpec(caType)) +} + +func (issCtx *IssuerContext) createNamespaceIssuerWithRole(ctx context.Context) error { + return issCtx.createNamespaceIssuerWithSpec(ctx, "RSA", getIssuerSpecWithRole("RSA")) +} + +func (issCtx *IssuerContext) createNamespaceIssuerWithSpec(ctx context.Context, caType string, spec v1beta1.AWSPCAIssuerSpec) error { issCtx.issuerName = uuid.New().String() + "--namespace-issuer--" + strings.ToLower(caType) issCtx.issuerType = "AWSPCAIssuer" issSpec := v1beta1.AWSPCAIssuer{ ObjectMeta: metav1.ObjectMeta{Name: issCtx.issuerName}, - Spec: getIssuerSpec(caType), + Spec: spec, } if issCtx.secretRef != (v1beta1.AWSCredentialsSecretReference{}) { diff --git a/pkg/api/v1beta1/awspcaissuer_types.go b/pkg/api/v1beta1/awspcaissuer_types.go index 22a14a03..8b05d522 100644 --- a/pkg/api/v1beta1/awspcaissuer_types.go +++ b/pkg/api/v1beta1/awspcaissuer_types.go @@ -37,6 +37,9 @@ type AWSPCAIssuerSpec struct { // Needs to be specified if you want to authorize with AWS using an access and secret key // +optional SecretRef AWSCredentialsSecretReference `json:"secretRef,omitempty"` + // Specifies the ARN of role to assume when issuing certificates. + // +optional + Role string `json:"role,omitempty"` } // AWSCredentialsSecretReference defines the secret used by the issuer diff --git a/pkg/aws/pca.go b/pkg/aws/pca.go index 4f10a47e..9c72f22a 100644 --- a/pkg/aws/pca.go +++ b/pkg/aws/pca.go @@ -32,8 +32,10 @@ import ( "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/acmpca" acmpcatypes "github.com/aws/aws-sdk-go-v2/service/acmpca/types" + "github.com/aws/aws-sdk-go-v2/service/sts" injections "github.com/cert-manager/aws-privateca-issuer/pkg/api/injections" api "github.com/cert-manager/aws-privateca-issuer/pkg/api/v1beta1" cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" @@ -89,6 +91,11 @@ func GetConfig(ctx context.Context, client client.Client, spec *api.AWSPCAIssuer } func LoadConfig(ctx context.Context, client client.Client, spec *api.AWSPCAIssuerSpec) (aws.Config, error) { + var configOptions []func(*config.LoadOptions) error + if spec.Region != "" { + configOptions = append(configOptions, config.WithRegion(spec.Region)) + } + if spec.SecretRef.Name != "" { secretNamespaceName := types.NamespacedName{ Namespace: spec.SecretRef.Namespace, @@ -118,23 +125,23 @@ func LoadConfig(ctx context.Context, client client.Client, spec *api.AWSPCAIssue return aws.Config{}, ErrNoSecretAccessKey } - if spec.Region != "" { - return config.LoadDefaultConfig(ctx, - config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(string(accessKey), string(secretKey), "")), - config.WithRegion(spec.Region), - ) - } - - return config.LoadDefaultConfig(ctx, - config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(string(accessKey), string(secretKey), "")), - ) - } else if spec.Region != "" { - return config.LoadDefaultConfig(ctx, - config.WithRegion(spec.Region), + configOptions = append(configOptions, config.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider(string(accessKey), string(secretKey), "")), ) } - return config.LoadDefaultConfig(ctx) + cfg, err := config.LoadDefaultConfig(ctx, configOptions...) + if err != nil { + return aws.Config{}, err + } + + if spec.Role != "" { + stsService := sts.NewFromConfig(cfg) + creds := stscreds.NewAssumeRoleProvider(stsService, spec.Role) + cfg.Credentials = aws.NewCredentialsCache(creds) + } + + return cfg, nil } func ClearProvisioners() {