Skip to content

fix(aws): send x-amz-security-token so temporary STS credentials work - #120

Open
whatticklesyou wants to merge 3 commits into
erickochen:masterfrom
whatticklesyou:aws-session-token
Open

fix(aws): send x-amz-security-token so temporary STS credentials work#120
whatticklesyou wants to merge 3 commits into
erickochen:masterfrom
whatticklesyou:aws-session-token

Conversation

@whatticklesyou

Copy link
Copy Markdown

Problem

AWS EC2 sync fails with AWS EC2: Authentication failed. Check your API token. for anyone whose credentials come from STS — IAM Identity Center (SSO), assume-role, get-session-token or any corporate federation wrapper. The request is signed and sent; EC2 rejects it.

The cause is in src/providers/aws.rs:

  1. AwsCredentials has only access_key and secret_key. There is no field for a session token.
  2. parse_credentials matches aws_access_key_id and aws_secret_access_key and drops everything else via _ => {}, so aws_session_token in ~/.aws/credentials is silently discarded. test_parse_credentials_extra_keys_ignored currently asserts this behaviour.
  3. sign_request hardcodes signed_headers = "host;x-amz-date" and ec2_get sends only Authorization and x-amz-date.

SigV4 with temporary credentials requires the session token to be sent as x-amz-security-token and included in the canonical request and SignedHeaders. Without it AWS cannot validate the signature, so every ASIA... credential fails 100% of the time regardless of expiry, region or IAM policy. Only long-lived AKIA keys work today — which is exactly the credential type most organisations prohibit.

Reproduce: configure the AWS provider with a profile whose credentials came from aws sts assume-role or SSO, then purple --verbose sync.

Fix

  • Add session_token: Option<String> to AwsCredentials.
  • Read aws_session_token in parse_credentials.
  • Read AWS_SESSION_TOKEN in the env-var fallback, via a new Env::aws_session_token() accessor alongside the existing Env::aws_credentials().
  • Accept an optional third component in the inline token field: ACCESS_KEY_ID:SECRET_ACCESS_KEY[:SESSION_TOKEN]. Two-part tokens keep working unchanged; secret access keys are base64 and never contain :, so the split is unambiguous.
  • In sign_request, when a session token is present, add x-amz-security-token to the canonical headers and to SignedHeaders. Header order is preserved: canonical headers must be sorted by lowercase name, and x-amz-security-token sorts after x-amz-date.
  • In ec2_get, send the x-amz-security-token header when present.

The None branch is byte-identical to the previous behaviour, so signatures for long-lived AKIA credentials are unchanged.

Tests

  • test_parse_credentials_extra_keys_ignored reworded — it no longer uses aws_session_token as its example of an ignored key, and now asserts session_token == None for a profile without one.
  • test_parse_credentials_session_token — token is parsed from the profile.
  • test_resolve_credentials_token_with_session_token — three-part inline token.
  • test_sign_request_includes_security_token_when_presentSignedHeaders=host;x-amz-date;x-amz-security-token.
  • test_sign_request_session_token_changes_signature — the token actually enters the signature rather than just being appended as a header.

Existing sign_request tests updated with session_token: None and still assert SignedHeaders=host;x-amz-date.

Notes

  • Docs: the AWS section of the Cloud Providers wiki page lists the auth options; --token AKID:SECRET there could be updated to --token AKID:SECRET[:SESSION_TOKEN].
  • Worth considering separately: resolve_credentials returns early on if !profile.is_empty(), so a configured-but-unreadable profile fails without ever reaching the token or env branches. That is defensible, but it means the documented env-var fallback never applies to anyone with a profile configured, which is surprising in practice.
  • Unrelated but adjacent: temporary credentials expire, often within the hour. Once this lands, a failed sync on stale credentials will still surface as a generic auth error. A distinct message for expired-vs-invalid would save users a lot of guessing.

@erickochen erickochen self-assigned this Aug 4, 2026
@erickochen erickochen added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request labels Aug 4, 2026
@erickochen

Copy link
Copy Markdown
Owner

Nice catch. Clean fix too!

I checked the signature against botocore and it matches byte for byte, with and without a session token, including tokens containing /, + and =. The None branch is untouched, so existing AKIA keys sign exactly as before. Build, clippy and the AWS tests are green here.

Two things and it can go in:

  • cargo fmt --check fails on src/providers/aws_tests.rs:234. The shortened line in test_parse_credentials_extra_keys_ignored needs a reformat. The Format job runs exactly that command, so CI stays red until it is.
  • Nothing covers Env::aws_session_token() or the env var fallback in resolve_credentials. Both are new paths, so both want a test.

