You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2023/10/30 11:48:13 UTC

(syncope) branch master updated (4bd06f1d51 -> 37fbbee87c)

This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


    from 4bd06f1d51 [SYNCOPE-1786] Swagger UI for Self Keymaster + new property keymaster.enableAutoRegistration true by default (#540)
     new 6c5c5e5c5d Upgrading cyclonedx-maven-plugin
     new 37fbbee87c  [SYNCOPE-1783] OpenSearch extension (#538)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../{fit_Zookeeper.yml => fit_OpenSearch.yml}      |  12 +-
 ext/elasticsearch/client-elasticsearch/pom.xml     |   9 --
 .../SyncopeElasticsearchHealthContributor.java     |   4 -
 .../jpa/dao/ElasticsearchAnySearchDAO.java         |   6 +-
 .../jpa/dao/ElasticsearchAuditConfDAO.java         |   8 +-
 .../client-opensearch}/pom.xml                     |  23 ++--
 .../client/OpenSearchClientContext.java}           |  40 +++----
 .../client/OpenSearchClientFactoryBean.java}       |  26 ++---
 .../opensearch/client/OpenSearchIndexLoader.java}  |  10 +-
 .../opensearch/client/OpenSearchIndexManager.java} | 102 ++++++++---------
 .../opensearch/client/OpenSearchProperties.java}   |   6 +-
 .../ext/opensearch/client/OpenSearchUtils.java}    |  12 +-
 .../SyncopeOpenSearchHealthContributor.java}       |  16 +--
 .../src/main/resources/META-INF/spring.factories}  |   4 +-
 ext/{elasticsearch => opensearch}/logic/pom.xml    |  14 +--
 .../core/logic/audit/OpenSearchAppender.java}      |  36 +++---
 .../core/logic/audit/OpenSearchAuditAppender.java} |  10 +-
 .../core/logic/audit/OpenSearchLogicContext.java}  |  10 +-
 .../src/main/resources/META-INF/spring.factories   |   5 +-
 .../persistence-jpa/pom.xml                        |  14 +--
 .../jpa/OpenSearchPersistenceContext.java}         |  26 ++---
 .../jpa/dao/OpenSearchAnySearchDAO.java}           |  88 ++++++++-------
 .../jpa/dao/OpenSearchAuditConfDAO.java}           |  59 +++++-----
 .../src/main/resources/META-INF/spring.factories}  |   4 +-
 .../jpa/dao/OpenSearchAnySearchDAOTest.java}       |  97 +++++++++-------
 ext/{elasticsearch => opensearch}/pom.xml          |   8 +-
 .../provisioning-java/pom.xml                      |  15 +--
 .../provisioning/java/job/OpenSearchReindex.java}  |  30 ++---
 ext/pom.xml                                        |   1 +
 fit/core-reference/pom.xml                         | 122 ++++++++++++++++++++-
 .../fit/core/reference/CoreReferenceContext.java   |  15 ++-
 .../fit/core/reference/ITImplementationLookup.java |  14 ++-
 ...{ElasticsearchInit.java => OpenSearchInit.java} |   8 +-
 .../src/main/resources/core-opensearch.properties  |   5 +-
 fit/core-reference/src/main/resources/log4j2.xml   |   3 +
 .../org/apache/syncope/fit/AbstractITCase.java     |   7 +-
 .../org/apache/syncope/fit/AbstractUIITCase.java   |   5 +-
 .../org/apache/syncope/fit/core/AuditITCase.java   |   6 +-
 .../syncope/fit/core/AuthenticationITCase.java     |   4 +-
 .../apache/syncope/fit/core/DelegationITCase.java  |   2 +-
 .../apache/syncope/fit/core/DynRealmITCase.java    |   6 +-
 .../org/apache/syncope/fit/core/GroupITCase.java   |   4 +-
 .../apache/syncope/fit/core/KeymasterITCase.java   |   2 +-
 .../apache/syncope/fit/core/MembershipITCase.java  |   2 +-
 .../syncope/fit/core/MultitenancyITCase.java       |   2 +-
 .../apache/syncope/fit/core/PullTaskITCase.java    |  12 +-
 .../org/apache/syncope/fit/core/SearchITCase.java  |  18 +--
 .../apache/syncope/fit/core/UserIssuesITCase.java  |   2 +-
 .../apache/syncope/fit/core/UserSelfITCase.java    |   2 +-
 pom.xml                                            |  21 +++-
 .../reference-guide/concepts/extensions.adoc       |  22 ++++
 .../reference-guide/usage/customization.adoc       |  58 ++++++++++
 52 files changed, 638 insertions(+), 399 deletions(-)
 copy .github/workflows/{fit_Zookeeper.yml => fit_OpenSearch.yml} (83%)
 copy ext/{elasticsearch/client-elasticsearch => opensearch/client-opensearch}/pom.xml (75%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchClientContext.java => opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientContext.java} (61%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchClientFactoryBean.java => opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientFactoryBean.java} (83%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexLoader.java => opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java} (89%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java => opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java} (73%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchProperties.java => opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchProperties.java} (93%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java => opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java} (96%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java => opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/SyncopeOpenSearchHealthContributor.java} (81%)
 copy ext/{oidcc4ui/persistence-jpa/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports => opensearch/client-opensearch/src/main/resources/META-INF/spring.factories} (85%)
 copy ext/{elasticsearch => opensearch}/logic/pom.xml (80%)
 copy ext/{elasticsearch/logic/src/main/java/org/apache/syncope/core/logic/audit/ElasticsearchAppender.java => opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAppender.java} (69%)
 copy ext/{elasticsearch/logic/src/main/java/org/apache/syncope/core/logic/audit/ElasticsearchAuditAppender.java => opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAuditAppender.java} (80%)
 copy ext/{elasticsearch/logic/src/main/java/org/apache/syncope/core/logic/audit/ElasticsearchLogicContext.java => opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchLogicContext.java} (87%)
 copy docker/console/src/main/resources/startup.sh => ext/opensearch/logic/src/main/resources/META-INF/spring.factories (86%)
 mode change 100755 => 100644
 copy ext/{elasticsearch => opensearch}/persistence-jpa/pom.xml (84%)
 copy ext/{elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/ElasticsearchPersistenceContext.java => opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java} (77%)
 copy ext/{elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java => opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java} (89%)
 copy ext/{elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java => opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAuditConfDAO.java} (79%)
 copy ext/{elasticsearch/client-elasticsearch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports => opensearch/persistence-jpa/src/main/resources/META-INF/spring.factories} (85%)
 copy ext/{elasticsearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAOTest.java => opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java} (84%)
 copy ext/{elasticsearch => opensearch}/pom.xml (87%)
 copy ext/{oidcc4ui => opensearch}/provisioning-java/pom.xml (80%)
 copy ext/{elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java => opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java} (87%)
 copy fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/{ElasticsearchInit.java => OpenSearchInit.java} (94%)
 copy client/idm/console/src/main/resources/org/apache/syncope/client/console/wizards/resources/ConnCapabilitiesPanel_ru.properties => fit/core-reference/src/main/resources/core-opensearch.properties (85%)


(syncope) 01/02: Upgrading cyclonedx-maven-plugin

Posted by il...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 6c5c5e5c5d8a8082acbfa9c944797eda5185968a
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Mon Oct 30 08:15:39 2023 +0100

    Upgrading cyclonedx-maven-plugin
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 11a97a88de..34268532b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1997,7 +1997,7 @@ under the License.
           <plugin>
             <groupId>org.cyclonedx</groupId>
             <artifactId>cyclonedx-maven-plugin</artifactId>
-            <version>2.7.9</version>
+            <version>2.7.10</version>
             <executions>
               <execution>
                 <phase>package</phase>


(syncope) 02/02: [SYNCOPE-1783] OpenSearch extension (#538)

Posted by il...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 37fbbee87c02695ad908f1c1359d827d118b2cbd
Author: Francesco Chicchiriccò <il...@users.noreply.github.com>
AuthorDate: Mon Oct 30 11:52:42 2023 +0100

     [SYNCOPE-1783] OpenSearch extension (#538)
---
 .github/workflows/fit_OpenSearch.yml               |  53 +++
 ext/elasticsearch/client-elasticsearch/pom.xml     |   9 -
 .../SyncopeElasticsearchHealthContributor.java     |   4 -
 .../jpa/dao/ElasticsearchAnySearchDAO.java         |   6 +-
 .../jpa/dao/ElasticsearchAuditConfDAO.java         |   8 +-
 .../client-opensearch}/pom.xml                     |  23 +-
 .../opensearch/client/OpenSearchClientContext.java |  83 +++++
 .../client/OpenSearchClientFactoryBean.java        | 140 ++++++++
 .../opensearch/client/OpenSearchIndexLoader.java   |  67 ++++
 .../opensearch/client/OpenSearchIndexManager.java  | 277 ++++++++++++++
 .../opensearch/client/OpenSearchProperties.java    |  67 ++++
 .../ext/opensearch/client/OpenSearchUtils.java     | 218 +++++++++++
 .../SyncopeOpenSearchHealthContributor.java}       |  16 +-
 .../src/main/resources/META-INF/spring.factories   |  19 +
 .../logic}/pom.xml                                 |  35 +-
 .../core/logic/audit/OpenSearchAppender.java       |  97 +++++
 .../core/logic/audit/OpenSearchAuditAppender.java  |  52 +++
 .../core/logic/audit/OpenSearchLogicContext.java   |  62 ++++
 .../src/main/resources/META-INF/spring.factories   |  19 +
 .../persistence-jpa}/pom.xml                       |  43 ++-
 .../jpa/OpenSearchPersistenceContext.java          |  81 +++++
 .../jpa/dao/OpenSearchAnySearchDAO.java}           |  88 +++--
 .../jpa/dao/OpenSearchAuditConfDAO.java}           |  59 +--
 .../src/main/resources/META-INF/spring.factories   |  19 +
 .../jpa/dao/OpenSearchAnySearchDAOTest.java        | 400 +++++++++++++++++++++
 ext/opensearch/pom.xml                             |  47 +++
 .../provisioning-java}/pom.xml                     |  31 +-
 .../provisioning/java/job/OpenSearchReindex.java   | 197 ++++++++++
 ext/pom.xml                                        |   1 +
 fit/core-reference/pom.xml                         | 122 ++++++-
 .../fit/core/reference/CoreReferenceContext.java   |  15 +-
 .../fit/core/reference/ITImplementationLookup.java |  14 +-
 .../syncope/fit/core/reference/OpenSearchInit.java |  73 ++++
 .../src/main/resources/core-opensearch.properties  |  21 ++
 fit/core-reference/src/main/resources/log4j2.xml   |   3 +
 .../org/apache/syncope/fit/AbstractITCase.java     |   7 +-
 .../org/apache/syncope/fit/AbstractUIITCase.java   |   5 +-
 .../org/apache/syncope/fit/core/AuditITCase.java   |   6 +-
 .../syncope/fit/core/AuthenticationITCase.java     |   4 +-
 .../apache/syncope/fit/core/DelegationITCase.java  |   2 +-
 .../apache/syncope/fit/core/DynRealmITCase.java    |   6 +-
 .../org/apache/syncope/fit/core/GroupITCase.java   |   4 +-
 .../apache/syncope/fit/core/KeymasterITCase.java   |   2 +-
 .../apache/syncope/fit/core/MembershipITCase.java  |   2 +-
 .../syncope/fit/core/MultitenancyITCase.java       |   2 +-
 .../apache/syncope/fit/core/PullTaskITCase.java    |  12 +-
 .../org/apache/syncope/fit/core/SearchITCase.java  |  18 +-
 .../apache/syncope/fit/core/UserIssuesITCase.java  |   2 +-
 .../apache/syncope/fit/core/UserSelfITCase.java    |   2 +-
 pom.xml                                            |  19 +
 .../reference-guide/concepts/extensions.adoc       |  22 ++
 .../reference-guide/usage/customization.adoc       |  58 +++
 52 files changed, 2418 insertions(+), 224 deletions(-)

diff --git a/.github/workflows/fit_OpenSearch.yml b/.github/workflows/fit_OpenSearch.yml
new file mode 100644
index 0000000000..4ff6815eee
--- /dev/null
+++ b/.github/workflows/fit_OpenSearch.yml
@@ -0,0 +1,53 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License
+name: "FIT OpenSearch"
+
+on:
+  push:
+    branches: ['master', 'pr-*']
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [master]
+  schedule:
+    - cron: '0 13 * * 4'
+
+jobs:
+  fit_OpenSearch:
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+    - name: Setup Java JDK
+      uses: actions/setup-java@v3
+      with:
+        distribution: 'zulu'
+        java-version: 11
+    - name: Setup Maven
+      uses: stCarolas/setup-maven@v4.5
+      with:
+        maven-version: 3.9.4
+    - uses: actions/cache@v3
+      with:
+        path: ~/.m2/repository
+        key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+        restore-keys: |
+          ${{ runner.os }}-maven-
+    - name: Build
+      run: mvn -U -T 1C -P 'skipTests,all'
+    - name: 'OpenSearch / H2 / JSON'
+      run: mvn -f fit/core-reference/pom.xml -P opensearch-it -Dinvoker.streamLogs=true -Dmodernizer.skip=true -Drat.skip=true -Dcheckstyle.skip=true -Djacoco.skip=true
diff --git a/ext/elasticsearch/client-elasticsearch/pom.xml b/ext/elasticsearch/client-elasticsearch/pom.xml
index d48d5edef3..0c98ada220 100644
--- a/ext/elasticsearch/client-elasticsearch/pom.xml
+++ b/ext/elasticsearch/client-elasticsearch/pom.xml
@@ -48,15 +48,6 @@ under the License.
       <groupId>co.elastic.clients</groupId>
       <artifactId>elasticsearch-java</artifactId>
     </dependency>
-    <dependency>
-      <groupId>jakarta.json</groupId>
-      <artifactId>jakarta.json-api</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
 
     <dependency>
       <groupId>org.springframework.boot</groupId>
diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java
index 163519c532..de935721b0 100644
--- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java
+++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java
@@ -18,10 +18,6 @@
  */
 package org.apache.syncope.ext.elasticsearch.client;
 
-import static co.elastic.clients.elasticsearch._types.HealthStatus.Green;
-import static co.elastic.clients.elasticsearch._types.HealthStatus.Red;
-import static co.elastic.clients.elasticsearch._types.HealthStatus.Yellow;
-
 import co.elastic.clients.elasticsearch.ElasticsearchClient;
 import co.elastic.clients.elasticsearch.cluster.HealthResponse;
 import org.springframework.boot.actuate.health.Health;
diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
index 9c41afdc39..31b4615658 100644
--- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
+++ b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
@@ -36,7 +36,6 @@ import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -311,10 +310,9 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
                 build();
         LOG.debug("Search JSON request: {}", request);
 
-        @SuppressWarnings("rawtypes")
-        List<Hit<Map>> esResult = null;
+        List<Hit<Void>> esResult = null;
         try {
-            esResult = client.search(request, Map.class).hits().hits();
+            esResult = client.search(request, Void.class).hits().hits();
         } catch (Exception e) {
             LOG.error("While searching in Elasticsearch", e);
         }
diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java
index 2abc1d997c..60d7cc0e01 100644
--- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java
+++ b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java
@@ -30,11 +30,11 @@ import co.elastic.clients.elasticsearch.core.CountRequest;
 import co.elastic.clients.elasticsearch.core.SearchRequest;
 import co.elastic.clients.elasticsearch.core.search.Hit;
 import co.elastic.clients.json.JsonData;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import java.io.IOException;
 import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
