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() {