The docs and the message strings still advertise the two part token format. That is messages::TOKEN_FORMAT_AWS, hints::PROVIDER_TOKEN_AWS with its golden, llms.txt, docs/providers-api.md, the wiki plus the changelog bullet. Leave all of that to me, no need to touch it here.

Both notes in your description hold up. The early return on profile really does mean the env fallback never applies once a profile is configured. And an inline session token expires within the hour, so that path is mostly for a one-off CLI sync. The expired versus invalid error message is worth doing, happy to take that separately.

Thank you!

@whatticklesyou

Copy link
Copy Markdown
Author

Thanks for the review! Both fixed, pushed.

Formatting: the string in test_parse_credentials_extra_keys_ignored is wrapped now, matching test_parse_credentials_whitespace_handling above it. Shortening the line is what caused this — the original was long enough that rustfmt couldn't fit it even on its own line and left it alone.

Tests: aws_session_token_reads_env_var and aws_session_token_is_independent_of_key_pair in env.rs, plus test_resolve_credentials_env_with_session_token and ..._without_session_token for the fallback. Also added test_resolve_credentials_profile_shadows_env_session_token, which pins the early-return behaviour rather than changing it — if you ever revisit that ordering, the test will say so.

Leaving the docs and message strings to you. Happy to take the expired-vs-invalid error separately.

Also checked Env's manual Debug — it only renders variable names, so AWS_SESSION_TOKEN is redacted for free.

@whatticklesyou

Copy link
Copy Markdown
Author

Separate from this PR, but found while testing it.

purple provider add aws --regions eu-central-1 fails with No token provided. Use --token, --token-stdin, or set PURPLE_TOKEN env var. For AWS the token is one of three credential sources, and resolve_token in runtime/helpers.rs bails before --profile or the env fallback are ever considered. So the env-var path documented in the wiki can't be configured from the CLI at all — you have to supply a token you don't intend to use.

This matters more for temporary credentials than long-lived ones. --token is stored in ~/.purple/providers, so for an STS triple you'd be persisting something that expires within the hour and re-running provider add before every sync. --profile avoids that, but only if your setup writes ~/.aws/credentials — mine caches elsewhere and exposes credentials through aws configure export-credentials, so the env fallback would be the natural fit.

Possible fix: skip the token requirement for AWS when --profile is passed, or when neither is passed and the env vars are present. Happy to send a PR if you want it that way — didn't want to change CLI validation semantics without asking, since it's your design call rather than a clear bug.

A token written as ACCESS_KEY:SECRET: kept the trailing separator on the
secret key, so every request signed with a key the account never issued.
An empty third component now means no session token.

Cover the security token on the wire. One test asserts the
x-amz-security-token header reaches the endpoint for STS credentials,
another asserts it stays absent for long-lived keys. Both fail when the
header call in ec2_get is removed, which nothing caught before.

The profile-over-environment test now writes a real credentials file into
a tempdir and asserts every resolved field comes from it, so it fails if
that precedence ever flips.
@erickochen

Copy link
Copy Markdown
Owner

Pushed a commit to your branch instead of bouncing this back for a third round. Say the word if you would rather have had it as notes.

A mutation pass turned up three things:

  • The header send in ec2_get had no cover. I swapped it for a no-op and all 64 aws tests stayed green, which is a cheerful way to ship SignatureDoesNotMatch.
  • Two mockito tests now pin it on the wire, present for STS and absent for long-lived keys.
  • test_resolve_credentials_profile_shadows_env_session_token still passes with all three env vars deleted, so it was proving that a profile shadows nothing in particular.
  • It now writes a real credentials file into a tempdir, which also hands read_credentials_file its first run against an actual file.
  • AKID:SECRET: resolved the secret key to SECRET:. The stray separator rides into the signature, so AWS turns down a key that looks flawless in your config.

Precommit is 16 of 16 green on the result.

On your CLI find: reproduced, though --profile does survive. cli.rs:514 skips resolve_token for AWS once a profile is set, so that half already works.

The env fallback is the real hole. It goes deeper than the CLI: handler/provider.rs:982 turns down the same shape, so no surface can save a config with neither token nor profile.

Which means the wiki has been advertising an auth option you can only reach by hand-writing ~/.purple/providers. Send that PR, it is yours.

@erickochen erickochen added the good first issue Good for newcomers label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants