Skip to content

Add credential auto-refresh support for web_identity/STS chains#1051

Open
HaoXuAI wants to merge 2 commits into
duckdb:mainfrom
HaoXuAI:feature/credential-auto-refresh
Open

Add credential auto-refresh support for web_identity/STS chains#1051
HaoXuAI wants to merge 2 commits into
duckdb:mainfrom
HaoXuAI:feature/credential-auto-refresh

Conversation

@HaoXuAI

@HaoXuAI HaoXuAI commented Jun 10, 2026

Copy link
Copy Markdown

Summary

Adds credential auto-refresh support for web_identity and sts credential chains (e.g., IRSA on EKS). Without this, STS credentials expire after ~1 hour and Iceberg queries fail with ExpiredToken.

Problem

The iceberg extension was:

  1. Stripping refresh_info from the catalog secret when building per-table secrets
  2. Snapshotting static credentials for SigV4 catalog API calls without triggering refresh

Even with duckdb-aws#136 (web_identity chain with auto-refresh) and duckdb-httpfs#165 (refresh in more locations), the iceberg extension bypassed the refresh path entirely.

Changes

sigv4.cpp / sigv4.hpp — Serialized credential refresh before SigV4 signing

Adds MaybeRefreshSecret() method to SIGV4Authorization that:

  • Checks if enough time has passed since the last refresh (300s interval, configurable via REFRESH_INTERVAL_SECONDS)
  • Acquires a mutex with try_to_lock — concurrent callers skip instead of blocking
  • Double-checks timing after acquiring the lock to avoid redundant refreshes
  • Reconstructs a CreateSecretInput from the secret's refresh_info struct and re-runs the credential chain via SecretManager::CreateSecret to get fresh STS tokens
  • On failure, skips updating the timestamp so the next request retries

The refresh_info is a STRUCT containing the original named parameters used to create the secret (e.g., chain, region, profile). These are stored by the aws extension when refresh='auto' is set (automatically for web_identity and sts chains).

iceberg_table_information.cpp — Propagate refresh_info to per-table secrets

  • Adds TryRefreshCatalogSecret() helper that mirrors httpfs's refresh logic using public DuckDB APIs (no cross-extension dependency)
  • Before snapshotting catalog credentials into per-table secrets, triggers a refresh to ensure fresh values
  • Stops stripping refresh_info so httpfs can auto-refresh per-table secrets during long-running data reads

Dependencies

  • duckdb-aws#136 (web_identity chain) — merged
  • duckdb-httpfs#165 (refresh in GLOBs/range reads) — open; our patch handles catalog API calls independently

Testing

  • Added test_web_identity_credential_refresh.test guarded by mode skip with environment documentation
  • Tested in production on EKS with IRSA — pods running 10+ hours with zero ExpiredToken errors, credentials refreshing transparently

@HaoXuAI
HaoXuAI force-pushed the feature/credential-auto-refresh branch 2 times, most recently from 39b711e to e2d667c Compare June 10, 2026 13:53

@Tmonster Tmonster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see some test for this, even if they are guarded by mode skip + a description of what kind of environment should be set up.

I think there is also a nicer way to handle the Vcpkg ports, I'll come back to this

refresh_input.storage_type = secret_entry->storage_mode;
refresh_input.scope = kv_secret_pre.GetScope();

auto child_count = StructType::GetChildCount(refresh_info.type());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you leave a comment as to what children are needed in a secret refresh? I can't think of any

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added a comment explaining that refresh_info is a STRUCT containing the original named parameters used to create the secret (e.g., chain, region, profile). These are stored by the aws extension when refresh='auto' is set. Re-passing them to SecretManager::CreateSecret re-runs the credential chain provider, which fetches fresh STS tokens.

//! Attempt to refresh the catalog's storage secret if it supports auto-refresh.
//! Rebuilds the secret via its original provider (e.g., web_identity/sts credential chain)
//! which fetches fresh STS tokens. No-op if the secret lacks refresh_info.
static bool TryRefreshStorageSecret(ClientContext &context, const SecretEntry &secret_entry) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be renamed to TryRefreshCatalogSecret?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — renamed to TryRefreshCatalogSecret.

