From 1d2ce4a20d1fb150b400ff9fcfaca256f7ca19ff Mon Sep 17 00:00:00 2001 From: Anatolii Vorona Date: Thu, 24 Sep 2026 19:57:08 +0200 Subject: [PATCH 1/6] Support the AWS default credential chain for S3 --- README.md | 8 +++-- cmd/cmd.go | 8 ++--- k8s/transfer.sh/README.md | 5 +++ k8s/transfer.sh/values.yaml | 3 +- server/storage/s3.go | 20 ++++++++---- server/storage/s3_test.go | 61 +++++++++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 server/storage/s3_test.go diff --git a/README.md b/README.md index b0d5fcce..bbd4256b 100644 --- a/README.md +++ b/README.md @@ -334,11 +334,15 @@ docker build -t transfer.sh-noroot --build-arg RUNAS=doesntmatter --build-arg PU For the usage with a AWS S3 Bucket, you just need to specify the following options: - provider `--provider s3` -- aws-access-key _(either via flag or environment variable `AWS_ACCESS_KEY`)_ -- aws-secret-key _(either via flag or environment variable `AWS_SECRET_KEY`)_ - bucket _(either via flag or environment variable `BUCKET`)_ - s3-region _(either via flag or environment variable `S3_REGION`)_ +Authentication uses the AWS SDK default credential chain. This supports sources such as +environment variables, shared AWS configuration files, ECS task roles, EC2 instance +profiles, and EKS IAM roles for service accounts (IRSA). To use transfer.sh's legacy +static credential options instead, set both `--aws-access-key` and `--aws-secret-key` +(or `AWS_ACCESS_KEY` and `AWS_SECRET_KEY`). + If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.
diff --git a/cmd/cmd.go b/cmd/cmd.go index 76a2e886..ef1f2f2d 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -484,13 +484,9 @@ func New() *Cmd { switch provider := c.String("provider"); provider { case "s3": - if accessKey := c.String("aws-access-key"); accessKey == "" { - return errors.New("access-key not set") - } else if secretKey := c.String("aws-secret-key"); secretKey == "" { - return errors.New("secret-key not set") - } else if bucket := c.String("bucket"); bucket == "" { + if bucket := c.String("bucket"); bucket == "" { return errors.New("bucket not set") - } else if store, err := storage.NewS3Storage(c.Context, accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { + } else if store, err := storage.NewS3Storage(c.Context, c.String("aws-access-key"), c.String("aws-secret-key"), bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { return err } else { options = append(options, server.UseStorage(store)) diff --git a/k8s/transfer.sh/README.md b/k8s/transfer.sh/README.md index eb4ad547..2da0ccac 100644 --- a/k8s/transfer.sh/README.md +++ b/k8s/transfer.sh/README.md @@ -46,6 +46,11 @@ persistence: Compatible with AWS S3 and any S3-compatible storage (MinIO, Ceph, etc.). +If no static credentials are configured, transfer.sh uses the AWS SDK default +credential chain. This supports EC2 instance profiles, ECS task roles, and EKS +IAM roles for service accounts (IRSA). For IRSA, annotate the chart's service +account with the role ARN and set `serviceAccount.automount: true`. + **Using a Kubernetes Secret (recommended):** ```bash diff --git a/k8s/transfer.sh/values.yaml b/k8s/transfer.sh/values.yaml index 4a234c98..339b5608 100644 --- a/k8s/transfer.sh/values.yaml +++ b/k8s/transfer.sh/values.yaml @@ -45,7 +45,8 @@ transfersh: pathStyle: false # Set to true to disable multipart uploads noMultipart: false - # Use an existing secret for AWS credentials + # Use an existing secret for static AWS credentials. If no credentials are set, + # the AWS SDK default credential chain is used (for example, EC2 roles or EKS IRSA). # Secret must contain AWS_ACCESS_KEY and AWS_SECRET_KEY keys existingSecret: "" # Or set credentials directly (stored in a chart-managed Secret) diff --git a/server/storage/s3.go b/server/storage/s3.go index 6cc6e0af..0b7872ff 100644 --- a/server/storage/s3.go +++ b/server/storage/s3.go @@ -180,15 +180,23 @@ func (s *S3Storage) Put(ctx context.Context, token string, filename string, read func (s *S3Storage) IsRangeSupported() bool { return true } func getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws.Config, error) { - return config.LoadDefaultConfig(ctx, - config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ + if (accessKey == "") != (secretKey == "") { + return aws.Config{}, errors.New("both AWS access key and secret key must be set") + } + + options := []func(*config.LoadOptions) error{ + config.WithRequestChecksumCalculation(aws.RequestChecksumCalculationWhenRequired), + config.WithResponseChecksumValidation(aws.ResponseChecksumValidationWhenRequired), + } + if accessKey != "" { + options = append(options, config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ Value: aws.Credentials{ AccessKeyID: accessKey, SecretAccessKey: secretKey, SessionToken: "", }, - }), - config.WithRequestChecksumCalculation(aws.RequestChecksumCalculationWhenRequired), - config.WithResponseChecksumValidation(aws.ResponseChecksumValidationWhenRequired), - ) + })) + } + + return config.LoadDefaultConfig(ctx, options...) } diff --git a/server/storage/s3_test.go b/server/storage/s3_test.go new file mode 100644 index 00000000..f7e06975 --- /dev/null +++ b/server/storage/s3_test.go @@ -0,0 +1,61 @@ +package storage + +import ( + "context" + "testing" +) + +func TestGetAwsConfigUsesStaticCredentialsWhenProvided(t *testing.T) { + t.Setenv("AWS_ACCESS_KEY_ID", "environment-access-key") + t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") + + cfg, err := getAwsConfig(context.Background(), "configured-access-key", "configured-secret-key") + if err != nil { + t.Fatalf("getAwsConfig returned an error: %v", err) + } + + credentials, err := cfg.Credentials.Retrieve(context.Background()) + if err != nil { + t.Fatalf("retrieve credentials: %v", err) + } + if credentials.AccessKeyID != "configured-access-key" || credentials.SecretAccessKey != "configured-secret-key" { + t.Fatalf("got credentials %q/%q, want configured static credentials", credentials.AccessKeyID, credentials.SecretAccessKey) + } +} + +func TestGetAwsConfigUsesDefaultCredentialChain(t *testing.T) { + t.Setenv("AWS_ACCESS_KEY_ID", "environment-access-key") + t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") + + cfg, err := getAwsConfig(context.Background(), "", "") + if err != nil { + t.Fatalf("getAwsConfig returned an error: %v", err) + } + + credentials, err := cfg.Credentials.Retrieve(context.Background()) + if err != nil { + t.Fatalf("retrieve credentials: %v", err) + } + if credentials.AccessKeyID != "environment-access-key" || credentials.SecretAccessKey != "environment-secret-key" { + t.Fatalf("got credentials %q/%q, want credentials from the default chain", credentials.AccessKeyID, credentials.SecretAccessKey) + } +} + +func TestGetAwsConfigRejectsPartialStaticCredentials(t *testing.T) { + testCases := []struct { + name string + accessKey string + secretKey string + }{ + {name: "missing secret key", accessKey: "access-key"}, + {name: "missing access key", secretKey: "secret-key"}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if _, err := getAwsConfig(context.Background(), testCase.accessKey, testCase.secretKey); err == nil { + t.Fatal("getAwsConfig returned no error for partial static credentials") + } + }) + } +} From e3700f33184dbca418f7120211ffdd4a1a3ea810 Mon Sep 17 00:00:00 2001 From: Anatolii Vorona Date: Thu, 24 Sep 2026 23:01:38 +0200 Subject: [PATCH 2/6] Add explicit S3 credential mode --- README.md | 13 ++-- cmd/cmd.go | 35 +++++++++- cmd/cmd_test.go | 78 +++++++++++++++++++++++ k8s/transfer.sh/README.md | 10 +-- k8s/transfer.sh/templates/configmap.yaml | 1 + k8s/transfer.sh/templates/deployment.yaml | 2 +- k8s/transfer.sh/templates/secret.yaml | 2 +- k8s/transfer.sh/values.yaml | 5 +- server/storage/s3_test.go | 6 +- 9 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 cmd/cmd_test.go diff --git a/README.md b/README.md index bbd4256b..2ea82b87 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ aws-secret-key | aws access key bucket | aws bucket | | BUCKET | s3-endpoint | Custom S3 endpoint. | | S3_ENDPOINT | s3-region | region of the s3 bucket | eu-west-1 | S3_REGION | +s3-credentials-type | S3 credential mode (`legacy` or `default-sdk-credential-chain`) | legacy | S3_CREDENTIALS_TYPE | s3-no-multipart | disables s3 multipart upload | false | S3_NO_MULTIPART | s3-path-style | Forces path style URLs, required for Minio. | false | S3_PATH_STYLE | storj-access | Access for the project | | STORJ_ACCESS | @@ -334,14 +335,16 @@ docker build -t transfer.sh-noroot --build-arg RUNAS=doesntmatter --build-arg PU For the usage with a AWS S3 Bucket, you just need to specify the following options: - provider `--provider s3` +- aws-access-key _(either via flag or environment variable `AWS_ACCESS_KEY`)_ +- aws-secret-key _(either via flag or environment variable `AWS_SECRET_KEY`)_ - bucket _(either via flag or environment variable `BUCKET`)_ - s3-region _(either via flag or environment variable `S3_REGION`)_ -Authentication uses the AWS SDK default credential chain. This supports sources such as -environment variables, shared AWS configuration files, ECS task roles, EC2 instance -profiles, and EKS IAM roles for service accounts (IRSA). To use transfer.sh's legacy -static credential options instead, set both `--aws-access-key` and `--aws-secret-key` -(or `AWS_ACCESS_KEY` and `AWS_SECRET_KEY`). +The `legacy` credential type is the default and requires both static credential options. +To use the AWS SDK default credential chain, explicitly set +`--s3-credentials-type default-sdk-credential-chain` (or +`S3_CREDENTIALS_TYPE=default-sdk-credential-chain`). The SDK chain supports environment credentials, shared AWS +configuration files, ECS task roles, EC2 instance profiles, and EKS IRSA. If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically. diff --git a/cmd/cmd.go b/cmd/cmd.go index ef1f2f2d..320592f9 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -17,6 +17,12 @@ import ( // Version is inject at build time var Version = "0.0.0" + +const ( + s3CredentialsTypeLegacy = "legacy" + s3CredentialsTypeDefaultSDKCredentialChain = "default-sdk-credential-chain" +) + var helpTemplate = `NAME: {{.Name}} - {{.Usage}} @@ -137,6 +143,12 @@ var globalFlags = []cli.Flag{ Value: "eu-west-1", EnvVars: []string{"S3_REGION"}, }, + &cli.StringFlag{ + Name: "s3-credentials-type", + Usage: "legacy|default-sdk-credential-chain", + Value: s3CredentialsTypeLegacy, + EnvVars: []string{"S3_CREDENTIALS_TYPE"}, + }, &cli.StringFlag{ Name: "aws-access-key", Usage: "", @@ -484,9 +496,13 @@ func New() *Cmd { switch provider := c.String("provider"); provider { case "s3": + accessKey, secretKey, err := resolveS3Credentials(c.String("s3-credentials-type"), c.String("aws-access-key"), c.String("aws-secret-key")) + if err != nil { + return err + } if bucket := c.String("bucket"); bucket == "" { return errors.New("bucket not set") - } else if store, err := storage.NewS3Storage(c.Context, c.String("aws-access-key"), c.String("aws-secret-key"), bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { + } else if store, err := storage.NewS3Storage(c.Context, accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { return err } else { options = append(options, server.UseStorage(store)) @@ -544,3 +560,20 @@ func New() *Cmd { App: app, } } + +func resolveS3Credentials(credentialsType, accessKey, secretKey string) (string, string, error) { + switch credentialsType { + case s3CredentialsTypeLegacy: + if accessKey == "" { + return "", "", errors.New("access-key not set") + } + if secretKey == "" { + return "", "", errors.New("secret-key not set") + } + return accessKey, secretKey, nil + case s3CredentialsTypeDefaultSDKCredentialChain: + return "", "", nil + default: + return "", "", fmt.Errorf("unsupported S3 credentials type %q", credentialsType) + } +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 00000000..88aaa0fc --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "testing" + + "github.com/urfave/cli/v2" +) + +func TestS3CredentialsTypeDefaultsToLegacy(t *testing.T) { + for _, flag := range globalFlags { + stringFlag, ok := flag.(*cli.StringFlag) + if !ok || stringFlag.Name != "s3-credentials-type" { + continue + } + if stringFlag.Value != s3CredentialsTypeLegacy { + t.Fatalf("s3-credentials-type default = %q, want %q", stringFlag.Value, s3CredentialsTypeLegacy) + } + return + } + + t.Fatal("s3-credentials-type flag not found") +} + +func TestResolveS3Credentials(t *testing.T) { + testCases := []struct { + name string + credentialsType string + accessKey string + secretKey string + wantAccessKey string + wantSecretKey string + wantError bool + }{ + { + name: "legacy credentials", + credentialsType: s3CredentialsTypeLegacy, + accessKey: "access-key", + secretKey: "secret-key", + wantAccessKey: "access-key", + wantSecretKey: "secret-key", + }, + { + name: "legacy credentials missing access key", + credentialsType: s3CredentialsTypeLegacy, + secretKey: "secret-key", + wantError: true, + }, + { + name: "legacy credentials missing secret key", + credentialsType: s3CredentialsTypeLegacy, + accessKey: "access-key", + wantError: true, + }, + { + name: "default SDK credential chain", + credentialsType: s3CredentialsTypeDefaultSDKCredentialChain, + accessKey: "ignored-access-key", + secretKey: "ignored-secret-key", + }, + { + name: "unsupported credentials type", + credentialsType: "unsupported", + wantError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + accessKey, secretKey, err := resolveS3Credentials(testCase.credentialsType, testCase.accessKey, testCase.secretKey) + if (err != nil) != testCase.wantError { + t.Fatalf("resolveS3Credentials() error = %v, wantError = %v", err, testCase.wantError) + } + if accessKey != testCase.wantAccessKey || secretKey != testCase.wantSecretKey { + t.Fatalf("resolveS3Credentials() = %q/%q, want %q/%q", accessKey, secretKey, testCase.wantAccessKey, testCase.wantSecretKey) + } + }) + } +} diff --git a/k8s/transfer.sh/README.md b/k8s/transfer.sh/README.md index 2da0ccac..5002cce5 100644 --- a/k8s/transfer.sh/README.md +++ b/k8s/transfer.sh/README.md @@ -46,10 +46,11 @@ persistence: Compatible with AWS S3 and any S3-compatible storage (MinIO, Ceph, etc.). -If no static credentials are configured, transfer.sh uses the AWS SDK default -credential chain. This supports EC2 instance profiles, ECS task roles, and EKS -IAM roles for service accounts (IRSA). For IRSA, annotate the chart's service -account with the role ARN and set `serviceAccount.automount: true`. +The `legacy` credential type is the default and requires static credentials. Set +`transfersh.s3.credentialsType: default-sdk-credential-chain` to use the AWS SDK +default credential chain with EC2 instance profiles, ECS task roles, or EKS IAM +roles for service accounts (IRSA). For IRSA, annotate the chart's service account +with the role ARN and set `serviceAccount.automount: true`. **Using a Kubernetes Secret (recommended):** @@ -320,6 +321,7 @@ gatewayApi: | `transfersh.randomTokenLength` | `6` | Length of the random token in file URLs | | `transfersh.local.basedir` | `/data` | Base directory for local storage | | `transfersh.s3.bucket` | `""` | S3 bucket name | +| `transfersh.s3.credentialsType` | `legacy` | Credential mode: `legacy` or `default-sdk-credential-chain` | | `transfersh.s3.region` | `eu-west-1` | S3 region | | `transfersh.s3.endpoint` | `""` | Custom S3 endpoint (MinIO, etc.) | | `transfersh.s3.pathStyle` | `false` | Force path-style URLs (required for MinIO) | diff --git a/k8s/transfer.sh/templates/configmap.yaml b/k8s/transfer.sh/templates/configmap.yaml index e5292813..617f6542 100644 --- a/k8s/transfer.sh/templates/configmap.yaml +++ b/k8s/transfer.sh/templates/configmap.yaml @@ -27,6 +27,7 @@ data: {{- with .Values.transfersh.s3 }} BUCKET: {{ .bucket | quote }} S3_REGION: {{ .region | quote }} + S3_CREDENTIALS_TYPE: {{ .credentialsType | quote }} {{- if .endpoint }} S3_ENDPOINT: {{ .endpoint | quote }} {{- end }} diff --git a/k8s/transfer.sh/templates/deployment.yaml b/k8s/transfer.sh/templates/deployment.yaml index 8f960c1b..09d63777 100644 --- a/k8s/transfer.sh/templates/deployment.yaml +++ b/k8s/transfer.sh/templates/deployment.yaml @@ -53,7 +53,7 @@ spec: {{- $s3 := .Values.transfersh.s3 }} {{- $storj := .Values.transfersh.storj }} {{- $httpAuth := .Values.transfersh.httpAuth }} - {{- $s3Creds := and (eq .Values.transfersh.provider "s3") (or $s3.existingSecret $s3.accessKey $s3.secretKey) }} + {{- $s3Creds := and (eq .Values.transfersh.provider "s3") (eq $s3.credentialsType "legacy") (or $s3.existingSecret $s3.accessKey $s3.secretKey) }} {{- $storjCreds := and (eq .Values.transfersh.provider "storj") (or $storj.existingSecret $storj.access) }} {{- $httpAuthCreds := and $httpAuth.enabled (not $httpAuth.htpasswd) }} {{- if or $s3Creds $storjCreds $httpAuthCreds }} diff --git a/k8s/transfer.sh/templates/secret.yaml b/k8s/transfer.sh/templates/secret.yaml index 09bd1eba..c4f04a31 100644 --- a/k8s/transfer.sh/templates/secret.yaml +++ b/k8s/transfer.sh/templates/secret.yaml @@ -1,7 +1,7 @@ {{- $data := dict }} {{- if eq .Values.transfersh.provider "s3" }} {{- with .Values.transfersh.s3 }} -{{- if and (not .existingSecret) (or .accessKey .secretKey) }} +{{- if and (eq .credentialsType "legacy") (not .existingSecret) (or .accessKey .secretKey) }} {{- $_ := set $data "AWS_ACCESS_KEY" .accessKey }} {{- $_ := set $data "AWS_SECRET_KEY" .secretKey }} {{- end }} diff --git a/k8s/transfer.sh/values.yaml b/k8s/transfer.sh/values.yaml index 339b5608..a41d842e 100644 --- a/k8s/transfer.sh/values.yaml +++ b/k8s/transfer.sh/values.yaml @@ -37,6 +37,8 @@ transfersh: # -- S3 storage settings (provider: s3) s3: bucket: "" + # Credential mode: legacy or default-sdk-credential-chain + credentialsType: "legacy" # Choose what region your provider is region: "eu-west-1" # Custom endpoint for S3-compatible storage (MinIO, etc.) @@ -45,8 +47,7 @@ transfersh: pathStyle: false # Set to true to disable multipart uploads noMultipart: false - # Use an existing secret for static AWS credentials. If no credentials are set, - # the AWS SDK default credential chain is used (for example, EC2 roles or EKS IRSA). + # Use an existing secret for static AWS credentials # Secret must contain AWS_ACCESS_KEY and AWS_SECRET_KEY keys existingSecret: "" # Or set credentials directly (stored in a chart-managed Secret) diff --git a/server/storage/s3_test.go b/server/storage/s3_test.go index f7e06975..438e20e7 100644 --- a/server/storage/s3_test.go +++ b/server/storage/s3_test.go @@ -8,6 +8,7 @@ import ( func TestGetAwsConfigUsesStaticCredentialsWhenProvided(t *testing.T) { t.Setenv("AWS_ACCESS_KEY_ID", "environment-access-key") t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") + t.Setenv("AWS_SESSION_TOKEN", "environment-session-token") cfg, err := getAwsConfig(context.Background(), "configured-access-key", "configured-secret-key") if err != nil { @@ -18,7 +19,7 @@ func TestGetAwsConfigUsesStaticCredentialsWhenProvided(t *testing.T) { if err != nil { t.Fatalf("retrieve credentials: %v", err) } - if credentials.AccessKeyID != "configured-access-key" || credentials.SecretAccessKey != "configured-secret-key" { + if credentials.AccessKeyID != "configured-access-key" || credentials.SecretAccessKey != "configured-secret-key" || credentials.SessionToken != "" { t.Fatalf("got credentials %q/%q, want configured static credentials", credentials.AccessKeyID, credentials.SecretAccessKey) } } @@ -26,6 +27,7 @@ func TestGetAwsConfigUsesStaticCredentialsWhenProvided(t *testing.T) { func TestGetAwsConfigUsesDefaultCredentialChain(t *testing.T) { t.Setenv("AWS_ACCESS_KEY_ID", "environment-access-key") t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") + t.Setenv("AWS_SESSION_TOKEN", "environment-session-token") cfg, err := getAwsConfig(context.Background(), "", "") if err != nil { @@ -36,7 +38,7 @@ func TestGetAwsConfigUsesDefaultCredentialChain(t *testing.T) { if err != nil { t.Fatalf("retrieve credentials: %v", err) } - if credentials.AccessKeyID != "environment-access-key" || credentials.SecretAccessKey != "environment-secret-key" { + if credentials.AccessKeyID != "environment-access-key" || credentials.SecretAccessKey != "environment-secret-key" || credentials.SessionToken != "environment-session-token" { t.Fatalf("got credentials %q/%q, want credentials from the default chain", credentials.AccessKeyID, credentials.SecretAccessKey) } } From fe9084e3ea7361c56011f2db81c095b40e0c8e36 Mon Sep 17 00:00:00 2001 From: Anatolii Vorona Date: Fri, 25 Sep 2026 23:08:06 +0200 Subject: [PATCH 3/6] Address S3 credential review feedback --- cmd/cmd.go | 28 ++------------------ cmd/cmd_test.go | 56 --------------------------------------- server/storage/s3.go | 28 ++++++++++++++------ server/storage/s3_test.go | 16 +++++++---- 4 files changed, 33 insertions(+), 95 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 320592f9..1f7deaf8 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -18,10 +18,7 @@ import ( // Version is inject at build time var Version = "0.0.0" -const ( - s3CredentialsTypeLegacy = "legacy" - s3CredentialsTypeDefaultSDKCredentialChain = "default-sdk-credential-chain" -) +const s3CredentialsTypeLegacy = "legacy" var helpTemplate = `NAME: {{.Name}} - {{.Usage}} @@ -496,13 +493,9 @@ func New() *Cmd { switch provider := c.String("provider"); provider { case "s3": - accessKey, secretKey, err := resolveS3Credentials(c.String("s3-credentials-type"), c.String("aws-access-key"), c.String("aws-secret-key")) - if err != nil { - return err - } if bucket := c.String("bucket"); bucket == "" { return errors.New("bucket not set") - } else if store, err := storage.NewS3Storage(c.Context, accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { + } else if store, err := storage.NewS3Storage(c.Context, c.String("s3-credentials-type"), c.String("aws-access-key"), c.String("aws-secret-key"), bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { return err } else { options = append(options, server.UseStorage(store)) @@ -560,20 +553,3 @@ func New() *Cmd { App: app, } } - -func resolveS3Credentials(credentialsType, accessKey, secretKey string) (string, string, error) { - switch credentialsType { - case s3CredentialsTypeLegacy: - if accessKey == "" { - return "", "", errors.New("access-key not set") - } - if secretKey == "" { - return "", "", errors.New("secret-key not set") - } - return accessKey, secretKey, nil - case s3CredentialsTypeDefaultSDKCredentialChain: - return "", "", nil - default: - return "", "", fmt.Errorf("unsupported S3 credentials type %q", credentialsType) - } -} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 88aaa0fc..8f86e19b 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -20,59 +20,3 @@ func TestS3CredentialsTypeDefaultsToLegacy(t *testing.T) { t.Fatal("s3-credentials-type flag not found") } - -func TestResolveS3Credentials(t *testing.T) { - testCases := []struct { - name string - credentialsType string - accessKey string - secretKey string - wantAccessKey string - wantSecretKey string - wantError bool - }{ - { - name: "legacy credentials", - credentialsType: s3CredentialsTypeLegacy, - accessKey: "access-key", - secretKey: "secret-key", - wantAccessKey: "access-key", - wantSecretKey: "secret-key", - }, - { - name: "legacy credentials missing access key", - credentialsType: s3CredentialsTypeLegacy, - secretKey: "secret-key", - wantError: true, - }, - { - name: "legacy credentials missing secret key", - credentialsType: s3CredentialsTypeLegacy, - accessKey: "access-key", - wantError: true, - }, - { - name: "default SDK credential chain", - credentialsType: s3CredentialsTypeDefaultSDKCredentialChain, - accessKey: "ignored-access-key", - secretKey: "ignored-secret-key", - }, - { - name: "unsupported credentials type", - credentialsType: "unsupported", - wantError: true, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - accessKey, secretKey, err := resolveS3Credentials(testCase.credentialsType, testCase.accessKey, testCase.secretKey) - if (err != nil) != testCase.wantError { - t.Fatalf("resolveS3Credentials() error = %v, wantError = %v", err, testCase.wantError) - } - if accessKey != testCase.wantAccessKey || secretKey != testCase.wantSecretKey { - t.Fatalf("resolveS3Credentials() = %q/%q, want %q/%q", accessKey, secretKey, testCase.wantAccessKey, testCase.wantSecretKey) - } - }) - } -} diff --git a/server/storage/s3.go b/server/storage/s3.go index 0b7872ff..663eccf4 100644 --- a/server/storage/s3.go +++ b/server/storage/s3.go @@ -16,6 +16,11 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" ) +const ( + s3CredentialsTypeLegacy = "legacy" + s3CredentialsTypeDefaultSDKCredentialChain = "default-sdk-credential-chain" +) + // S3Storage is a storage backed by AWS S3 type S3Storage struct { Storage @@ -27,8 +32,8 @@ type S3Storage struct { } // NewS3Storage is the factory for S3Storage -func NewS3Storage(ctx context.Context, accessKey, secretKey, bucketName string, purgeDays int, region, endpoint string, disableMultipart bool, forcePathStyle bool, logger *log.Logger) (*S3Storage, error) { - cfg, err := getAwsConfig(ctx, accessKey, secretKey) +func NewS3Storage(ctx context.Context, credentialsType, accessKey, secretKey, bucketName string, purgeDays int, region, endpoint string, disableMultipart bool, forcePathStyle bool, logger *log.Logger) (*S3Storage, error) { + cfg, err := getAwsConfig(ctx, credentialsType, accessKey, secretKey) if err != nil { return nil, err } @@ -179,16 +184,20 @@ func (s *S3Storage) Put(ctx context.Context, token string, filename string, read func (s *S3Storage) IsRangeSupported() bool { return true } -func getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws.Config, error) { - if (accessKey == "") != (secretKey == "") { - return aws.Config{}, errors.New("both AWS access key and secret key must be set") - } - +func getAwsConfig(ctx context.Context, credentialsType, accessKey, secretKey string) (aws.Config, error) { options := []func(*config.LoadOptions) error{ config.WithRequestChecksumCalculation(aws.RequestChecksumCalculationWhenRequired), config.WithResponseChecksumValidation(aws.ResponseChecksumValidationWhenRequired), } - if accessKey != "" { + + switch credentialsType { + case s3CredentialsTypeLegacy: + if accessKey == "" { + return aws.Config{}, errors.New("access-key not set") + } + if secretKey == "" { + return aws.Config{}, errors.New("secret-key not set") + } options = append(options, config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ Value: aws.Credentials{ AccessKeyID: accessKey, @@ -196,6 +205,9 @@ func getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws.Config, SessionToken: "", }, })) + case s3CredentialsTypeDefaultSDKCredentialChain: + default: + return aws.Config{}, fmt.Errorf("unsupported S3 credentials type %q", credentialsType) } return config.LoadDefaultConfig(ctx, options...) diff --git a/server/storage/s3_test.go b/server/storage/s3_test.go index 438e20e7..18a5dc2f 100644 --- a/server/storage/s3_test.go +++ b/server/storage/s3_test.go @@ -10,7 +10,7 @@ func TestGetAwsConfigUsesStaticCredentialsWhenProvided(t *testing.T) { t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") t.Setenv("AWS_SESSION_TOKEN", "environment-session-token") - cfg, err := getAwsConfig(context.Background(), "configured-access-key", "configured-secret-key") + cfg, err := getAwsConfig(context.Background(), s3CredentialsTypeLegacy, "configured-access-key", "configured-secret-key") if err != nil { t.Fatalf("getAwsConfig returned an error: %v", err) } @@ -20,7 +20,7 @@ func TestGetAwsConfigUsesStaticCredentialsWhenProvided(t *testing.T) { t.Fatalf("retrieve credentials: %v", err) } if credentials.AccessKeyID != "configured-access-key" || credentials.SecretAccessKey != "configured-secret-key" || credentials.SessionToken != "" { - t.Fatalf("got credentials %q/%q, want configured static credentials", credentials.AccessKeyID, credentials.SecretAccessKey) + t.Fatalf("got credentials %q/%q, want configured static credentials and empty session token", credentials.AccessKeyID, credentials.SecretAccessKey) } } @@ -29,7 +29,7 @@ func TestGetAwsConfigUsesDefaultCredentialChain(t *testing.T) { t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") t.Setenv("AWS_SESSION_TOKEN", "environment-session-token") - cfg, err := getAwsConfig(context.Background(), "", "") + cfg, err := getAwsConfig(context.Background(), s3CredentialsTypeDefaultSDKCredentialChain, "ignored-access-key", "ignored-secret-key") if err != nil { t.Fatalf("getAwsConfig returned an error: %v", err) } @@ -39,7 +39,7 @@ func TestGetAwsConfigUsesDefaultCredentialChain(t *testing.T) { t.Fatalf("retrieve credentials: %v", err) } if credentials.AccessKeyID != "environment-access-key" || credentials.SecretAccessKey != "environment-secret-key" || credentials.SessionToken != "environment-session-token" { - t.Fatalf("got credentials %q/%q, want credentials from the default chain", credentials.AccessKeyID, credentials.SecretAccessKey) + t.Fatalf("got credentials %q/%q, want credentials from the default chain and session token", credentials.AccessKeyID, credentials.SecretAccessKey) } } @@ -55,9 +55,15 @@ func TestGetAwsConfigRejectsPartialStaticCredentials(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if _, err := getAwsConfig(context.Background(), testCase.accessKey, testCase.secretKey); err == nil { + if _, err := getAwsConfig(context.Background(), s3CredentialsTypeLegacy, testCase.accessKey, testCase.secretKey); err == nil { t.Fatal("getAwsConfig returned no error for partial static credentials") } }) } } + +func TestGetAwsConfigRejectsUnsupportedCredentialsType(t *testing.T) { + if _, err := getAwsConfig(context.Background(), "unsupported", "", ""); err == nil { + t.Fatal("getAwsConfig returned no error for unsupported credentials type") + } +} From edcd2fe9a2e67351e096f5f0ff094f0e5a5afca9 Mon Sep 17 00:00:00 2001 From: Anatolii Vorona Date: Fri, 25 Sep 2026 23:13:57 +0200 Subject: [PATCH 4/6] Remove unnecessary S3 comment change --- k8s/transfer.sh/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/transfer.sh/values.yaml b/k8s/transfer.sh/values.yaml index a41d842e..65a299c5 100644 --- a/k8s/transfer.sh/values.yaml +++ b/k8s/transfer.sh/values.yaml @@ -47,7 +47,7 @@ transfersh: pathStyle: false # Set to true to disable multipart uploads noMultipart: false - # Use an existing secret for static AWS credentials + # Use an existing secret for AWS credentials # Secret must contain AWS_ACCESS_KEY and AWS_SECRET_KEY keys existingSecret: "" # Or set credentials directly (stored in a chart-managed Secret) From cc2472ffa13a5e061486dfbddef0f9bffa1d6890 Mon Sep 17 00:00:00 2001 From: Anatolii Vorona Date: Fri, 25 Sep 2026 23:24:36 +0200 Subject: [PATCH 5/6] Centralize S3 credential type constants --- cmd/cmd.go | 4 +--- cmd/cmd_test.go | 5 +++-- server/storage/s3.go | 8 ++++---- server/storage/s3_test.go | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 1f7deaf8..c49ade58 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -18,8 +18,6 @@ import ( // Version is inject at build time var Version = "0.0.0" -const s3CredentialsTypeLegacy = "legacy" - var helpTemplate = `NAME: {{.Name}} - {{.Usage}} @@ -143,7 +141,7 @@ var globalFlags = []cli.Flag{ &cli.StringFlag{ Name: "s3-credentials-type", Usage: "legacy|default-sdk-credential-chain", - Value: s3CredentialsTypeLegacy, + Value: storage.S3CredentialsTypeLegacy, EnvVars: []string{"S3_CREDENTIALS_TYPE"}, }, &cli.StringFlag{ diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 8f86e19b..d3568288 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -3,6 +3,7 @@ package cmd import ( "testing" + "github.com/dutchcoders/transfer.sh/server/storage" "github.com/urfave/cli/v2" ) @@ -12,8 +13,8 @@ func TestS3CredentialsTypeDefaultsToLegacy(t *testing.T) { if !ok || stringFlag.Name != "s3-credentials-type" { continue } - if stringFlag.Value != s3CredentialsTypeLegacy { - t.Fatalf("s3-credentials-type default = %q, want %q", stringFlag.Value, s3CredentialsTypeLegacy) + if stringFlag.Value != storage.S3CredentialsTypeLegacy { + t.Fatalf("s3-credentials-type default = %q, want %q", stringFlag.Value, storage.S3CredentialsTypeLegacy) } return } diff --git a/server/storage/s3.go b/server/storage/s3.go index 663eccf4..fc4087ca 100644 --- a/server/storage/s3.go +++ b/server/storage/s3.go @@ -17,8 +17,8 @@ import ( ) const ( - s3CredentialsTypeLegacy = "legacy" - s3CredentialsTypeDefaultSDKCredentialChain = "default-sdk-credential-chain" + S3CredentialsTypeLegacy = "legacy" + S3CredentialsTypeDefault = "default-sdk-credential-chain" ) // S3Storage is a storage backed by AWS S3 @@ -191,7 +191,7 @@ func getAwsConfig(ctx context.Context, credentialsType, accessKey, secretKey str } switch credentialsType { - case s3CredentialsTypeLegacy: + case S3CredentialsTypeLegacy: if accessKey == "" { return aws.Config{}, errors.New("access-key not set") } @@ -205,7 +205,7 @@ func getAwsConfig(ctx context.Context, credentialsType, accessKey, secretKey str SessionToken: "", }, })) - case s3CredentialsTypeDefaultSDKCredentialChain: + case S3CredentialsTypeDefault: default: return aws.Config{}, fmt.Errorf("unsupported S3 credentials type %q", credentialsType) } diff --git a/server/storage/s3_test.go b/server/storage/s3_test.go index 18a5dc2f..c6fb0c60 100644 --- a/server/storage/s3_test.go +++ b/server/storage/s3_test.go @@ -10,7 +10,7 @@ func TestGetAwsConfigUsesStaticCredentialsWhenProvided(t *testing.T) { t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") t.Setenv("AWS_SESSION_TOKEN", "environment-session-token") - cfg, err := getAwsConfig(context.Background(), s3CredentialsTypeLegacy, "configured-access-key", "configured-secret-key") + cfg, err := getAwsConfig(context.Background(), S3CredentialsTypeLegacy, "configured-access-key", "configured-secret-key") if err != nil { t.Fatalf("getAwsConfig returned an error: %v", err) } @@ -29,7 +29,7 @@ func TestGetAwsConfigUsesDefaultCredentialChain(t *testing.T) { t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret-key") t.Setenv("AWS_SESSION_TOKEN", "environment-session-token") - cfg, err := getAwsConfig(context.Background(), s3CredentialsTypeDefaultSDKCredentialChain, "ignored-access-key", "ignored-secret-key") + cfg, err := getAwsConfig(context.Background(), S3CredentialsTypeDefault, "ignored-access-key", "ignored-secret-key") if err != nil { t.Fatalf("getAwsConfig returned an error: %v", err) } @@ -55,7 +55,7 @@ func TestGetAwsConfigRejectsPartialStaticCredentials(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if _, err := getAwsConfig(context.Background(), s3CredentialsTypeLegacy, testCase.accessKey, testCase.secretKey); err == nil { + if _, err := getAwsConfig(context.Background(), S3CredentialsTypeLegacy, testCase.accessKey, testCase.secretKey); err == nil { t.Fatal("getAwsConfig returned no error for partial static credentials") } }) From 389126ab6bee705746153989c07ba636bab47c40 Mon Sep 17 00:00:00 2001 From: Anatolii Vorona Date: Fri, 25 Sep 2026 23:30:48 +0200 Subject: [PATCH 6/6] Remove unrelated cmd formatting change --- cmd/cmd.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index c49ade58..d56bf922 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -17,7 +17,6 @@ import ( // Version is inject at build time var Version = "0.0.0" - var helpTemplate = `NAME: {{.Name}} - {{.Usage}}