@@ -184,15 +184,15 @@ public class ElasticsearchAuditConfDAO extends JPAAuditConfDAO {
                 index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
                 query(getQuery(entityKey, type, category, subcategory, events, result, before, after)).
+                fields(f -> f.field("message")).
                 from(itemsPerPage * (page <= 0 ? 0 : page - 1)).
                 size(itemsPerPage < 0 ? indexMaxResultWindow : itemsPerPage).
                 sort(sortBuilders(orderBy)).
                 build();
 
-        @SuppressWarnings("rawtypes")
-        List<Hit<Map>> esResult = null;
+        List<Hit<ObjectNode>> esResult = null;
         try {
-            esResult = client.search(request, Map.class).hits().hits();
+            esResult = client.search(request, ObjectNode.class).hits().hits();
         } catch (Exception e) {
             LOG.error("While searching in Elasticsearch", e);
         }
diff --git a/ext/elasticsearch/client-elasticsearch/pom.xml b/ext/opensearch/client-opensearch/pom.xml
similarity index 75%
copy from ext/elasticsearch/client-elasticsearch/pom.xml
copy to ext/opensearch/client-opensearch/pom.xml
index d48d5edef3..47a89de202 100644
--- a/ext/elasticsearch/client-elasticsearch/pom.xml
+++ b/ext/opensearch/client-opensearch/pom.xml
@@ -23,14 +23,14 @@ under the License.
 
   <parent>
     <groupId>org.apache.syncope.ext</groupId>
-    <artifactId>syncope-ext-elasticsearch</artifactId>
+    <artifactId>syncope-ext-opensearch</artifactId>
     <version>4.0.0-SNAPSHOT</version>
   </parent>
 
-  <name>Apache Syncope Ext: Elasticsearch Client</name>
-  <description>Apache Syncope Ext: Elasticsearch Client</description>
-  <groupId>org.apache.syncope.ext.elasticsearch</groupId>
-  <artifactId>syncope-ext-elasticsearch-client</artifactId>
+  <name>Apache Syncope Ext: OpenSearch Client</name>
+  <description>Apache Syncope Ext: OpenSearch Client</description>
+  <groupId>org.apache.syncope.ext.opensearch</groupId>
+  <artifactId>syncope-ext-opensearch-client</artifactId>
   <packaging>jar</packaging>
   
   <properties>
@@ -45,17 +45,12 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>co.elastic.clients</groupId>
-      <artifactId>elasticsearch-java</artifactId>
+      <groupId>org.opensearch.client</groupId>
+      <artifactId>opensearch-rest-client</artifactId>
     </dependency>
     <dependency>
-      <groupId>jakarta.json</groupId>
-      <artifactId>jakarta.json-api</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
+      <groupId>org.opensearch.client</groupId>
+      <artifactId>opensearch-java</artifactId>
     </dependency>
 
     <dependency>
diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientContext.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientContext.java
new file mode 100644
index 0000000000..89f0965940
--- /dev/null
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientContext.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.opensearch.client;
+
+import java.util.stream.Collectors;
+import org.apache.http.HttpHost;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.identityconnectors.common.CollectionUtil;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.springframework.boot.actuate.health.HealthContributor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+@EnableConfigurationProperties(OpenSearchProperties.class)
+@Configuration(proxyBeanMethods = false)
+public class OpenSearchClientContext {
+
+    @ConditionalOnMissingBean
+    @Bean
+    public OpenSearchClientFactoryBean openSearchClientFactoryBean(final OpenSearchProperties props) {
+        return new OpenSearchClientFactoryBean(
+                CollectionUtil.nullAsEmpty(props.getHosts()).stream().
+                        map(HttpHost::create).collect(Collectors.toList()));
+    }
+
+    @ConditionalOnMissingBean
+    @Bean
+    public OpenSearchUtils openSearchUtils(
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final @Lazy AnyObjectDAO anyObjectDAO) {
+
+        return new OpenSearchUtils(userDAO, groupDAO, anyObjectDAO);
+    }
+
+    @ConditionalOnMissingBean
+    @Bean
+    public OpenSearchIndexManager openSearchIndexManager(
+            final OpenSearchProperties props,
+            final OpenSearchClient client,
+            final OpenSearchUtils openSearchUtils) {
+
+        return new OpenSearchIndexManager(
+                client,
+                openSearchUtils,
+                props.getNumberOfShards(),
+                props.getNumberOfReplicas());
+    }
+
+    @ConditionalOnMissingBean
+    @Bean
+    public OpenSearchIndexLoader openSearchIndexLoader(final OpenSearchIndexManager indexManager) {
+        return new OpenSearchIndexLoader(indexManager);
+    }
+
+    @ConditionalOnMissingBean(name = "syncopeOpenSearchHealthContributor")
+    @Bean(name = {
+        "syncopeOpenSearchHealthContributor", "openSearchHealthIndicator", "openSearchHealthContributor" })
+    public HealthContributor syncopeOpenSearchHealthContributor(final OpenSearchClient client) {
+        return new SyncopeOpenSearchHealthContributor(client);
+    }
+}
diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientFactoryBean.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientFactoryBean.java
new file mode 100644
index 0000000000..7e85a07998
--- /dev/null
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientFactoryBean.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.opensearch.client;
+
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import org.apache.http.Header;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.message.BasicHeader;
+import org.opensearch.client.RestClient;
+import org.opensearch.client.RestClientBuilder;
+import org.opensearch.client.json.jackson.JacksonJsonpMapper;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.transport.rest_client.RestClientTransport;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.FactoryBean;
+
+/**
+ * Spring {@link FactoryBean} for getting the {@link OpenSearchClient} singleton instance.
+ */
+public class OpenSearchClientFactoryBean implements FactoryBean<OpenSearchClient>, DisposableBean {
+
+    private final List<HttpHost> hosts;
+
+    private String username;
+
+    private String password;
+
+    private String serviceToken;
+
+    private String apiKeyId;
+
+    private String apiKeySecret;
+
+    private RestClient restClient;
+
+    private OpenSearchClient client;
+
+    public OpenSearchClientFactoryBean(final List<HttpHost> hosts) {
+        this.hosts = hosts;
+    }
+
+    public void setUsername(final String username) {
+        this.username = username;
+    }
+
+    public void setPassword(final String password) {
+        this.password = password;
+    }
+
+    public String getServiceToken() {
+        return serviceToken;
+    }
+
+    public void setServiceToken(final String serviceToken) {
+        this.serviceToken = serviceToken;
+    }
+
+    public String getApiKeyId() {
+        return apiKeyId;
+    }
+
+    public void setApiKeyId(final String apiKeyId) {
+        this.apiKeyId = apiKeyId;
+    }
+
+    public String getApiKeySecret() {
+        return apiKeySecret;
+    }
+
+    public void setApiKeySecret(final String apiKeySecret) {
+        this.apiKeySecret = apiKeySecret;
+    }
+
+    @Override
+    public OpenSearchClient getObject() throws Exception {
+        synchronized (this) {
+            if (client == null) {
+                RestClientBuilder builder = RestClient.builder(hosts.toArray(HttpHost[]::new));
+                if (username != null && password != null) {
+                    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+                    credentialsProvider.setCredentials(
+                            AuthScope.ANY, new UsernamePasswordCredentials(username, password));
+                    builder.setHttpClientConfigCallback(b -> b.setDefaultCredentialsProvider(credentialsProvider));
+                } else if (serviceToken != null) {
+                    builder.setDefaultHeaders(
+                            new Header[] { new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + serviceToken) });
+                } else if (apiKeyId != null && apiKeySecret != null) {
+                    String apiKeyAuth = Base64.getEncoder().encodeToString(
+                            (apiKeyId + ":" + apiKeySecret).getBytes(StandardCharsets.UTF_8));
+                    builder.setDefaultHeaders(
+                            new Header[] { new BasicHeader(HttpHeaders.AUTHORIZATION, "ApiKey " + apiKeyAuth) });
+                }
+
+                restClient = builder.build();
+                client = new OpenSearchClient(new RestClientTransport(
+                        restClient,
+                        new JacksonJsonpMapper(JsonMapper.builder().
+                                findAndAddModules().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build())));
+            }
+        }
+        return client;
+    }
+
+    @Override
+    public Class<?> getObjectType() {
+        return OpenSearchClient.class;
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        if (restClient != null) {
+            restClient.close();
+        }
+    }
+}
diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java
new file mode 100644
index 0000000000..49f8b21370
--- /dev/null
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.opensearch.client;
+
+import javax.sql.DataSource;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.SyncopeCoreLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+
+public class OpenSearchIndexLoader implements SyncopeCoreLoader {
+
+    protected static final Logger LOG = LoggerFactory.getLogger(OpenSearchIndexLoader.class);
+
+    protected final OpenSearchIndexManager indexManager;
+
+    public OpenSearchIndexLoader(final OpenSearchIndexManager indexManager) {
+        this.indexManager = indexManager;
+    }
+
+    @Override
+    public int getOrder() {
+        return Ordered.LOWEST_PRECEDENCE;
+    }
+
+    @Override
+    public void load(final String domain, final DataSource datasource) {
+        try {
+            if (!indexManager.existsAnyIndex(domain, AnyTypeKind.USER)) {
+                indexManager.createAnyIndex(domain, AnyTypeKind.USER,
+                        indexManager.defaultSettings(), indexManager.defaultAnyMapping());
+            }
+            if (!indexManager.existsAnyIndex(domain, AnyTypeKind.GROUP)) {
+                indexManager.createAnyIndex(domain, AnyTypeKind.GROUP,
+                        indexManager.defaultSettings(), indexManager.defaultAnyMapping());
+            }
+            if (!indexManager.existsAnyIndex(domain, AnyTypeKind.ANY_OBJECT)) {
+                indexManager.createAnyIndex(domain, AnyTypeKind.ANY_OBJECT,
+                        indexManager.defaultSettings(), indexManager.defaultAnyMapping());
+            }
+
+            if (!indexManager.existsAuditIndex(domain)) {
+                indexManager.createAuditIndex(domain,
+                        indexManager.defaultSettings(), indexManager.defaultAuditMapping());
+            }
+        } catch (Exception e) {
+            LOG.error("While creating indexes for domain {}", domain, e);
+        }
+    }
+}
diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java
new file mode 100644
index 0000000000..fb5f57581e
--- /dev/null
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.opensearch.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
+import org.identityconnectors.framework.common.objects.SyncDeltaType;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.opensearch._types.OpenSearchException;
+import org.opensearch.client.opensearch._types.analysis.CustomNormalizer;
+import org.opensearch.client.opensearch._types.analysis.Normalizer;
+import org.opensearch.client.opensearch._types.mapping.DynamicTemplate;
+import org.opensearch.client.opensearch._types.mapping.KeywordProperty;
+import org.opensearch.client.opensearch._types.mapping.ObjectProperty;
+import org.opensearch.client.opensearch._types.mapping.Property;
+import org.opensearch.client.opensearch._types.mapping.TextProperty;
+import org.opensearch.client.opensearch._types.mapping.TypeMapping;
+import org.opensearch.client.opensearch.core.DeleteRequest;
+import org.opensearch.client.opensearch.core.DeleteResponse;
+import org.opensearch.client.opensearch.core.IndexRequest;
+import org.opensearch.client.opensearch.core.IndexResponse;
+import org.opensearch.client.opensearch.indices.CreateIndexRequest;
+import org.opensearch.client.opensearch.indices.CreateIndexResponse;
+import org.opensearch.client.opensearch.indices.DeleteIndexRequest;
+import org.opensearch.client.opensearch.indices.DeleteIndexResponse;
+import org.opensearch.client.opensearch.indices.ExistsRequest;
+import org.opensearch.client.opensearch.indices.IndexSettings;
+import org.opensearch.client.opensearch.indices.IndexSettingsAnalysis;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+/**
+ * Listen to any create / update and delete in order to keep the OpenSearch indexes consistent.
+ */
+public class OpenSearchIndexManager {
+
+    private static final Logger LOG = LoggerFactory.getLogger(OpenSearchIndexManager.class);
+
+    protected final OpenSearchClient client;
+
+    protected final OpenSearchUtils ppenSearchUtils;
+
+    protected final String numberOfShards;
+
+    protected final String numberOfReplicas;
+
+    public OpenSearchIndexManager(
+            final OpenSearchClient client,
+            final OpenSearchUtils ppenSearchUtils,
+            final String numberOfShards,
+            final String numberOfReplicas) {
+
+        this.client = client;
+        this.ppenSearchUtils = ppenSearchUtils;
+        this.numberOfShards = numberOfShards;
+        this.numberOfReplicas = numberOfReplicas;
+    }
+
+    public boolean existsAnyIndex(final String domain, final AnyTypeKind kind) throws IOException {
+        return client.indices().exists(new ExistsRequest.Builder().
+                index(OpenSearchUtils.getAnyIndex(domain, kind)).build()).
+                value();
+    }
+
+    public boolean existsAuditIndex(final String domain) throws IOException {
+        return client.indices().exists(new ExistsRequest.Builder().
+                index(OpenSearchUtils.getAuditIndex(domain)).build()).
+                value();
+    }
+
+    public IndexSettings defaultSettings() throws IOException {
+        return new IndexSettings.Builder().
+                analysis(new IndexSettingsAnalysis.Builder().
+                        normalizer("string_lowercase", new Normalizer.Builder().
+                                custom(new CustomNormalizer.Builder().
+                                        charFilter(List.of()).
+                                        filter("lowercase").
+                                        build()).
+                                build()).
+                        build()).
+                numberOfShards(numberOfShards).
+                numberOfReplicas(numberOfReplicas).
+                build();
+    }
+
+    public TypeMapping defaultAnyMapping() throws IOException {
+        return new TypeMapping.Builder().
+                dynamicTemplates(List.of(Map.of(
+                        "strings",
+                        new DynamicTemplate.Builder().
+                                matchMappingType("string").
+                                mapping(new Property.Builder().
+                                        keyword(new KeywordProperty.Builder().normalizer("string_lowercase").build()).
+                                        build()).
+                                build()))).
+                build();
+    }
+
+    public TypeMapping defaultAuditMapping() throws IOException {
+        return new TypeMapping.Builder().
+                dynamicTemplates(List.of(Map.of(
+                        "strings",
+                        new DynamicTemplate.Builder().
+                                matchMappingType("string").
+                                mapping(new Property.Builder().
+                                        keyword(new KeywordProperty.Builder().normalizer("string_lowercase").build()).
+                                        build()).
+                                build()))).
+                properties(
+                        "message",
+                        new Property.Builder().object(new ObjectProperty.Builder().
+                                properties(
+                                        "before",
+                                        new Property.Builder().
+                                                text(new TextProperty.Builder().analyzer("standard").build()).
+                                                build()).
+                                properties(
+                                        "inputs",
+                                        new Property.Builder().
+                                                text(new TextProperty.Builder().analyzer("standard").build()).
+                                                build()).
+                                properties(
+                                        "output",
+                                        new Property.Builder().
+                                                text(new TextProperty.Builder().analyzer("standard").build()).
+                                                build()).
+                                properties(
+                                        "throwable",
+                                        new Property.Builder().
+                                                text(new TextProperty.Builder().analyzer("standard").build()).
+                                                build()).
+                                build()).
+                                build()).
+                build();
+    }
+
+    protected CreateIndexResponse doCreateAnyIndex(
+            final String domain,
+            final AnyTypeKind kind,
+            final IndexSettings settings,
+            final TypeMapping mappings) throws IOException {
+
+        return client.indices().create(
+                new CreateIndexRequest.Builder().
+                        index(OpenSearchUtils.getAnyIndex(domain, kind)).
+                        settings(settings).
+                        mappings(mappings).
+                        build());
+    }
+
+    public void createAnyIndex(
+            final String domain,
+            final AnyTypeKind kind,
+            final IndexSettings settings,
+            final TypeMapping mappings)
+            throws IOException {
+
+        try {
+            CreateIndexResponse response = doCreateAnyIndex(domain, kind, settings, mappings);
+
+            LOG.debug("Successfully created {} for {}: {}",
+                    OpenSearchUtils.getAnyIndex(domain, kind), kind.name(), response);
+        } catch (OpenSearchException e) {
+            LOG.debug("Could not create index {} because it already exists",
+                    OpenSearchUtils.getAnyIndex(domain, kind), e);
+
+            removeAnyIndex(domain, kind);
+            doCreateAnyIndex(domain, kind, settings, mappings);
+        }
+    }
+
+    public void removeAnyIndex(final String domain, final AnyTypeKind kind) throws IOException {
+        DeleteIndexResponse response = client.indices().delete(
+                new DeleteIndexRequest.Builder().index(OpenSearchUtils.getAnyIndex(domain, kind)).build());
+        LOG.debug("Successfully removed {}: {}", OpenSearchUtils.getAnyIndex(domain, kind), response);
+    }
+
+    protected CreateIndexResponse doCreateAuditIndex(
+            final String domain,
+            final IndexSettings settings,
+            final TypeMapping mappings) throws IOException {
+
+        return client.indices().create(
+                new CreateIndexRequest.Builder().
+                        index(OpenSearchUtils.getAuditIndex(domain)).
+                        settings(settings).
+                        mappings(mappings).
+                        build());
+    }
+
+    public void createAuditIndex(
+            final String domain,
+            final IndexSettings settings,
+            final TypeMapping mappings)
+            throws IOException {
+
+        try {
+            CreateIndexResponse response = doCreateAuditIndex(domain, settings, mappings);
+
+            LOG.debug("Successfully created audit index {}: {}",
+                    OpenSearchUtils.getAuditIndex(domain), response);
+        } catch (OpenSearchException e) {
+            LOG.debug("Could not create audit index {} because it already exists",
+                    OpenSearchUtils.getAuditIndex(domain), e);
+
+            removeAuditIndex(domain);
+            doCreateAuditIndex(domain, settings, mappings);
+        }
+    }
+
+    public void removeAuditIndex(final String domain) throws IOException {
+        DeleteIndexResponse response = client.indices().delete(
+                new DeleteIndexRequest.Builder().index(OpenSearchUtils.getAuditIndex(domain)).build());
+        LOG.debug("Successfully removed {}: {}", OpenSearchUtils.getAuditIndex(domain), response);
+    }
+
+    @TransactionalEventListener
+    public void any(final AnyLifecycleEvent<Any<?>> event) throws IOException {
+        LOG.debug("About to {} index for {}", event.getType().name(), event.getAny());
+
+        if (event.getType() == SyncDeltaType.DELETE) {
+            DeleteRequest request = new DeleteRequest.Builder().index(
+                    OpenSearchUtils.getAnyIndex(event.getDomain(), event.getAny().getType().getKind())).
+                    id(event.getAny().getKey()).
+                    build();
+            DeleteResponse response = client.delete(request);
+            LOG.debug("Index successfully deleted for {}[{}]: {}",
+                    event.getAny().getType().getKind(), event.getAny().getKey(), response);
+        } else {
+            IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>().
+                    index(OpenSearchUtils.getAnyIndex(event.getDomain(), event.getAny().getType().getKind())).
+                    id(event.getAny().getKey()).
+                    document(ppenSearchUtils.document(event.getAny())).
+                    build();
+            IndexResponse response = client.index(request);
+            LOG.debug("Index successfully created or updated for {}: {}", event.getAny(), response);
+        }
+    }
+
+    public void audit(final String domain, final long instant, final JsonNode message)
+            throws IOException {
+
+        LOG.debug("About to audit");
+
+        IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>().
+                index(OpenSearchUtils.getAuditIndex(domain)).
+                id(SecureRandomUtils.generateRandomUUID().toString()).
+                document(ppenSearchUtils.document(instant, message, domain)).
+                build();
+        IndexResponse response = client.index(request);
+
+        LOG.debug("Audit successfully created: {}", response);
+    }
+}
diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchProperties.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchProperties.java
new file mode 100644
index 0000000000..78bf78e641
--- /dev/null
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchProperties.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.opensearch.client;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("opensearch")
+public class OpenSearchProperties {
+
+    private List<String> hosts = new ArrayList<>();
+
+    private int indexMaxResultWindow = 10000;
+
+    private String numberOfShards = "1";
+
+    private String numberOfReplicas = "1";
+
+    public List<String> getHosts() {
+        return hosts;
+    }
+
+    public void setHosts(final List<String> hosts) {
+        this.hosts = hosts;
+    }
+
+    public int getIndexMaxResultWindow() {
+        return indexMaxResultWindow;
+    }
+
+    public void setIndexMaxResultWindow(final int indexMaxResultWindow) {
+        this.indexMaxResultWindow = indexMaxResultWindow;
+    }
+
+    public String getNumberOfShards() {
+        return numberOfShards;
+    }
+
+    public void setNumberOfShards(final String numberOfShards) {
+        this.numberOfShards = numberOfShards;
+    }
+
+    public String getNumberOfReplicas() {
+        return numberOfReplicas;
+    }
+
+    public void setNumberOfReplicas(final String numberOfReplicas) {
+        this.numberOfReplicas = numberOfReplicas;
+    }
+}
diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java
new file mode 100644
index 0000000000..7329456237
--- /dev/null
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.opensearch.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
+import org.apache.syncope.core.persistence.api.entity.PlainAttr;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.Privilege;
+import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
+import org.apache.syncope.core.persistence.api.entity.group.Group;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Utility methods for usage with OpenSearch.
+ */
+public class OpenSearchUtils {
+
+    public static String getAnyIndex(final String domain, final AnyTypeKind kind) {
+        return domain.toLowerCase() + '_' + kind.name().toLowerCase();
+    }
+
+    public static String getAuditIndex(final String domain) {
+        return domain.toLowerCase() + "_audit";
+    }
+
+    protected final UserDAO userDAO;
+
+    protected final GroupDAO groupDAO;
+
+    protected final AnyObjectDAO anyObjectDAO;
+
+    public OpenSearchUtils(
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO) {
+
+        this.userDAO = userDAO;
+        this.groupDAO = groupDAO;
+        this.anyObjectDAO = anyObjectDAO;
+    }
+
+    /**
+     * Returns the document specialized with content from the provided any.
+     *
+     * @param any user, group or any object to index
+     * @return document specialized with content from the provided any
+     */
+    @Transactional
+    public Map<String, Object> document(final Any<?> any) {
+        Collection<String> resources = any instanceof User
+                ? userDAO.findAllResourceKeys(any.getKey())
+                : any instanceof AnyObject
+                        ? anyObjectDAO.findAllResourceKeys(any.getKey())
+                        : groupDAO.findAllResourceKeys(any.getKey());
+        Collection<String> dynRealms = any instanceof User
+                ? userDAO.findDynRealms(any.getKey())
+                : any instanceof AnyObject
+                        ? anyObjectDAO.findDynRealms(any.getKey())
+                        : groupDAO.findDynRealms(any.getKey());
+
+        Map<String, Object> builder = new HashMap<>();
+        builder.put("id", any.getKey());
+        builder.put("realm", any.getRealm().getKey());
+        builder.put("anyType", any.getType().getKey());
+        builder.put("creationDate", any.getCreationDate());
+        builder.put("creationContext", any.getCreationContext());
+        builder.put("creator", any.getCreator());
+        builder.put("lastChangeDate", any.getLastChangeDate());
+        builder.put("lastModifier", any.getLastModifier());
+        builder.put("lastChangeContext", any.getLastChangeContext());
+        builder.put("status", any.getStatus());
+        builder.put("auxClasses", any.getAuxClasses().stream().map(AnyTypeClass::getKey).collect(Collectors.toList()));
+        builder.put("resources", resources);
+        builder.put("dynRealms", dynRealms);
+
+        if (any instanceof AnyObject) {
+            AnyObject anyObject = ((AnyObject) any);
+            builder.put("name", anyObject.getName());
+
+            builder.put("memberships", anyObjectDAO.findAllGroupKeys(anyObject));
+
+            List<String> relationships = new ArrayList<>();
+            List<String> relationshipTypes = new ArrayList<>();
+            anyObjectDAO.findAllRelationships(anyObject).forEach(relationship -> {
+                relationships.add(relationship.getRightEnd().getKey());
+                relationshipTypes.add(relationship.getType().getKey());
+            });
+            builder.put("relationships", relationships);
+            builder.put("relationshipTypes", relationshipTypes);
+
+            OpenSearchUtils.this.customizeDocument(builder, anyObject);
+        } else if (any instanceof Group) {
+            Group group = ((Group) any);
+            builder.put("name", group.getName());
+            Optional.ofNullable(group.getUserOwner()).ifPresent(uo -> builder.put("userOwner", uo.getKey()));
+            Optional.ofNullable(group.getGroupOwner()).ifPresent(go -> builder.put("groupOwner", go.getKey()));
+
+            Set<String> members = new HashSet<>();
+            members.addAll(groupDAO.findUMemberships(group).stream().
+                    map(membership -> membership.getLeftEnd().getKey()).collect(Collectors.toList()));
+            members.addAll(groupDAO.findUDynMembers(group));
+            members.addAll(groupDAO.findAMemberships(group).stream().
+                    map(membership -> membership.getLeftEnd().getKey()).collect(Collectors.toList()));
+            members.addAll(groupDAO.findADynMembers(group));
+            builder.put("members", members);
+
+            OpenSearchUtils.this.customizeDocument(builder, group);
+        } else if (any instanceof User) {
+            User user = ((User) any);
+            builder.put("username", user.getUsername());
+            builder.put("token", user.getToken());
+            builder.put("tokenExpireTime", user.getTokenExpireTime());
+            builder.put("changePwdDate", user.getChangePwdDate());
+            builder.put("failedLogins", user.getFailedLogins());
+            builder.put("lastLoginDate", user.getLastLoginDate());
+            builder.put("suspended", user.isSuspended());
+            builder.put("mustChangePassword", user.isMustChangePassword());
+
+            List<String> roles = new ArrayList<>();
+            Set<String> privileges = new HashSet<>();
+            userDAO.findAllRoles(user).forEach(role -> {
+                roles.add(role.getKey());
+                privileges.addAll(role.getPrivileges().stream().map(Privilege::getKey).collect(Collectors.toSet()));
+            });
+            builder.put("roles", roles);
+            builder.put("privileges", privileges);
+
+            builder.put("memberships", userDAO.findAllGroupKeys(user));
+
+            List<String> relationships = new ArrayList<>();
+            Set<String> relationshipTypes = new HashSet<>();
+            user.getRelationships().forEach(relationship -> {
+                relationships.add(relationship.getRightEnd().getKey());
+                relationshipTypes.add(relationship.getType().getKey());
+            });
+            builder.put("relationships", relationships);
+            builder.put("relationshipTypes", relationshipTypes);
+
+            customizeDocument(builder, user);
+        }
+
+        for (PlainAttr<?> plainAttr : any.getPlainAttrs()) {
+            List<Object> values = plainAttr.getValues().stream().
+                    map(PlainAttrValue::getValue).collect(Collectors.toList());
+
+            Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue()));
+
+            builder.put(plainAttr.getSchema().getKey(), values.size() == 1 ? values.get(0) : values);
+        }
+
+        return builder;
+    }
+
+    protected void customizeDocument(final Map<String, Object> builder, final AnyObject anyObject) {
+    }
+
+    protected void customizeDocument(final Map<String, Object> builder, final Group group) {
+    }
+
+    protected void customizeDocument(final Map<String, Object> builder, final User user) {
+    }
+
+    public Map<String, Object> document(
+            final long instant,
+            final JsonNode message,
+            final String domain) throws IOException {
+
+        Map<String, Object> builder = new HashMap<>();
+
+        builder.put("instant", instant);
+        builder.put("message", message);
+
+        customizeDocument(builder, instant, message, domain);
+
+        return builder;
+    }
+
+    protected void customizeDocument(
+            final Map<String, Object> builder,
+            final long instant,
+            final JsonNode message,
+            final String domain)
+            throws IOException {
+    }
+}
diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/SyncopeOpenSearchHealthContributor.java
similarity index 81%
copy from ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java
copy to ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/SyncopeOpenSearchHealthContributor.java
index 163519c532..d163fa4b67 100644
--- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/SyncopeElasticsearchHealthContributor.java
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/SyncopeOpenSearchHealthContributor.java
@@ -16,22 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.ext.elasticsearch.client;
+package org.apache.syncope.ext.opensearch.client;
 
