Hadoop SDK: Kerberos AP-REQ fails intermittently when static LoginUser is overwritten by another component
What happened
JuiceFileSystemImpl#buildAuthCredential and io.juicefs.kerberos.KerberosUtil#genApReq both derive the authenticating principal from UserGroupInformation.getLoginUser().
UserGroupInformation.loginUserRef is a static field on the UGI class. In a normal deployment, the class is loaded once by the AppClassLoader, so this static reference is shared across the entire JVM. Any component in the same JVM can overwrite it — for example via UserGroupInformation.loginUserFromSubject(new Subject()) — replacing the "real" login user with one whose Subject carries no KerberosTicket.
When the JuiceFS Hadoop SDK is later invoked in that JVM:
getLoginUser().hasKerberosCredentials()still returnstrue(becauseauthMethodis only an enum on theUserprincipal and isn't reset when Subject content changes), sobuildAuthCredentialroutes into the Kerberos branch and callsKerberosUtil.genApReq.- Inside
genApReq,loginUser.doAs(...)executes under a Subject that has no TGT, so JGSSKrb5InitCredential.getInstancefails withGSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt).
Stack trace (abridged):
java.lang.reflect.UndeclaredThrowableException at ...KerberosUtil.lambda$genApReq$0(KerberosUtil.java:...) at UserGroupInformation.doAs(...) at io.juicefs.kerberos.KerberosUtil.genApReq(...) at io.juicefs.JuiceFileSystemImpl.buildAuthCredential(...) Caused by: GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt) at sun.security.jgss.krb5.Krb5InitCredential.getInstance(...) at sun.security.jgss.GSSContextImpl.initSecContext(...)Reproduction
A very common trigger is a service that logs in its own principal via loginUserFromKeytabAndReturnUGI and then wraps all downstream work in catalogUgi.doAs(...). The private UGI stays in the current AccessControlContext (observable via getCurrentUser()), but never becomes the global LoginUser — so relying on getLoginUser() picks up the wrong identity.
Minimal reproducer:
java // (1) Some other component in the JVM has already installed a Subject-less UGI // as the global LoginUser. Anything that calls loginUserFromSubject() does this. UserGroupInformation.loginUserFromSubject(new javax.security.auth.Subject()); // getLoginUser() now returns a UGI whose Subject has no KerberosTicket, // but authMethod is still KERBEROS from an earlier login, // so hasKerberosCredentials() lies and returns true.
// (2) The application does its own private Kerberos login and drives all work // through doAs — the standard AndReturnUGI + doAs pattern. UserGroupInformation catalogUgi = UserGroupInformation.loginUserFromKeytabAndReturnUGI( "hadoop/host@REALM", "/path/to/host.keytab");
catalogUgi.doAs((PrivilegedExceptionAction ) () -> { FileSystem fs = FileSystem.get(new URI("jfs://vol1/"), conf); fs.listStatus(new Path("/")); // ← throws GSSException: No valid credentials provided return null; });Both UGIs are authMethod=KERBEROS and hasKerberosCredentials()=true, but only catalogUgi actually owns a TGT.
Why the code is fragile
Hadoop mainline itself does not use getLoginUser() to build the SASL/Kerberos identity for outbound RPCs. It uses getCurrentUser():
org.apache.hadoop.ipc.Client#setupIOstreamsputs the SASL handshake insideticket.doAs(...), whereticket = remoteId.getTicket() = getCurrentUser(), and switches toticket.getRealUser()if it is a PROXY user.org.apache.hadoop.security.SaslRpcClient#createSaslClientdecides "am I allowed to use KERBEROS?" withugi.getRealAuthenticationMethod().getAuthMethod() == KERBEROS, so PROXY users still route through Kerberos as long as their RealUser is Kerberos-authenticated.
getLoginUser() in Hadoop is only used to trigger relogin (e.g. handleSaslConnectionFailure), not to pick the authenticating identity.
The JuiceFS SDK's current use of getLoginUser() therefore diverges from Hadoop mainline and breaks whenever the static LoginUser doesn't match the true caller — which is the common case for services that use AndReturnUGI + doAs (Impala/Kyuubi/Hive Metastore/Ranger-embedded workloads, Flink UserCodeClassLoader isolation, etc.).
Proposed fix
Align the two entry points with Hadoop mainline:
- In
KerberosUtil.genApReq, derive the authenticating UGI fromgetCurrentUser(), fall back togetRealUser()when it is PROXY, and docheckTGTAndReloginFromKeytabagainst the UGI that actually owns the keytab. - In
JuiceFileSystemImpl.buildAuthCredential, judge Kerberos eligibility withgetRealAuthenticationMethod() == KERBEROS, and select delegation tokens fromgetCurrentUser().getCredentials()instead of thethis.ugicaptured atinitialize()time.
A PR implementing this is on the way and links back to this issue.
Scenarios verified end-to-end (against a Kerberized deployment)
| # | Scenario | Result |
|---|---|---|
| A | kinit + direct hadoop fs -ls (LoginUser == CurrentUser) |
✅ |
| B | createProxyUser(alice, hdfs).doAs(...) — CurrentUser is PROXY, RealUser is Kerberos |
✅ |
| C | loginUserFromKeytabAndReturnUGI + doAs with a clean LoginUser |
✅ |
| C2 | Same as C but with LoginUser deliberately poisoned via loginUserFromSubject(new Subject()) — reproduces the reported failure; passes with the fix |
✅ |
| C2' | Same as C2 but with kdestroy (no external ticket cache at all) — passes because authUgi.isFromKeytab() triggers the correct relogin path |
✅ |
| D | SIMPLE UGI with a JuiceFS DT (Spark executor style) | ✅ |
| D2 | Same as D with LoginUser also poisoned | ✅ |
No behavior change for callers that were already correct (LoginUser == CurrentUser).
Environment
- JuiceFS Hadoop SDK:
main(verified against commitc9a67b23) - Hadoop client: 3.x
- JDK: 8/11/17
- Deployment: KDC + keytab-based Kerberos, running the SDK from inside a multi-tenant JVM (service embeds its own
loginUserFromKeytabAndReturnUGI + doAsflow)
Source: juicedata/juicefs