@HaoXuAI
HaoXuAI force-pushed the feature/credential-auto-refresh branch 3 times, most recently from 77625e9 to b817338 Compare June 12, 2026 07:44

@Tmonster Tmonster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks good to me, are we worried at all that this might refresh the secret unnecessarily? I.e if are credentials are valid for an hour, we don't need to refresh every time we read the table within that hour right?

@HaoXuAI
HaoXuAI force-pushed the feature/credential-auto-refresh branch from b817338 to 89eb657 Compare June 16, 2026 06:48
@HaoXuAI

HaoXuAI commented Jun 16, 2026

Copy link
Copy Markdown
Author

@Tmonster actually the biggest problem for us it got into a race situation when multiple queries landed and all try to refresh the creds. And I'm trying to resolve this issue now

@Tmonster Tmonster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think having one sigv4 refresh for all sigv4 authenticated catalogs is a good idea. Can we make the refresh a per catalog thing?

Also, if you can figure out a way to add tests that would be awesome. I understand it will require some more setup, but if you could include the setup in some script unders scripts, you can guard your test with a mode skip


// Static member definitions — shared across all SIGV4Authorization instances
// AND TryRefreshStorageSecret (via GetRefreshMutex/GetLastRefreshTime accessors)
std::mutex SIGV4Authorization::refresh_mutex;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we need this shared across all SigV4Authorization instances? If I attach different sigv4 catalogs with different secrets, the refresh logic for both of them should not interfere

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the wording — the mutex and last_refresh_time are actually instance members of SIGV4Authorization (declared in sigv4.hpp), not static/shared. Each attached catalog gets its own SIGV4Authorization instance, so the refresh logic is already per-catalog and won't interfere between different catalogs with different secrets.

The #include <mutex> at the top of the header is just the standard library include — it doesn't make anything shared.


// Fast path: skip if refreshed recently
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - last_refresh_time).count();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we run the if we refreshed recently return check here and also in MaybeRefreshSecret? I think we can gfet away with only doing this once?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — these two serve different purposes but the duplication is confusing:

  1. MaybeRefreshSecret() in sigv4.cpp — called on every catalog API request, time-gated to 300s intervals. This is the hot path.
  2. TryRefreshCatalogSecret() in iceberg_table_information.cpp — called once during GetVendedCredentials() to ensure fresh creds are snapshotted into per-table secrets. This isn't called per-request.

I'll simplify by removing the time-gate from TryRefreshCatalogSecret (it doesn't need one since it's not in the hot path) and just keeping it as a simple "refresh if refresh_info exists" helper. The time-gating only matters in MaybeRefreshSecret where it's called on every request.

@HaoXuAI
HaoXuAI force-pushed the feature/credential-auto-refresh branch from b395689 to 9e37263 Compare June 26, 2026 22:42
HaoXuAI added 2 commits July 15, 2026 13:40
- Add per-catalog MaybeRefreshSecret() on SIGV4Authorization with:
  - Per-instance mutex (catalogs with different secrets don't interfere)
  - 300s time-gate (avoids refreshing on every request)
  - try_to_lock (concurrent callers skip instead of blocking)
- Call MaybeRefreshSecret before SigV4 signing in CreateAWSInput
- Call MaybeRefreshSecret before snapshotting credentials into per-table secrets
- Propagate refresh_info to per-table secrets for httpfs auto-refresh
Extends the existing scripts/sigv4_mock_server.py with an STS
AssumeRoleWithWebIdentity endpoint that issues rotating credentials
(MOCKKEY0, MOCKKEY1, ...) and a /refresh_count endpoint, reusing the
upstream mock instead of adding a second one.

The test verifies the SigV4 credential refresh is time-gated: it fires
on first use (ATTACH) and does not re-fetch on subsequent requests
within the 300s window. Guarded by require-env
CREDENTIAL_REFRESH_MOCK_AVAILABLE; source
scripts/credential_refresh_test_setup.sh to run.
@HaoXuAI
HaoXuAI force-pushed the feature/credential-auto-refresh branch from 9e37263 to 6926e1a Compare July 15, 2026 20:41
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