Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased 3.x]
### Added
- Add support for Basic Authentication in webhook audit log sink using `plugins.security.audit.config.username` and `plugins.security.audit.config.password` ([#5792](https://github.com/opensearch-project/security/pull/5792))

### Changed
- Ensure all restHeaders from ActionPlugin.getRestHeaders are carried to threadContext for tracing ([#5396](https://github.com/opensearch-project/security/pull/5396))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1827,14 +1827,14 @@ public List<Setting<?>> getSettings() {
); // not filtered here
settings.add(
Setting.simpleString(
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME,
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME,
Property.NodeScope,
Property.Filtered
)
);
settings.add(
Setting.simpleString(
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD,
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD,
Property.NodeScope,
Property.Filtered
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ public ExternalOpenSearchSink(
ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH,
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT
);
final String user = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME);
final String password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD);
final String user = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME);
final String password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD);

final HttpClientBuilder builder = HttpClient.builder(servers.toArray(new String[0]));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;

Expand Down Expand Up @@ -60,6 +61,9 @@ public class WebhookSink extends AuditLogSink {
WebhookFormat webhookFormat = null;
final boolean verifySSL;
final KeyStore effectiveTruststore;
private final String username;
private final String password;
private final String basicAuthHeader;

public WebhookSink(
final String name,
Expand All @@ -77,6 +81,19 @@ public WebhookSink(
final String webhookUrl = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_URL);
final String format = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_FORMAT);

// Read basic auth credentials
this.username = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME);
this.password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD);

// Generate Basic Auth header if credentials are provided
if (this.username != null && this.password != null) {
String credentials = this.username + ":" + this.password;
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
this.basicAuthHeader = "Basic " + encodedCredentials;
} else {
this.basicAuthHeader = null;
}

verifySSL = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_WEBHOOK_SSL_VERIFY, true);
httpClient = getHttpClient();

