aws/credentials

Credentials and credential providers.

A Provider is a thin record wrapping a fetch function; providers compose into a chain that returns the first success and reports every attempt when it exhausts. The actual provider implementations (static, env, profile, IMDS, ECS, STS web identity, SSO, process) live in aws/internal/providers/* and are surfaced through builder functions on this module.

The same Credentials value flows into SigV4 signing; the signer ignores the expiry/source metadata that’s relevant only to the chain.

Types

An AWS credentials triple plus optional expiry and provenance.

  • expires_at is unix seconds since epoch. None means non-expiring (typical for static or environment credentials).
  • source records the provider that produced the credentials, useful for logging and for debugging chain selection.
pub type Credentials {
  Credentials(
    access_key_id: String,
    secret_access_key: String,
    session_token: option.Option(String),
    expires_at: option.Option(Int),
    source: String,
  )
}

Constructors

  • Credentials(
      access_key_id: String,
      secret_access_key: String,
      session_token: option.Option(String),
      expires_at: option.Option(Int),
      source: String,
    )

A credential provider. Library code threads Provider values around the way it would thread a Box<dyn ProvideCredentials> in Rust or a CredentialsProvider interface in Go — the call site doesn’t care how the credentials get sourced.

pub type Provider {
  Provider(
    name: String,
    fetch: fn() -> Result(Credentials, ProviderError),
  )
}

Constructors

Why a single provider failed. The chain collects one of these per provider it tried, then bundles them in ChainExhausted if none succeeded.

pub type ProviderError {
  NotConfigured(reason: String)
  FetchFailed(reason: String)
  ChainExhausted(attempts: List(#(String, ProviderError)))
}

Constructors

  • NotConfigured(reason: String)

    Provider is not configured for this environment (e.g. env vars unset, IMDS not reachable). Distinct from an actual fetch failure so the chain can keep going without surfacing a noisy error.

  • FetchFailed(reason: String)

    Provider was configured and tried to fetch, but failed (e.g. HTTP error from IMDS, malformed credentials file, STS rejected the request).

  • ChainExhausted(attempts: List(#(String, ProviderError)))

    Every provider in the chain failed. Carries the per-provider attempt log so callers can see which providers were tried in what order and why each one declined.

Values

pub fn chain(providers: List(Provider)) -> Provider

Compose providers into a single provider that walks them in order and returns the first success. If every provider fails, the resulting error is ChainExhausted with one (name, error) entry per attempt in the order they were tried — useful for debugging “why didn’t my IMDS creds get picked up?” without having to instrument each provider individually.

pub fn default_chain(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  profile profile: String,
) -> Provider

Standard AWS credential-provider chain, in the precedence order other AWS SDKs use:

  1. Environment variables (AWS_ACCESS_KEY_ID and friends)
  2. AssumeRoleWithWebIdentity / IRSA (AWS_WEB_IDENTITY_TOKEN_FILE)
  3. SSO session, via ~/.aws/config + the cached SSO token
  4. Shared credentials file (~/.aws/credentials)
  5. credential_process from the named profile
  6. aws configure export-credentials (covers Identity Center / SSO sessions and other CLI-only auth flows when the native providers don’t recognise the profile shape)
  7. ECS container metadata (AWS_CONTAINER_CREDENTIALS_*_URI)
  8. EC2 IMDSv2

The returned Provider is the bare chain — it does not cache. Wrap it in aws/internal/credentials_cache.start_default to get the cache + refresh behaviour every long-running process wants.

profile selects which profile name is used by the profile, SSO, and credential_process branches (they all share the AWS-CLI profile concept). Pass "default" to mimic the AWS CLI’s default behaviour.

pub fn default_chain_with(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  imds_send imds_send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  profile profile: String,
  env env: fn(String) -> Result(String, Nil),
  read_file read_file: fn(String) -> Result(String, Nil),
  runner runner: fn(String, List(String)) -> Result(
    #(Int, BitArray),
    Nil,
  ),
) -> Provider

Injectable variant of default_chain. Every OS-touching seam (env-var lookup, file reading, OS-process spawning, HTTP send) is a parameter, so a test can drive the chain end-to-end without touching real env or filesystem.

send is the HTTP transport used by web-identity, SSO, and ECS; imds_send is the short-timeout variant used by IMDS specifically — they’re separate arguments because the production wiring picks distinct senders for them.

pub fn fetch(
  provider: Provider,
) -> Result(Credentials, ProviderError)

Run a provider and return whatever it produced.

pub fn from_assume_role(
  source source: Provider,
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  region region: String,
  role_arn role_arn: String,
  role_session_name role_session_name: String,
  external_id external_id: option.Option(String),
) -> Provider

Provider that wraps a source provider with an STS AssumeRole call.

Fetch order on every call:

  1. The wrapped source provider resolves “outer” credentials.
  2. Those credentials sign a AssumeRole request to STS, which hands back temporary credentials for the target role.

region is what STS signs against; the global endpoint accepts any region so "us-east-1" is a safe default.

Use this when your profile carries a role_arn / source_profile chain, or when you need a programmatic assume-role hop without editing your shared config.

pub fn from_assume_role_with(
  source source: Provider,
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  region region: String,
  role_arn role_arn: String,
  role_session_name role_session_name: String,
  external_id external_id: option.Option(String),
  endpoint endpoint: String,
  duration_seconds duration_seconds: Int,
  timestamp timestamp: fn() -> String,
) -> Provider

Fully-explicit form — used by tests and callers that need a regional STS endpoint or a non-default session duration.

pub fn from_aws_cli(profile profile: String) -> Provider

Use the AWS CLI (aws configure export-credentials) to resolve credentials for a profile. Covers any auth flow the CLI supports — SSO, IRSA, login_session, anything we haven’t natively implemented yet.

The CLI’s --format process output is the same shape as credential_process (Version: 1, AccessKeyId, etc.), so this is effectively a thin wrapper that runs the right command and feeds the output through the existing credential_process decoder.

Specifically a deliberate alternative to a native login_session provider: the upstream Go SDK’s credentials/logincreds uses DPoP (RFC 9449) — every portal request needs a JWT signed with an ECDSA P-256 private key from the local cache. Implementing that natively would add JWK parsing, JWS signing, and a new crypto FFI, plus the cache file schema isn’t published outside the Go implementation. Until we take on that work, shelling out to the AWS CLI is the practical bridge.

Requires AWS CLI v2 (aws configure export-credentials was added in 2022). Returns NotConfigured if the binary isn’t on PATH or the profile doesn’t exist; FetchFailed if the CLI exits non-zero or emits malformed JSON.

pub fn from_aws_cli_with(
  profile profile: String,
  runner runner: fn(String, List(String)) -> Result(
    #(Int, BitArray),
    Nil,
  ),
) -> Provider

from_aws_cli with an injectable runner so tests don’t actually spawn aws.

pub fn from_ecs(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
) -> Provider

ECS / EKS / Fargate container metadata provider. Resolves the metadata URL from the standard environment variables (AWS_CONTAINER_- CREDENTIALS_FULL_URI takes precedence; otherwise AWS_CONTAINER_- CREDENTIALS_RELATIVE_URI is appended to http://169.254.170.2). The Authorization header value is read from AWS_CONTAINER_AUTHORIZATION_- TOKEN (or _TOKEN_FILE, if set instead).

If neither URI env var is set, the provider always returns NotConfigured so the chain falls through quietly.

pub fn from_ecs_with(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  url url: String,
  auth_token auth_token: option.Option(String),
) -> Provider

ECS provider with the URL and auth token supplied explicitly. Useful when the env-resolution logic isn’t a fit (e.g. a sidecar configures things programmatically).

pub fn from_ecs_with_env(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  lookup lookup: fn(String) -> Result(String, Nil),
  read_file read_file: fn(String) -> Result(String, Nil),
) -> Provider

Like from_ecs but with injectable env-var lookup and file reader so tests can drive the provider without mutating real OS state.

pub fn from_environment() -> Provider

Environment-variable provider using real OS env. Production default.

pub fn from_environment_with(
  lookup lookup: fn(String) -> Result(String, Nil),
) -> Provider

Environment-variable provider. Reads AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and (optionally) AWS_SESSION_TOKEN.

lookup is injected so tests can drive the provider with a fixed map instead of mutating real process env. Use from_environment for the default production wiring.

pub fn from_imds(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
) -> Provider

IMDSv2 credentials provider. Performs the standard PUT-token / GET-role / GET-creds dance against the link-local metadata endpoint at http://169.254.169.254 and parses the JSON credentials response.

Failure of step 1 (the token PUT) is treated as NotConfigured so the chain quietly falls through to the next provider when we’re not on EC2 or Lambda. Failures past that point are FetchFailed.

send is the HTTP transport — pass aws/internal/http_send.default_send in production, or a stub in tests.

pub fn from_imds_with(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  endpoint endpoint: String,
  token_ttl_seconds token_ttl_seconds: Int,
) -> Provider

IMDSv2 provider with overridable endpoint and token TTL. Test stubs and fleet-specific deployments (e.g. when AWS_EC2_METADATA_SERVICE_ENDPOINT is set) use this form.

pub fn from_process(profile profile: String) -> Provider

Production credential-process provider. Reads credential_process from the named profile in ~/.aws/config (or ~/.aws/credentials for the [default] profile only — both files are checked).

pub fn from_process_with_command(
  command command: String,
) -> Provider

Credential-process provider. Runs the configured command and parses its stdout as the AWS credential-process JSON.

from_process_with_command takes a command line literally; tests and programmatic configs use this form.

pub fn from_process_with_env(
  profile profile: String,
  config_reader config_reader: fn() -> Result(String, Nil),
  credentials_reader credentials_reader: fn() -> Result(
    String,
    Nil,
  ),
  runner runner: fn(String, List(String)) -> Result(
    #(Int, BitArray),
    Nil,
  ),
) -> Provider

Injectable variant for tests.

pub fn from_process_with_runner(
  command command: String,
  runner runner: fn(String, List(String)) -> Result(
    #(Int, BitArray),
    Nil,
  ),
) -> Provider

Same shape but with an injectable runner so tests can drive scripted stdout/exit values without spawning real processes.

pub fn from_profile(name profile_name: String) -> Provider

Profile provider using the canonical default file paths (~/.aws/credentials + ~/.aws/config).

pub fn from_profile_assume_role(
  name profile_name: String,
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  region region: String,
) -> Provider

Profile provider that auto-chains via STS AssumeRole when the requested profile carries role_arn / source_profile. The source profile must hold static keys; multi-hop chains are deferred. Falls through to the same static-keys path as from_profile when role_arn is absent, so a single chain entry covers both forms.

pub fn from_profile_assume_role_with(
  name profile_name: String,
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  region region: String,
  endpoint endpoint: String,
  credentials_reader credentials_reader: fn() -> Result(
    String,
    Nil,
  ),
  config_reader config_reader: fn() -> Result(String, Nil),
  timestamp timestamp: fn() -> String,
) -> Provider

Fully-explicit form — used by tests and callers that need a regional STS endpoint, custom file readers, or a pinned timestamp source for the SigV4 signer.

pub fn from_profile_with(
  name profile_name: String,
  credentials_reader credentials_reader: fn() -> Result(
    String,
    Nil,
  ),
  config_reader config_reader: fn() -> Result(String, Nil),
) -> Provider

AWS shared credentials provider. Reads [profile_name] from both ~/.aws/credentials (section name: [name]) and ~/.aws/config (section name: [profile name], or [default] for the default profile). If a property is set in both files, the credentials file wins — that’s the AWS CLI convention. Either file may be missing.

Both readers are injected so tests can drive the provider with in-memory strings; from_profile plugs in real readers for the two canonical paths.

Errors:

  • both readers fail → NotConfigured (no AWS config on this host)
  • either file parses badly → FetchFailed (file exists but corrupt)
  • profile section absent from both files → NotConfigured
  • aws_access_key_id missing → NotConfigured (treat as “this profile isn’t a static-key profile; chain should keep going”)
  • aws_access_key_id present without aws_secret_access_key → FetchFailed
pub fn from_sso(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  profile profile: String,
) -> Provider

Production SSO provider. Reads the named profile from ~/.aws/config, pulls the cached SSO access token from ~/.aws/sso/cache/<sha1>.json, and exchanges it at the portal. The cache filename is sha1(session-or- start-url) per the AWS CLI convention.

pub fn from_sso_with(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  region region: String,
  account_id account_id: String,
  role_name role_name: String,
  access_token access_token: String,
) -> Provider

SSO credentials provider. Consumes a cached SSO access token (produced by aws sso login) and exchanges it at the portal for short-lived credentials.

from_sso_with is the explicit form — used by tests and by callers that resolve their own session / role configuration.

pub fn from_sso_with_endpoint(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  region region: String,
  account_id account_id: String,
  role_name role_name: String,
  access_token access_token: String,
  endpoint endpoint: String,
) -> Provider

Same shape as from_sso_with but with an overridable portal endpoint. Tests aim a stub server at the portal path and pass it here.

pub fn from_sso_with_env(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  profile profile: String,
  config_reader config_reader: fn() -> Result(String, Nil),
  cache_reader cache_reader: fn(String) -> Result(String, Nil),
) -> Provider

Injectable variant for tests.

pub fn from_web_identity(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
) -> Provider

IRSA / STS Web Identity provider. Reads the token from AWS_WEB_IDENTITY_TOKEN_FILE each fetch (IRSA rotates the file), reads AWS_ROLE_ARN once at construction, and POSTs to STS.

pub fn from_web_identity_with(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  endpoint endpoint: String,
  role_arn role_arn: String,
  role_session_name role_session_name: String,
  token_file token_file: String,
  duration_seconds duration_seconds: Int,
  read_file read_file: fn(String) -> Result(String, Nil),
) -> Provider

Fully-explicit variant — caller provides every parameter. Used by tests to point at a stub endpoint, and by callers who configure programmatically.

pub fn from_web_identity_with_env(
  send send: fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    http_send.HttpError,
  ),
  lookup lookup: fn(String) -> Result(String, Nil),
  read_file read_file: fn(String) -> Result(String, Nil),
) -> Provider

Injectable env / file reader variant for tests.

pub fn static_provider(credentials: Credentials) -> Provider

A provider that always returns the same hardcoded credentials. The primary use is tests and scripts where you have keys in hand; in production the chain pulls from env/profile/IMDS instead.

Search Document