This is an automated email from the ASF dual-hosted git repository.
ramackri pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new 7017225e6 RANGER-5654: Fix Solr audit dispatcher Kerberos TGT relogin
(No key to store) via keytab-aware JAAS relogin (#1030)
7017225e6 is described below
commit 7017225e604c3506b52db1d87c44325794204056
Author: Ramachandran Krishnan <[email protected]>
AuthorDate: Wed Jun 24 16:25:42 2026 +0530
RANGER-5654: Fix Solr audit dispatcher Kerberos TGT relogin (No key to
store) via keytab-aware JAAS relogin (#1030)
* RANGER-5654:Solr audit dispatcher fails to index after Kerberos TGT
relogin (No key to store) with default useTicketCache=true
Co-authored-by: Cursor <[email protected]>
* RANGER-5654: Restore site XML descriptions and document relogin recovery
Co-authored-by: Cursor <[email protected]>
* RANGER-5654: Drop AbstractKerberosUser change; config-only fix
Co-authored-by: Cursor <[email protected]>
* RANGER-5654: Keytab-aware JAAS relogin; restore useTicketCache=true
Replace config-only useTicketCache=false with in-place keytab relogin in
AbstractKerberosUser (agents-audit + security-admin). KerberosAction uses
performRelogin() on SecurityException retry. Revert shipped and docker site
XML to useTicketCache=true so the Java fix alone addresses "No key to store"
at TGT renewal. Add unit test for keytab relogin without logout.
Co-authored-by: Cursor <[email protected]>
* RANGER-5654: Wrap Kerberos javadoc and log lines for checkstyle
Keep LineLength within 80 characters in AbstractKerberosUser,
KerberosAction, and KerberosJAASConfigUser; no new checkstyle
violations versus the pre-change baseline in those files.
Co-authored-by: Cursor <[email protected]>
* RANGER-5654: Use single-line Kerberos relogin log statements
Co-authored-by: Cursor <[email protected]>
* RANGER-5654: Address KerberosJAASConfigUser PR review feedback
Cache useKeyTab in the constructor, rename helper to getBooleanOption,
and use a single return per method in both agents-audit and security-admin.
---------
Co-authored-by: ramk <[email protected]>
Co-authored-by: Cursor <[email protected]>
---
.../ranger/audit/utils/AbstractKerberosUser.java | 76 ++++++++++++++++++++--
.../apache/ranger/audit/utils/KerberosAction.java | 27 ++++++--
.../ranger/audit/utils/KerberosJAASConfigUser.java | 38 ++++++++++-
.../ranger/solr/krb/AbstractKerberosUser.java | 76 ++++++++++++++++++++--
.../org/apache/ranger/solr/krb/KerberosAction.java | 30 +++++++--
.../ranger/solr/krb/KerberosJAASConfigUser.java | 37 ++++++++++-
.../ranger/solr/krb/AbstractKerberosUserTest.java | 34 ++++++++++
7 files changed, 293 insertions(+), 25 deletions(-)
diff --git
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
index 47d16f31a..b78d64d5b 100644
---
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
+++
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
@@ -152,9 +152,16 @@ public <T> T doAs(final PrivilegedExceptionAction<T>
action) throws IllegalState
}
/**
- * Re-login a user from keytab if TGT is expired or is close to expiry.
+ * Proactively renew credentials when the TGT is missing or has passed
+ * {@link #TICKET_RENEW_WINDOW} (80%) of its lifetime.
*
- * @throws LoginException if an error happens performing the re-login
+ * <p>Called on every {@link KerberosAction#execute()} before the
privileged
+ * action runs, so long-lived Solr/Kafka dispatchers refresh before the TGT
+ * expires.
+ *
+ * @return {@code true} if relogin was performed, {@code false} if the TGT
+ * is still valid
+ * @throws LoginException if relogin fails
*/
@Override
public synchronized boolean checkTGTAndRelogin() throws LoginException {
@@ -172,10 +179,62 @@ public synchronized boolean checkTGTAndRelogin() throws
LoginException {
LOG.debug("Performing relogin for {}", getPrincipal());
+ performRelogin();
+
+ return true;
+ }
+
+ /**
+ * Renews JAAS credentials for proactive TGT refresh and for error
recovery.
+ *
+ * <p>With {@code useKeyTab=true} and {@code useTicketCache=true}
+ * (shipped Solr dispatcher default), the previous {@code logout();
login()}
+ * sequence at the 80% TGT renewal window leaves {@code Krb5LoginModule}
+ * with no encryption key for the ticket cache and fails with
+ * {@code "No key to store"}, stopping audit indexing until process
restart.
+ * For keytab-backed users, calling {@code loginContext.login()} on the
+ * existing {@link LoginContext} and {@link Subject} renews from the keytab
+ * without clearing credentials first. The {@link Subject} is preserved
+ * across relogin because other components may hold references to it.
+ *
+ * <p>Non-keytab principals, or callers that are not yet logged in, use the
+ * traditional {@code logout(); login()} path. In-place keytab relogin
+ * failures also fall back to that path.
+ *
+ * @throws LoginException if relogin fails
+ * @see KerberosJAASConfigUser#useKeytabRelogin()
+ */
+ public synchronized void performRelogin() throws LoginException {
+ if (useKeytabRelogin() && isLoggedIn() && loginContext != null) {
+ try {
+ // In-place keytab relogin without logout()
(useTicketCache=true).
+ loginContext.login();
+ loggedIn.set(true);
+
+ LOG.debug("Successful in-place keytab relogin for {}",
getPrincipal());
+
+ return;
+ } catch (LoginException e) {
+ LOG.warn("In-place keytab relogin failed for {}, falling back
to logout/login: {}", getPrincipal(), e.getMessage());
+ }
+ }
+
logout();
login();
+ }
- return true;
+ /**
+ * Whether {@link #performRelogin()} should renew from keytab in place (no
+ * {@code logout()} first). Default {@code false};
+ * {@link KerberosJAASConfigUser} returns {@code true} when JAAS
+ * {@code useKeyTab} is enabled. Gated on keytab capability, not on
+ * {@code useTicketCache}, because only keytab principals can safely skip
+ * logout.
+ *
+ * @return {@code true} to use in-place keytab relogin
+ */
+ protected boolean useKeytabRelogin() {
+ return false;
}
/**
@@ -197,11 +256,18 @@ public String toString() {
protected abstract LoginContext createLoginContext(Subject subject) throws
LoginException;
/**
- * Get the Kerberos TGT.
+ * Returns the Kerberos TGT from the JAAS {@link Subject}, if present.
*
- * @return the user's TGT or null if none was found
+ * @return the user's TGT, or {@code null} if the subject is uninitialized
+ * or has no TGT
*/
private synchronized KerberosTicket getTGT() {
+ // Subject is created lazily on first login(); null before that is
+ // expected.
+ if (subject == null) {
+ return null;
+ }
+
final Set<KerberosTicket> tickets =
subject.getPrivateCredentials(KerberosTicket.class);
for (KerberosTicket ticket : tickets) {
diff --git
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosAction.java
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosAction.java
index 4754b15d1..e53eb077c 100644
---
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosAction.java
+++
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosAction.java
@@ -45,6 +45,15 @@ public KerberosAction(final KerberosUser kerberosUser, final
PrivilegedException
Validate.notNull(this.logger);
}
+ /**
+ * Runs {@code action} as {@code kerberosUser}: lazy login, proactive TGT
+ * refresh at 80% lifetime ({@link KerberosUser#checkTGTAndRelogin()}),
+ * then the privileged action. On {@link SecurityException}, relogin once
via
+ * {@link AbstractKerberosUser#performRelogin()} (keytab-safe) and retry.
+ *
+ * @return the result of {@code action}
+ * @throws Exception if login, relogin, or the privileged action fails
+ */
public T execute() throws Exception {
T result;
@@ -59,14 +68,17 @@ public T execute() throws Exception {
}
}
- // check if we need to re-login, will only happen if re-login window
is reached (80% of TGT life)
+ // check if we need to re-login, will only happen if re-login window is
+ // reached (80% of TGT life)
try {
kerberosUser.checkTGTAndRelogin();
} catch (LoginException e) {
throw new Exception("Relogin check failed due to: " +
e.getMessage(), e);
}
- // attempt to execute the action, if an exception is caught attempt to
logout/login and retry
+ // On SecurityException, relogin and retry once. Use performRelogin()
+ // for AbstractKerberosUser so keytab clients skip logout first; bare
+ // logout/login reproduces "No key to store" when useTicketCache is
true.
try {
result = kerberosUser.doAs(action);
} catch (SecurityException se) {
@@ -74,12 +86,17 @@ public T execute() throws Exception {
logger.debug("", se);
try {
- kerberosUser.logout();
- kerberosUser.login();
+ if (kerberosUser instanceof AbstractKerberosUser) {
+ ((AbstractKerberosUser) kerberosUser).performRelogin();
+ } else {
+ kerberosUser.logout();
+ kerberosUser.login();
+ }
result = kerberosUser.doAs(action);
} catch (Exception e) {
- throw new Exception("Retrying privileged action failed due to:
" + e.getMessage(), e);
+ throw new Exception(
+ "Retrying privileged action failed due to: " +
e.getMessage(), e);
}
} catch (PrivilegedActionException pae) {
final Exception cause = pae.getException();
diff --git
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosJAASConfigUser.java
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosJAASConfigUser.java
index 149364d4b..92e6a2dfa 100644
---
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosJAASConfigUser.java
+++
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/KerberosJAASConfigUser.java
@@ -34,12 +34,16 @@
public class KerberosJAASConfigUser extends AbstractKerberosUser {
private static final Logger LOG =
LoggerFactory.getLogger(KerberosJAASConfigUser.class);
+ private static final String JAAS_USE_KEYTAB = "useKeyTab";
+
private final String configName;
private final Configuration config;
+ private final boolean optionUseKeyTab;
public KerberosJAASConfigUser(final String configName, final Configuration
config) {
- this.configName = configName;
- this.config = config;
+ this.configName = configName;
+ this.config = config;
+ this.optionUseKeyTab = getBooleanOption(JAAS_USE_KEYTAB);
}
@Override
@@ -60,6 +64,36 @@ public String getPrincipal() {
return ret;
}
+ /**
+ * Solr/Kafka outbound JAAS clients (audit dispatcher, plugin Solr
+ * destination) use {@code useKeyTab=true}. Opt those principals into
+ * in-place keytab relogin so shipped {@code useTicketCache=true} does not
+ * fail at TGT renewal with {@code "No key to store"}.
+ */
+ @Override
+ protected boolean useKeytabRelogin() {
+ return optionUseKeyTab;
+ }
+
+ private boolean getBooleanOption(String optionName) {
+ boolean ret = false;
+ AppConfigurationEntry[] entries =
config.getAppConfigurationEntry(configName);
+
+ if (entries != null) {
+ for (AppConfigurationEntry entry : entries) {
+ Object value = entry.getOptions().get(optionName);
+
+ if (value != null && Boolean.parseBoolean(value.toString())) {
+ ret = true;
+
+ break;
+ }
+ }
+ }
+
+ return ret;
+ }
+
@Override
protected LoginContext createLoginContext(Subject subject) throws
LoginException {
LOG.debug("==> KerberosJAASConfigUser.createLoginContext()");
diff --git
a/security-admin/src/main/java/org/apache/ranger/solr/krb/AbstractKerberosUser.java
b/security-admin/src/main/java/org/apache/ranger/solr/krb/AbstractKerberosUser.java
index f4bfb5b3f..22f6d1818 100644
---
a/security-admin/src/main/java/org/apache/ranger/solr/krb/AbstractKerberosUser.java
+++
b/security-admin/src/main/java/org/apache/ranger/solr/krb/AbstractKerberosUser.java
@@ -151,9 +151,16 @@ public <T> T doAs(final PrivilegedExceptionAction<T>
action) throws IllegalState
}
/**
- * Re-login a user from keytab if TGT is expired or is close to expiry.
+ * Proactively renew credentials when the TGT is missing or has passed
+ * {@link #TICKET_RENEW_WINDOW} (80%) of its lifetime.
*
- * @throws LoginException if an error happens performing the re-login
+ * <p>Called on every {@link KerberosAction#execute()} before the
privileged
+ * action runs, so long-lived Solr audit queries refresh before the TGT
+ * expires.
+ *
+ * @return {@code true} if relogin was performed, {@code false} if the TGT
+ * is still valid
+ * @throws LoginException if relogin fails
*/
@Override
public synchronized boolean checkTGTAndRelogin() throws LoginException {
@@ -171,10 +178,62 @@ public synchronized boolean checkTGTAndRelogin() throws
LoginException {
LOG.info("Performing relogin for {}", getPrincipal());
+ performRelogin();
+
+ return true;
+ }
+
+ /**
+ * Renews JAAS credentials for proactive TGT refresh and for error
recovery.
+ *
+ * <p>With {@code useKeyTab=true} and {@code useTicketCache=true}
+ * (shipped Solr dispatcher default), the previous {@code logout();
login()}
+ * sequence at the 80% TGT renewal window leaves {@code Krb5LoginModule}
+ * with no encryption key for the ticket cache and fails with
+ * {@code "No key to store"}, stopping audit indexing until process
restart.
+ * For keytab-backed users, calling {@code loginContext.login()} on the
+ * existing {@link LoginContext} and {@link Subject} renews from the keytab
+ * without clearing credentials first. The {@link Subject} is preserved
+ * across relogin because other components may hold references to it.
+ *
+ * <p>Non-keytab principals, or callers that are not yet logged in, use the
+ * traditional {@code logout(); login()} path. In-place keytab relogin
+ * failures also fall back to that path.
+ *
+ * @throws LoginException if relogin fails
+ * @see KerberosJAASConfigUser#useKeytabRelogin()
+ */
+ public synchronized void performRelogin() throws LoginException {
+ if (useKeytabRelogin() && isLoggedIn() && loginContext != null) {
+ try {
+ // In-place keytab relogin without logout()
(useTicketCache=true).
+ loginContext.login();
+ loggedIn.set(true);
+
+ LOG.info("Successful in-place keytab relogin for {}",
getPrincipal());
+
+ return;
+ } catch (LoginException e) {
+ LOG.warn("In-place keytab relogin failed for {}, falling back
to logout/login: {}", getPrincipal(), e.getMessage());
+ }
+ }
+
logout();
login();
+ }
- return true;
+ /**
+ * Whether {@link #performRelogin()} should renew from keytab in place (no
+ * {@code logout()} first). Default {@code false};
+ * {@link KerberosJAASConfigUser} returns {@code true} when JAAS
+ * {@code useKeyTab} is enabled. Gated on keytab capability, not on
+ * {@code useTicketCache}, because only keytab principals can safely skip
+ * logout.
+ *
+ * @return {@code true} to use in-place keytab relogin
+ */
+ protected boolean useKeytabRelogin() {
+ return false;
}
/**
@@ -193,11 +252,18 @@ public String toString() {
protected abstract LoginContext createLoginContext(Subject subject) throws
LoginException;
/**
- * Get the Kerberos TGT.
+ * Returns the Kerberos TGT from the JAAS {@link Subject}, if present.
*
- * @return the user's TGT or null if none was found
+ * @return the user's TGT, or {@code null} if the subject is uninitialized
+ * or has no TGT
*/
private synchronized KerberosTicket getTGT() {
+ // Subject is created lazily on first login(); null before that is
+ // expected.
+ if (subject == null) {
+ return null;
+ }
+
final Set<KerberosTicket> tickets =
subject.getPrivateCredentials(KerberosTicket.class);
for (KerberosTicket ticket : tickets) {
diff --git
a/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosAction.java
b/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosAction.java
index 9ed1d4484..d544175a4 100644
---
a/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosAction.java
+++
b/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosAction.java
@@ -45,6 +45,15 @@ public KerberosAction(final KerberosUser kerberosUser, final
PrivilegedException
Validate.notNull(this.logger);
}
+ /**
+ * Runs {@code action} as {@code kerberosUser}: lazy login, proactive TGT
+ * refresh at 80% lifetime ({@link KerberosUser#checkTGTAndRelogin()}),
+ * then the privileged action. On {@link SecurityException}, relogin once
via
+ * {@link AbstractKerberosUser#performRelogin()} (keytab-safe) and retry.
+ *
+ * @return the result of {@code action}
+ * @throws Exception if login, relogin, or the privileged action fails
+ */
public T execute() throws Exception {
T result;
@@ -61,14 +70,17 @@ public T execute() throws Exception {
}
}
- // check if we need to re-login, will only happen if re-login window
is reached (80% of TGT life)
+ // check if we need to re-login, will only happen if re-login window is
+ // reached (80% of TGT life)
try {
kerberosUser.checkTGTAndRelogin();
} catch (LoginException e) {
throw new Exception("Relogin check failed due to: " +
e.getMessage(), e);
}
- // attempt to execute the action, if an exception is caught attempt to
logout/login and retry
+ // On SecurityException, relogin and retry once. Use performRelogin()
+ // for AbstractKerberosUser so keytab clients skip logout first; bare
+ // logout/login reproduces "No key to store" when useTicketCache is
true.
try {
result = kerberosUser.doAs(action);
} catch (SecurityException se) {
@@ -78,17 +90,23 @@ public T execute() throws Exception {
}
try {
- kerberosUser.logout();
- kerberosUser.login();
+ if (kerberosUser instanceof AbstractKerberosUser) {
+ ((AbstractKerberosUser) kerberosUser).performRelogin();
+ } else {
+ kerberosUser.logout();
+ kerberosUser.login();
+ }
result = kerberosUser.doAs(action);
} catch (Exception e) {
- throw new Exception("Retrying privileged action failed due to:
" + e.getMessage(), e);
+ throw new Exception(
+ "Retrying privileged action failed due to: " +
e.getMessage(), e);
}
} catch (PrivilegedActionException pae) {
final Exception cause = pae.getException();
- throw new Exception("Privileged action failed due to: " +
cause.getMessage(), cause);
+ throw new Exception(
+ "Privileged action failed due to: " + cause.getMessage(),
cause);
}
return result;
diff --git
a/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosJAASConfigUser.java
b/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosJAASConfigUser.java
index c170ab728..c1431477b 100644
---
a/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosJAASConfigUser.java
+++
b/security-admin/src/main/java/org/apache/ranger/solr/krb/KerberosJAASConfigUser.java
@@ -34,12 +34,16 @@
public class KerberosJAASConfigUser extends AbstractKerberosUser {
private static final Logger LOG =
LoggerFactory.getLogger(KerberosJAASConfigUser.class);
+ private static final String JAAS_USE_KEYTAB = "useKeyTab";
+
private final String configName;
private final Configuration config;
+ private final boolean optionUseKeyTab;
public KerberosJAASConfigUser(final String configName, final Configuration
config) {
- this.configName = configName;
- this.config = config;
+ this.configName = configName;
+ this.config = config;
+ this.optionUseKeyTab = getBooleanOption(JAAS_USE_KEYTAB);
}
@Override
@@ -60,6 +64,35 @@ public String getPrincipal() {
return ret;
}
+ /**
+ * Admin Solr audit queries use {@code useKeyTab=true}. Opt those
principals
+ * into in-place keytab relogin so shipped {@code useTicketCache=true} does
+ * not fail at TGT renewal with {@code "No key to store"}.
+ */
+ @Override
+ protected boolean useKeytabRelogin() {
+ return optionUseKeyTab;
+ }
+
+ private boolean getBooleanOption(String optionName) {
+ boolean ret = false;
+ AppConfigurationEntry[] entries =
config.getAppConfigurationEntry(configName);
+
+ if (entries != null) {
+ for (AppConfigurationEntry entry : entries) {
+ Object value = entry.getOptions().get(optionName);
+
+ if (value != null && Boolean.parseBoolean(value.toString())) {
+ ret = true;
+
+ break;
+ }
+ }
+ }
+
+ return ret;
+ }
+
@Override
protected LoginContext createLoginContext(Subject subject) throws
LoginException {
LOG.debug("==> KerberosJAASConfigUser.createLoginContext()");
diff --git
a/security-admin/src/test/java/org/apache/ranger/solr/krb/AbstractKerberosUserTest.java
b/security-admin/src/test/java/org/apache/ranger/solr/krb/AbstractKerberosUserTest.java
index cbd69fd9c..18b4e4c1d 100644
---
a/security-admin/src/test/java/org/apache/ranger/solr/krb/AbstractKerberosUserTest.java
+++
b/security-admin/src/test/java/org/apache/ranger/solr/krb/AbstractKerberosUserTest.java
@@ -142,6 +142,29 @@ public void
checkTGTAndRelogin_reloginWhenOnlyNonTGSTicketPresent() throws Excep
verify(ctx, times(1)).login();
}
+ @Test
+ public void checkTGTAndRelogin_keytabUserSkipsLogoutWhenLoggedIn() throws
Exception {
+ Subject subject = new Subject();
+ KerberosTicket tgt = mock(KerberosTicket.class);
+ when(tgt.getServer()).thenReturn(new
KerberosPrincipal("krbtgt/[email protected]"));
+ Date start = new Date(System.currentTimeMillis() - 60 * 60_000);
+ Date end = new Date(System.currentTimeMillis() + 60_000);
+ when(tgt.getStartTime()).thenReturn(start);
+ when(tgt.getEndTime()).thenReturn(end);
+ subject.getPrivateCredentials().add(tgt);
+
+ LoginContext ctx = mock(LoginContext.class);
+ KeytabKerberosUser user = new KeytabKerberosUser("[email protected]",
subject, ctx);
+
+ user.login();
+ Mockito.reset(ctx);
+
+ boolean relogin = user.checkTGTAndRelogin();
+ assertTrue(relogin);
+ verify(ctx, never()).logout();
+ verify(ctx, times(1)).login();
+ }
+
private static class TestKerberosUser extends AbstractKerberosUser {
private final String principal;
private final LoginContext ctx;
@@ -162,4 +185,15 @@ protected LoginContext createLoginContext(Subject subject)
throws LoginException
return ctx;
}
}
+
+ private static class KeytabKerberosUser extends TestKerberosUser {
+ KeytabKerberosUser(String principal, Subject subject, LoginContext
ctx) {
+ super(principal, subject, ctx);
+ }
+
+ @Override
+ protected boolean useKeytabRelogin() {
+ return true;
+ }
+ }
}