-import static co.elastic.clients.elasticsearch._types.HealthStatus.Green;
-import static co.elastic.clients.elasticsearch._types.HealthStatus.Red;
-import static co.elastic.clients.elasticsearch._types.HealthStatus.Yellow;
-
-import co.elastic.clients.elasticsearch.ElasticsearchClient;
-import co.elastic.clients.elasticsearch.cluster.HealthResponse;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.opensearch.cluster.HealthResponse;
 import org.springframework.boot.actuate.health.Health;
 import org.springframework.boot.actuate.health.HealthIndicator;
 
-public class SyncopeElasticsearchHealthContributor implements HealthIndicator {
+public class SyncopeOpenSearchHealthContributor implements HealthIndicator {
 
-    protected final ElasticsearchClient client;
+    protected final OpenSearchClient client;
 
-    public SyncopeElasticsearchHealthContributor(final ElasticsearchClient client) {
+    public SyncopeOpenSearchHealthContributor(final OpenSearchClient client) {
         this.client = client;
     }
 
diff --git a/ext/opensearch/client-opensearch/src/main/resources/META-INF/spring.factories b/ext/opensearch/client-opensearch/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..19f4482588
--- /dev/null
+++ b/ext/opensearch/client-opensearch/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  org.apache.syncope.ext.opensearch.client.OpenSearchClientContext
diff --git a/ext/elasticsearch/client-elasticsearch/pom.xml b/ext/opensearch/logic/pom.xml
similarity index 64%
copy from ext/elasticsearch/client-elasticsearch/pom.xml
copy to ext/opensearch/logic/pom.xml
index d48d5edef3..02efdeace8 100644
--- a/ext/elasticsearch/client-elasticsearch/pom.xml
+++ b/ext/opensearch/logic/pom.xml
@@ -23,14 +23,14 @@ under the License.
 
   <parent>
     <groupId>org.apache.syncope.ext</groupId>
-    <artifactId>syncope-ext-elasticsearch</artifactId>
+    <artifactId>syncope-ext-opensearch</artifactId>
     <version>4.0.0-SNAPSHOT</version>
   </parent>
 
-  <name>Apache Syncope Ext: Elasticsearch Client</name>
-  <description>Apache Syncope Ext: Elasticsearch Client</description>
-  <groupId>org.apache.syncope.ext.elasticsearch</groupId>
-  <artifactId>syncope-ext-elasticsearch-client</artifactId>
+  <name>Apache Syncope Ext: OpenSearch Logic</name>
+  <description>Apache Syncope Ext: OpenSearch Logic</description>
+  <groupId>org.apache.syncope.ext.opensearch</groupId>
+  <artifactId>syncope-ext-opensearch-logic</artifactId>
   <packaging>jar</packaging>
   
   <properties>
@@ -39,28 +39,15 @@ under the License.
 
   <dependencies>
     <dependency>
-      <groupId>org.apache.syncope.core</groupId>
-      <artifactId>syncope-core-spring</artifactId>
+      <groupId>org.apache.syncope.core.idrepo</groupId>
+      <artifactId>syncope-core-idrepo-logic</artifactId>
       <version>${project.version}</version>
     </dependency>
-
-    <dependency>
-      <groupId>co.elastic.clients</groupId>
-      <artifactId>elasticsearch-java</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>jakarta.json</groupId>
-      <artifactId>jakarta.json-api</artifactId>
-    </dependency>
-
+    
     <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.springframework.boot</groupId>
-      <artifactId>spring-boot-starter-actuator</artifactId>
+      <groupId>org.apache.syncope.ext.opensearch</groupId>
+      <artifactId>syncope-ext-opensearch-client</artifactId>
+      <version>${project.version}</version>
     </dependency>
   </dependencies>
 
diff --git a/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAppender.java b/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAppender.java
new file mode 100644
index 0000000000..39bbee6e8b
--- /dev/null
+++ b/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAppender.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic.audit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.Serializable;
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.ext.opensearch.client.OpenSearchIndexManager;
+
+public class OpenSearchAppender extends AbstractAppender {
+
+    public static class Builder extends AbstractAppender.Builder<Builder>
+            implements org.apache.logging.log4j.core.util.Builder<OpenSearchAppender> {
+
+        private OpenSearchIndexManager openSearchIndexManager;
+
+        private String domain;
+
+        public OpenSearchAppender.Builder setDomain(final String domain) {
+            this.domain = domain;
+            return this;
+        }
+
+        public OpenSearchAppender.Builder setIndexManager(
+                final OpenSearchIndexManager openSearchIndexManager) {
+
+            this.openSearchIndexManager = openSearchIndexManager;
+            return this;
+        }
+
+        @Override
+        public OpenSearchAppender build() {
+            if (domain == null || openSearchIndexManager == null) {
+                LOGGER.error("Cannot create OpenSearchAppender without Domain or IndexManager.");
+                return null;
+            }
+            return new OpenSearchAppender(
+                    getName(), getFilter(), getLayout(), isIgnoreExceptions(), domain, openSearchIndexManager);
+        }
+    }
+
+    @PluginBuilderFactory
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    private final String domain;
+
+    protected final OpenSearchIndexManager openSearchIndexManager;
+
+    protected OpenSearchAppender(
+            final String name,
+            final Filter filter,
+            final Layout<? extends Serializable> layout,
+            final boolean ignoreExceptions,
+            final String domain,
+            final OpenSearchIndexManager openSearchIndexManager) {
+
+        super(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY);
+        this.domain = domain;
+        this.openSearchIndexManager = openSearchIndexManager;
+    }
+
+    @Override
+    public void append(final LogEvent event) {
+        try {
+            openSearchIndexManager.audit(
+                    domain,
+                    event.getTimeMillis(),
+                    POJOHelper.deserialize(event.getMessage().getFormattedMessage(), JsonNode.class));
+        } catch (Exception e) {
+            LOGGER.error("While requesting to index event for appender [{}]", getName(), e);
+        }
+    }
+}
diff --git a/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAuditAppender.java b/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAuditAppender.java
new file mode 100644
index 0000000000..1cf41112fd
--- /dev/null
+++ b/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchAuditAppender.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic.audit;
+
+import java.util.Optional;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.syncope.ext.opensearch.client.OpenSearchIndexManager;
+
+public class OpenSearchAuditAppender extends DefaultAuditAppender {
+
+    public OpenSearchAuditAppender(final String domain, final OpenSearchIndexManager openSearchIndexManager) {
+        super(domain);
+
+        LoggerContext logCtx = (LoggerContext) LogManager.getContext(false);
+
+        targetAppender = Optional.ofNullable(logCtx.getConfiguration().<Appender>getAppender(getTargetAppenderName())).
+                orElseGet(() -> {
+                    OpenSearchAppender a = OpenSearchAppender.newBuilder().
+                            setName(getTargetAppenderName()).
+                            setIgnoreExceptions(false).
+                            setDomain(domain).
+                            setIndexManager(openSearchIndexManager).
+                            build();
+                    a.start();
+                    logCtx.getConfiguration().addAppender(a);
+                    return a;
+                });
+    }
+
+    @Override
+    public String getTargetAppenderName() {
+        return "audit_for_" + domain;
+    }
+}
diff --git a/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchLogicContext.java b/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchLogicContext.java
new file mode 100644
index 0000000000..a0ff23d7d4
--- /dev/null
+++ b/ext/opensearch/logic/src/main/java/org/apache/syncope/core/logic/audit/OpenSearchLogicContext.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic.audit;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.syncope.common.lib.types.AuditLoggerName;
+import org.apache.syncope.core.logic.IdRepoLogicContext;
+import org.apache.syncope.core.persistence.api.DomainHolder;
+import org.apache.syncope.ext.opensearch.client.OpenSearchIndexManager;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@AutoConfigureBefore(IdRepoLogicContext.class)
+@Configuration(proxyBeanMethods = false)
+public class OpenSearchLogicContext {
+
+    @ConditionalOnMissingBean(name = { "defaultAuditAppenders", "openSearchDefaultAuditAppenders" })
+    @Bean
+    public List<AuditAppender> defaultAuditAppenders(
+            final DomainHolder domainHolder,
+            final OpenSearchIndexManager openSearchIndexManager) {
+
+        List<AuditAppender> auditAppenders = new ArrayList<>();
+
+        LoggerContext logCtx = (LoggerContext) LogManager.getContext(false);
+        domainHolder.getDomains().forEach((domain, dataSource) -> {
+            AuditAppender appender = new OpenSearchAuditAppender(domain, openSearchIndexManager);
+
+            LoggerConfig logConf = new LoggerConfig(AuditLoggerName.getAuditLoggerName(domain), null, false);
+            logConf.addAppender(appender.getTargetAppender(), Level.DEBUG, null);
+            logConf.setLevel(Level.DEBUG);
+            logCtx.getConfiguration().addLogger(logConf.getName(), logConf);
+
+            auditAppenders.add(appender);
+        });
+
+        return auditAppenders;
+    }
+}
diff --git a/ext/opensearch/logic/src/main/resources/META-INF/spring.factories b/ext/opensearch/logic/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..1171a0276e
--- /dev/null
+++ b/ext/opensearch/logic/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  org.apache.syncope.core.logic.audit.OpenSearchLogicContext
diff --git a/ext/elasticsearch/client-elasticsearch/pom.xml b/ext/opensearch/persistence-jpa/pom.xml
similarity index 62%
copy from ext/elasticsearch/client-elasticsearch/pom.xml
copy to ext/opensearch/persistence-jpa/pom.xml
index d48d5edef3..3dbc6ef636 100644
--- a/ext/elasticsearch/client-elasticsearch/pom.xml
+++ b/ext/opensearch/persistence-jpa/pom.xml
@@ -23,44 +23,53 @@ under the License.
 
   <parent>
     <groupId>org.apache.syncope.ext</groupId>
-    <artifactId>syncope-ext-elasticsearch</artifactId>
+    <artifactId>syncope-ext-opensearch</artifactId>
     <version>4.0.0-SNAPSHOT</version>
   </parent>
 
-  <name>Apache Syncope Ext: Elasticsearch Client</name>
-  <description>Apache Syncope Ext: Elasticsearch Client</description>
-  <groupId>org.apache.syncope.ext.elasticsearch</groupId>
-  <artifactId>syncope-ext-elasticsearch-client</artifactId>
+  <name>Apache Syncope Ext: OpenSearch Persistence JPA</name>
+  <description>Apache Syncope Ext: OpenSearch Persistence JPA</description>
+  <groupId>org.apache.syncope.ext.opensearch</groupId>
+  <artifactId>syncope-ext-opensearch-persistence-jpa</artifactId>
   <packaging>jar</packaging>
   
   <properties>
     <rootpom.basedir>${basedir}/../../..</rootpom.basedir>
   </properties>
 
-  <dependencies>
+  <dependencies>    
     <dependency>
       <groupId>org.apache.syncope.core</groupId>
-      <artifactId>syncope-core-spring</artifactId>
+      <artifactId>syncope-core-persistence-jpa</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.syncope.ext.opensearch</groupId>
+      <artifactId>syncope-ext-opensearch-client</artifactId>
       <version>${project.version}</version>
     </dependency>
 
+    <!-- TEST -->
     <dependency>
-      <groupId>co.elastic.clients</groupId>
-      <artifactId>elasticsearch-java</artifactId>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>jakarta.json</groupId>
-      <artifactId>jakarta.json-api</artifactId>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
     </dependency>
-
     <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-junit-jupiter</artifactId>
+      <scope>test</scope>
     </dependency>
-
     <dependency>
-      <groupId>org.springframework.boot</groupId>
-      <artifactId>spring-boot-starter-actuator</artifactId>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 
diff --git a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java
new file mode 100644
index 0000000000..b5fbebdb73
--- /dev/null
+++ b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa;
+
+import org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+import org.apache.syncope.core.persistence.api.dao.AuditConfDAO;
+import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.jpa.dao.OpenSearchAnySearchDAO;
+import org.apache.syncope.core.persistence.jpa.dao.OpenSearchAuditConfDAO;
+import org.apache.syncope.ext.opensearch.client.OpenSearchProperties;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+@Configuration(proxyBeanMethods = false)
+public class OpenSearchPersistenceContext {
+
+    @ConditionalOnMissingBean(name = "openSearchAnySearchDAO")
+    @Bean
+    public AnySearchDAO anySearchDAO(
+            final OpenSearchProperties props,
+            final RealmDAO realmDAO,
+            final @Lazy DynRealmDAO dynRealmDAO,
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final @Lazy AnyObjectDAO anyObjectDAO,
+            final PlainSchemaDAO schemaDAO,
+            final EntityFactory entityFactory,
+            final AnyUtilsFactory anyUtilsFactory,
+            final PlainAttrValidationManager validator,
+            final OpenSearchClient client) {
+
+        return new OpenSearchAnySearchDAO(
+                realmDAO,
+                dynRealmDAO,
+                userDAO,
+                groupDAO,
+                anyObjectDAO,
+                schemaDAO,
+                entityFactory,
+                anyUtilsFactory,
+                validator,
+                client,
+                props.getIndexMaxResultWindow());
+    }
+
+    @ConditionalOnMissingBean(name = "openSearchAuditConfDAO")
+    @Bean
+    public AuditConfDAO auditConfDAO(
+            final OpenSearchProperties props,
+            final OpenSearchClient client) {
+
+        return new OpenSearchAuditConfDAO(client, props.getIndexMaxResultWindow());
+    }
+}
diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java
similarity index 89%
copy from ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
copy to ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java
index 9c41afdc39..447e168e10 100644
--- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
+++ b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java
@@ -18,25 +18,10 @@
  */
 package org.apache.syncope.core.persistence.jpa.dao;
 
-import co.elastic.clients.elasticsearch.ElasticsearchClient;
-import co.elastic.clients.elasticsearch._types.FieldSort;
-import co.elastic.clients.elasticsearch._types.FieldValue;
-import co.elastic.clients.elasticsearch._types.SearchType;
-import co.elastic.clients.elasticsearch._types.SortOptions;
-import co.elastic.clients.elasticsearch._types.SortOrder;
-import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
-import co.elastic.clients.elasticsearch._types.query_dsl.DisMaxQuery;
-import co.elastic.clients.elasticsearch._types.query_dsl.Query;
-import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
-import co.elastic.clients.elasticsearch.core.CountRequest;
-import co.elastic.clients.elasticsearch.core.SearchRequest;
-import co.elastic.clients.elasticsearch.core.search.Hit;
-import co.elastic.clients.json.JsonData;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -81,21 +66,35 @@ import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
 import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
-import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils;
+import org.apache.syncope.ext.opensearch.client.OpenSearchUtils;
+import org.opensearch.client.json.JsonData;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.opensearch._types.FieldSort;
+import org.opensearch.client.opensearch._types.FieldValue;
+import org.opensearch.client.opensearch._types.SearchType;
+import org.opensearch.client.opensearch._types.SortOptions;
+import org.opensearch.client.opensearch._types.SortOrder;
+import org.opensearch.client.opensearch._types.query_dsl.BoolQuery;
+import org.opensearch.client.opensearch._types.query_dsl.DisMaxQuery;
+import org.opensearch.client.opensearch._types.query_dsl.Query;
+import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders;
+import org.opensearch.client.opensearch.core.CountRequest;
+import org.opensearch.client.opensearch.core.SearchRequest;
+import org.opensearch.client.opensearch.core.search.Hit;
 import org.springframework.util.CollectionUtils;
 
 /**
- * Search engine implementation for users, groups and any objects, based on Elasticsearch.
+ * Search engine implementation for users, groups and any objects, based on OpenSearch.
  */
-public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
+public class OpenSearchAnySearchDAO extends AbstractAnySearchDAO {
 
-    protected static final char[] ELASTICSEARCH_REGEX_CHARS = new char[] {
+    protected static final char[] OPENSEARCH_REGEX_CHARS = new char[] {
         '.', '?', '+', '*', '|', '{', '}', '[', ']', '(', ')', '"', '\\', '&' };
 
     protected static String escapeForLikeRegex(final char c) {
         StringBuilder output = new StringBuilder();
 
-        if (ArrayUtils.contains(ELASTICSEARCH_REGEX_CHARS, c)) {
+        if (ArrayUtils.contains(OPENSEARCH_REGEX_CHARS, c)) {
             output.append('\\');
         }
 
@@ -104,11 +103,11 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
         return output.toString();
     }
 
-    protected final ElasticsearchClient client;
+    protected final OpenSearchClient client;
 
     protected final int indexMaxResultWindow;
 
-    public ElasticsearchAnySearchDAO(
+    public OpenSearchAnySearchDAO(
             final RealmDAO realmDAO,
             final DynRealmDAO dynRealmDAO,
             final UserDAO userDAO,
@@ -118,7 +117,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
             final EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final ElasticsearchClient client,
+            final OpenSearchClient client,
             final int indexMaxResultWindow) {
 
         super(
@@ -163,7 +162,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
                             filter(r -> r.getFullPath().startsWith(base.getFullPath())).
                             forEach(descendant -> queries.add(
                             new Query.Builder().term(QueryBuilders.term().
-                                    field("realm").value(descendant.getKey()).build()).
+                                    field("realm").value(FieldValue.of(descendant.getKey())).build()).
                                     build()));
                 } else {
                     DynRealm dynRealm = dynRealmDAO.find(realmPath);
@@ -172,7 +171,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
                     } else {
                         dynRealmKeys.add(dynRealm.getKey());
                         queries.add(new Query.Builder().term(QueryBuilders.term().
-                                field("dynRealm").value(dynRealm.getKey()).build()).
+                                field("dynRealm").value(FieldValue.of(dynRealm.getKey())).build()).
                                 build());
                     }
                 }
@@ -180,7 +179,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
         } else {
             if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) {
                 queries.add(new Query.Builder().term(QueryBuilders.term().
-                        field("realm").value(base.getKey()).build()).
+                        field("realm").value(FieldValue.of(base.getKey())).build()).
                         build());
             }
         }
@@ -208,7 +207,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
                 query = new Query.Builder().bool(
                         QueryBuilders.bool().
                                 must(new Query.Builder().term(QueryBuilders.term().
-                                        field("realm").value(base.getKey()).build()).
+                                        field("realm").value(FieldValue.of(base.getKey())).build()).
                                         build()).
                                 must(query).build()).
                         build();
@@ -239,7 +238,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
             final AnyTypeKind kind) {
 
         CountRequest request = new CountRequest.Builder().
-                index(ElasticsearchUtils.getAnyIndex(AuthContextUtils.getDomain(), kind)).
+                index(OpenSearchUtils.getAnyIndex(AuthContextUtils.getDomain(), kind)).
                 query(getQuery(base, recursive, adminRealms, cond, kind)).
                 build();
         LOG.debug("Count JSON request: {}", request);
@@ -247,7 +246,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
         try {
             return (int) client.count(request).count();
         } catch (Exception e) {
-            LOG.error("While counting in Elasticsearch", e);
+            LOG.error("While counting in OpenSearch", e);
             return 0;
         }
     }
