Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2313,6 +2313,8 @@ public List<Setting<?>> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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));
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/org/opensearch/security/auth/BackendRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<AuthCredentials, User> userCache; // rest standard
private Cache<String, User> restImpersonationCache; // used for rest impersonation
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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
Comment thread
itsmevichu marked this conversation as resolved.
Outdated
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) {
Comment thread
itsmevichu marked this conversation as resolved.
Outdated
final String providedSecret = request.header(ConfigConstants.SECURITY_SUPERADMIN_SECRET_HEADER);
if (isSuperadminSecretValid(providedSecret)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we want to audit log on a failed check since this is similar to a failed basic auth attempt?

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).
*/
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public RestHandler wrap(RestHandler original, AdminDNs adminDNs, Set<RestHeaderD
* Checks if a given user is a SuperAdmin
*/
boolean userIsSuperAdmin(User user, AdminDNs adminDNs) {
return user != null && adminDNs.isAdmin(user);
return user != null && (adminDNs.isAdmin(user) || ConfigConstants.SECURITY_SUPERADMIN_SECRET_USER.equals(user.getName()));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What happens if I configure a user on my cluster with the same name as this hard coded SECURITY_SUPERADMIN_SECRET_USER? Is there a path to escalate privileges here?

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@ public enum RolesMappingResolution {
public static final List<String> 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<String> getSettingAsSet(
final Settings settings,
final String key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> LEGACY_OPENDISTRO_SSL_DUAL_MODE_SETTING = Setting.boolSetting(
Expand Down Expand Up @@ -57,4 +63,40 @@ public class SecuritySettings {
Setting.Property.Dynamic,
Setting.Property.Sensitive
);

public static final Setting<SecureString> SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING = new InsecureFallbackStringSetting(
ConfigConstants.SECURITY_SUPERADMIN_SECRET
);

public static final Setting<SecureString> SECURITY_SUPERADMIN_SECRET_SETTING = SecureSetting.secureString(
ConfigConstants.SECURITY_SUPERADMIN_SECRET_SECURE,
SECURITY_SUPERADMIN_SECRET_INSECURE_SETTING
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IMHO, this should be accompanied with a very prominent note that using such a setting is inherently insecure and should be only if there is no other way. It should be noted that using TLS client cert auth should be always preferred.


/**
* 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<SecureString> {
Comment thread
itsmevichu marked this conversation as resolved.
Outdated
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);
}
}
}
28 changes: 25 additions & 3 deletions src/main/java/org/opensearch/security/tools/SecurityAdmin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -532,7 +546,8 @@ public static int execute(final String[] args) throws Exception {
enabledProtocols,
enabledCiphers,
hostname,
port
port,
superadminSecret
)
) {

Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand Down
Loading