From 3ba87a7309626960ec54fcb2e784eb6e194adc99 Mon Sep 17 00:00:00 2001 From: Vishnutheep B Date: Mon, 6 Apr 2026 23:03:17 +0530 Subject: [PATCH 1/4] Add plugins.security.superadmin.secret for super admin access Signed-off-by: Vishnutheep B --- .../security/OpenSearchSecurityPlugin.java | 2 + .../auditlog/impl/AbstractAuditLog.java | 15 ++++++- .../security/auditlog/impl/AuditMessage.java | 5 +++ .../security/auth/BackendRegistry.java | 44 +++++++++++++++++++ .../security/filter/SecurityRestFilter.java | 2 +- .../security/support/ConfigConstants.java | 6 +++ .../security/support/SecuritySettings.java | 42 ++++++++++++++++++ .../security/tools/SecurityAdmin.java | 28 ++++++++++-- 8 files changed, 138 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 3cdeaa2e31..1c2022b06f 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2313,6 +2313,8 @@ public List> getSettings() { settings.add(SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING); settings.add(SecuritySettings.DLS_WRITE_BLOCKED); + settings.add(SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING); + settings.add(SecuritySettings.SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING); } return settings; diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java index 748fdec2c0..3635d9b892 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java @@ -165,7 +165,8 @@ public void logFailedLogin(String effectiveUser, boolean securityadmin, String i msg.addRestRequestInfo(request, auditConfigFilter); msg.addInitiatingUser(initiatingUser); msg.addEffectiveUser(effectiveUser); - msg.addIsAdminDn(securityadmin); + msg.addIsAdminDn(isDnAdmin(securityadmin, request)); + msg.addIsSuperadminSecret(isSecretAdmin(securityadmin, request)); save(msg); } @@ -183,7 +184,9 @@ public void logSucceededLogin(String effectiveUser, boolean securityadmin, Strin msg.addRestRequestInfo(request, auditConfigFilter); msg.addInitiatingUser(initiatingUser); msg.addEffectiveUser(effectiveUser); - msg.addIsAdminDn(securityadmin); + msg.addIsAdminDn(isDnAdmin(securityadmin, request)); + msg.addIsSuperadminSecret(isSecretAdmin(securityadmin, request)); + save(msg); } @@ -1031,4 +1034,12 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { ignoredUrlParams.addAll(authDomain.getHttpAuthenticator().getSensitiveUrlParams()); } } + + private boolean isDnAdmin(boolean securityadmin, SecurityRequest request) { + return (securityadmin && request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER) == null); + } + + private boolean isSecretAdmin(boolean securityadmin, SecurityRequest request) { + return (securityadmin && request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER) != null); + } } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java index 41d0228e74..9495504264 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java @@ -119,6 +119,7 @@ public final class AuditMessage { public static final String EXCEPTION = "audit_request_exception_stacktrace"; public static final String IS_ADMIN_DN = "audit_request_effective_user_is_admin"; + public static final String IS_SUPERADMIN_SECRET = "audit_request_effective_user_is_superadmin_secret"; public static final String PRIVILEGE = "audit_request_privilege"; public static final String TASK_ID = "audit_trace_task_id"; @@ -174,6 +175,10 @@ public void addIsAdminDn(boolean isAdminDn) { auditInfo.put(IS_ADMIN_DN, isAdminDn); } + public void addIsSuperadminSecret(boolean isSuperadminSecret) { + auditInfo.put(IS_SUPERADMIN_SECRET, isSuperadminSecret); + } + public void addException(Throwable t) { if (t != null) { auditInfo.put(EXCEPTION, ExceptionsHelper.stackTrace(t)); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 53baa1034e..bbe1f2f721 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -28,6 +28,8 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -46,6 +48,7 @@ import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.collect.Multimap; +import org.apache.commons.lang3.ObjectUtils; import org.apache.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -108,6 +111,7 @@ public class BackendRegistry { private final ThreadPool threadPool; private final UserInjector userInjector; private final ClusterInfoHolder clusterInfoHolder; + private final String superadminSecret; private int ttlInMin; private Cache userCache; // rest standard private Cache restImpersonationCache; // used for rest impersonation @@ -169,6 +173,7 @@ public BackendRegistry( this.threadPool = threadPool; this.clusterInfoHolder = clusterInfoHolder; this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); + this.superadminSecret = SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING.get(settings).toString(); this.restAuthDomains = Collections.emptySortedSet(); this.ipAuthFailureListeners = Collections.emptyList(); @@ -278,6 +283,24 @@ public boolean authenticate(final SecurityRequestChannel request) { return true; } + /* + Authenticates superuser based on superadmin secret. The secret is read from thread context + and compared against the configured superadmin secret. If superuser is authenticated here we skip the remaining + authentication flow. This mechanism is independent of the security index and serves as an out-of-band recovery + path for HTTP deployments. + */ + if (!gRPC) { + final String providedSecret = request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER); + if (isSuperadminSecretValid(providedSecret)) { + log.debug("Superadmin authentication successful via secret"); + User superuser = new User(ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER); + UserSubject subject = new UserSubjectImpl(threadPool, superuser); + threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); + return true; + } + } + /* Authenticate users injected in the thread context by internal components (plugins). */ @@ -613,6 +636,27 @@ private boolean checkRemoteAddrBlocked(SecurityRequestChannel request) { return false; } + /** + * Validates given superadmin secret against configured secret. + * @param providedSecret the secret from the request header + * @return true if valid, false otherwise + */ + private boolean isSuperadminSecretValid(String providedSecret) { + if (ObjectUtils.isEmpty(superadminSecret) || ObjectUtils.isEmpty(providedSecret)) { + return false; + } + + try { + return MessageDigest.isEqual( + superadminSecret.getBytes(StandardCharsets.UTF_8), + providedSecret.getBytes(StandardCharsets.UTF_8) + ); + } catch (Exception e) { + log.debug("Error comparing superadmin secret", e); + return false; + } + } + /** * Resolve and stash client IP in thread context. * @param request with remote address. diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 210d3f20c5..731b965c3c 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -271,7 +271,7 @@ public RestHandler wrap(RestHandler original, AdminDNs adminDNs, Set OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT = List.of(); // defaults to no registered types as // protected + // Super admin secret + public static final String SECURITY_SUPERADMIN_SECRET_USER = "superadmin_secret_user"; + public static final String SECURITY_SUPERADMIN_SECRET = SECURITY_SETTINGS_PREFIX + "superadmin.secret"; + public static final String SECURITY_SUPERADMIN_SECRET_SECURE = SECURITY_SUPERADMIN_SECRET + "_secure"; + public static final String SECURITY_SUPERADMIN_SECRET_HEADER = "X-OpenSearch-Superadmin-Secret"; + public static Set getSettingAsSet( final Settings settings, final String key, diff --git a/src/main/java/org/opensearch/security/support/SecuritySettings.java b/src/main/java/org/opensearch/security/support/SecuritySettings.java index 61a17a953e..6e324cf8b0 100644 --- a/src/main/java/org/opensearch/security/support/SecuritySettings.java +++ b/src/main/java/org/opensearch/security/support/SecuritySettings.java @@ -11,7 +11,13 @@ package org.opensearch.security.support; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.SecureSetting; import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.settings.SecureString; public class SecuritySettings { public static final Setting LEGACY_OPENDISTRO_SSL_DUAL_MODE_SETTING = Setting.boolSetting( @@ -57,4 +63,40 @@ public class SecuritySettings { Setting.Property.Dynamic, Setting.Property.Sensitive ); + + public static final Setting SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING = new InsecureFallbackStringSetting( + ConfigConstants.SECURITY_SUPERADMIN_SECRET + ); + + public static final Setting SECURITY_SUPERADMIN_SECRET_SETTING = SecureSetting.secureString( + ConfigConstants.SECURITY_SUPERADMIN_SECRET_SECURE, + SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING + ); + + /** + * Alternative to InsecureStringSetting, which doesn't raise an exception if allow_insecure_settings is false, but + * instead logs a warning. This is to allow insecure settings for container-based deployments while recommending + * secure keystore usage. + */ + private static class InsecureFallbackStringSetting extends Setting { + private static final Logger LOG = LogManager.getLogger(InsecureFallbackStringSetting.class); + private final String name; + + private InsecureFallbackStringSetting(String name) { + super(name, "", s -> new SecureString(s.toCharArray()), Property.NodeScope, Property.Deprecated, Property.Filtered); + this.name = name; + } + + public SecureString get(Settings settings) { + if (this.exists(settings)) { + LOG.warn( + "Setting [{}] has a secure counterpart [{}] which should be used instead - allowing for container-based deployments", + this.name, + ConfigConstants.SECURITY_SUPERADMIN_SECRET_SECURE + ); + } + + return super.get(settings); + } + } } diff --git a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java index f21135c9df..0de7ea7a41 100644 --- a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java +++ b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java @@ -69,13 +69,16 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.ObjectUtils; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.reactor.ssl.TlsDetails; import org.apache.hc.core5.ssl.SSLContextBuilder; @@ -299,6 +302,15 @@ public static int execute(final String[] args) throws Exception { Option.builder("keypass").hasArg().argName("password").desc("Password of the key of admin certificate (optional)").build() ); + options.addOption( + Option.builder("sas") + .longOpt("superadmin-secret") + .hasArg() + .argName("secret") + .desc("Superadmin secret for HTTP header authentication") + .build() + ); + options.addOption(Option.builder("si").longOpt("show-info").desc("Show system and license info").build()); options.addOption(Option.builder("w").longOpt("whoami").desc("Show information about the used admin certificate").build()); @@ -373,6 +385,7 @@ public static int execute(final String[] args) throws Exception { final boolean promptForPassword; String explicitReplicas = null; String backup = null; + String superadminSecret = null; final boolean resolveEnvVars; Integer validateConfig = null; @@ -460,6 +473,7 @@ public static int execute(final String[] args) throws Exception { explicitReplicas = line.getOptionValue("er", explicitReplicas); backup = line.getOptionValue("backup"); + superadminSecret = line.getOptionValue("sas", superadminSecret); resolveEnvVars = line.hasOption("rev"); @@ -532,7 +546,8 @@ public static int execute(final String[] args) throws Exception { enabledProtocols, enabledCiphers, hostname, - port + port, + superadminSecret ) ) { @@ -1462,7 +1477,8 @@ private static RestHighLevelClient getRestHighLevelClient( String[] enabledProtocols, String[] enabledCiphers, String hostname, - int port + int port, + String superadminSecret ) { final HostnameVerifier hnv = !nhnv ? new DefaultHostnameVerifier() : NoopHostnameVerifier.INSTANCE; @@ -1472,7 +1488,13 @@ private static RestHighLevelClient getRestHighLevelClient( HttpHost httpHost = new HttpHost("https", hostname, port); - RestClientBuilder restClientBuilder = RestClient.builder(httpHost).setHttpClientConfigCallback(builder -> { + RestClientBuilder restClientBuilder = RestClient.builder(httpHost); + if (!ObjectUtils.isEmpty(superadminSecret)) { + restClientBuilder.setDefaultHeaders( + new Header[] { new BasicHeader(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER, superadminSecret) } + ); + } + restClientBuilder = restClientBuilder.setHttpClientConfigCallback(builder -> { TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() .setSslContext(sslContext) .setTlsVersions(supportedProtocols) From 51f78dfdcb1c5110ff2bcb9d894288c4b6becffb Mon Sep 17 00:00:00 2001 From: Vishnutheep B Date: Sun, 10 May 2026 11:25:25 +0530 Subject: [PATCH 2/4] Abstract superadmin access validation Signed-off-by: Vishnutheep B --- .../security/OpenSearchSecurityPlugin.java | 26 +++--- .../security/auth/BackendRegistry.java | 79 ++++++------------- .../configuration/DlsFlsValveImpl.java | 8 +- .../SecurityFlsDlsIndexSearcherWrapper.java | 4 +- .../configuration/SuperAdminAuthority.java | 79 +++++++++++++++++++ .../SystemIndexSearcherWrapper.java | 10 +-- .../dlic/rest/api/PermissionsInfoAction.java | 6 +- .../api/RestApiAdminPrivilegesEvaluator.java | 10 +-- .../rest/api/RestApiPrivilegesEvaluator.java | 40 +++++----- .../dlic/rest/api/SecurityRestApiActions.java | 12 +-- .../security/filter/SecurityFilter.java | 14 ++-- .../security/filter/SecurityRestFilter.java | 23 ++---- .../privileges/dlsfls/DlsFlsBaseContext.java | 14 ++-- .../resources/ResourceAccessHandler.java | 14 ++-- .../rest/SecurityConfigUpdateAction.java | 12 +-- .../security/rest/SecurityWhoAmIAction.java | 20 +++-- .../security/rest/TenantInfoAction.java | 9 ++- .../security/support/ConfigConstants.java | 2 +- .../security/support/SecuritySettings.java | 31 +------- 19 files changed, 221 insertions(+), 192 deletions(-) create mode 100644 src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 1c2022b06f..b32c56d7a6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -156,6 +156,7 @@ import org.opensearch.security.configuration.DlsFlsValveImpl; import org.opensearch.security.configuration.SecurityConfigVersionHandler; import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.dlic.rest.api.SecurityRestApiActions; import org.opensearch.security.dlic.rest.api.ssl.CertificatesActionType; @@ -287,6 +288,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile AtomicReference localNode = new AtomicReference<>(); private volatile AuditLog auditLog; private volatile BackendRegistry backendRegistry; + private volatile SuperAdminAuthority superAdminAuthority; private volatile SslExceptionHandler sslExceptionHandler; private volatile Client localClient; private final boolean disabled; @@ -660,7 +662,7 @@ public List getRestHandlers( Objects.requireNonNull(privilegesConfiguration), Objects.requireNonNull(threadPool), Objects.requireNonNull(cs), - Objects.requireNonNull(adminDns), + Objects.requireNonNull(superAdminAuthority), Objects.requireNonNull(cr) ) ); @@ -669,7 +671,7 @@ public List getRestHandlers( settings, restController, Objects.requireNonNull(threadPool), - adminDns, + Objects.requireNonNull(superAdminAuthority), configPath, principalExtractor ) @@ -679,7 +681,7 @@ public List getRestHandlers( settings, restController, Objects.requireNonNull(threadPool), - adminDns, + Objects.requireNonNull(superAdminAuthority), configPath, principalExtractor ) @@ -691,7 +693,7 @@ public List getRestHandlers( configPath, restController, localClient, - adminDns, + Objects.requireNonNull(superAdminAuthority), cr, cs, principalExtractor, @@ -736,7 +738,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre return (rh) -> rh; } - return (rh) -> securityRestHandler.wrap(rh, adminDns, headersToCopy); + return (rh) -> securityRestHandler.wrap(rh, superAdminAuthority, headersToCopy); } @Override @@ -770,7 +772,7 @@ public void onIndexModule(IndexModule indexModule) { indexService -> new SecurityFlsDlsIndexSearcherWrapper( indexService, settings, - adminDns, + superAdminAuthority, cs, auditLog, ciol, @@ -1200,7 +1202,8 @@ public Collection createComponents( userService = new UserService(cs, cr, passwordHasher, settings, localClient); final XFFResolver xffResolver = new XFFResolver(threadPool); - backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool, cih); + superAdminAuthority = new SuperAdminAuthority(adminDns, settings, threadPool); + backendRegistry = new BackendRegistry(settings, superAdminAuthority, xffResolver, auditLog, threadPool, cih); backendRegistry.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); cr.subscribeOnChange(configMap -> { backendRegistry.invalidateCache(); }); @@ -1231,7 +1234,7 @@ public Collection createComponents( ); this.privilegesConfiguration = privilegesConfiguration; - dlsFlsBaseContext = new DlsFlsBaseContext(privilegesConfiguration, threadPool.getThreadContext(), adminDns); + dlsFlsBaseContext = new DlsFlsBaseContext(privilegesConfiguration, threadPool.getThreadContext(), superAdminAuthority); if (SSLConfig.isSslOnlyMode()) { dlsFlsValve = new DlsFlsRequestValve.NoopDlsFlsRequestValve(); @@ -1244,13 +1247,13 @@ public Collection createComponents( xContentRegistry, threadPool, dlsFlsBaseContext, - adminDns, + superAdminAuthority, resourcePluginInfo, resourceSharingEnabledSetting ); } - resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns, resourcePluginInfo); + resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, superAdminAuthority, resourcePluginInfo); // Assign resource sharing client to each extension // Using the non-gated client (i.e. no additional permissions required) @@ -1276,7 +1279,7 @@ public Collection createComponents( sf = new SecurityFilter( settings, privilegesConfiguration, - adminDns, + superAdminAuthority, dlsFlsValve, auditLog, threadPool, @@ -1352,6 +1355,7 @@ public Collection createComponents( components.add(cr); components.add(xffResolver); components.add(backendRegistry); + components.add(superAdminAuthority); components.add(auditLog); components.add(privilegesConfiguration); components.add(restLayerEvaluator); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index bbe1f2f721..1f757ed2fc 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -28,8 +28,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -48,7 +46,6 @@ import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.collect.Multimap; -import org.apache.commons.lang3.ObjectUtils; import org.apache.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -65,6 +62,7 @@ import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.GrpcRequestChannel; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestChannel; @@ -104,6 +102,7 @@ public class BackendRegistry { private volatile boolean initialized; private volatile boolean injectedUserEnabled = false; private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final XFFResolver xffResolver; private volatile boolean anonymousAuthEnabled = false; private final Settings opensearchSettings; @@ -111,7 +110,6 @@ public class BackendRegistry { private final ThreadPool threadPool; private final UserInjector userInjector; private final ClusterInfoHolder clusterInfoHolder; - private final String superadminSecret; private int ttlInMin; private Cache userCache; // rest standard private Cache restImpersonationCache; // used for rest impersonation @@ -160,20 +158,20 @@ public void registerClusterSettingsChangeListener(final ClusterSettings clusterS public BackendRegistry( final Settings settings, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, final XFFResolver xffResolver, final AuditLog auditLog, final ThreadPool threadPool, final ClusterInfoHolder clusterInfoHolder ) { - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; + this.adminDns = superAdminAuthority.getAdminDns(); this.opensearchSettings = settings; this.xffResolver = xffResolver; this.auditLog = auditLog; this.threadPool = threadPool; this.clusterInfoHolder = clusterInfoHolder; this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); - this.superadminSecret = SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING.get(settings).toString(); this.restAuthDomains = Collections.emptySortedSet(); this.ipAuthFailureListeners = Collections.emptyList(); @@ -267,38 +265,32 @@ public boolean authenticate(final SecurityRequestChannel request) { } /* - Authenticates superuser based on client certificate auth. The certificate DN is read from thread context and - compared against adminDNs. If superuser is authenticated here we skip the remaining authentication flow. - Note that non superuser client/cert authentication is handled separately by the HTTPClientCertAuthenticator - auth backend. + Authenticates superadmin based on either certificate or superadmin secret. + Uses SuperAdminAuthority to coordinate authentication methods. + If superadmin is authenticated here, skip the remaining auth flow. */ - ThreadContext threadContext = this.threadPool.getThreadContext(); - final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); - if (adminDns.isAdminDN(sslPrincipal)) { - // PKI authenticated REST call - User superuser = new User(sslPrincipal); - UserSubject subject = new UserSubjectImpl(threadPool, superuser); - threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); - return true; - } - - /* - Authenticates superuser based on superadmin secret. The secret is read from thread context - and compared against the configured superadmin secret. If superuser is authenticated here we skip the remaining - authentication flow. This mechanism is independent of the security index and serves as an out-of-band recovery - path for HTTP deployments. - */ - if (!gRPC) { - final String providedSecret = request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER); - if (isSuperadminSecretValid(providedSecret)) { - log.debug("Superadmin authentication successful via secret"); - User superuser = new User(ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER); + if (!gRPC && superAdminAuthority.isRequestFromSuperAdmin(request)) { + // Determine which type of superadmin authentication succeeded + ThreadContext threadContext = this.threadPool.getThreadContext(); + final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); + if (adminDns.isAdminDN(sslPrincipal)) { + // Certificate-based admin + User superuser = new User(sslPrincipal); + UserSubject subject = new UserSubjectImpl(threadPool, superuser); + threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); + return true; + } else { + // Secret-based superadmin + User superuser = new User(superAdminAuthority.getSuperadminSecretUserName()); UserSubject subject = new UserSubjectImpl(threadPool, superuser); threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); return true; } + } else if (!gRPC && superAdminAuthority.hasSecretHeader(request)) { + // Failed superadmin secret authentication attempt + auditLog.logFailedLogin("superadmin", false, null, request); } /* @@ -636,27 +628,6 @@ private boolean checkRemoteAddrBlocked(SecurityRequestChannel request) { return false; } - /** - * Validates given superadmin secret against configured secret. - * @param providedSecret the secret from the request header - * @return true if valid, false otherwise - */ - private boolean isSuperadminSecretValid(String providedSecret) { - if (ObjectUtils.isEmpty(superadminSecret) || ObjectUtils.isEmpty(providedSecret)) { - return false; - } - - try { - return MessageDigest.isEqual( - superadminSecret.getBytes(StandardCharsets.UTF_8), - providedSecret.getBytes(StandardCharsets.UTF_8) - ); - } catch (Exception e) { - log.debug("Error comparing superadmin secret", e); - return false; - } - } - /** * Resolve and stash client IP in thread context. * @param request with remote address. diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index f0d0988b00..1a1b1c9f67 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -105,7 +105,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final DlsFlsBaseContext dlsFlsBaseContext; private final FieldMasking.Config fieldMaskingConfig; private final Settings settings; - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; private final OpensearchDynamicSetting resourceSharingEnabledSetting; private final ResourcePluginInfo resourcePluginInfo; private volatile boolean dlsWriteBlockedEnabled; @@ -118,7 +118,7 @@ public DlsFlsValveImpl( NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool, DlsFlsBaseContext dlsFlsBaseContext, - AdminDNs adminDNs, + SuperAdminAuthority superAdminAuthority, ResourcePluginInfo resourcePluginInfo, OpensearchDynamicSetting resourceSharingEnabledSetting ) { @@ -132,7 +132,7 @@ public DlsFlsValveImpl( this.fieldMaskingConfig = FieldMasking.Config.fromSettings(settings); this.dlsFlsBaseContext = dlsFlsBaseContext; this.settings = settings; - this.adminDNs = adminDNs; + this.superAdminAuthority = superAdminAuthority; this.resourcePluginInfo = resourcePluginInfo; clusterService.addListener(event -> { @@ -171,7 +171,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< if (isClusterPerm(context.getAction()) && !MultiGetAction.NAME.equals(context.getAction())) { return true; } - if (userSubject != null && adminDNs.isAdmin(userSubject.getUser())) { + if (userSubject != null && superAdminAuthority.isSuperAdmin(userSubject.getUser())) { return true; } ActionRequest request = context.getRequest(); diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 96c1616183..03924362aa 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -66,7 +66,7 @@ public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapp public SecurityFlsDlsIndexSearcherWrapper( final IndexService indexService, final Settings settings, - final AdminDNs adminDNs, + final SuperAdminAuthority superAdminAuthority, final ClusterService clusterService, final AuditLog auditlog, final ComplianceIndexingOperationListener ciol, @@ -75,7 +75,7 @@ public SecurityFlsDlsIndexSearcherWrapper( final Supplier dlsFlsProcessedConfigSupplier, final DlsFlsBaseContext dlsFlsBaseContext ) { - super(indexService, settings, adminDNs, privilegesConfiguration, roleMapper); + super(indexService, settings, superAdminAuthority, privilegesConfiguration, roleMapper); Set metadataFieldsCopy; if (indexService.getMetadata().getState() == IndexMetadata.State.CLOSE) { if (log.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java new file mode 100644 index 0000000000..30efd984bf --- /dev/null +++ b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java @@ -0,0 +1,79 @@ +package org.opensearch.security.configuration; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SecuritySettings; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + + +public class SuperAdminAuthority { + private static final Logger log = LogManager.getLogger(SuperAdminAuthority.class); + + private final AdminDNs adminDns; + private final ThreadContext threadContext; + private final String superadminSecret; + + public SuperAdminAuthority(final AdminDNs adminDns, final Settings settings, final ThreadPool threadPool) { + this.adminDns = adminDns; + this.threadContext = threadPool.getThreadContext(); + this.superadminSecret = SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING.get(settings).toString(); + } + + public boolean isRequestFromSuperAdmin(final SecurityRequest request) { + return isAdminViaDn(request) || isAdminViaSecret(request); + } + + public AdminDNs getAdminDns() { + return adminDns; + } + + public boolean isAdminViaDn(final SecurityRequest request) { + final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); + if(adminDns.isAdminDN(sslPrincipal)) { + return true; + } + return false; + } + + public boolean isAdminViaSecret(final SecurityRequest request) { + return isSuperadminSecretValid(request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER)); + } + + public boolean hasSecretHeader(final SecurityRequest request) { + return !ObjectUtils.isEmpty(request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER)); + } + + public boolean isSuperAdmin(final User user) { + return user != null && (adminDns.isAdmin(user) || ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER.equals(user.getName())); + } + + public String getSuperadminSecretUserName() { + return ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER; + } + + private boolean isSuperadminSecretValid(final String providedSecret) { + if (ObjectUtils.isEmpty(superadminSecret) || ObjectUtils.isEmpty(providedSecret)) { + return false; + } + + try { + return MessageDigest.isEqual( + superadminSecret.getBytes(StandardCharsets.UTF_8), + providedSecret.getBytes(StandardCharsets.UTF_8) + ); + } catch (Exception e) { + log.debug("Error comparing superadmin secret", e); + return false; + } + } +} diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index cb3e858284..a337c42a18 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -56,7 +56,7 @@ public class SystemIndexSearcherWrapper implements CheckedFunction getHandler( final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, final ConfigurationRepository configurationRepository, final ClusterService clusterService, final PrincipalExtractor principalExtractor, @@ -62,14 +62,14 @@ public static Collection getHandler( final ResourcePluginInfo resourcePluginInfo ) { final var securityApiDependencies = new SecurityApiDependencies( - adminDns, + superAdminAuthority.getAdminDns(), configurationRepository, privilegesConfiguration, - new RestApiPrivilegesEvaluator(settings, adminDns, roleMapper, principalExtractor, configPath, threadPool), + new RestApiPrivilegesEvaluator(settings, superAdminAuthority, roleMapper, principalExtractor, configPath, threadPool), new RestApiAdminPrivilegesEvaluator( threadPool.getThreadContext(), privilegesConfiguration, - adminDns, + superAdminAuthority, settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) ), auditLog, @@ -89,7 +89,7 @@ public static Collection getHandler( configPath, controller, client, - adminDns, + superAdminAuthority, configurationRepository, clusterService, principalExtractor, diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index ef1196fafa..849f1ab899 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -84,10 +84,10 @@ import org.opensearch.security.auth.UserInjector; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.compliance.ComplianceConfig; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; @@ -113,7 +113,7 @@ public class SecurityFilter implements ActionFilter { protected final Logger log = LogManager.getLogger(this.getClass()); private final PrivilegesConfiguration privilegesConfiguration; - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final DlsFlsRequestValve dlsFlsValve; private final AuditLog auditLog; private final ThreadPool threadPool; @@ -131,7 +131,7 @@ public class SecurityFilter implements ActionFilter { public SecurityFilter( final Settings settings, PrivilegesConfiguration privilegesConfiguration, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, DlsFlsRequestValve dlsFlsValve, AuditLog auditLog, ThreadPool threadPool, @@ -143,7 +143,7 @@ public SecurityFilter( ResourceAccessEvaluator resourceAccessEvaluator ) { this.privilegesConfiguration = privilegesConfiguration; - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.dlsFlsValve = dlsFlsValve; this.auditLog = auditLog; this.threadPool = threadPool; @@ -227,7 +227,7 @@ private void ap if (user != null && threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER) == null) { threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, new UserSubjectImpl(threadPool, user)); } - final boolean userIsAdmin = isUserAdmin(user, adminDns); + final boolean userIsAdmin = isUserAdmin(user, superAdminAuthority); final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadContext); final boolean trustedClusterRequest = HeaderHelper.isTrustedClusterRequest(threadContext); final boolean confRequest = "true".equals( @@ -561,8 +561,8 @@ private boolean return false; } - private static boolean isUserAdmin(User user, final AdminDNs adminDns) { - return user != null && adminDns.isAdmin(user); + private static boolean isUserAdmin(User user, final SuperAdminAuthority superAdminAuthority) { + return superAdminAuthority.isSuperAdmin(user); } private void attachSourceFieldContext(ActionRequest request) { diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 731b965c3c..4f5958a759 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -57,7 +57,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.dlic.rest.api.AllowlistApiAction; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; @@ -125,12 +125,12 @@ public SecurityRestFilter( } class AuthczRestHandler extends DelegatingRestHandler { - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; private final Set headersToCopy; - public AuthczRestHandler(RestHandler original, AdminDNs adminDNs, Set headersToCopy) { + public AuthczRestHandler(RestHandler original, SuperAdminAuthority superAdminAuthority, Set headersToCopy) { super(original); - this.adminDNs = adminDNs; + this.superAdminAuthority = superAdminAuthority; this.headersToCopy = headersToCopy; } @@ -199,7 +199,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c // Authorize Request final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); String intiatingUser = threadContext.getTransient(OPENDISTRO_SECURITY_INITIATING_USER); - if (userIsSuperAdmin(user, adminDNs)) { + if (superAdminAuthority.isSuperAdmin(user)) { // Super admins are always authorized auditLog.logSucceededLogin(user.getName(), true, intiatingUser, filteredRequestChannel); if (performPermissionCheck) { @@ -256,22 +256,15 @@ RestRequest maybeFilterRestRequest(RestRequest request) throws IOException { * The allowlisting check works as follows: * If allowlisting is not enabled, then requests are handled normally. * If allowlisting is enabled, then SuperAdmin is allowed access to all APIs, regardless of what is currently allowlisted. - * If allowlisting is enabled, then Non-SuperAdmin is allowed to access only those APIs that are allowlisted in {@link #requests} + * If allowlisting is enabled, then Non-SuperAdmin is allowed to access only those APIs that are allowlisted in * For example: if allowlisting is enabled and requests = ["/_cat/nodes"], then SuperAdmin can access all APIs, but non SuperAdmin * can only access "/_cat/nodes" * Further note: Some APIs are only accessible by SuperAdmin, regardless of allowlisting. For example: /_opendistro/_security/api/allowlist is only accessible by SuperAdmin. * See {@link AllowlistApiAction} for the implementation of this API. * SuperAdmin is identified by credentials, which can be passed in the curl request. */ - public RestHandler wrap(RestHandler original, AdminDNs adminDNs, Set headersToCopy) { - return new AuthczRestHandler(original, adminDNs, headersToCopy); - } - - /** - * Checks if a given user is a SuperAdmin - */ - boolean userIsSuperAdmin(User user, AdminDNs adminDNs) { - return user != null && (adminDNs.isAdmin(user) || ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER.equals(user.getName())); + public RestHandler wrap(RestHandler original, SuperAdminAuthority superAdminAuthority, Set headersToCopy) { + return new AuthczRestHandler(original, superAdminAuthority, headersToCopy); } /** diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java index dc3636df38..c26e1dd4ce 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java @@ -11,7 +11,7 @@ package org.opensearch.security.privileges.dlsfls; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.support.ConfigConstants; @@ -24,12 +24,16 @@ public class DlsFlsBaseContext { private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; - public DlsFlsBaseContext(PrivilegesConfiguration privilegesConfiguration, ThreadContext threadContext, AdminDNs adminDNs) { + public DlsFlsBaseContext( + PrivilegesConfiguration privilegesConfiguration, + ThreadContext threadContext, + SuperAdminAuthority superAdminAuthority + ) { this.privilegesConfiguration = privilegesConfiguration; this.threadContext = threadContext; - this.adminDNs = adminDNs; + this.superAdminAuthority = superAdminAuthority; } /** @@ -42,7 +46,7 @@ public PrivilegesEvaluationContext getPrivilegesEvaluationContext() { } User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if (HeaderHelper.isInternalOrPluginRequest(threadContext) || adminDNs.isAdmin(user)) { + if (HeaderHelper.isInternalOrPluginRequest(threadContext) || superAdminAuthority.isSuperAdmin(user)) { return null; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index adbcbd1eb9..ec5ddb4785 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -27,7 +27,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.UserSubjectImpl; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -50,19 +50,19 @@ public class ResourceAccessHandler { private final ThreadContext threadContext; private final ResourceSharingIndexHandler resourceSharingIndexHandler; - private final AdminDNs adminDNs; + private final SuperAdminAuthority superAdminAuthority; private final ResourcePluginInfo resourcePluginInfo; @Inject public ResourceAccessHandler( final ThreadPool threadPool, final ResourceSharingIndexHandler resourceSharingIndexHandler, - AdminDNs adminDns, + SuperAdminAuthority superAdminAuthority, ResourcePluginInfo resourcePluginInfo ) { this.threadContext = threadPool.getThreadContext(); this.resourceSharingIndexHandler = resourceSharingIndexHandler; - this.adminDNs = adminDns; + this.superAdminAuthority = superAdminAuthority; this.resourcePluginInfo = resourcePluginInfo; } @@ -84,7 +84,7 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceTyp String resourceIndex = resourcePluginInfo.indexByType(resourceType); - if (adminDNs.isAdmin(user)) { + if (superAdminAuthority.isSuperAdmin(user)) { loadAllResourceIds(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); return; } @@ -110,7 +110,7 @@ public void getResourceSharingInfoForCurrentUser(@NonNull String resourceType, A return; } - if (adminDNs.isAdmin(user)) { + if (superAdminAuthority.isSuperAdmin(user)) { loadAllResourceSharingRecords(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); return; } @@ -150,7 +150,7 @@ public void hasPermission( LOGGER.info("Checking if user '{}' has permission to resource '{}'", user.getName(), resourceId); - if (adminDNs.isAdmin(user)) { + if (superAdminAuthority.isSuperAdmin(user)) { LOGGER.debug("User '{}' is admin, automatically granted permission on '{}'", user.getName(), resourceId); listener.onResponse(true); return; diff --git a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java index d6d3843566..cd17d7fe95 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java @@ -28,6 +28,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -51,7 +52,7 @@ public class SecurityConfigUpdateAction extends BaseRestHandler { ); private final ThreadContext threadContext; - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final Settings settings; private final Path configPath; private final PrincipalExtractor principalExtractor; @@ -60,13 +61,13 @@ public SecurityConfigUpdateAction( final Settings settings, final RestController controller, final ThreadPool threadPool, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, Path configPath, PrincipalExtractor principalExtractor ) { super(); this.threadContext = threadPool.getThreadContext(); - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.settings = settings; this.configPath = configPath; this.principalExtractor = principalExtractor; @@ -92,15 +93,16 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli SecurityRequestFactory.from(request), principalExtractor ); + final boolean isSecretAdmin = superAdminAuthority.isAdminViaSecret(SecurityRequestFactory.from(request)); - if (sslInfo == null) { + if (sslInfo == null && !isSecretAdmin) { return channel -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "")); } final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); // only allowed for admins - if (user == null || !adminDns.isAdmin(user)) { + if (user == null || !superAdminAuthority.isSuperAdmin(user)) { return channel -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "")); } else { ConfigUpdateRequest configUpdateRequest = new ConfigUpdateRequest(configTypes); diff --git a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java index 30a0506a9e..f8dae3046d 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java @@ -30,7 +30,7 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -60,7 +60,7 @@ public class SecurityWhoAmIAction extends BaseRestHandler { ); private final Logger log = LogManager.getLogger(this.getClass()); - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final Settings settings; private final Path configPath; private final PrincipalExtractor principalExtractor; @@ -70,12 +70,12 @@ public SecurityWhoAmIAction( final Settings settings, final RestController controller, final ThreadPool threadPool, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, Path configPath, PrincipalExtractor principalExtractor ) { super(); - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.settings = settings; this.configPath = configPath; this.principalExtractor = principalExtractor; @@ -96,8 +96,8 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli public void accept(RestChannel channel) throws Exception { XContentBuilder builder = channel.newBuilder(); BytesRestResponse response = null; - try { + final boolean isSecretAdmin = superAdminAuthority.isAdminViaSecret(SecurityRequestFactory.from(request)); SSLInfo sslInfo = SSLRequestHelper.getSSLInfo( settings, configPath, @@ -105,14 +105,13 @@ public void accept(RestChannel channel) throws Exception { principalExtractor ); - if (sslInfo == null) { + if (sslInfo == null && !isSecretAdmin) { response = new BytesRestResponse(RestStatus.FORBIDDEN, "No security data"); } else { - - final String dn = sslInfo.getPrincipal(); - final boolean isAdmin = adminDns.isAdminDN(dn); + final String dn = sslInfo == null ? null : sslInfo.getPrincipal(); + final boolean isAdmin = isSecretAdmin || superAdminAuthority.getAdminDns().isAdminDN(dn); final boolean isNodeCertificateRequest = dn != null && WildcardMatcher.from(nodesDn).ignoreCase().matchAny(dn); - + builder.startObject(); builder.field("dn", dn); builder.field("is_admin", isAdmin); @@ -120,7 +119,6 @@ public void accept(RestChannel channel) throws Exception { builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); - } } catch (final Exception e1) { log.error(e1.toString(), e1); diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index ecf1561992..5ca3d1614e 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -49,6 +49,7 @@ import org.opensearch.rest.RestRequest; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.TenantPrivileges; @@ -87,7 +88,7 @@ public class TenantInfoAction extends BaseRestHandler { private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; private final ClusterService clusterService; - private final AdminDNs adminDns; + private final SuperAdminAuthority superAdminAuthority; private final ConfigurationRepository configurationRepository; public TenantInfoAction( @@ -96,14 +97,14 @@ public TenantInfoAction( final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool, final ClusterService clusterService, - final AdminDNs adminDns, + final SuperAdminAuthority superAdminAuthority, final ConfigurationRepository configurationRepository ) { super(); this.threadContext = threadPool.getThreadContext(); this.privilegesConfiguration = privilegesConfiguration; this.clusterService = clusterService; - this.adminDns = adminDns; + this.superAdminAuthority = superAdminAuthority; this.configurationRepository = configurationRepository; } @@ -179,7 +180,7 @@ private boolean isAuthorized() { DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); // check if the user is a kibanauser or super admin - if (user.getName().equals(multiTenancyConfiguration.dashboardsServerUsername()) || adminDns.isAdmin(user)) { + if (user.getName().equals(multiTenancyConfiguration.dashboardsServerUsername()) || superAdminAuthority.isSuperAdmin(user)) { return true; } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index f7a9fb447a..0d445bac2f 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -436,7 +436,7 @@ public enum RolesMappingResolution { // protected // Super admin secret - public static final String SECURITY_SUPERADMIN_SECRET_USER = "superadmin_secret_user"; + public static final String SECURITY_SUPERADMIN_SECRET_USER = "_opendistro_security_superadmin_secret_user_"; public static final String SECURITY_SUPERADMIN_SECRET = SECURITY_SETTINGS_PREFIX + "superadmin.secret"; public static final String SECURITY_SUPERADMIN_SECRET_SECURE = SECURITY_SUPERADMIN_SECRET + "_secure"; public static final String SECURITY_SUPERADMIN_SECRET_HEADER = "X-OpenSearch-Superadmin-Secret"; diff --git a/src/main/java/org/opensearch/security/support/SecuritySettings.java b/src/main/java/org/opensearch/security/support/SecuritySettings.java index 6e324cf8b0..d00468c2d1 100644 --- a/src/main/java/org/opensearch/security/support/SecuritySettings.java +++ b/src/main/java/org/opensearch/security/support/SecuritySettings.java @@ -64,39 +64,12 @@ public class SecuritySettings { Setting.Property.Sensitive ); - public static final Setting SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING = new InsecureFallbackStringSetting( + public static final Setting SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING = SecureSetting.insecureString( ConfigConstants.SECURITY_SUPERADMIN_SECRET ); public static final Setting SECURITY_SUPERADMIN_SECRET_SETTING = SecureSetting.secureString( ConfigConstants.SECURITY_SUPERADMIN_SECRET_SECURE, - SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING + SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING ); - - /** - * Alternative to InsecureStringSetting, which doesn't raise an exception if allow_insecure_settings is false, but - * instead logs a warning. This is to allow insecure settings for container-based deployments while recommending - * secure keystore usage. - */ - private static class InsecureFallbackStringSetting extends Setting { - private static final Logger LOG = LogManager.getLogger(InsecureFallbackStringSetting.class); - private final String name; - - private InsecureFallbackStringSetting(String name) { - super(name, "", s -> new SecureString(s.toCharArray()), Property.NodeScope, Property.Deprecated, Property.Filtered); - this.name = name; - } - - public SecureString get(Settings settings) { - if (this.exists(settings)) { - LOG.warn( - "Setting [{}] has a secure counterpart [{}] which should be used instead - allowing for container-based deployments", - this.name, - ConfigConstants.SECURITY_SUPERADMIN_SECRET_SECURE - ); - } - - return super.get(settings); - } - } } From 4564e9f6a047a08dd76dfa7f08b7e8e0e9fd6cb3 Mon Sep 17 00:00:00 2001 From: Vishnutheep B Date: Sun, 10 May 2026 12:02:14 +0530 Subject: [PATCH 3/4] Fix spotless checks Signed-off-by: Vishnutheep B --- .../configuration/DlsFlsValveImpl.java | 1 - .../configuration/SuperAdminAuthority.java | 14 ++++++++-- .../rest/api/RestApiPrivilegesEvaluator.java | 14 +++++----- .../dlic/rest/api/SecurityRestApiActions.java | 2 +- .../security/filter/SecurityRestFilter.java | 2 +- .../rest/SecurityConfigUpdateAction.java | 1 - .../security/rest/SecurityWhoAmIAction.java | 2 +- .../security/rest/TenantInfoAction.java | 1 - .../security/support/SecuritySettings.java | 6 +--- .../SecurityAdminIEndpointsTests.java | 28 +++++++++++++++++++ .../auth/BackendRegistryGrpcAuthTest.java | 7 ++++- .../api/RestApiPrivilegesEvaluatorTest.java | 4 +-- .../security/filter/SecurityFilterTests.java | 6 ++-- .../filter/SecurityRestFilterUnitTests.java | 14 ++++------ .../resources/ResourceAccessHandlerTests.java | 20 ++++++------- 15 files changed, 77 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index d083b4666a..5af3a4f9c1 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -170,7 +170,6 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< return true; } - UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); if (userSubject != null && superAdminAuthority.isSuperAdmin(userSubject.getUser())) { return true; diff --git a/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java index 30efd984bf..98f09cbbbd 100644 --- a/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java +++ b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + package org.opensearch.security.configuration; import java.nio.charset.StandardCharsets; @@ -15,7 +26,6 @@ import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; - public class SuperAdminAuthority { private static final Logger log = LogManager.getLogger(SuperAdminAuthority.class); @@ -39,7 +49,7 @@ public AdminDNs getAdminDns() { public boolean isAdminViaDn(final SecurityRequest request) { final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); - if(adminDns.isAdminDN(sslPrincipal)) { + if (adminDns.isAdminDN(sslPrincipal)) { return true; } return false; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java index 48d0e411dd..ffc7e27100 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java @@ -75,12 +75,12 @@ public class RestApiPrivilegesEvaluator { private final Boolean roleBasedAccessEnabled; public RestApiPrivilegesEvaluator( - final Settings settings, - SuperAdminAuthority superAdminAuthority, - final RoleMapper roleMapper, - final PrincipalExtractor principalExtractor, - final Path configPath, - ThreadPool threadPool + final Settings settings, + SuperAdminAuthority superAdminAuthority, + final RoleMapper roleMapper, + final PrincipalExtractor principalExtractor, + final Path configPath, + ThreadPool threadPool ) { this.superAdminAuthority = superAdminAuthority; this.roleMapper = roleMapper; @@ -449,7 +449,7 @@ private String checkAdminBasedAccessPermissions(RestRequest request) throws IOEx // Check if superadmin secret is present final SecurityRequest securityRequest = SecurityRequestFactory.from(request); final boolean isSecretAdmin = superAdminAuthority.isAdminViaSecret(securityRequest); - if(isSecretAdmin) { + if (isSecretAdmin) { return null; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 85d2747184..6e18d648be 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -22,9 +22,9 @@ import org.opensearch.rest.RestHandler; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.configuration.SecurityConfigVersionHandler; import org.opensearch.security.configuration.SecurityConfigVersionsLoader; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.RoleMapper; diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 4f5958a759..cd95604c39 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -57,8 +57,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.configuration.CompatConfig; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.dlic.rest.api.AllowlistApiAction; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; diff --git a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java index cd17d7fe95..3a492a0950 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java @@ -27,7 +27,6 @@ import org.opensearch.rest.action.RestActions.NodesResponseRestListener; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.ssl.transport.PrincipalExtractor; diff --git a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java index f8dae3046d..0a8a4d5734 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java @@ -111,7 +111,7 @@ public void accept(RestChannel channel) throws Exception { final String dn = sslInfo == null ? null : sslInfo.getPrincipal(); final boolean isAdmin = isSecretAdmin || superAdminAuthority.getAdminDns().isAdminDN(dn); final boolean isNodeCertificateRequest = dn != null && WildcardMatcher.from(nodesDn).ignoreCase().matchAny(dn); - + builder.startObject(); builder.field("dn", dn); builder.field("is_admin", isAdmin); diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index 5ca3d1614e..1dcd7fde95 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -47,7 +47,6 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; diff --git a/src/main/java/org/opensearch/security/support/SecuritySettings.java b/src/main/java/org/opensearch/security/support/SecuritySettings.java index d00468c2d1..458d32dda4 100644 --- a/src/main/java/org/opensearch/security/support/SecuritySettings.java +++ b/src/main/java/org/opensearch/security/support/SecuritySettings.java @@ -11,12 +11,8 @@ package org.opensearch.security.support; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import org.opensearch.common.settings.SecureSetting; import org.opensearch.common.settings.Setting; -import org.opensearch.common.settings.Settings; import org.opensearch.core.common.settings.SecureString; public class SecuritySettings { @@ -70,6 +66,6 @@ public class SecuritySettings { public static final Setting SECURITY_SUPERADMIN_SECRET_SETTING = SecureSetting.secureString( ConfigConstants.SECURITY_SUPERADMIN_SECRET_SECURE, - SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING + SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING ); } diff --git a/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java b/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java index 97f9372693..8b6ddfd736 100644 --- a/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java +++ b/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java @@ -11,10 +11,12 @@ package org.opensearch.security; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper; @@ -149,4 +151,30 @@ public void testEndpoints() throws Exception { assertThat(rh.executePutRequest("_plugins/_security/configupdate", "").getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); assertThat(HttpStatus.SC_OK, is(rh.executePutRequest("_plugins/_security/configupdate?config_types=roles", "").getStatusCode())); } + + @Test + public void testEndpointsWithSuperAdminSecret() throws Exception { + final Settings settings = Settings.builder() + .put("plugins.security.ssl.http.enabled", true) + .put("plugins.security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) + .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .put(ConfigConstants.SECURITY_SUPERADMIN_SECRET, "top-secret") + .build(); + setup(settings); + final RestHelper rh = restHelper(); + rh.enableHTTPClientSSL = true; + rh.trustHTTPServerCertificate = true; + rh.sendAdminCertificate = false; + + final var secretHeader = new BasicHeader(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER, "top-secret"); + + RestHelper.HttpResponse res = rh.executeGetRequest("_plugins/_security/whoami", secretHeader); + assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); + assertContains(res, "*\"is_admin\":true*"); + + assertThat( + rh.executePutRequest("_plugins/_security/configupdate?config_types=roles", "{}", secretHeader).getStatusCode(), + is(HttpStatus.SC_OK) + ); + } } diff --git a/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java b/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java index 67d65be9bf..64e380abfd 100644 --- a/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java +++ b/src/test/java/org/opensearch/security/auth/BackendRegistryGrpcAuthTest.java @@ -31,6 +31,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.filter.GrpcRequestChannel; import org.opensearch.security.http.HTTPBasicAuthenticator; import org.opensearch.security.http.XFFResolver; @@ -70,6 +71,9 @@ public class BackendRegistryGrpcAuthTest { @Mock private ClusterInfoHolder clusterInfoHolder; + @Mock + private SuperAdminAuthority superAdminAuthority; + private BackendRegistry backendRegistry; @Before @@ -79,6 +83,7 @@ public void setUp() { // no admin user configured - ensure these checks are false when(adminDns.isAdmin(any())).thenReturn(false); when(adminDns.isAdminDN(any())).thenReturn(false); + when(superAdminAuthority.getAdminDns()).thenReturn(adminDns); when(threadPool.getThreadContext()).thenReturn(threadContext); when(clusterInfoHolder.hasClusterManager()).thenReturn(true); when(xffResolver.resolve(any())).thenReturn(new TransportAddress(new InetSocketAddress("127.0.0.1", 9200))); @@ -86,7 +91,7 @@ public void setUp() { // backend registry requires at least one auth path is available to initialize. // here we enable user injection to allow us to mock/test other failure cases. Settings settings = Settings.builder().put("plugins.security.unsupported.inject_user.enabled", true).build(); - backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool, clusterInfoHolder); + backendRegistry = new BackendRegistry(settings, superAdminAuthority, xffResolver, auditLog, threadPool, clusterInfoHolder); } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java index e8172d7723..0a4959d4c6 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java @@ -19,7 +19,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestRequest; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; @@ -35,7 +35,7 @@ public class RestApiPrivilegesEvaluatorTest { public void setUp() { this.privilegesEvaluator = new RestApiPrivilegesEvaluator( Settings.EMPTY, - mock(AdminDNs.class), + mock(SuperAdminAuthority.class), (user, caller) -> user.getSecurityRoles(), mock(PrincipalExtractor.class), mock(Path.class), diff --git a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java index fa461b1fea..836f78ea6a 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java @@ -26,9 +26,9 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.action.ActionResponse; import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.ResourceAccessEvaluator; @@ -80,7 +80,7 @@ public void testImmutableIndicesWildcardMatcher() { final SecurityFilter filter = new SecurityFilter( settings, mock(PrivilegesConfiguration.class), - mock(AdminDNs.class), + mock(SuperAdminAuthority.class), mock(DlsFlsRequestValve.class), mock(AuditLog.class), mock(ThreadPool.class), @@ -103,7 +103,7 @@ public void testUnexepectedCausesAreNotSendToCallers() { final SecurityFilter filter = new SecurityFilter( settings, mock(PrivilegesConfiguration.class), - mock(AdminDNs.class), + mock(SuperAdminAuthority.class), mock(DlsFlsRequestValve.class), auditLog, new ThreadPool(Settings.builder().put("node.name", "mock").build()), diff --git a/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java b/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java index 1cbb4eea1a..af6e7caf36 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityRestFilterUnitTests.java @@ -27,8 +27,8 @@ import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.CompatConfig; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; @@ -79,9 +79,8 @@ public void setUp() throws NoSuchMethodException { */ @Test public void testSecurityRestFilterWrap() throws Exception { - AdminDNs adminDNs = mock(AdminDNs.class); - - RestHandler wrappedRestHandler = sf.wrap(testRestHandler, adminDNs, new HashSet<>()); + SuperAdminAuthority superAdminAuthority = mock(SuperAdminAuthority.class); + RestHandler wrappedRestHandler = sf.wrap(testRestHandler, superAdminAuthority, new HashSet<>()); assertTrue(wrappedRestHandler instanceof SecurityRestFilter.AuthczRestHandler); assertFalse(wrappedRestHandler instanceof TestRestHandler); @@ -90,12 +89,9 @@ public void testSecurityRestFilterWrap() throws Exception { @Test public void testDoesCallDelegateOnSuccessfulAuthorization() throws Exception { SecurityRestFilter filterSpy = spy(sf); - AdminDNs adminDNs = mock(AdminDNs.class); - + SuperAdminAuthority superAdminAuthority = mock(SuperAdminAuthority.class); RestHandler testRestHandlerSpy = spy(testRestHandler); - RestHandler wrappedRestHandler = filterSpy.wrap(testRestHandlerSpy, adminDNs, new HashSet<>()); - - doReturn(false).when(filterSpy).userIsSuperAdmin(any(), any()); + RestHandler wrappedRestHandler = filterSpy.wrap(testRestHandlerSpy, superAdminAuthority, new HashSet<>()); wrappedRestHandler.handleRequest(mock(RestRequest.class), mock(RestChannel.class), mock(NodeClient.class)); diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java index b6b42e1f5a..c7924134f7 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTests.java @@ -22,7 +22,7 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; -import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.SuperAdminAuthority; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -49,7 +49,7 @@ public class ResourceAccessHandlerTests { @Mock private ResourceSharingIndexHandler sharingIndexHandler; @Mock - private AdminDNs adminDNs; + private SuperAdminAuthority superAdminAuthority; @Mock private ResourcePluginInfo resourcePluginInfo; @@ -66,7 +66,7 @@ public class ResourceAccessHandlerTests { public void setup() { threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); - handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, resourcePluginInfo); + handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, superAdminAuthority, resourcePluginInfo); // For tests that verify permission with action-group when(resourcePluginInfo.flattenedForType(any())).thenReturn(mock(FlattenedActionGroups.class)); @@ -83,7 +83,7 @@ private void injectUser(User user) { public void testHasPermission_adminUserAllowed() { User user = new User("admin", ImmutableSet.of("admin"), ImmutableSet.of(), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(true); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(true); ActionListener listener = mock(ActionListener.class); handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); @@ -95,7 +95,7 @@ public void testHasPermission_adminUserAllowed() { public void testHasPermission_ownerAllowed() { User user = new User("alice", ImmutableSet.of("r1"), ImmutableSet.of("b1"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); ResourceSharing doc = mock(ResourceSharing.class); when(doc.isCreatedBy("alice")).thenReturn(true); @@ -116,7 +116,7 @@ public void testHasPermission_ownerAllowed() { public void testHasPermission_sharedWithUserAllowed() { User user = new User("bob", ImmutableSet.of("role1"), ImmutableSet.of("backend1"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); // Document setup: shared with the user at access-level "read" ResourceSharing doc = mock(ResourceSharing.class); @@ -144,7 +144,7 @@ public void testHasPermission_sharedWithUserAllowed() { public void testHasPermission_noAccessLevelsDenied() { User user = new User("charlie", ImmutableSet.of("roleA"), ImmutableSet.of("backendA"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); ResourceSharing doc = mock(ResourceSharing.class); when(doc.getAccessLevelsForUser(user)).thenReturn(Collections.emptySet()); @@ -165,7 +165,7 @@ public void testHasPermission_noAccessLevelsDenied() { public void testHasPermission_nullDocumentDenied() { User user = new User("dave", ImmutableSet.of("x"), ImmutableSet.of("y"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); doAnswer(inv -> { ActionListener l = inv.getArgument(2); @@ -183,7 +183,7 @@ public void testHasPermission_nullDocumentDenied() { public void testGetOwnAndSharedResources_asAdmin() { User admin = new User("admin", ImmutableSet.of(), ImmutableSet.of(), null, ImmutableMap.of(), false); injectUser(admin); - when(adminDNs.isAdmin(admin)).thenReturn(true); + when(superAdminAuthority.isSuperAdmin(admin)).thenReturn(true); ActionListener> listener = mock(ActionListener.class); @@ -201,7 +201,7 @@ public void testGetOwnAndSharedResources_asAdmin() { public void testGetOwnAndSharedResources_asNormalUser() { User user = new User("alice", ImmutableSet.of("r1"), ImmutableSet.of("b1"), null, ImmutableMap.of(), false); injectUser(user); - when(adminDNs.isAdmin(user)).thenReturn(false); + when(superAdminAuthority.isSuperAdmin(user)).thenReturn(false); ActionListener> listener = mock(ActionListener.class); From 9a27345112e237b1c178b7a35ee3ad4e75f327ff Mon Sep 17 00:00:00 2001 From: Vishnutheep B Date: Sun, 10 May 2026 12:57:25 +0530 Subject: [PATCH 4/4] Fix isSuperadminSecretValid Signed-off-by: Vishnutheep B --- .../security/auth/BackendRegistry.java | 2 +- .../configuration/SuperAdminAuthority.java | 24 ++++++++++++------- .../rest/SecurityConfigUpdateAction.java | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 1f757ed2fc..59c8bc6e65 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -290,7 +290,7 @@ public boolean authenticate(final SecurityRequestChannel request) { } } else if (!gRPC && superAdminAuthority.hasSecretHeader(request)) { // Failed superadmin secret authentication attempt - auditLog.logFailedLogin("superadmin", false, null, request); + auditLog.logFailedLogin("superadmin", true, null, request); } /* diff --git a/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java index 98f09cbbbd..78eabf7174 100644 --- a/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java +++ b/src/main/java/org/opensearch/security/configuration/SuperAdminAuthority.java @@ -20,23 +20,26 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.settings.SecureString; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.SecuritySettings; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; +import static java.util.Arrays.fill; + public class SuperAdminAuthority { private static final Logger log = LogManager.getLogger(SuperAdminAuthority.class); private final AdminDNs adminDns; private final ThreadContext threadContext; - private final String superadminSecret; + private final SecureString superadminSecret; public SuperAdminAuthority(final AdminDNs adminDns, final Settings settings, final ThreadPool threadPool) { this.adminDns = adminDns; this.threadContext = threadPool.getThreadContext(); - this.superadminSecret = SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING.get(settings).toString(); + this.superadminSecret = SecuritySettings.SECURITY_SUPERADMIN_SECRET_SETTING.get(settings); } public boolean isRequestFromSuperAdmin(final SecurityRequest request) { @@ -48,7 +51,7 @@ public AdminDNs getAdminDns() { } public boolean isAdminViaDn(final SecurityRequest request) { - final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); + final String sslPrincipal = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); if (adminDns.isAdminDN(sslPrincipal)) { return true; } @@ -72,18 +75,21 @@ public String getSuperadminSecretUserName() { } private boolean isSuperadminSecretValid(final String providedSecret) { - if (ObjectUtils.isEmpty(superadminSecret) || ObjectUtils.isEmpty(providedSecret)) { + if ((superadminSecret == null || superadminSecret.isEmpty()) || ObjectUtils.isEmpty(providedSecret)) { return false; } + byte[] expectedAsBytes = new String(superadminSecret.getChars()).getBytes(StandardCharsets.UTF_8); + byte[] providedAsBytes = providedSecret.getBytes(StandardCharsets.UTF_8); + try { - return MessageDigest.isEqual( - superadminSecret.getBytes(StandardCharsets.UTF_8), - providedSecret.getBytes(StandardCharsets.UTF_8) - ); + return MessageDigest.isEqual(expectedAsBytes, providedAsBytes); } catch (Exception e) { - log.debug("Error comparing superadmin secret", e); + log.error("Failed to validate superadmin secret", e); return false; + } finally { + fill(providedAsBytes, (byte) 0); + fill(expectedAsBytes, (byte) 0); } } } diff --git a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java index 3a492a0950..f8465ce452 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java @@ -101,7 +101,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); // only allowed for admins - if (user == null || !superAdminAuthority.isSuperAdmin(user)) { + if (!isSecretAdmin && (user == null || !superAdminAuthority.isSuperAdmin(user))) { return channel -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "")); } else { ConfigUpdateRequest configUpdateRequest = new ConfigUpdateRequest(configTypes);