@@ -302,7 +301,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
             final AnyTypeKind kind) {
 
         SearchRequest request = new SearchRequest.Builder().
-                index(ElasticsearchUtils.getAnyIndex(AuthContextUtils.getDomain(), kind)).
+                index(OpenSearchUtils.getAnyIndex(AuthContextUtils.getDomain(), kind)).
                 searchType(SearchType.QueryThenFetch).
                 query(getQuery(base, recursive, adminRealms, cond, kind)).
                 from(itemsPerPage * (page <= 0 ? 0 : page - 1)).
@@ -311,12 +310,11 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
                 build();
         LOG.debug("Search JSON request: {}", request);
 
-        @SuppressWarnings("rawtypes")
-        List<Hit<Map>> esResult = null;
+        List<Hit<Void>> esResult = null;
         try {
-            esResult = client.search(request, Map.class).hits().hits();
+            esResult = client.search(request, Void.class).hits().hits();
         } catch (Exception e) {
-            LOG.error("While searching in Elasticsearch", e);
+            LOG.error("While searching in OpenSearch", e);
         }
 
         return CollectionUtils.isEmpty(esResult)
@@ -463,20 +461,20 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
 
     protected Query getQuery(final AnyTypeCond cond) {
         return new Query.Builder().term(QueryBuilders.term().
-                field("anyType").value(cond.getAnyTypeKey()).build()).
+                field("anyType").value(FieldValue.of(cond.getAnyTypeKey())).build()).
                 build();
     }
 
     protected Query getQuery(final RelationshipTypeCond cond) {
         return new Query.Builder().term(QueryBuilders.term().
-                field("relationshipTypes").value(cond.getRelationshipTypeKey()).build()).
+                field("relationshipTypes").value(FieldValue.of(cond.getRelationshipTypeKey())).build()).
                 build();
     }
 
     protected Query getQuery(final RelationshipCond cond) {
         List<Query> queries = check(cond).stream().
                 map(key -> new Query.Builder().term(QueryBuilders.term().
-                field("relationships").value(key).build()).
+                field("relationships").value(FieldValue.of(key)).build()).
                 build()).collect(Collectors.toList());
 
         return queries.size() == 1
