JuliusBrussee/caveman · error

awscreds: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must…

Error message

awscreds: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must both be set

What it means

fromEnv treats a half-configured static key pair as an error rather than silently skipping: if exactly one of AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY is set, the operator clearly intended those credentials, and signing with a pair missing one half is impossible. Only the env spelling is validated here.

Solutions

  1. Export both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY together
  2. Check your .env / deployment secret list so both keys are present and mounted
  3. If you only intended temporary role creds, unset the lone leftover variable so the chain falls through to web identity/container/IMDS

Example fix

// before
export AWS_ACCESS_KEY_ID=AKIA...
// after
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=XXXXXXXX
Defensive patterns

Strategy: validation

Validate before calling

id, sec := os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY")
if (id == "") != (sec == "") {
	return errors.New("set both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or neither")
}

Try / catch

creds, err := provider.Credentials(ctx)
if err != nil {
	if strings.Contains(err.Error(), "must both be set") {
		// log which var is missing and abort
	}
	return err
}

Prevention

When it happens

Trigger: Setting AWS_ACCESS_KEY_ID without AWS_SECRET_ACCESS_KEY (or vice versa) in the proxy environment and then resolving credentials via Credentials() → fetch → fromEnv.

Common situations: Partial .env files, secrets injected individually by an orchestrator where one secret failed to mount, shell rc files exporting only one var, copy-pasting only the access key ID.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/30c95ec204b7a188. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/awscreds/awscreds.go:290

		return res, nil
	}
	return nil, errors.New("awscreds: no AWS credentials found (env, web identity, container, IMDS)")
}

func (p *Provider) env(name string) string { return strings.TrimSpace(p.getenv(name)) }

// fromEnv reads static keys. A half-configured pair is an error, not a skip:
// the operator clearly meant to sign as these keys, and falling through would
// silently sign as whatever ambient role the host carries — a different
// principal, bill, and CloudTrail identity — with no disclosure. A lone
// AWS_SESSION_TOKEN is not a pair and does not trigger this.
func (p *Provider) fromEnv(context.Context) (*result, error) {
	access, secret := p.env("AWS_ACCESS_KEY_ID"), p.env("AWS_SECRET_ACCESS_KEY")
	if access == "" && secret == "" {
		return nil, nil
	}
	if access == "" || secret == "" {
		return nil, errors.New("awscreds: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must both be set")
	}
	return &result{
		creds: awssig.Credentials{
			AccessKeyID:     access,
			SecretAccessKey: secret,
			SessionToken:    p.env("AWS_SESSION_TOKEN"),
		},
		source: "env",
	}, nil
}

// fromWebIdentity implements the EKS IRSA / generic OIDC flow: exchange the
// projected service account token for role credentials at STS. The call is
// unsigned by definition — the token is the proof.
func (p *Provider) fromWebIdentity(ctx context.Context) (*result, error) {
	tokenFile, roleARN := p.env("AWS_WEB_IDENTITY_TOKEN_FILE"), p.env("AWS_ROLE_ARN")
	if tokenFile == "" || roleARN == "" {
		return nil, nil

View on GitHub (pinned to 3ee70a1026)