diff --git a/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java b/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java
new file mode 100644
index 0000000000..5f812b921b
--- /dev/null
+++ b/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java
@@ -0,0 +1,435 @@
+/*
+* 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.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.opensearch.action.search.SearchRequest;
+import org.opensearch.action.search.SearchResponse;
+import org.opensearch.client.Client;
+import org.opensearch.client.RestHighLevelClient;
+import org.opensearch.test.framework.TestSecurityConfig.Role;
+import org.opensearch.test.framework.TestSecurityConfig.User;
+import org.opensearch.test.framework.certificate.TestCertificates;
+import org.opensearch.test.framework.cluster.ClusterManager;
+import org.opensearch.test.framework.cluster.LocalCluster;
+import org.opensearch.test.framework.cluster.SearchRequestFactory;
+import org.opensearch.test.framework.cluster.TestRestClient;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
+import static org.opensearch.client.RequestOptions.DEFAULT;
+import static org.opensearch.rest.RestStatus.FORBIDDEN;
+import static org.opensearch.security.Song.ARTIST_FIRST;
+import static org.opensearch.security.Song.FIELD_ARTIST;
+import static org.opensearch.security.Song.FIELD_GENRE;
+import static org.opensearch.security.Song.FIELD_LYRICS;
+import static org.opensearch.security.Song.FIELD_STARS;
+import static org.opensearch.security.Song.FIELD_TITLE;
+import static org.opensearch.security.Song.GENRE_JAZZ;
+import static org.opensearch.security.Song.GENRE_ROCK;
+import static org.opensearch.security.Song.QUERY_TITLE_MAGNUM_OPUS;
+import static org.opensearch.security.Song.SONGS;
+import static org.opensearch.security.Song.TITLE_MAGNUM_OPUS;
+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.cluster.SearchRequestFactory.queryStringQueryRequest;
+import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy;
+import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException;
+import static org.opensearch.test.framework.matcher.SearchResponseMatchers.isSuccessfulSearchResponse;
+import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfTotalHitsIsEqualTo;
+import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue;
+import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitDoesNotContainField;
+import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitsContainDocumentWithId;
+import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitsContainDocumentsInAnyOrder;
+
+/**
+* This is a parameterized test so that one test class is used to test security plugin behaviour when ccsMinimizeRoundtrips
+* option is enabled or disabled. Method {@link #parameters()} is a source of parameters values.
+*/
+@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
+@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
+public class CrossClusterSearchTests {
+
+ private static final String SONG_INDEX_NAME = "song_lyrics";
+
+ private static final String PROHIBITED_SONG_INDEX_NAME = "prohibited_song_lyrics";
+
+ public static final String REMOTE_CLUSTER_NAME = "ccsRemote";
+ public static final String REMOTE_SONG_INDEX = REMOTE_CLUSTER_NAME + ":" + SONG_INDEX_NAME;
+
+ public static final String SONG_ID_1R = "remote-00001";
+ public static final String SONG_ID_2L = "local-00002";
+ public static final String SONG_ID_3R = "remote-00003";
+ public static final String SONG_ID_4L = "local-00004";
+ public static final String SONG_ID_5R = "remote-00005";
+ public static final String SONG_ID_6R = "remote-00006";
+
+ private static final Role LIMITED_ROLE = new Role("limited_role")
+ .indexPermissions("indices:data/read/search", "indices:admin/shards/search_shards")
+ .on(SONG_INDEX_NAME, "user-${user.name}-${attr.internal.type}");
+
+ private static final Role DLS_ROLE_ROCK = new Role("dls_role_rock")
+ .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards")
+ .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_GENRE, GENRE_ROCK))
+ .on(SONG_INDEX_NAME);
+
+ private static final Role DLS_ROLE_JAZZ = new Role("dls_role_jazz")
+ .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards")
+ .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_GENRE, GENRE_JAZZ))
+ .on(SONG_INDEX_NAME);
+
+ private static final Role FLS_EXCLUDE_LYRICS_ROLE = new Role("fls_exclude_lyrics_role")
+ .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards")
+ .fls("~" + FIELD_LYRICS)
+ .on(SONG_INDEX_NAME);
+
+ private static final Role FLS_INCLUDE_TITLE_ROLE = new Role("fls_include_title_role")
+ .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards")
+ .fls(FIELD_TITLE)
+ .on(SONG_INDEX_NAME);
+
+ public static final String TYPE_ATTRIBUTE = "type";
+
+ private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS).attr(TYPE_ATTRIBUTE, "administrative");
+ private static final User LIMITED_USER = new User("limited_user").attr(TYPE_ATTRIBUTE, "personal");
+
+ private static final User FLS_INCLUDE_TITLE_USER = new User("fls_include_title_user");
+
+ private static final User FLS_EXCLUDE_LYRICS_USER = new User("fls_exclude_lyrics_user");
+
+ private static final User DLS_USER_ROCK = new User("dls-user-rock");
+
+ private static final User DLS_USER_JAZZ = new User("dls-user-jazz");
+
+ public static final String LIMITED_USER_INDEX_NAME = "user-" + LIMITED_USER.getName() + "-" + LIMITED_USER.getAttribute(TYPE_ATTRIBUTE);
+ public static final String ADMIN_USER_INDEX_NAME = "user-" + ADMIN_USER.getName() + "-" + ADMIN_USER.getAttribute(TYPE_ATTRIBUTE);
+
+ private static final TestCertificates TEST_CERTIFICATES = new TestCertificates();
+
+ private final boolean ccsMinimizeRoundtrips;
+
+ public static final String PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED = "plugins.security.restapi.roles_enabled";
+ @ClassRule
+ public static final LocalCluster remoteCluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES)
+ .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).clusterName(REMOTE_CLUSTER_NAME)
+ .authc(AUTHC_HTTPBASIC_INTERNAL)
+ .roles(LIMITED_ROLE, DLS_ROLE_ROCK, DLS_ROLE_JAZZ, FLS_EXCLUDE_LYRICS_ROLE, FLS_INCLUDE_TITLE_ROLE).users(ADMIN_USER)
+ .build();
+
+ @ClassRule
+ public static final LocalCluster cluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES)
+ .clusterManager(ClusterManager.SINGLE_REMOTE_CLIENT).anonymousAuth(false).clusterName("ccsLocal")
+ .nodeSettings(Map.of(PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName())))
+ .remote(REMOTE_CLUSTER_NAME, remoteCluster)
+ .roles(LIMITED_ROLE, DLS_ROLE_ROCK, DLS_ROLE_JAZZ, FLS_EXCLUDE_LYRICS_ROLE, FLS_INCLUDE_TITLE_ROLE).authc(AUTHC_HTTPBASIC_INTERNAL)
+ .users(ADMIN_USER, LIMITED_USER, DLS_USER_ROCK, DLS_USER_JAZZ, FLS_INCLUDE_TITLE_USER, FLS_EXCLUDE_LYRICS_USER)
+ .build();
+
+ @ParametersFactory(shuffle = false)
+ public static Iterable