Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests related to brute force attack prevention. #2245

Merged
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
@@ -0,0 +1,149 @@
/*
* Copyright OpenSearch Contributors
* 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.
*
*/
package org.opensearch.security;

import java.util.concurrent.TimeUnit;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.test.framework.AuthFailureListeners;
import org.opensearch.test.framework.RateLimiting;
import org.opensearch.test.framework.TestSecurityConfig.User;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;
import org.opensearch.test.framework.cluster.TestRestClientConfiguration;

import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.userWithSourceIp;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class IpBruteForceAttacksPreventionTests {
private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS);
private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS);

public static final int ALLOWED_TRIES = 3;
public static final int TIME_WINDOW_SECONDS = 3;

public static final String CLIENT_IP_2 = "127.0.0.2";
public static final String CLIENT_IP_3 = "127.0.0.3";
public static final String CLIENT_IP_4 = "127.0.0.4";
public static final String CLIENT_IP_5 = "127.0.0.5";
public static final String CLIENT_IP_6 = "127.0.0.6";
public static final String CLIENT_IP_7 = "127.0.0.7";
public static final String CLIENT_IP_8 = "127.0.0.8";
public static final String CLIENT_IP_9 = "127.0.0.9";

private static final AuthFailureListeners listener = new AuthFailureListeners()
.addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("ip")
.allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500)
.maxTrackedClients(500));

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder()
.clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener)
.authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE).users(USER_1, USER_2).build();

@Test
public void shouldAuthenticateUserWhenBlockadeIsNotActive() {
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_2))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldBlockIpAddress() {
authenticateUserWithIncorrectPassword(CLIENT_IP_3, USER_2, ALLOWED_TRIES);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_3))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, how do you test that an IP is blocked after multiple failed logins? I mean, that end user will still see UNAUTHORIZED error but on server side how do we know that the IP is blocked?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added an explicit check of logs in the commit c95fbdb

Copy link
Member

Choose a reason for hiding this comment

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

nice

}
}

@Test
public void shouldBlockUsersWhoUseTheSameIpAddress() {
authenticateUserWithIncorrectPassword(CLIENT_IP_4, USER_1, ALLOWED_TRIES);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_4))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
}

@Test
public void testUserShouldBeAbleToAuthenticateFromAnotherNotBlockedIpAddress() {
authenticateUserWithIncorrectPassword(CLIENT_IP_5, USER_1, ALLOWED_TRIES);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_6))) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldNotBlockIpWhenFailureAuthenticationCountIsLessThanAllowedTries() {
authenticateUserWithIncorrectPassword(CLIENT_IP_7, USER_1, ALLOWED_TRIES - 1);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_7))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldBlockIpWhenFailureAuthenticationCountIsGraterThanAllowedTries() {
Copy link
Member

Choose a reason for hiding this comment

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

nit: Greater

authenticateUserWithIncorrectPassword(CLIENT_IP_8, USER_1, ALLOWED_TRIES * 2);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_8))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
}

@Test
public void shouldReleaseIpAddressLock() throws InterruptedException {
authenticateUserWithIncorrectPassword(CLIENT_IP_9, USER_1, ALLOWED_TRIES * 2);
TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_9))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

private static void authenticateUserWithIncorrectPassword(String sourceIpAddress, User user, int numberOfRequests) {
var clientConfiguration = new TestRestClientConfiguration().username(user.getName())
.password("incorrect password").sourceInetAddress(sourceIpAddress);
try(TestRestClient client = cluster.createGenericClientRestClient(clientConfiguration)) {
for(int i = 0; i < numberOfRequests; ++i) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
}
}
}
26 changes: 20 additions & 6 deletions src/integrationTest/java/org/opensearch/security/TlsTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,29 @@
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.apache.hc.core5.http.message.BasicHeader;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.security.auditlog.impl.AuditCategory;
import org.opensearch.test.framework.AuditCompliance;
import org.opensearch.test.framework.AuditConfiguration;
import org.opensearch.test.framework.AuditFilters;
import org.opensearch.test.framework.TestSecurityConfig.User;
import org.opensearch.test.framework.audit.AuditLogsRule;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.opensearch.security.auditlog.AuditLog.Origin.REST;
import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
import static org.opensearch.test.framework.audit.AuditMessagePredicate.auditPredicate;
import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.getBasicAuthHeader;
import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
Expand All @@ -49,10 +57,17 @@ public class TlsTests {
public static final String AUTH_INFO_ENDPOINT = "/_opendistro/_security/authinfo?pretty";

@ClassRule
public static LocalCluster cluster = new LocalCluster.Builder()
public static final LocalCluster cluster = new LocalCluster.Builder()
.clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false)
.nodeSettings(Map.of(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of(SUPPORTED_CIPHER_SUIT)))
.authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).build();
.authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN)
.audit(new AuditConfiguration(true)
.compliance(new AuditCompliance().enabled(true))
.filters(new AuditFilters().enabledRest(true).enabledTransport(true))
).build();