@@ -487,7 +485,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
     protected Query getQuery(final MembershipCond cond) {
         List<Query> queries = check(cond).stream().
                 map(key -> new Query.Builder().term(QueryBuilders.term().
-                field("memberships").value(key).build()).
+                field("memberships").value(FieldValue.of(key)).build()).
                 build()).collect(Collectors.toList());
 
         return queries.size() == 1
@@ -497,26 +495,26 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
 
     protected Query getQuery(final RoleCond cond) {
         return new Query.Builder().term(QueryBuilders.term().
-                field("roles").value(cond.getRole()).build()).
+                field("roles").value(FieldValue.of(cond.getRole())).build()).
                 build();
     }
 
     protected Query getQuery(final PrivilegeCond cond) {
         return new Query.Builder().term(QueryBuilders.term().
-                field("privileges").value(cond.getPrivilege()).build()).
+                field("privileges").value(FieldValue.of(cond.getPrivilege())).build()).
                 build();
     }
 
     protected Query getQuery(final DynRealmCond cond) {
         return new Query.Builder().term(QueryBuilders.term().
-                field("dynRealms").value(cond.getDynRealm()).build()).
+                field("dynRealms").value(FieldValue.of(cond.getDynRealm())).build()).
                 build();
     }
 
     protected Query getQuery(final MemberCond cond) {
         List<Query> queries = check(cond).stream().
                 map(key -> new Query.Builder().term(QueryBuilders.term().
-                field("members").value(key).build()).
+                field("members").value(FieldValue.of(key)).build()).
                 build()).collect(Collectors.toList());
 
         return queries.size() == 1
@@ -526,13 +524,13 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
 
     protected Query getQuery(final AuxClassCond cond) {
         return new Query.Builder().term(QueryBuilders.term().
-                field("auxClasses").value(cond.getAuxClass()).build()).
+                field("auxClasses").value(FieldValue.of(cond.getAuxClass())).build()).
                 build();
     }
 
     protected Query getQuery(final ResourceCond cond) {
         return new Query.Builder().term(QueryBuilders.term().
-                field("resources").value(cond.getResource()).build()).
+                field("resources").value(FieldValue.of(cond.getResource())).build()).
                 build();
     }
 
@@ -583,7 +581,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
 
             case IEQ:
                 query = new Query.Builder().match(QueryBuilders.match().
-                        field(schema.getKey()).query(cond.getExpression().toLowerCase()).build()).
+                        field(schema.getKey()).query(FieldValue.of(cond.getExpression().toLowerCase())).build()).
                         build();
                 break;
 
diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAuditConfDAO.java
similarity index 79%
copy from ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java
copy to ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAuditConfDAO.java
index 2abc1d997c..45de90b259 100644
--- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAuditConfDAO.java
+++ b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAuditConfDAO.java
@@ -18,23 +18,11 @@
  */
 package org.apache.syncope.core.persistence.jpa.dao;
 
-import co.elastic.clients.elasticsearch.ElasticsearchClient;
-import co.elastic.clients.elasticsearch._types.FieldSort;
-import co.elastic.clients.elasticsearch._types.SearchType;
-import co.elastic.clients.elasticsearch._types.SortOptions;
-import co.elastic.clients.elasticsearch._types.SortOrder;
-import co.elastic.clients.elasticsearch._types.query_dsl.Query;
-import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
-import co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType;
-import co.elastic.clients.elasticsearch.core.CountRequest;
-import co.elastic.clients.elasticsearch.core.SearchRequest;
-import co.elastic.clients.elasticsearch.core.search.Hit;
-import co.elastic.clients.json.JsonData;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import java.io.IOException;
 import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
@@ -43,16 +31,29 @@ import org.apache.syncope.common.lib.types.AuditElements;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
-import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils;
+import org.apache.syncope.ext.opensearch.client.OpenSearchUtils;
+import org.opensearch.client.json.JsonData;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.opensearch._types.FieldSort;
+import org.opensearch.client.opensearch._types.FieldValue;
+import org.opensearch.client.opensearch._types.SearchType;
+import org.opensearch.client.opensearch._types.SortOptions;
+import org.opensearch.client.opensearch._types.SortOrder;
+import org.opensearch.client.opensearch._types.query_dsl.Query;
+import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders;
+import org.opensearch.client.opensearch._types.query_dsl.TextQueryType;
+import org.opensearch.client.opensearch.core.CountRequest;
+import org.opensearch.client.opensearch.core.SearchRequest;
+import org.opensearch.client.opensearch.core.search.Hit;
 import org.springframework.util.CollectionUtils;
 
-public class ElasticsearchAuditConfDAO extends JPAAuditConfDAO {
+public class OpenSearchAuditConfDAO extends JPAAuditConfDAO {
 
-    protected final ElasticsearchClient client;
+    protected final OpenSearchClient client;
 
     protected final int indexMaxResultWindow;
 
-    public ElasticsearchAuditConfDAO(final ElasticsearchClient client, final int indexMaxResultWindow) {
+    public OpenSearchAuditConfDAO(final OpenSearchClient client, final int indexMaxResultWindow) {
         this.client = client;
         this.indexMaxResultWindow = indexMaxResultWindow;
     }
@@ -79,24 +80,25 @@ public class ElasticsearchAuditConfDAO extends JPAAuditConfDAO {
 
         if (type != null) {
             queries.add(new Query.Builder().
-                    term(QueryBuilders.term().field("message.logger.type").value(type.name()).build()).
+                    term(QueryBuilders.term().field("message.logger.type").value(FieldValue.of(type.name())).build()).
                     build());
         }
 
         if (StringUtils.isNotBlank(category)) {
             queries.add(new Query.Builder().
-                    term(QueryBuilders.term().field("message.logger.category").value(category).build()).
+                    term(QueryBuilders.term().field("message.logger.category").value(FieldValue.of(category)).build()).
                     build());
         }
 
         if (StringUtils.isNotBlank(subcategory)) {
             queries.add(new Query.Builder().
-                    term(QueryBuilders.term().field("message.logger.subcategory").value(subcategory).build()).
+                    term(QueryBuilders.term().field("message.logger.subcategory").
+                            value(FieldValue.of(subcategory)).build()).
                     build());
         }
 
         List<Query> eventQueries = events.stream().map(event -> new Query.Builder().
-                term(QueryBuilders.term().field("message.logger.event").value(event).build()).
+                term(QueryBuilders.term().field("message.logger.event").value(FieldValue.of(event)).build()).
                 build()).
                 collect(Collectors.toList());
         if (!eventQueries.isEmpty()) {
@@ -105,7 +107,8 @@ public class ElasticsearchAuditConfDAO extends JPAAuditConfDAO {
 
         if (result != null) {
             queries.add(new Query.Builder().
-                    term(QueryBuilders.term().field("message.logger.result").value(result.name()).build()).
+                    term(QueryBuilders.term().field("message.logger.result").
+                            value(FieldValue.of(result.name())).build()).
                     build());
         }
 
@@ -138,7 +141,7 @@ public class ElasticsearchAuditConfDAO extends JPAAuditConfDAO {
             final OffsetDateTime after) {
 
         CountRequest request = new CountRequest.Builder().
-                index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
+                index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
                 query(getQuery(entityKey, type, category, subcategory, events, result, before, after)).
                 build();
         try {
@@ -181,20 +184,20 @@ public class ElasticsearchAuditConfDAO extends JPAAuditConfDAO {
             final List<OrderByClause> orderBy) {
 
         SearchRequest request = new SearchRequest.Builder().
-                index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
+                index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
                 query(getQuery(entityKey, type, category, subcategory, events, result, before, after)).
+                fields(f -> f.field("message")).
                 from(itemsPerPage * (page <= 0 ? 0 : page - 1)).
                 size(itemsPerPage < 0 ? indexMaxResultWindow : itemsPerPage).
                 sort(sortBuilders(orderBy)).
                 build();
 
-        @SuppressWarnings("rawtypes")
-        List<Hit<Map>> esResult = null;
+        List<Hit<ObjectNode>> esResult = null;
         try {
-            esResult = client.search(request, Map.class).hits().hits();
+            esResult = client.search(request, ObjectNode.class).hits().hits();
         } catch (Exception e) {
-            LOG.error("While searching in Elasticsearch", e);
+            LOG.error("While searching in OpenSearch", e);
         }
 
         return CollectionUtils.isEmpty(esResult)
diff --git a/ext/opensearch/persistence-jpa/src/main/resources/META-INF/spring.factories b/ext/opensearch/persistence-jpa/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..690d72d55e
--- /dev/null
+++ b/ext/opensearch/persistence-jpa/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  org.apache.syncope.core.persistence.jpa.OpenSearchPersistenceContext
diff --git a/ext/opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java b/ext/opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java
new file mode 100644
index 0000000000..ef6deea7ff
--- /dev/null
+++ b/ext/opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java
@@ -0,0 +1,400 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.dao;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.commons.lang3.tuple.Triple;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.DynRealm;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.jpa.entity.JPAPlainSchema;
+import org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue;
+import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
+import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.ext.opensearch.client.OpenSearchUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opensearch.client.opensearch._types.FieldValue;
+import org.opensearch.client.opensearch._types.SearchType;
+import org.opensearch.client.opensearch._types.query_dsl.BoolQuery;
+import org.opensearch.client.opensearch._types.query_dsl.DisMaxQuery;
+import org.opensearch.client.opensearch._types.query_dsl.Query;
+import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders;
+import org.opensearch.client.opensearch.core.SearchRequest;
+import org.springframework.util.ReflectionUtils;
+
+@ExtendWith(MockitoExtension.class)
+public class OpenSearchAnySearchDAOTest {
+
+    @Mock
+    private RealmDAO realmDAO;
+
+    @Mock
+    private DynRealmDAO dynRealmDAO;
+
+    @Mock
+    private GroupDAO groupDAO;
+
+    @Mock
+    private EntityFactory entityFactory;
+
+    @Mock
+    private AnyUtilsFactory anyUtilsFactory;
+
+    @Mock
+    private PlainAttrValidationManager validator;
+
+    private OpenSearchAnySearchDAO searchDAO;
+
+    @BeforeEach
+    protected void setupSearchDAO() {
+        searchDAO = new OpenSearchAnySearchDAO(
+                realmDAO,
+                dynRealmDAO,
+                null,
+                groupDAO,
+                null,
+                null,
+                entityFactory,
+                anyUtilsFactory,
+                validator,
+                null,
+                10000);
+    }
+
+    @Test
+    public void getAdminRealmsFilter4realm() throws IOException {
+        // 1. mock
+        Realm root = mock(Realm.class);
+        when(root.getKey()).thenReturn("rootKey");
+        when(root.getFullPath()).thenReturn(SyncopeConstants.ROOT_REALM);
+
+        when(realmDAO.findByFullPath(SyncopeConstants.ROOT_REALM)).thenReturn(root);
+        when(realmDAO.findDescendants(SyncopeConstants.ROOT_REALM, null, -1, -1)).thenReturn(List.of(root));
+
+        // 2. test
+        Set<String> adminRealms = Set.of(SyncopeConstants.ROOT_REALM);
+        Triple<Optional<Query>, Set<String>, Set<String>> filter =
+                searchDAO.getAdminRealmsFilter(root, true, adminRealms, AnyTypeKind.USER);
+
+        assertThat(
+                new Query.Builder().disMax(QueryBuilders.disMax().queries(
+                        new Query.Builder().term(QueryBuilders.term().field("realm").value(
+                                FieldValue.of("rootKey")).build()).build()).build()).
+                        build()).
+                usingRecursiveComparison().isEqualTo(filter.getLeft().get());
+        assertEquals(Set.of(), filter.getMiddle());
+        assertEquals(Set.of(), filter.getRight());
+    }
+
+    @Test
+    public void getAdminRealmsFilter4dynRealm() {
+        // 1. mock
+        DynRealm dyn = mock(DynRealm.class);
+        when(dyn.getKey()).thenReturn("dyn");
+
+        when(dynRealmDAO.find("dyn")).thenReturn(dyn);
+
+        // 2. test
+        Set<String> adminRealms = Set.of("dyn");
+        Triple<Optional<Query>, Set<String>, Set<String>> filter =
+                searchDAO.getAdminRealmsFilter(realmDAO.getRoot(), true, adminRealms, AnyTypeKind.USER);
+        assertFalse(filter.getLeft().isPresent());
+        assertEquals(Set.of("dyn"), filter.getMiddle());
+        assertEquals(Set.of(), filter.getRight());
+    }
+
+    @Test
+    public void getAdminRealmsFilter4groupOwner() {
+        Set<String> adminRealms = Set.of(RealmUtils.getGroupOwnerRealm("/any", "groupKey"));
+        Triple<Optional<Query>, Set<String>, Set<String>> filter =
+                searchDAO.getAdminRealmsFilter(realmDAO.getRoot(), true, adminRealms, AnyTypeKind.USER);
+        assertFalse(filter.getLeft().isPresent());
+        assertEquals(Set.of(), filter.getMiddle());
+        assertEquals(Set.of("groupKey"), filter.getRight());
+    }
+
+    @Test
+    public void searchRequest4groupOwner() throws IOException {
+        // 1. mock
+        AnyUtils anyUtils = mock(AnyUtils.class);
+        when(anyUtils.getField("key")).thenReturn(ReflectionUtils.findField(JPAUser.class, "id"));
+        when(anyUtils.newPlainAttrValue()).thenReturn(new JPAUPlainAttrValue());
+
+        when(anyUtilsFactory.getInstance(AnyTypeKind.USER)).thenReturn(anyUtils);
+
+        when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema());
+
+        when(groupDAO.findKey("groupKey")).thenReturn("groupKey");
+
+        try (MockedStatic<OpenSearchUtils> utils = Mockito.mockStatic(OpenSearchUtils.class)) {
+            utils.when(() -> OpenSearchUtils.getAnyIndex(
+                    SyncopeConstants.MASTER_DOMAIN, AnyTypeKind.USER)).thenReturn("master_user");
+
+            // 2. test
+            Set<String> adminRealms = Set.of(RealmUtils.getGroupOwnerRealm("/any", "groupKey"));
+
+            AnyCond anyCond = new AnyCond(AttrCond.Type.ISNOTNULL);
+            anyCond.setSchema("key");
+
+            SearchRequest request = new SearchRequest.Builder().
+                    index(OpenSearchUtils.getAnyIndex(AuthContextUtils.getDomain(), AnyTypeKind.USER)).
+                    searchType(SearchType.QueryThenFetch).
+                    query(searchDAO.getQuery(realmDAO.findByFullPath("/any"), true,
+                            adminRealms, SearchCond.getLeaf(anyCond), AnyTypeKind.USER)).
+                    from(1).
+                    size(10).
+                    build();
+
+            assertThat(
+                    new Query.Builder().bool(QueryBuilders.bool().
+                            must(new Query.Builder().exists(QueryBuilders.exists().field("id").build()).build()).
+                            must(new Query.Builder().term(QueryBuilders.term().field("memberships").
+                                    value(FieldValue.of("groupKey")).build()).build()).build()).build()).
+                    usingRecursiveComparison().
+                    isEqualTo(request.query());
+        }
+    }
+
+    @Test
+    public void issueSYNCOPE1725() throws IOException {
+        // 1. mock
+        AnyUtils anyUtils = mock(AnyUtils.class);
+        when(anyUtils.getField("key")).thenReturn(ReflectionUtils.findField(JPAUser.class, "id"));
+        JPAUPlainAttrValue value = new JPAUPlainAttrValue();
+        when(anyUtils.newPlainAttrValue()).thenReturn(value);
+
+        when(anyUtilsFactory.getInstance(AnyTypeKind.USER)).thenReturn(anyUtils);
+
+        when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema());
+
+        doAnswer(ic -> {
+            value.setStringValue(ic.getArgument(1));
+            return null;
+        }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class));
+
+        AnyCond cond1 = new AnyCond(AttrCond.Type.EQ);
+        cond1.setSchema("key");
+        cond1.setExpression("1");
+
+        AnyCond cond2 = new AnyCond(AttrCond.Type.EQ);
+        cond2.setSchema("key");
+        cond2.setExpression("2");
+
+        AnyCond cond3 = new AnyCond(AttrCond.Type.EQ);
+        cond3.setSchema("key");
+        cond3.setExpression("3");
+
+        AnyCond cond4 = new AnyCond(AttrCond.Type.EQ);
+        cond4.setSchema("key");
+        cond4.setExpression("4");
+
+        AnyCond cond5 = new AnyCond(AttrCond.Type.EQ);
+        cond5.setSchema("key");
+        cond5.setExpression("5");
+
+        AnyCond cond6 = new AnyCond(AttrCond.Type.EQ);
+        cond6.setSchema("key");
+        cond6.setExpression("6");
+
+        try (MockedStatic<OpenSearchUtils> utils = Mockito.mockStatic(OpenSearchUtils.class)) {
+            utils.when(() -> OpenSearchUtils.getAnyIndex(
+                    SyncopeConstants.MASTER_DOMAIN, AnyTypeKind.USER)).thenReturn("master_user");
+
+            Query query = searchDAO.getQuery(
+                    SearchCond.getAnd(
+                            List.of(SearchCond.getLeaf(cond1),
+                                    SearchCond.getLeaf(cond2),
+                                    SearchCond.getLeaf(cond3),
+                                    SearchCond.getLeaf(cond4),
+                                    SearchCond.getLeaf(cond5),
+                                    SearchCond.getLeaf(cond6))),
+                    AnyTypeKind.USER);
+            assertEquals(Query.Kind.Bool, query._kind());
+            assertEquals(6, ((BoolQuery) query._get()).must().size());
+            assertThat(
+                    new Query.Builder().bool(QueryBuilders.bool().
+                            must(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("1")).build()).build()).
+                            must(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("2")).build()).build()).
+                            must(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("3")).build()).build()).
+                            must(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("4")).build()).build()).
+                            must(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("5")).build()).build()).
+                            must(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("6")).build()).build()).
+                            build()).build()).
+                    usingRecursiveComparison().isEqualTo(query);
+
+            query = searchDAO.getQuery(
+                    SearchCond.getOr(
+                            List.of(SearchCond.getLeaf(cond1),
+                                    SearchCond.getLeaf(cond2),
+                                    SearchCond.getLeaf(cond3),
+                                    SearchCond.getLeaf(cond4),
+                                    SearchCond.getLeaf(cond5),
+                                    SearchCond.getLeaf(cond6))),
+                    AnyTypeKind.USER);
+            assertEquals(Query.Kind.DisMax, query._kind());
+            assertEquals(6, ((DisMaxQuery) query._get()).queries().size());
+            assertThat(
+                    new Query.Builder().disMax(QueryBuilders.disMax().
+                            queries(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("1")).build()).build()).
+                            queries(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("2")).build()).build()).
+                            queries(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("3")).build()).build()).
+                            queries(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("4")).build()).build()).
+                            queries(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("5")).build()).build()).
+                            queries(new Query.Builder().term(
+                                    QueryBuilders.term().field("id").value(FieldValue.of("6")).build()).build()).
+                            build()).build()).
+                    usingRecursiveComparison().isEqualTo(query);
+
+            query = searchDAO.getQuery(
+                    SearchCond.getAnd(List.of(
+                            SearchCond.getOr(List.of(
+                                    SearchCond.getLeaf(cond1),
+                                    SearchCond.getLeaf(cond2),
+                                    SearchCond.getLeaf(cond3))),
+                            SearchCond.getOr(List.of(
+                                    SearchCond.getLeaf(cond4),
+                                    SearchCond.getLeaf(cond5),
+                                    SearchCond.getLeaf(cond6))))),
+                    AnyTypeKind.USER);
+            assertEquals(Query.Kind.Bool, query._kind());
+            assertEquals(2, ((BoolQuery) query._get()).must().size());
+            Query left = ((BoolQuery) query._get()).must().get(0);
+            assertEquals(Query.Kind.DisMax, left._kind());
+            assertEquals(3, ((DisMaxQuery) left._get()).queries().size());
+            Query right = ((BoolQuery) query._get()).must().get(1);
+            assertEquals(Query.Kind.DisMax, right._kind());
+            assertEquals(3, ((DisMaxQuery) right._get()).queries().size());
+            assertThat(
+                    new Query.Builder().bool(QueryBuilders.bool().
+                            must(new Query.Builder().disMax(QueryBuilders.disMax().
+                                    queries(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("1")).build()).build()).
+                                    queries(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("2")).build()).build()).
+                                    queries(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("3")).build()).build()).build()).
+                                    build()).
+                            must(new Query.Builder().disMax(QueryBuilders.disMax().
+                                    queries(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("4")).build()).build()).
+                                    queries(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("5")).build()).build()).
+                                    queries(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("6")).build()).build()).build()).
+                                    build()).
+                            build()).build()).
+                    usingRecursiveComparison().isEqualTo(query);
+
+            query = searchDAO.getQuery(
+                    SearchCond.getOr(List.of(
+                            SearchCond.getAnd(List.of(
+                                    SearchCond.getLeaf(cond1),
+                                    SearchCond.getLeaf(cond2),
+                                    SearchCond.getLeaf(cond3))),
+                            SearchCond.getAnd(List.of(
+                                    SearchCond.getLeaf(cond4),
+                                    SearchCond.getLeaf(cond5),
+                                    SearchCond.getLeaf(cond6))))),
+                    AnyTypeKind.USER);
+            assertEquals(Query.Kind.DisMax, query._kind());
+            assertEquals(2, ((DisMaxQuery) query._get()).queries().size());
+            left = ((DisMaxQuery) query._get()).queries().get(0);
+            assertEquals(Query.Kind.Bool, left._kind());
+            assertEquals(3, ((BoolQuery) left._get()).must().size());
+            right = ((DisMaxQuery) query._get()).queries().get(1);
+            assertEquals(Query.Kind.Bool, right._kind());
+            assertEquals(3, ((BoolQuery) right._get()).must().size());
+            assertThat(
+                    new Query.Builder().disMax(QueryBuilders.disMax().
+                            queries(new Query.Builder().bool(QueryBuilders.bool().
+                                    must(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("1")).build()).build()).
+                                    must(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("2")).build()).build()).
+                                    must(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("3")).build()).build()).build()).
+                                    build()).
+                            queries(new Query.Builder().bool(QueryBuilders.bool().
+                                    must(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("4")).build()).build()).
+                                    must(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("5")).build()).build()).
+                                    must(new Query.Builder().term(
+                                            QueryBuilders.term().field("id").
+                                                    value(FieldValue.of("6")).build()).build()).build()).
+                                    build()).
+                            build()).build()).
+                    usingRecursiveComparison().isEqualTo(query);
+        }
+    }
+}
diff --git a/ext/opensearch/pom.xml b/ext/opensearch/pom.xml
new file mode 100644
index 0000000000..daa967444c
--- /dev/null
+++ b/ext/opensearch/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.syncope</groupId>
+    <artifactId>syncope-ext</artifactId>
+    <version>4.0.0-SNAPSHOT</version>
+  </parent>
+
+  <name>Apache Syncope Ext: OpenSearch</name>
+  <description>Apache Syncope Ext: OpenSearch</description>
+  <groupId>org.apache.syncope.ext</groupId>
+  <artifactId>syncope-ext-opensearch</artifactId>
+  <packaging>pom</packaging>
+  
+  <properties>
+    <rootpom.basedir>${basedir}/../..</rootpom.basedir>
+  </properties>
+  
+  <modules>
+    <module>client-opensearch</module>
+    <module>persistence-jpa</module>
+    <module>provisioning-java</module>
+    <module>logic</module>
+  </modules>
+
+</project>
diff --git a/ext/elasticsearch/client-elasticsearch/pom.xml b/ext/opensearch/provisioning-java/pom.xml
similarity index 66%
copy from ext/elasticsearch/client-elasticsearch/pom.xml
copy to ext/opensearch/provisioning-java/pom.xml
index d48d5edef3..7120ebd10d 100644
--- a/ext/elasticsearch/client-elasticsearch/pom.xml
+++ b/ext/opensearch/provisioning-java/pom.xml
@@ -23,14 +23,14 @@ under the License.
 
   <parent>
     <groupId>org.apache.syncope.ext</groupId>
-    <artifactId>syncope-ext-elasticsearch</artifactId>
+    <artifactId>syncope-ext-opensearch</artifactId>
     <version>4.0.0-SNAPSHOT</version>
   </parent>
 
-  <name>Apache Syncope Ext: Elasticsearch Client</name>
-  <description>Apache Syncope Ext: Elasticsearch Client</description>
-  <groupId>org.apache.syncope.ext.elasticsearch</groupId>
-  <artifactId>syncope-ext-elasticsearch-client</artifactId>
+  <name>Apache Syncope Ext: OpenSearch Provisioning Java</name>
+  <description>Apache Syncope Ext: OpenSearch Provisioning Java</description>
+  <groupId>org.apache.syncope.ext.opensearch</groupId>
+  <artifactId>syncope-ext-opensearch-provisioning-java</artifactId>
   <packaging>jar</packaging>
   
   <properties>
@@ -40,27 +40,14 @@ under the License.
   <dependencies>
     <dependency>
       <groupId>org.apache.syncope.core</groupId>
-      <artifactId>syncope-core-spring</artifactId>
+      <artifactId>syncope-core-provisioning-java</artifactId>
       <version>${project.version}</version>
     </dependency>
 
     <dependency>
-      <groupId>co.elastic.clients</groupId>
-      <artifactId>elasticsearch-java</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>jakarta.json</groupId>
-      <artifactId>jakarta.json-api</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.springframework.boot</groupId>
-      <artifactId>spring-boot-starter-actuator</artifactId>
+      <groupId>org.apache.syncope.ext.opensearch</groupId>
+      <artifactId>syncope-ext-opensearch-client</artifactId>
+      <version>${project.version}</version>
     </dependency>
   </dependencies>
 
