Skip to content

fix(core): freeze credentials before signing SageMaker bearer token - #6360

Merged
nargokul merged 1 commit into
aws:masterfrom
HollowTube:fix/token-generator-freeze-credentials
Sep 29, 2026
Merged

nargokul merged 1 commit into
aws:masterfrom
HollowTube:fix/token-generator-freeze-credentials

Conversation

@HollowTube

Copy link
Copy Markdown
Contributor

Issue

sagemaker.core.token_generator._generate_token intermittently produces bearer tokens that are rejected with InvalidSignature. It is a race between botocore's SigV4 signer and credential refresh.

Root cause

_generate_token passes botocore RefreshableCredentials straight into SigV4QueryAuth(...).add_auth(). add_auth reads the credential fields as three separate, non-atomic accesses — access_key (→ X-Amz-Credential), then token (→ X-Amz-Security-Token), then secret_key (→ the signing key). If a credential refresh lands between those reads, the presigned URL carries one version's access-key/token but a signature keyed on another version's secret, so verification fails. botocore's own RefreshableCredentials.access_key docstring warns to use get_frozen_credentials() for exactly this reason.

Fix

Snapshot the credentials with get_frozen_credentials() once, before constructing the signer. The snapshot is taken under botocore's refresh lock, so all three reads come from a single credential version.

Behavior-preserving: static (non-refreshable) Credentials also expose get_frozen_credentials() and return a coherent ReadOnlyCredentials, so their output is unchanged; the hasattr guard leaves any custom credential object without the method untouched.

Reproduction / validation (offline, no AWS)

A rotate-on-read RefreshableCredentials (expiry pinned inside botocore's advisory refresh window so every field access rotates) makes the race deterministic. On botocore 1.43.104:

mode before fix after fix
deterministic rotate-on-read (200 signs) 100% corrupt 0
96-thread concurrent (3840 signs) ~57.7% corrupt 0

Tests

Adds TestGenerateTokenFreezesCredentials to tests/unit/test_token_generator.py: a deterministic rotate-on-read regression (primary, CI-stable), a concurrency case, and an assertion that the signer is constructed with a single immutable ReadOnlyCredentials snapshot. Corruption is detected offline by decoding the presigned URL and recomputing the SigV4 signature with the secret paired to the embedded access key. Existing static-credential tests remain green (16 passed).

_generate_token passed botocore RefreshableCredentials straight into
SigV4QueryAuth.add_auth(), which reads access_key, token, and secret_key
as three separate, non-atomic property accesses. A credential refresh
landing mid-signature mixes key material from two versions, producing a
presigned URL whose signature fails verification with InvalidSignature
(intermittent under concurrency; ~58% of tokens in a 96-thread repro).

Snapshot the credentials with get_frozen_credentials() before signing so
all three reads come from a single credential version. The read is taken
under botocore's refresh lock. Static credentials also expose the method
and return a coherent snapshot, so the change is behavior-preserving for
non-refreshable credentials; a hasattr guard leaves any custom credential
object without the method unchanged.

Adds regression tests: a deterministic rotate-on-read case (100% corrupt
without the fix), a concurrency case, and a signer-snapshot assertion.
@nargokul
nargokul merged commit c0bc32f into aws:master Sep 29, 2026
22 of 26 checks passed

This branch had an error being deployed

1 failed deployment
manual-approval — fbcd84e3 Deployed Sep 29, 2026 by HollowTube via wait-for-approval #1581
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants