diff --git a/docs/docs/guides/parent-ecs-fargate.md b/docs/docs/guides/parent-ecs-fargate.md index 08c72c3b..3521d603 100644 --- a/docs/docs/guides/parent-ecs-fargate.md +++ b/docs/docs/guides/parent-ecs-fargate.md @@ -58,6 +58,12 @@ values: MONGODB_ATLAS_PRIVATE_KEY: "private-key-456" ``` +> **Ambient credentials (OIDC / instance profile):** `accessKey` and `secretAccessKey` +> are optional. Omit them (keep `region`) and `sc` falls back to the standard AWS +> credential chain — GitHub Actions OIDC web-identity, an EC2/ECS instance profile, +> or the `AWS_*` environment — so CI can assume a short-lived federated role instead +> of storing long-lived keys in `secrets.yaml`. + ### **What This Does** Stores **AWS credentials** for programmatic access. diff --git a/pkg/clouds/pulumi/aws/provider.go b/pkg/clouds/pulumi/aws/provider.go index c9f04b8e..0647fb78 100644 --- a/pkg/clouds/pulumi/aws/provider.go +++ b/pkg/clouds/pulumi/aws/provider.go @@ -28,15 +28,24 @@ func InitStateStore(ctx context.Context, stateStoreCfg api.StateStorageConfig, l return errors.Wrapf(err, "failed to convert auth config to aws.AccountConfig") } - // hackily set aws creds env variable, so that we can access AWS state storage - if err := os.Setenv("AWS_ACCESS_KEY", pcfg.AccessKey); err != nil { - fmt.Println("Failed to set AWS_ACCESS_KEY env variable: ", err.Error()) + // Export static creds for AWS state-store access ONLY when configured. When + // empty (OIDC web-identity / instance profile / ambient credentials), leave the + // environment untouched — otherwise we would blank out the credentials the AWS + // default chain (e.g. the GitHub OIDC creds the runner already exported) relies on. + if pcfg.AccessKey != "" { + if err := os.Setenv("AWS_ACCESS_KEY", pcfg.AccessKey); err != nil { + fmt.Println("Failed to set AWS_ACCESS_KEY env variable: ", err.Error()) + } } - if err := os.Setenv("AWS_SECRET_ACCESS_KEY", pcfg.SecretAccessKey); err != nil { - fmt.Println("Failed to set AWS_SECRET_ACCESS_KEY env variable: ", err.Error()) + if pcfg.SecretAccessKey != "" { + if err := os.Setenv("AWS_SECRET_ACCESS_KEY", pcfg.SecretAccessKey); err != nil { + fmt.Println("Failed to set AWS_SECRET_ACCESS_KEY env variable: ", err.Error()) + } } - if err := os.Setenv("AWS_DEFAULT_REGION", pcfg.Region); err != nil { - fmt.Println("Failed to set AWS_DEFAULT_REGION env variable: ", err.Error()) + if pcfg.Region != "" { + if err := os.Setenv("AWS_DEFAULT_REGION", pcfg.Region); err != nil { + fmt.Println("Failed to set AWS_DEFAULT_REGION env variable: ", err.Error()) + } } return nil } @@ -52,11 +61,19 @@ func Provider(ctx *sdk.Context, stack api.Stack, input api.ResourceInput, params return nil, errors.Wrapf(err, "failed to convert auth config to aws.AccountConfig") } - provider, err := sdkAws.NewProvider(ctx, input.ToResName(input.Descriptor.Name), &sdkAws.ProviderArgs{ - AccessKey: sdk.String(pcfg.AccessKey), - SecretKey: sdk.String(pcfg.SecretAccessKey), - Region: sdk.String(pcfg.Region), - }) + providerArgs := &sdkAws.ProviderArgs{ + Region: sdk.String(pcfg.Region), + } + // Pin static creds only when configured; otherwise fall back to the AWS default + // credential chain (OIDC web-identity / instance profile / env). Mirrors the + // guarded handling already in cloudtrail_security_alerts.go. + if pcfg.AccessKey != "" { + providerArgs.AccessKey = sdk.String(pcfg.AccessKey) + } + if pcfg.SecretAccessKey != "" { + providerArgs.SecretKey = sdk.String(pcfg.SecretAccessKey) + } + provider, err := sdkAws.NewProvider(ctx, input.ToResName(input.Descriptor.Name), providerArgs) return &api.ResourceOutput{ Ref: provider, }, err diff --git a/pkg/clouds/pulumi/aws/provider_test.go b/pkg/clouds/pulumi/aws/provider_test.go new file mode 100644 index 00000000..80347400 --- /dev/null +++ b/pkg/clouds/pulumi/aws/provider_test.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) Simple Container + +package aws + +import ( + "context" + "os" + "testing" + + . "github.com/onsi/gomega" + + "github.com/simple-container-com/api/pkg/api/logger" + "github.com/simple-container-com/api/pkg/clouds/aws" +) + +// TestInitStateStore_AmbientCredsPreserved is the regression guard for OIDC / +// instance-profile deploys: when the auth config carries NO static keys, +// InitStateStore must not touch the AWS_* env the runner already exported, so +// the AWS default credential chain (e.g. the GitHub OIDC web-identity creds) +// still works. The previous code always ran os.Setenv("AWS_SECRET_ACCESS_KEY", +// "") which blanked those ambient credentials. +func TestInitStateStore_AmbientCredsPreserved(t *testing.T) { + RegisterTestingT(t) + t.Setenv("AWS_ACCESS_KEY_ID", "ASIAAMBIENT") + t.Setenv("AWS_SECRET_ACCESS_KEY", "ambient-secret") + t.Setenv("AWS_SESSION_TOKEN", "ambient-session") + + // Auth config with region only — no accessKey/secretAccessKey (OIDC). + cfg := &aws.StateStorageConfig{AccountConfig: aws.AccountConfig{Region: "eu-central-1"}} + Expect(InitStateStore(context.Background(), cfg, logger.New())).To(Succeed()) + + Expect(os.Getenv("AWS_SECRET_ACCESS_KEY")).To(Equal("ambient-secret")) + Expect(os.Getenv("AWS_ACCESS_KEY_ID")).To(Equal("ASIAAMBIENT")) + Expect(os.Getenv("AWS_SESSION_TOKEN")).To(Equal("ambient-session")) + Expect(os.Getenv("AWS_DEFAULT_REGION")).To(Equal("eu-central-1")) +} + +// TestInitStateStore_StaticCredsExported confirms back-compat: a config WITH +// static keys still exports them unchanged. +func TestInitStateStore_StaticCredsExported(t *testing.T) { + RegisterTestingT(t) + t.Setenv("AWS_ACCESS_KEY", "") + t.Setenv("AWS_SECRET_ACCESS_KEY", "") + t.Setenv("AWS_DEFAULT_REGION", "") + + cfg := &aws.StateStorageConfig{AccountConfig: aws.AccountConfig{ + AccessKey: "AKIASTATIC", + SecretAccessKey: "static-secret", + Region: "us-east-1", + }} + Expect(InitStateStore(context.Background(), cfg, logger.New())).To(Succeed()) + + Expect(os.Getenv("AWS_ACCESS_KEY")).To(Equal("AKIASTATIC")) + Expect(os.Getenv("AWS_SECRET_ACCESS_KEY")).To(Equal("static-secret")) + Expect(os.Getenv("AWS_DEFAULT_REGION")).To(Equal("us-east-1")) +} diff --git a/pkg/clouds/pulumi/aws/static_website.go b/pkg/clouds/pulumi/aws/static_website.go index 05addd37..3af88b0b 100644 --- a/pkg/clouds/pulumi/aws/static_website.go +++ b/pkg/clouds/pulumi/aws/static_website.go @@ -174,15 +174,21 @@ func provisionStaticSite(input *StaticSiteInput) (*StaticSiteOutput, error) { checksum = time.Now().String() // fixme: implement own s3 uploader instead of aws s3 sync + // Pass static creds only when configured; otherwise inherit the ambient AWS + // default chain (OIDC web-identity / instance profile) for the s3 sync. + syncEnv := map[string]string{} + if input.Account.Region != "" { + syncEnv["AWS_DEFAULT_REGION"] = input.Account.Region + } + if input.Account.AccessKey != "" { + syncEnv["AWS_ACCESS_KEY_ID"] = input.Account.AccessKey + syncEnv["AWS_SECRET_ACCESS_KEY"] = input.Account.SecretAccessKey + } _, err = local.NewCommand(ctx, fmt.Sprintf("%s-sync", input.ServiceName), &local.CommandArgs{ - Create: sdk.Sprintf("aws s3 sync %s s3://%s", input.BundleDir, mainBucket.Bucket), - Update: sdk.Sprintf("aws s3 sync %s s3://%s", input.BundleDir, mainBucket.Bucket), - Triggers: sdk.ArrayInput(sdk.Array{sdk.String(checksum)}), - Environment: sdk.ToStringMap(map[string]string{ - "AWS_ACCESS_KEY_ID": input.Account.AccessKey, - "AWS_SECRET_ACCESS_KEY": input.Account.SecretAccessKey, - "AWS_DEFAULT_REGION": input.Account.Region, - }), + Create: sdk.Sprintf("aws s3 sync %s s3://%s", input.BundleDir, mainBucket.Bucket), + Update: sdk.Sprintf("aws s3 sync %s s3://%s", input.BundleDir, mainBucket.Bucket), + Triggers: sdk.ArrayInput(sdk.Array{sdk.String(checksum)}), + Environment: sdk.ToStringMap(syncEnv), }, sdk.DependsOn([]sdk.Resource{mainBucket, publicAccessBlock, ownershipControls, mainBucketPolicy})) if err != nil { return nil, errors.Wrapf(err, "failed to invoke aws s3 sync")