Expand Down Expand Up @@ -225,6 +242,12 @@ boolean get(AuditMessage msg) {

protected boolean doGet(String url) {
HttpGet httpGet = new HttpGet(url);

// Add Basic Auth header if credentials are configured
if (basicAuthHeader != null) {
httpGet.setHeader("Authorization", basicAuthHeader);
}

CloseableHttpResponse serverResponse = null;
try {
serverResponse = httpClient.execute(httpGet);
Expand Down Expand Up @@ -280,6 +303,11 @@ protected boolean doPost(String url, String payload) {

HttpPost postRequest = new HttpPost(url);

// Add Basic Auth header if credentials are configured
if (basicAuthHeader != null) {
postRequest.setHeader("Authorization", basicAuthHeader);
}

StringEntity input = new StringEntity(payload, webhookFormat.contentType.withCharset(StandardCharsets.UTF_8));
postRequest.setEntity(input);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ public class ConfigConstants {

// External OpenSearch
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS = "http_endpoints";
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME = "username";
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD = "password";
public static final String SECURITY_AUDIT_CONFIG_USERNAME = "username";
public static final String SECURITY_AUDIT_CONFIG_PASSWORD = "password";
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL = "enable_ssl";
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES = "verify_hostnames";
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH = "enable_ssl_client_auth";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.io.HttpRequestHandler;
Expand All @@ -26,12 +29,19 @@ public class TestHttpHandler implements HttpRequestHandler {
public String method;
public String uri;
public String body;
public Map<String, String> headers;

@Override
public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException {
this.method = request.getMethod();
this.uri = request.getRequestUri();

// Capture headers
this.headers = new HashMap<>();
for (Header header : request.getHeaders()) {
this.headers.put(header.getName(), header.getValue());
}

HttpEntity entity = request.getEntity();
body = EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
Expand All @@ -40,5 +50,6 @@ public void reset() {
this.body = null;
this.uri = null;
this.method = null;
this.headers = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,8 @@ public void testExternalPemUserPass() throws Exception {
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH,
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/spock.key.pem")
)
.put(
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME,
"admin"
)
.put(
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD,
"admin"
)
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME, "admin")
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD, "admin")
.build();

setup(additionalSettings);
Expand Down Expand Up @@ -174,14 +168,8 @@ public void testExternalPemUserPassTp() throws Exception {
+ ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH,
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/chain-ca.pem")
)
.put(
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME,
"admin"
)
.put(
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD,
"admin"
)
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME, "admin")
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD, "admin")
.build();

setup(additionalSettings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,162 @@ private SSLContext createSSLContext() throws Exception {
return sslContext;
}

@Test
public void basicAuthPostTest() throws Exception {
TestHttpHandler handler = new TestHttpHandler();

int port = findFreePort();
server = ServerBootstrap.bootstrap()
.setListenerPort(port)
.setHttpProcessor(HttpProcessors.server("Test/1.1"))
.setRequestRouter((request, context) -> handler)
.create();

server.start();

String url = "http://localhost:" + port + "/endpoint";
String username = "test_user";
String password = "test_password";

// Test with basic auth credentials - POST JSON
Settings settings = Settings.builder()
.put("plugins.security.audit.config.webhook.url", url)
.put("plugins.security.audit.config.webhook.format", "json")
.put("plugins.security.audit.config.username", username)
.put("plugins.security.audit.config.password", password)
.put("path.home", ".")
.put(
SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH,
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")
)
.build();

LoggingSink fallback = new LoggingSink("test", Settings.EMPTY, null, null);
WebhookSink auditlog = new WebhookSink("name", settings, ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT, null, fallback);
AuditMessage msg = MockAuditMessageFactory.validAuditMessage();
auditlog.store(msg);

// Verify request was made
assertThat(handler.method, is("POST"));
Assert.assertNotNull(handler.body);
Assert.assertTrue(handler.body.contains("{"));
assertStringContainsAllKeysAndValues(handler.body);

// Verify Authorization header is present and correct
Assert.assertNotNull(handler.headers);
Assert.assertTrue(handler.headers.containsKey("Authorization"));
String authHeader = handler.headers.get("Authorization");
Assert.assertTrue(authHeader.startsWith("Basic "));

// Decode and verify credentials
String encodedCredentials = authHeader.substring("Basic ".length());
String decodedCredentials = new String(java.util.Base64.getDecoder().decode(encodedCredentials), StandardCharsets.UTF_8);
Assert.assertEquals(username + ":" + password, decodedCredentials);

// no message stored on fallback
assertThat(fallback.messages.size(), is(0));
auditlog.close();
server.awaitTermination(TimeValue.ofSeconds(3));
}

@Test
public void basicAuthGetTest() throws Exception {
TestHttpHandler handler = new TestHttpHandler();

int port = findFreePort();
server = ServerBootstrap.bootstrap()
.setListenerPort(port)
.setHttpProcessor(HttpProcessors.server("Test/1.1"))
.setRequestRouter((request, context) -> handler)
.create();

server.start();

String url = "http://localhost:" + port + "/endpoint";
String username = "test_user";
String password = "test_password";

// Test with basic auth credentials - GET
Settings settings = Settings.builder()
.put("plugins.security.audit.config.webhook.url", url)
.put("plugins.security.audit.config.webhook.format", "URL_PARAMETER_GET")
.put("plugins.security.audit.config.username", username)
.put("plugins.security.audit.config.password", password)
.put("path.home", ".")
.put(
SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH,
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")
)
.build();

LoggingSink fallback = new LoggingSink("test", Settings.EMPTY, null, null);
WebhookSink auditlog = new WebhookSink("name", settings, ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT, null, fallback);
AuditMessage msg = MockAuditMessageFactory.validAuditMessage();
auditlog.store(msg);

// Verify request was made with GET method
assertThat(handler.method, is("GET"));

// Verify Authorization header is present and correct
Assert.assertNotNull(handler.headers);
Assert.assertTrue(handler.headers.containsKey("Authorization"));
String authHeader = handler.headers.get("Authorization");
Assert.assertTrue(authHeader.startsWith("Basic "));

// Decode and verify credentials
String encodedCredentials = authHeader.substring("Basic ".length());
String decodedCredentials = new String(java.util.Base64.getDecoder().decode(encodedCredentials), StandardCharsets.UTF_8);
Assert.assertEquals(username + ":" + password, decodedCredentials);

auditlog.close();
server.awaitTermination(TimeValue.ofSeconds(3));
}

@Test
public void webhookWithoutAuthTest() throws Exception {
TestHttpHandler handler = new TestHttpHandler();

int port = findFreePort();
server = ServerBootstrap.bootstrap()
.setListenerPort(port)
.setHttpProcessor(HttpProcessors.server("Test/1.1"))
.setRequestRouter((request, context) -> handler)
.create();

server.start();

String url = "http://localhost:" + port + "/endpoint";

// Test without credentials - should not have Authorization header
Settings settings = Settings.builder()
.put("plugins.security.audit.config.webhook.url", url)
.put("plugins.security.audit.config.webhook.format", "json")
.put("path.home", ".")
.put(
SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH,
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")
)
.build();

LoggingSink fallback = new LoggingSink("test", Settings.EMPTY, null, null);
WebhookSink auditlog = new WebhookSink("name", settings, ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT, null, fallback);
AuditMessage msg = MockAuditMessageFactory.validAuditMessage();
auditlog.store(msg);

// Verify request was made
assertThat(handler.method, is("POST"));
Assert.assertNotNull(handler.body);

// Verify Authorization header is NOT present
Assert.assertFalse(handler.headers.containsKey("Authorization"));

// no message stored on fallback
assertThat(fallback.messages.size(), is(0));

auditlog.close();
server.awaitTermination(TimeValue.ofSeconds(3));
}

private void assertStringContainsAllKeysAndValues(String in) {
Assert.assertTrue(in, in.contains(AuditMessage.FORMAT_VERSION));
Assert.assertTrue(in, in.contains(AuditMessage.CATEGORY));
Expand Down
Loading