Skip to content

AWS, S3 Signing: Fix leaked credentials when contacting multiple catalogs#14178

Merged
danielcweeks merged 8 commits into
apache:mainfrom
adutra:signer-http-client-fix
Nov 7, 2025
Merged

AWS, S3 Signing: Fix leaked credentials when contacting multiple catalogs#14178
danielcweeks merged 8 commits into
apache:mainfrom
adutra:signer-http-client-fix

Conversation

@adutra

@adutra adutra commented Sep 24, 2025

Copy link
Copy Markdown
Contributor

Fixes #14100.

@adutra

adutra commented Sep 24, 2025

Copy link
Copy Markdown
Contributor Author

FYI @nastra

sessionCache = newSessionCache(name, properties);
}

String oauth2ServerUri = properties.get(OAuth2Properties.OAUTH2_SERVER_URI);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My rationale here is: two auth sessions for 2 different catalogs, but with the same credential, would be wrongly conflated since they would share the same cache key.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

isn't that a separate issue from the signer one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's connected imo: in the original issue reported by @c-thiel, if both catalogs use the same credential we would still get a 401 from the second one without this change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this something we could reproduce in a small test?

sessionCache = newSessionCache(name, properties);
}

String oauth2ServerUri = properties.get(OAuth2Properties.OAUTH2_SERVER_URI);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this should be falling back to ResourcePaths.tokens() because this property is not mandatory

void standaloneTableSessionEmptyProperties() {
Map<String, String> properties = Map.of();
Map<String, String> properties =
Map.of(OAuth2Properties.OAUTH2_SERVER_URI, "https://auth-server.com/v1/token");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess this can be reverted, since the test says "empty properties" and we should be falling back to the tokens endpoint if the property isn't specified

Comment thread core/src/test/java/org/apache/iceberg/rest/auth/TestOAuth2Manager.java Outdated
@nastra nastra added this to the Iceberg 1.10.1 milestone Oct 1, 2025
}

String oauth2ServerUri =
properties.getOrDefault(OAuth2Properties.OAUTH2_SERVER_URI, ResourcePaths.tokens());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@nastra while adapting the Dremio AuthManager to this, I realized that there might be a better cache key:
the signer endpoint URI itself.

    String cacheKey = properties.getOrDefault("s3.signer.uri", properties.get(CatalogProperties.URI));
    
    if (config.token() != null) {
      return sessionCache.cachedSession(
          cacheKey, k -> newSessionFromAccessToken(config.token(), properties, parent));
    }

    if (config.credential() != null && !config.credential().isEmpty()) {
      return sessionCache.cachedSession(cacheKey, k -> newSessionFromTokenResponse(config, parent));
    }

WDYT? Caching by signer endpoint seems more natural to me, since that's what we want conceptually: one session for each signing catalog.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess the only issue with that is that we're leaking something s3-specific into this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

True. I'll leave the decision up to you 🙂

@danielcweeks danielcweeks Oct 3, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure that signer endpoint works in isolation. You can have two different tokens/sessions from two table loads and they're going to have the same sigher uri (the uri can be the same for all signing requests).

@adutra adutra Oct 6, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The signer endpoint could be just the first part of the key. The full cache key could be <uri>:<token> or <uri>:<credential>.

Comment thread core/src/main/java/org/apache/iceberg/rest/auth/OAuth2Manager.java
@adutra adutra force-pushed the signer-http-client-fix branch from e7c33df to 56b1aa7 Compare October 2, 2025 14:47
Comment thread core/src/main/java/org/apache/iceberg/rest/auth/OAuth2Manager.java Outdated
if (refreshClient == null) {
synchronized (this) {
if (refreshClient == null) {
refreshClient = sharedClient.withAuthSession(session);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
refreshClient = sharedClient.withAuthSession(session);
this.refreshClient = sharedClient.withAuthSession(session);
}

private AuthSessionCache getOrCreateSessionCache(Map<String, String> properties) {
AuthSessionCache cache = this.sessionCache;

Copy link
Copy Markdown
Contributor

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 simplified to

private AuthSessionCache sessionCache(Map<String, String> properties) {
    if (sessionCache == null) {
      synchronized (this) {
        if (sessionCache == null) {
          this.sessionCache = newSessionCache(name, properties);
        }
      }
    }

    return sessionCache;
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was trying to save one volatile read.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

also all places need to call sessionCache()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This synchronization is not needed when calling catalogSession().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did some simplification, PTAL again.

properties.getOrDefault(OAuth2Properties.OAUTH2_SERVER_URI, ResourcePaths.tokens());

if (config.token() != null) {
refreshClient(sharedClient, parent);

@nastra nastra Oct 2, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sorry I'm just realizing now that the refresh client and the cache is used differently than I was expecting, which actually makes just calling refreshClient(..) here weird (since I was assuming we would update all places to just call refreshClient(..) and that method would return the initialized client.
Let me think a bit about this


protected OAuth2Util.AuthSession newSessionFromTokenResponse(
AuthConfig config, OAuth2Util.AuthSession parent) {
RESTClient client = refreshClient;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: seems like an unrelated change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Was just trying to avoid 2 volatile reads while 1 is enough. I will revert.

}

@Value.Lazy
public String endpointUri() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why aren't we just changing the endpoint() behavior? It looks like it's now only being used by endpointUri(), so can't we just update how we return the endpoint?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This method is public, so I didn't want to change its semantics.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this actually changing the behavior though? We're just returning the endpoint constructed at request time rather than at creation time? I think the expectation is that whatever comes from the endpoint() call should reflect the url to be called.

If we think it's a significant behavior change, then we should deprecate the other so that we don't have two different paths for getting the endpoint (that's awkward to carry forward)

@adutra

adutra commented Oct 20, 2025

Copy link
Copy Markdown
Contributor Author

@danielcweeks PTAL again 🙏

@danielcweeks danielcweeks self-requested a review November 7, 2025 22:57
@danielcweeks danielcweeks merged commit 08d9ee0 into apache:main Nov 7, 2025
42 checks passed
@danielcweeks

Copy link
Copy Markdown
Contributor

Thanks @adutra !

@huaxingao

Copy link
Copy Markdown
Contributor

@adutra Could you please back-port this fix to 1.10.x? Thanks!

@adutra

adutra commented Nov 14, 2025

Copy link
Copy Markdown
Contributor Author

@huaxingao here it is: #14586

thomaschow pushed a commit to thomaschow/iceberg that referenced this pull request Jan 19, 2026
…logs (apache#14178)

* AWS, S3 Signing: Fix leaked credentials when contacting multiple catalogs

Fixes apache#14100.

* review

* make refresh client and auth cache volatile

* review

* review

* fix test

* revert

* review
talatuyarer pushed a commit to talatuyarer/iceberg that referenced this pull request Apr 1, 2026
…logs (apache#14178)

* AWS, S3 Signing: Fix leaked credentials when contacting multiple catalogs

Fixes apache#14100.

* review

* make refresh client and auth cache volatile

* review

* review

* fix test

* revert

* review
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

5 participants