diff --git a/ext/opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java b/ext/opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java
new file mode 100644
index 0000000000..02273c540a
--- /dev/null
+++ b/ext/opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.provisioning.java.job;
+
+import java.io.IOException;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.dao.AnyDAO;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
+import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.ext.opensearch.client.OpenSearchIndexManager;
+import org.apache.syncope.ext.opensearch.client.OpenSearchUtils;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.opensearch._types.mapping.TypeMapping;
+import org.opensearch.client.opensearch.core.BulkRequest;
+import org.opensearch.client.opensearch.core.BulkResponse;
+import org.opensearch.client.opensearch.indices.IndexSettings;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Remove and rebuild all OpenSearch indexes with information from existing users, groups and any objects.
+ */
+public class OpenSearchReindex extends AbstractSchedTaskJobDelegate<SchedTask> {
+
+    @Autowired
+    protected OpenSearchClient client;
+
+    @Autowired
+    protected OpenSearchIndexManager indexManager;
+
+    @Autowired
+    protected OpenSearchUtils utils;
+
+    @Autowired
+    protected UserDAO userDAO;
+
+    @Autowired
+    protected GroupDAO groupDAO;
+
+    @Autowired
+    protected AnyObjectDAO anyObjectDAO;
+
+    protected IndexSettings userSettings() throws IOException {
+        return indexManager.defaultSettings();
+    }
+
+    protected IndexSettings groupSettings() throws IOException {
+        return indexManager.defaultSettings();
+    }
+
+    protected IndexSettings anyObjectSettings() throws IOException {
+        return indexManager.defaultSettings();
+    }
+
+    protected IndexSettings auditSettings() throws IOException {
+        return indexManager.defaultSettings();
+    }
+
+    protected TypeMapping userMapping() throws IOException {
+        return indexManager.defaultAnyMapping();
+    }
+
+    protected TypeMapping groupMapping() throws IOException {
+        return indexManager.defaultAnyMapping();
+    }
+
+    protected TypeMapping anyObjectMapping() throws IOException {
+        return indexManager.defaultAnyMapping();
+    }
+
+    protected TypeMapping auditMapping() throws IOException {
+        return indexManager.defaultAuditMapping();
+    }
+
+    @Override
+    protected String doExecute(final boolean dryRun, final String executor, final JobExecutionContext context)
+            throws JobExecutionException {
+
+        if (!dryRun) {
+            setStatus("Start rebuilding indexes");
+
+            try {
+                indexManager.createAnyIndex(
+                        AuthContextUtils.getDomain(), AnyTypeKind.USER, userSettings(), userMapping());
+
+                indexManager.createAnyIndex(
+                        AuthContextUtils.getDomain(), AnyTypeKind.GROUP, groupSettings(), groupMapping());
+
+                indexManager.createAnyIndex(
+                        AuthContextUtils.getDomain(), AnyTypeKind.ANY_OBJECT, anyObjectSettings(), anyObjectMapping());
+
+                int users = userDAO.count();
+                String uindex = OpenSearchUtils.getAnyIndex(AuthContextUtils.getDomain(), AnyTypeKind.USER);
+                setStatus("Indexing " + users + " users under " + uindex + "...");
+                for (int page = 1; page <= (users / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+                    BulkRequest.Builder bulkRequest = new BulkRequest.Builder();
+
+                    for (String user : userDAO.findAllKeys(page, AnyDAO.DEFAULT_PAGE_SIZE)) {
+                        bulkRequest.operations(op -> op.index(idx -> idx.
+                                index(uindex).
+                                id(user).
+                                document(utils.document(userDAO.find(user)))));
+                    }
+
+                    try {
+                        BulkResponse response = client.bulk(bulkRequest.build());
+                        LOG.debug("Index successfully created for {} [{}/{}]: {}",
+                                uindex, page, AnyDAO.DEFAULT_PAGE_SIZE, response);
+                    } catch (Exception e) {
+                        LOG.error("Could not create index for {} [{}/{}]: {}",
+                                uindex, page, AnyDAO.DEFAULT_PAGE_SIZE, e);
+                    }
+                }
+
+                int groups = groupDAO.count();
+                String gindex = OpenSearchUtils.getAnyIndex(AuthContextUtils.getDomain(), AnyTypeKind.GROUP);
+                setStatus("Indexing " + groups + " groups under " + gindex + "...");
+                for (int page = 1; page <= (groups / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+                    BulkRequest.Builder bulkRequest = new BulkRequest.Builder();
+
+                    for (String group : groupDAO.findAllKeys(page, AnyDAO.DEFAULT_PAGE_SIZE)) {
+                        bulkRequest.operations(op -> op.index(idx -> idx.
+                                index(gindex).
+                                id(group).
+                                document(utils.document(groupDAO.find(group)))));
+                    }
+
+                    try {
+                        BulkResponse response = client.bulk(bulkRequest.build());
+                        LOG.debug("Index successfully created for {} [{}/{}]: {}",
+                                gindex, page, AnyDAO.DEFAULT_PAGE_SIZE, response);
+                    } catch (Exception e) {
+                        LOG.error("Could not create index for {} [{}/{}]: {}",
+                                gindex, page, AnyDAO.DEFAULT_PAGE_SIZE, e);
+                    }
+                }
+
+                int anyObjects = anyObjectDAO.count();
+                String aindex = OpenSearchUtils.getAnyIndex(AuthContextUtils.getDomain(), AnyTypeKind.ANY_OBJECT);
+                setStatus("Indexing " + anyObjects + " any objects under " + aindex + "...");
+                for (int page = 1; page <= (anyObjects / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+                    BulkRequest.Builder bulkRequest = new BulkRequest.Builder();
+
+                    for (String anyObject : anyObjectDAO.findAllKeys(page, AnyDAO.DEFAULT_PAGE_SIZE)) {
+                        bulkRequest.operations(op -> op.index(idx -> idx.
+                                index(aindex).
+                                id(anyObject).
+                                document(utils.document(anyObjectDAO.find(anyObject)))));
+                    }
+
+                    try {
+                        BulkResponse response = client.bulk(bulkRequest.build());
+                        LOG.debug("Index successfully created for {} [{}/{}]: {}",
+                                aindex, page, AnyDAO.DEFAULT_PAGE_SIZE, response);
+                    } catch (Exception e) {
+                        LOG.error("Could not create index for {} [{}/{}]: {}",
+                                aindex, page, AnyDAO.DEFAULT_PAGE_SIZE, e);
+                    }
+                }
+
+                indexManager.createAuditIndex(AuthContextUtils.getDomain(), auditSettings(), auditMapping());
+
+                setStatus("Rebuild indexes for domain " + AuthContextUtils.getDomain() + " successfully completed");
+            } catch (Exception e) {
+                throw new JobExecutionException("While rebuilding index for domain " + AuthContextUtils.getDomain(), e);
+            }
+        }
+
+        return "SUCCESS";
+    }
+
+    @Override
+    protected boolean hasToBeRegistered(final TaskExec<?> execution) {
+        return true;
+    }
+}
diff --git a/ext/pom.xml b/ext/pom.xml
index 60a5b0773f..b8c3b1e9c6 100644
--- a/ext/pom.xml
+++ b/ext/pom.xml
@@ -81,6 +81,7 @@ under the License.
     <module>saml2sp4ui</module>
     <module>oidcc4ui</module>
     <module>elasticsearch</module>
+    <module>opensearch</module>
     <module>scimv2</module>
   </modules>
 
diff --git a/fit/core-reference/pom.xml b/fit/core-reference/pom.xml
index 1cf4846713..ac17be60f7 100644
--- a/fit/core-reference/pom.xml
+++ b/fit/core-reference/pom.xml
@@ -508,7 +508,7 @@ under the License.
 
     <profile>
       <id>elasticsearch-it</id>
-      
+
       <properties>
         <spring.profiles.active>embedded,elasticsearch</spring.profiles.active>
       </properties>
@@ -530,7 +530,7 @@ under the License.
           <version>${project.version}</version>
         </dependency>
       </dependencies>
-      
+
       <build>
         <plugins>
           <plugin>
@@ -586,14 +586,14 @@ under the License.
               </execution>
             </executions>
           </plugin>
-          
+
           <plugin>
             <groupId>org.codehaus.cargo</groupId>
             <artifactId>cargo-maven3-plugin</artifactId>
             <inherited>true</inherited>
           </plugin>
         </plugins>
-        
+
         <testResources>
           <testResource>
             <directory>src/test/resources</directory>
@@ -621,6 +621,120 @@ under the License.
       </build>
     </profile>
   
+    <profile>
+      <id>opensearch-it</id>
+
+      <properties>
+        <spring.profiles.active>embedded,opensearch</spring.profiles.active>
+      </properties>
+
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.syncope.ext.opensearch</groupId>
+          <artifactId>syncope-ext-opensearch-logic</artifactId>
+          <version>${project.version}</version>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.syncope.ext.opensearch</groupId>
+          <artifactId>syncope-ext-opensearch-provisioning-java</artifactId>
+          <version>${project.version}</version>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.syncope.ext.opensearch</groupId>
+          <artifactId>syncope-ext-opensearch-persistence-jpa</artifactId>
+          <version>${project.version}</version>
+        </dependency>
+      </dependencies>
+
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <groupId>io.fabric8</groupId>
+            <artifactId>docker-maven-plugin</artifactId>
+            <configuration>
+              <images>
+                <image>
+                  <name>opensearchproject/opensearch:${opensearch.version}</name>
+                  <run>
+                    <env>
+                      <discovery.type>single-node</discovery.type>
+                      <cluster.name>elasticsearch</cluster.name>
+                      <cluster.routing.allocation.disk.threshold_enabled>false</cluster.routing.allocation.disk.threshold_enabled>
+                      <plugins.security.disabled>true</plugins.security.disabled>
+                      <OPENSEARCH_JAVA_OPTS>-Xms750m -Xmx750m</OPENSEARCH_JAVA_OPTS>
+                    </env>
+                    <ports>
+                      <port>9200:9200</port>
+                      <port>9300:9300</port>
+                    </ports>
+                  </run>
+                </image>
+              </images>
+            </configuration>
+            <executions>
+              <execution>
+                <id>start-opensearch</id>
+                <phase>pre-integration-test</phase>
+                <goals>
+                  <goal>start</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>stop-opensearch</id>
+                <phase>post-integration-test</phase>
+                <goals>
+                  <goal>stop</goal>
+                  <goal>remove</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+
+          <plugin>
+            <groupId>org.codehaus.cargo</groupId>
+            <artifactId>cargo-maven3-plugin</artifactId>
+            <inherited>true</inherited>
+          </plugin>
+        </plugins>
+
+        <testResources>
+          <testResource>
+            <directory>src/test/resources</directory>
+            <filtering>true</filtering>
+            <excludes>
+              <exclude>saml.keystore.jks</exclude>
+              <exclude>test.pdf</exclude>
+            </excludes>
+          </testResource>
+          <testResource>
+            <directory>src/test/resources</directory>
+            <filtering>false</filtering>
+            <includes>
+              <include>saml.keystore.jks</include>
+              <include>test.pdf</include>
+            </includes>
+          </testResource>
+          <testResource>
+            <directory>${basedir}/../../core/rest-cxf/src/main/resources</directory>
+            <includes>
+              <include>errorMessages.properties</include>
+            </includes>
+          </testResource>
+        </testResources>
+      </build>
+    </profile>
+
     <profile>
       <id>pgjsonb-it</id>
 
diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/CoreReferenceContext.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/CoreReferenceContext.java
index 88b71ba560..e0ca9c5a12 100644
--- a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/CoreReferenceContext.java
+++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/CoreReferenceContext.java
@@ -47,6 +47,15 @@ public class CoreReferenceContext {
         return new ElasticsearchInit(implementationDAO, entityFactory, taskLogic);
     }
 
+    @Bean
+    public OpenSearchInit openSearchInit(
+            final ImplementationDAO implementationDAO,
+            final EntityFactory entityFactory,
+            final TaskLogic taskLogic) {
+
+        return new OpenSearchInit(implementationDAO, entityFactory, taskLogic);
+    }
+
     @Bean
     public EnableFlowableForTestUsers enableFlowableForTestUsers(final UserDAO userDAO) {
         return new EnableFlowableForTestUsers(userDAO);
@@ -57,9 +66,11 @@ public class CoreReferenceContext {
             final UserWorkflowAdapter uwf,
             final AnySearchDAO anySearchDAO,
             final EnableFlowableForTestUsers enableFlowableForTestUsers,
-            final ElasticsearchInit elasticsearchInit) {
+            final ElasticsearchInit elasticsearchInit,
+            final OpenSearchInit openSearchInit) {
 
-        return new ITImplementationLookup(uwf, anySearchDAO, enableFlowableForTestUsers, elasticsearchInit);
+        return new ITImplementationLookup(
+                uwf, anySearchDAO, enableFlowableForTestUsers, elasticsearchInit, openSearchInit);
     }
 
     @Bean
diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java
index 79b36ed0da..6d06f55848 100644
--- a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java
+++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java
@@ -212,18 +212,22 @@ public class ITImplementationLookup implements ImplementationLookup {
 
     private final ElasticsearchInit elasticsearchInit;
 
+    private final OpenSearchInit openSearchInit;
+
     private boolean loaded;
 
     public ITImplementationLookup(
             final UserWorkflowAdapter uwf,
             final AnySearchDAO anySearchDAO,
             final EnableFlowableForTestUsers enableFlowableForTestUsers,
-            final ElasticsearchInit elasticsearchInit) {
+            final ElasticsearchInit elasticsearchInit,
+            final OpenSearchInit openSearchInit) {
 
         this.uwf = uwf;
         this.anySearchDAO = anySearchDAO;
         this.enableFlowableForTestUsers = enableFlowableForTestUsers;
         this.elasticsearchInit = elasticsearchInit;
+        this.openSearchInit = openSearchInit;
     }
 
     @Override
@@ -254,6 +258,14 @@ public class ITImplementationLookup implements ImplementationLookup {
             });
         }
 
+        // in case the OpenSearch extension is enabled, reinit a clean index for all available domains
+        if (openSearchInit != null && AopUtils.getTargetClass(anySearchDAO).getName().contains("OpenSearch")) {
+            AuthContextUtils.callAsAdmin(domain, () -> {
+                openSearchInit.init();
+                return null;
+            });
+        }
+
         loaded = true;
     }
 
diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/OpenSearchInit.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/OpenSearchInit.java
new file mode 100644
index 0000000000..229ff04cf1
--- /dev/null
+++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/OpenSearchInit.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.fit.core.reference;
+
+import org.apache.syncope.common.lib.to.SchedTaskTO;
+import org.apache.syncope.common.lib.types.IdRepoImplementationType;
+import org.apache.syncope.common.lib.types.ImplementationEngine;
+import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.syncope.core.logic.TaskLogic;
+import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.springframework.transaction.annotation.Transactional;
+
+public class OpenSearchInit {
+
+    private static final String ES_REINDEX = "org.apache.syncope.core.provisioning.java.job.OpenSearchReindex";
+
+    private final ImplementationDAO implementationDAO;
+
+    private final EntityFactory entityFactory;
+
+    private final TaskLogic taskLogic;
+
+    public OpenSearchInit(
+            final ImplementationDAO implementationDAO,
+            final EntityFactory entityFactory,
+            final TaskLogic taskLogic) {
+
+        this.implementationDAO = implementationDAO;
+        this.entityFactory = entityFactory;
+        this.taskLogic = taskLogic;
+    }
+
+    @Transactional
+    public void init() {
+        Implementation reindex = implementationDAO.findByType(IdRepoImplementationType.TASKJOB_DELEGATE).
+                stream().
+                filter(impl -> impl.getEngine() == ImplementationEngine.JAVA && ES_REINDEX.equals(impl.getBody())).
+                findAny().
+                orElseGet(() -> {
+                    Implementation impl = entityFactory.newEntity(Implementation.class);
+                    impl.setKey(ES_REINDEX);
+                    impl.setEngine(ImplementationEngine.JAVA);
+                    impl.setType(IdRepoImplementationType.TASKJOB_DELEGATE);
+                    impl.setBody(ES_REINDEX);
+                    return implementationDAO.save(impl);
+                });
+
+        SchedTaskTO task = new SchedTaskTO();
+        task.setJobDelegate(reindex.getKey());
+        task.setName("OpenSearch Reindex");
+        task = taskLogic.createSchedTask(TaskType.SCHEDULED, task);
+
+        taskLogic.execute(task.getKey(), null, false);
+    }
+}
diff --git a/fit/core-reference/src/main/resources/core-opensearch.properties b/fit/core-reference/src/main/resources/core-opensearch.properties
new file mode 100644
index 0000000000..351d0a9ab7
--- /dev/null
+++ b/fit/core-reference/src/main/resources/core-opensearch.properties
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+opensearch.hosts[0]=http://localhost:9200
+opensearch.indexMaxResultWindow=10000
+opensearch.numberOfShards=1
+opensearch.numberOfReplicas=1
diff --git a/fit/core-reference/src/main/resources/log4j2.xml b/fit/core-reference/src/main/resources/log4j2.xml
index 476e7b9641..445aff41e2 100644
--- a/fit/core-reference/src/main/resources/log4j2.xml
+++ b/fit/core-reference/src/main/resources/log4j2.xml
@@ -153,6 +153,9 @@ under the License.
     <asyncLogger name="org.elasticsearch" additivity="false" level="INFO">
       <appender-ref ref="main"/>
     </asyncLogger>
+    <asyncLogger name="org.opensearch" additivity="false" level="INFO">
+      <appender-ref ref="main"/>
+    </asyncLogger>
     <asyncLogger name="io.netty" additivity="false" level="ERROR">
       <appender-ref ref="main"/>
     </asyncLogger>
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 3e3743a751..b5a604710b 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -391,7 +391,7 @@ public abstract class AbstractITCase {
 
     protected static boolean IS_FLOWABLE_ENABLED = false;
 
-    protected static boolean IS_ELASTICSEARCH_ENABLED = false;
+    protected static boolean IS_EXT_SEARCH_ENABLED = false;
 
     @BeforeAll
     public static void anonymousSetup() throws IOException {
@@ -433,7 +433,8 @@ public abstract class AbstractITCase {
         IS_FLOWABLE_ENABLED = uwfAdapter.get("resource").asText().contains("Flowable");
 
         JsonNode anySearchDAO = beans.findValues("anySearchDAO").get(0);
-        IS_ELASTICSEARCH_ENABLED = anySearchDAO.get("type").asText().contains("Elasticsearch");
+        IS_EXT_SEARCH_ENABLED = anySearchDAO.get("type").asText().contains("Elasticsearch")
+                || anySearchDAO.get("type").asText().contains("OpenSearch");
     }
 
     @BeforeAll
@@ -983,7 +984,7 @@ public abstract class AbstractITCase {
     }
 
     protected static List<AuditEntry> query(final AuditQuery query, final int maxWaitSeconds) {
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractUIITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractUIITCase.java
index c4a36d8ffb..bae049623a 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractUIITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractUIITCase.java
@@ -74,7 +74,7 @@ public abstract class AbstractUIITCase {
 
     protected static boolean IS_FLOWABLE_ENABLED = false;
 
-    protected static boolean IS_ELASTICSEARCH_ENABLED = false;
+    protected static boolean IS_EXT_SEARCH_ENABLED = false;
 
     @BeforeAll
     public static void anonymousSetup() throws IOException {
@@ -103,7 +103,8 @@ public abstract class AbstractUIITCase {
         IS_FLOWABLE_ENABLED = uwfAdapter.get("resource").asText().contains("Flowable");
 
         JsonNode anySearchDAO = beans.findValues("anySearchDAO").get(0);
-        IS_ELASTICSEARCH_ENABLED = anySearchDAO.get("type").asText().contains("Elasticsearch");
+        IS_EXT_SEARCH_ENABLED = anySearchDAO.get("type").asText().contains("Elasticsearch")
+                || anySearchDAO.get("type").asText().contains("OpenSearch");
     }
 
     protected static <V extends Serializable> Component findComponentByProp(
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
index 66a83a2987..25b83886b7 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
@@ -402,7 +402,7 @@ public class AuditITCase extends AbstractITCase {
         auditEntry.setOutput(UUID.randomUUID().toString());
         assertDoesNotThrow(() -> AUDIT_SERVICE.create(auditEntry));
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -437,7 +437,7 @@ public class AuditITCase extends AbstractITCase {
         auditEntry.setOutput(UUID.randomUUID().toString());
         assertDoesNotThrow(() -> AUDIT_SERVICE.create(auditEntry));
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -619,7 +619,7 @@ public class AuditITCase extends AbstractITCase {
                     build()).getEntity();
 
             // search by empty type and category events and get both events on testfromLDAP
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
index bec73598c9..c2763a1f7b 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
@@ -182,7 +182,7 @@ public class AuthenticationITCase extends AbstractITCase {
         UserService userService2 = CLIENT_FACTORY.create(userTO.getUsername(), "password123").
                 getService(UserService.class);
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -482,7 +482,7 @@ public class AuthenticationITCase extends AbstractITCase {
         assertEquals(2, member.getMemberships().size());
         String memberKey = member.getKey();
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DelegationITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DelegationITCase.java
index e7530a4622..88211a9148 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DelegationITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DelegationITCase.java
@@ -240,7 +240,7 @@ public class DelegationITCase extends AbstractITCase {
         // 3b. search users as rossini with delegation -> SUCCESS
         int forRossini = rossini.delegatedBy("bellini").getService(UserService.class).search(
                 new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).build()).getTotalCount();
-        if (!IS_ELASTICSEARCH_ENABLED) {
+        if (!IS_EXT_SEARCH_ENABLED) {
             assertEquals(forBellini, forRossini);
         }
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java
index 6697a04b5b..42b9cd3420 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java
@@ -149,7 +149,7 @@ public class DynRealmITCase extends AbstractITCase {
             assertNotNull(group);
             final String groupKey = group.getKey();
 
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
@@ -270,7 +270,7 @@ public class DynRealmITCase extends AbstractITCase {
             assertNotNull(user.getKey());
 
             // 4a. check that Elasticsearch index was updated correctly
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
@@ -292,7 +292,7 @@ public class DynRealmITCase extends AbstractITCase {
             DYN_REALM_SERVICE.update(dynRealm);
 
             // 6a. check that Elasticsearch index was updated correctly
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
index 2c91159cfb..536924d2c4 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
@@ -703,7 +703,7 @@ public class GroupITCase extends AbstractITCase {
         GroupTO group = createGroup(groupCR).getEntity();
         assertEquals(fiql, group.getADynMembershipConds().get(PRINTER));
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -930,7 +930,7 @@ public class GroupITCase extends AbstractITCase {
 
     @Test
     public void provisionMembers() throws InterruptedException {
-        assumeFalse(IS_ELASTICSEARCH_ENABLED);
+        assumeFalse(IS_EXT_SEARCH_ENABLED);
 
         // 1. create group without resources
         GroupCR groupCR = getBasicSample("forProvision");
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java
index a2fd92d8ff..f4b6147b4e 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java
@@ -281,7 +281,7 @@ public class KeymasterITCase extends AbstractITCase {
         assertNotNull(user);
         assertEquals("monteverdi", user.getUsername());
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java
index ca2095f93d..a8ec3c0011 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java
@@ -280,7 +280,7 @@ public class MembershipITCase extends AbstractITCase {
             assertEquals(ExecStatus.SUCCESS, ExecStatus.valueOf(execution.getStatus()));
 
             // 5. verify that pulled user has
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
index b8c2f3df5c..f76cddabfa 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
@@ -206,7 +206,7 @@ public class MultitenancyITCase extends AbstractITCase {
             assertEquals(ExecStatus.SUCCESS, ExecStatus.valueOf(status));
 
             // verify that pulled user is found
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
index f8d9997ce6..33ded0fa4a 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
@@ -231,7 +231,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
 
     @Test
     public void fromCSV() throws Exception {
-        assumeFalse(IS_ELASTICSEARCH_ENABLED);
+        assumeFalse(IS_EXT_SEARCH_ENABLED);
 
         removeTestUsers();
 
@@ -421,7 +421,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
         PullTaskTO task = TASK_SERVICE.read(TaskType.PULL, "1e419ca4-ea81-4493-a14f-28b90113686d", false);
         assertEquals(SyncopeConstants.ROOT_REALM, task.getDestinationRealm());
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -595,7 +595,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
             assertTrue(connObjectTO.getAttr("LOCATION").get().getValues().get(0).startsWith(prefix));
 
             // 3. unlink any existing printer and delete from Syncope (printer is now only on external resource)
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
@@ -622,7 +622,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
             // 4. pull
             execProvisioningTask(TASK_SERVICE, TaskType.PULL, pullTask.getKey(), MAX_WAIT_SECONDS, false);
 
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
@@ -632,7 +632,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
 
             // 5. verify that printer was re-created in Syncope (implies that location does not start with given prefix,
             // hence PrefixItemTransformer was applied during pull)
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
@@ -1215,7 +1215,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
 
     @Test
     public void issueSYNCOPE307() {
-        assumeFalse(IS_ELASTICSEARCH_ENABLED);
+        assumeFalse(IS_EXT_SEARCH_ENABLED);
 
         UserCR userCR = UserITCase.getUniqueSample("s307@apache.org");
         userCR.setUsername("test0");
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
index bb13fe418f..e0139c5906 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
@@ -176,7 +176,7 @@ public class SearchITCase extends AbstractITCase {
         GroupTO group = createGroup(groupCR).getEntity();
         assertNotNull(group);
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -234,7 +234,7 @@ public class SearchITCase extends AbstractITCase {
         role = getObject(response.getLocation(), RoleService.class, RoleTO.class);
         assertNotNull(role);
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -329,7 +329,7 @@ public class SearchITCase extends AbstractITCase {
 
     @Test
     public void searchByDate() {
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -475,7 +475,7 @@ public class SearchITCase extends AbstractITCase {
                 build();
         String printer = createAnyObject(anyObjectCR).getEntity().getKey();
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -665,7 +665,7 @@ public class SearchITCase extends AbstractITCase {
                     build();
             updateAnyObject(anyObjectUR);
 
-            if (IS_ELASTICSEARCH_ENABLED) {
+            if (IS_EXT_SEARCH_ENABLED) {
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException ex) {
@@ -704,7 +704,7 @@ public class SearchITCase extends AbstractITCase {
         req.getPlainAttrs().add(new AttrPatch.Builder(attr("ctype", "ou=sample,o=isp")).build());
         USER_SERVICE.update(req);
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -769,7 +769,7 @@ public class SearchITCase extends AbstractITCase {
             USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
                     fiql(SyncopeClient.getUserSearchConditionBuilder().is("userId").equalTo("*@apache.org").query()).
                     orderBy(SyncopeClient.getOrderByClauseBuilder().asc("surname").desc("firstname").build()).build());
-            if (!IS_ELASTICSEARCH_ENABLED) {
+            if (!IS_EXT_SEARCH_ENABLED) {
                 fail();
             }
         } catch (SyncopeClientException e) {
@@ -849,7 +849,7 @@ public class SearchITCase extends AbstractITCase {
         UserTO user = createUser(userCR).getEntity();
 
         // 3. search for user
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
@@ -884,7 +884,7 @@ public class SearchITCase extends AbstractITCase {
         createUser(UserITCase.getSample("syncope1779test@syncope.apache.org")).getEntity();
 
         // 3. search for user
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
index 16d4131492..833d29aec2 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
@@ -1079,7 +1079,7 @@ public class UserIssuesITCase extends AbstractITCase {
 
     @Test
     public void issueSYNCOPE391() {
-        assumeFalse(IS_ELASTICSEARCH_ENABLED);
+        assumeFalse(IS_EXT_SEARCH_ENABLED);
 
         // 1. create user on Syncope with null password
         UserCR userCR = UserITCase.getUniqueSample("syncope391@syncope.apache.org");
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
index 69ff1e6d47..2878c0e906 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
@@ -334,7 +334,7 @@ public class UserSelfITCase extends AbstractITCase {
         }
         ANONYMOUS_CLIENT.getService(UserSelfService.class).requestPasswordReset(user.getUsername(), "Rossi");
 
-        if (IS_ELASTICSEARCH_ENABLED) {
+        if (IS_EXT_SEARCH_ENABLED) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {
diff --git a/pom.xml b/pom.xml
index 34268532b0..b6b58f5d73 100644
--- a/pom.xml
+++ b/pom.xml
@@ -428,6 +428,8 @@ under the License.
     <disruptor.version>3.4.4</disruptor.version>
 
     <elasticsearch.version>8.10.4</elasticsearch.version>
+    <opensearch.version>2.11.0</opensearch.version>
+    <opensearch-java.version>2.7.0</opensearch-java.version>
 
     <commons-jexl.version>3.3</commons-jexl.version>
     <commons-text.version>1.10.0</commons-text.version>
@@ -613,6 +615,23 @@ under the License.
         <version>${elasticsearch.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.opensearch.client</groupId>
+        <artifactId>opensearch-rest-client</artifactId>
+        <version>${opensearch.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.opensearch.client</groupId>
+        <artifactId>opensearch-java</artifactId>
+        <version>${opensearch-java.version}</version>
+      </dependency>
+
+      <dependency>
+        <groupId>jakarta.json</groupId>
+        <artifactId>jakarta.json-api</artifactId>
+        <version>2.1.2</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.openjpa</groupId>
         <artifactId>openjpa</artifactId>
diff --git a/src/main/asciidoc/reference-guide/concepts/extensions.adoc b/src/main/asciidoc/reference-guide/concepts/extensions.adoc
index 1c09ec00fe..9464c8f551 100644
--- a/src/main/asciidoc/reference-guide/concepts/extensions.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/extensions.adoc
@@ -124,6 +124,28 @@ endif::[]
 .
 ====
 
+==== OpenSearch
+
+This extension provides an alternate internal search engine for <<users-groups-and-any-objects>> and <<audit-events>>,
+requiring an external https://opensearch.org/[OpenSearch^] cluster.
+
+[TIP]
+As search operations are central for different aspects of the <<provisioning,provisioning process>>, the global
+performance is expected to improve when using this extension.
+
+[NOTE]
+.Extension Sources
+====
+The source code of this extension is available from the Apache Syncope
+ifeval::["{snapshotOrRelease}" == "release"]
+https://github.com/apache/syncope/tree/syncope-{docVersion}/ext/opensearch[source tree^]
+endif::[]
+ifeval::["{snapshotOrRelease}" == "snapshot"]
+https://github.com/apache/syncope/tree/master/ext/opensearch[source tree^]
+endif::[]
+.
+====
+
 ==== SCIM
 
 http://www.simplecloud.info/[SCIM^] (System for Cross-domain Identity Management) 2.0 is the open API for managing
diff --git a/src/main/asciidoc/reference-guide/usage/customization.adoc b/src/main/asciidoc/reference-guide/usage/customization.adoc
index fd7f735dcc..c56a611de1 100644
--- a/src/main/asciidoc/reference-guide/usage/customization.adoc
+++ b/src/main/asciidoc/reference-guide/usage/customization.adoc
@@ -415,6 +415,64 @@ The `org.apache.syncope.core.provisioning.java.job.ElasticsearchReindex` task cr
 scheduled execution; rather, it can be run every time you want to blank and re-create the Elasticsearch indexes
 starting from Syncope's internal storage.
 
+[discrete]
+===== Enable the <<opensearch>> extension
+
+Add the following dependencies to `core/pom.xml`:
+
+[source,xml,subs="verbatim,attributes"]
+----
+<dependency>
+  <groupId>org.apache.syncope.ext.opensearch</groupId>
+  <artifactId>syncope-ext-opensearch-logic</artifactId>
+  <version>${syncope.version}</version>
+</dependency>
+<dependency>
+  <groupId>org.apache.syncope.ext.opensearch</groupId>
+  <artifactId>syncope-ext-opensearch-provisioning-java</artifactId>
+  <version>${syncope.version}</version>
+</dependency>
+<dependency>
+  <groupId>org.apache.syncope.ext.opensearch</groupId>
+  <artifactId>syncope-ext-opensearch-persistence-jpa</artifactId>
+  <version>${syncope.version}</version>
+</dependency>
+----
+
+Create
+
+[source]
+....
+opensearch.hosts[0]=http://localhost:9200
+opensearch.indexMaxResultWindow=10000
+opensearch.numberOfShards=1
+opensearch.numberOfReplicas=1
+....
+
+as `core/src/main/resources/core-opensearch.properties`.
+
+Do not forget to include `opensearch` as 
+https://docs.spring.io/spring-boot/docs/2.7.x/reference/html/features.html#features.profiles.adding-active-profiles[Spring Boot profile^]
+for the Core application.
+
+If needed, customize the `@Bean` declarations from
+ifeval::["{snapshotOrRelease}" == "release"]
+https://github.com/apache/syncope/blob/syncope-{docVersion}/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientContext.java[OpenSearchClientContext^]
+endif::[]
+ifeval::["{snapshotOrRelease}" == "snapshot"]
+https://github.com/apache/syncope/blob/master/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchClientContext.java[OpenSearchClientContext^]
+endif::[]
+as explained <<extending-configuration,above>>.
+
+It is also required to initialize the OpenSearch indexes: add a new Java <<implementations,implementation>> for
+`TASKJOB_DELEGATE` and use `org.apache.syncope.core.provisioning.java.job.OpenSearchReindex` as class. +
+Then, create a new <<tasks-scheduled, scheduled task>>, select the implementation just created as job delegate and execute it.
+
+[TIP]
+The `org.apache.syncope.core.provisioning.java.job.OpenSearchReindex` task created above is not meant for
+scheduled execution; rather, it can be run every time you want to blank and re-create the OpenSearch indexes
+starting from Syncope's internal storage.
+
 [discrete]
 ===== Enable the <<SCIM>> extension