JuliusBrussee/caveman · error

awscreds: returned incomplete credentials

Error message

awscreds: %s returned incomplete credentials

What it means

awscreds.fetch iterates its credential sources (env, web identity, container, IMDS) and returns the first result. If a source returns a non-nil result whose awssig.Credentials fail Valid() — i.e. missing AccessKeyID, SecretAccessKey, or SessionToken where required — the whole fetch fails with this error instead of returning half-usable credentials.

Solutions

  1. Check which source produced it — the %s verb names it (web identity, container, IMDS) — and inspect that source's raw response
  2. Verify environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (and AWS_SESSION_TOKEN if using session creds) are all set and non-empty
  3. Test the metadata endpoint directly (169.254.169.254 / AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) from the same host; a proxy or firewall may be returning junk
  4. Prefer an explicit, complete source: set full static creds in env or use a valid web identity token file so a healthy source wins

Example fix

// before
creds, err := awscreds.Credentials(ctx, p) // "awscreds: imds returned incomplete credentials"
// after
if os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
    // fix partial env config before calling
    log.Fatal("set both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY")
}
creds, err := awscreds.Credentials(ctx, p)
Defensive patterns

Strategy: fallback

Validate before calling

// sanity-check env creds before relying on the chain
id, sec := os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY")
if id != "" && sec == "" { return errors.New("AWS_SECRET_ACCESS_KEY missing while AWS_ACCESS_KEY_ID set") }

Type guard

func credsComplete(err error) bool {
    return err != nil && !strings.Contains(err.Error(), "returned incomplete credentials")
}

Try / catch

creds, err := awscreds.Credentials(ctx, p)
if err != nil {
    if strings.Contains(err.Error(), "incomplete credentials") {
        // fall back to an explicit static/profile provider
        creds = staticCredsFromProfile("default")
    }
    if creds == nil { return fmt.Errorf("aws credentials: %w", err) }
}

Prevention

When it happens

Trigger: Credentials() -> fetch() -> some source (e.g. fromWebIdentity, fromContainer, fromIMDS) parses a response and builds a result whose creds.Valid() is false: empty access key ID, empty secret, or a required session token missing (e.g. role-credential responses missing SessionToken).

Common situations: An IMDS/container metadata endpoint returns malformed or partial JSON; the STS AssumeRoleWithWebIdentity XML parse yields empty fields; a corporate proxy intercepts metadata requests and returns stub data; env vars set partially (only AWS_ACCESS_KEY_ID set, no secret).

Related errors


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

Appendix: source

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

type result struct {
	creds   awssig.Credentials
	expires time.Time
	source  string
}

func (p *Provider) fetch(ctx context.Context) (*result, error) {
	for _, source := range []func(context.Context) (*result, error){
		p.fromEnv, p.fromWebIdentity, p.fromContainer, p.fromIMDS,
	} {
		res, err := source(ctx)
		if err != nil {
			return nil, err
		}
		if res == nil {
			continue
		}
		if !res.creds.Valid() {
			return nil, fmt.Errorf("awscreds: %s returned incomplete credentials", res.source)
		}
		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
	}

View on GitHub (pinned to 3ee70a1026)