@Rule
public AuditLogsRule auditLogsRule = new AuditLogsRule();

@Test
public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException {
Expand All @@ -61,15 +76,14 @@ public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException {

assertThatThrownBy(() -> httpClient.execute(request), instanceOf(NoHttpResponseException.class));
}
//TODO check if audit is created, audit_category = SSL_EXCEPTION
auditLogsRule.assertAtLeast(1, auditPredicate(AuditCategory.SSL_EXCEPTION).withLayer(REST));
}

@Test
public void shouldSupportClientCipherSuite_positive() throws IOException {
try(CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { SUPPORTED_CIPHER_SUIT })) {
HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT);
BasicHeader header = cluster.getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword());
httpGet.addHeader(header);
httpGet.addHeader(getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword()));

try(CloseableHttpResponse response = client.execute(httpGet)) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright OpenSearch Contributors
* 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.
*
*/
package org.opensearch.security;

import java.util.concurrent.TimeUnit;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.test.framework.AuthFailureListeners;
import org.opensearch.test.framework.RateLimiting;
import org.opensearch.test.framework.TestSecurityConfig.User;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;

import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class UserBruteForceAttacksPreventionTests {

private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS);
private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS);
private static final User USER_3 = new User("simple-user-3").roles(ALL_ACCESS);
private static final User USER_4 = new User("simple-user-4").roles(ALL_ACCESS);
private static final User USER_5 = new User("simple-user-5").roles(ALL_ACCESS);

public static final int ALLOWED_TRIES = 3;
public static final int TIME_WINDOW_SECONDS = 3;
private static final AuthFailureListeners listener = new AuthFailureListeners()
.addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("username").authenticationBackend("intern")
.allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500)
.maxTrackedClients(500));

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder()
.clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener)
.authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_1, USER_2, USER_3, USER_4, USER_5).build();

@Test
public void shouldAuthenticateUserWhenBlockadeIsNotActive() {
try(TestRestClient client = cluster.getRestClient(USER_1)) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() {
authenticateUserWithIncorrectPassword(USER_2, ALLOWED_TRIES);
try(TestRestClient client = cluster.getRestClient(USER_2)) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
}

@Test
public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGraterThanLimit() {
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

authenticateUserWithIncorrectPassword(USER_3, ALLOWED_TRIES * 2);
try(TestRestClient client = cluster.getRestClient(USER_3)) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
}

@Test
public void shouldNotBlockUserWhenNumberOfLoginAttemptIsBelowLimit() {
authenticateUserWithIncorrectPassword(USER_4, ALLOWED_TRIES - 1);
try(TestRestClient client = cluster.getRestClient(USER_4)) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldReleaseLock() throws InterruptedException {
authenticateUserWithIncorrectPassword(USER_5, ALLOWED_TRIES);
try(TestRestClient client = cluster.getRestClient(USER_5)) {
HttpResponse response = client.getAuthInfo();
response.assertStatusCode(SC_UNAUTHORIZED);
TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS);

response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

private static void authenticateUserWithIncorrectPassword(User user, int numberOfAttempts) {
try(TestRestClient client = cluster.getRestClient(user.getName(), "incorrect password")) {
for(int i = 0; i < numberOfAttempts; ++i) {
HttpResponse response = client.getAuthInfo();
response.assertStatusCode(SC_UNAUTHORIZED);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright OpenSearch Contributors
* 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.
*
*/
package org.opensearch.test.framework;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import org.opensearch.common.xcontent.ToXContentObject;
import org.opensearch.common.xcontent.XContentBuilder;

public class AuthFailureListeners implements ToXContentObject {

private Map<String, RateLimiting> limits = new LinkedHashMap<>();

public AuthFailureListeners addRateLimit(RateLimiting rateLimiting) {
Objects.requireNonNull(rateLimiting, "Rate limiting is required");
limits.put(rateLimiting.getName(), rateLimiting);
return this;
}

@Override
public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException {
xContentBuilder.startObject();
for(Map.Entry<String, RateLimiting> entry : limits.entrySet()) {
xContentBuilder.field(entry.getKey(), entry.getValue());
}
xContentBuilder.endObject();
return xContentBuilder;
}
}
Loading