diff --git a/GIDs b/GIDs
--- a/GIDs
+++ b/GIDs
@@ -1,6 +1,7 @@
 827	chaton-dev
 828	deployment
 829	nasqueron-irc
+835 opensearch
 3001 ops
 #3002 is intentionally left unassigned
 3003 deployment
diff --git a/UIDs b/UIDs
--- a/UIDs
+++ b/UIDs
@@ -3,6 +3,7 @@
 832	chaton
 833 viper
 834 tc2
+835 opensearch
 3004 mediawiki
 9001 salt
 9002 deploy
diff --git a/_modules/opensearch.py b/_modules/opensearch.py
new file mode 100644
--- /dev/null
+++ b/_modules/opensearch.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+
+#   -------------------------------------------------------------
+#   Salt — PaaS OpenSearch execution module
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   Description:    Functions related to OpenSearch configuration
+#   License:        BSD-2-Clause
+#   -------------------------------------------------------------
+
+
+from salt.exceptions import CommandExecutionError, SaltCloudConfigError
+
+
+def get_config(nodename=None):
+    """
+    A function to get relevant values for OpenSearch configuration.
+
+    CLI Example:
+
+        salt * opensearch.get_config
+    """
+    if nodename is None:
+        nodename = __grains__['id']
+
+    try:
+        clusters = __pillar__['opensearch_clusters']
+    except KeyError:
+        clusters = []
+
+    for _, cluster in clusters.items():
+        if nodename in cluster['nodes']:
+            return _expand_cluster_config(nodename, cluster)
+
+    raise CommandExecutionError(
+        SaltCloudConfigError(
+            "Node {0} not declared in pillar opensearch_clusters.".format(nodename)
+        )
+    )
+
+
+def _expand_cluster_config(nodename, config):
+    config = dict(config)
+    nodes = _convert_to_ip(config["nodes"])
+
+    config.update({
+        "nodes": nodes,
+        "nodes_certificates": _get_nodes_info(config["nodes"]),
+        "node_name": nodename,
+        "network_host": _get_ip(nodename),
+        "lead_nodes": nodes,
+    })
+
+    return config
+
+
+def _convert_to_ip(ids):
+    return [_get_ip(id) for id in ids]
+
+
+def _get_ip(nodename):
+    try:
+        network = __pillar__['nodes'][nodename]['network']
+    except KeyError:
+        raise CommandExecutionError(
+            SaltCloudConfigError(
+                "Node {0} not declared in pillar nodes.".format(nodename)
+            )
+        )
+
+    for field in ['ipv4_address', 'ipv6_address']:
+        if field in network:
+            return network[field]
+
+
+def _get_nodes_info(ids):
+    return [_get_node_info(id) for id in ids]
+
+
+def _get_node_info(nodename):
+    return {
+        "id": nodename,
+        "fqdn": __pillar__['nodes'][nodename]['hostname'],
+        "ip": _get_ip(nodename),
+    }
+
+
+def hash_password(clear_password):
+    command = "/opt/opensearch/plugins/opensearch-security/tools/hash.sh -p '{0}'".format(clear_password)
+    env = {
+        "JAVA_HOME": "/opt/opensearch/jdk",
+    }
+
+    return __salt__['cmd.shell'](command, env=env)
+
+
+def list_certificates(nodename=None):
+    config = get_config(nodename=None)
+
+    certificates = ['admin', 'root-ca']
+
+    for node in config["nodes_certificates"]:
+        certificates.extend([node['id'], node['id'] + '_http'])
+
+    return certificates
diff --git a/pillar/credentials/zr.sls b/pillar/credentials/zr.sls
--- a/pillar/credentials/zr.sls
+++ b/pillar/credentials/zr.sls
@@ -40,6 +40,13 @@
       mailgun: 82
       sentry: 141
 
+    # OpenSearch clusters
+    opensearch:
+      infra-logs:
+        internal_users:
+          admin: 163
+          dashboards: 164
+
     # photos.nasqueron.org
     pixelfed:
       mysql: 142
@@ -108,6 +115,12 @@
    restrictCommand:
    comment: Zemke-Rhyne
 
+ - key: 162
+   allowedConnectionFrom:
+     - cloudhugger.nasqueron.org
+   restrictCommand:
+   comment: Zemke-Rhyne
+
  - key: 152
    allowedConnectionFrom:
      - docker-001.nasqueron.org
diff --git a/pillar/nodes/nodes.sls b/pillar/nodes/nodes.sls
--- a/pillar/nodes/nodes.sls
+++ b/pillar/nodes/nodes.sls
@@ -17,7 +17,7 @@
     forest: nasqueron-infra
     hostname: cloudhugger.nasqueron.org
     roles:
-      - paas-kubernetes
+      - opensearch
     network:
       ipv4_interface: eno1
       ipv4_address: 188.165.200.229
diff --git a/pillar/opensearch/clusters.sls b/pillar/opensearch/clusters.sls
new file mode 100644
--- /dev/null
+++ b/pillar/opensearch/clusters.sls
@@ -0,0 +1,18 @@
+#
+# Currently, declare OpenSearch clusters as single-node, per machine.
+#
+
+opensearch_clusters:
+
+  #
+  # Infrastructure: logs and metrics
+  #
+
+  infra_logs:
+    cluster_name: infra-logs
+    cluster_type: single-node
+    nodes:
+      - cloudhugger
+    users:
+      admin: nasqueron.opensearch.infra-logs.internal_users.admin
+      dashboards: nasqueron.opensearch.infra-logs.internal_users.dashboards
diff --git a/pillar/opensearch/software.sls b/pillar/opensearch/software.sls
new file mode 100644
--- /dev/null
+++ b/pillar/opensearch/software.sls
@@ -0,0 +1,19 @@
+#   -------------------------------------------------------------
+#   Current versions for Linux x64
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+opensearch_products:
+  opensearch:
+    version: 1.2.3
+    hash: a594ac2808e6e476d647e47e45e837fba3e21671285ce39b2cee1c32d5e6887d
+
+  opensearch-dashboards:
+    version: 1.2.0
+    hash: 14623798e61be6913e2a218d6ba3e308e5036359d7bda58482ad2f1340aa3c85
+
+#   -------------------------------------------------------------
+#   Cleanup legacy versions
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+opensearch_legacy_products:
+  opensearch: []
diff --git a/pillar/top.sls b/pillar/top.sls
--- a/pillar/top.sls
+++ b/pillar/top.sls
@@ -17,7 +17,10 @@
     - hotfixes.roles
     - webserver.sites
 
-  cloudhugger: []
+  cloudhugger:
+    - credentials.zr
+    - opensearch.software
+    - opensearch.clusters
 
   docker-001:
     - credentials.zr
diff --git a/roles/opensearch/init.sls b/roles/opensearch/init.sls
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/init.sls
@@ -0,0 +1,9 @@
+#   -------------------------------------------------------------
+#   Salt — Provision OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   -------------------------------------------------------------
+
+include:
+  - .opensearch
diff --git a/roles/opensearch/opensearch/config.sls b/roles/opensearch/opensearch/config.sls
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/config.sls
@@ -0,0 +1,104 @@
+#   -------------------------------------------------------------
+#   Salt — Provision OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   -------------------------------------------------------------
+
+{% set config = salt['opensearch.get_config']() %}
+
+#   -------------------------------------------------------------
+#   OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/opt/opensearch/config/opensearch.yml:
+  file.managed:
+    - source: salt://roles/opensearch/opensearch/files/opensearch.conf
+    - user: opensearch
+    - group: opensearch
+    - template: jinja
+    - context:
+        config: {{ config }}
+
+#   -------------------------------------------------------------
+#   TLS certificates
+#
+#   This method is based on OpenSearch Ansible playbook to
+#   generate self-signed certificates for node to node (transport)
+#   communication, and for the rest API.
+#
+#   The certificates are generated by Search Guard Offline TLS Tool.
+#
+#   This should only run on one node, then provisioned everywhere.
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/usr/local/dl/search-guard-tlstool.zip:
+  file.managed:
+    - source: https://maven.search-guard.com/search-guard-tlstool/1.8/search-guard-tlstool-1.8.zip
+    - source_hash: f59f963c7ee28d557849ccde297660a3c593a6bf3531d7852fb9ab8b4fc7597e
+
+/opt/tlstool:
+  archive.extracted:
+    - source: /usr/local/dl/search-guard-tlstool.zip
+    - enforce_toplevel: False
+    - user: opensearch
+    - group: opensearch
+
+/opt/tlstool/config/tlsconfig.yml:
+  file.managed:
+    - source: salt://roles/opensearch/opensearch/files/tlsconfig.yml.jinja
+    - template: jinja
+    - context:
+        config: {{ config }}
+        domain_name: {{ grains['domain'] }}
+        node_full_domain_name: {{ grains['fqdn'] }}
+
+opensearch_generate_certificates:
+  cmd.run:
+     - name: /opt/tlstool/tools/sgtlstool.sh -c /opt/tlstool/config/tlsconfig.yml -ca -crt -t /opt/tlstool/config/
+     - env:
+         JAVA_HOME: /opt/opensearch/jdk
+     - creates: /opt/tlstool/config/root-ca.pem
+
+{% for certificate in salt['opensearch.list_certificates']() %}
+
+opensearch_deploy_certificate_{{ certificate }}:
+   cmd.run:
+     - name: install --mode=0600 --owner=opensearch {{ certificate }}.pem {{ certificate }}.key /opt/opensearch/config
+     - cwd: /opt/tlstool/config
+     - creates: /opt/opensearch/config/{{ certificate }}.pem
+
+{% endfor %}
+
+#   -------------------------------------------------------------
+#   Security plugin
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/opt/opensearch/plugins/opensearch-security/securityconfig/internal_users.yml:
+  file.managed:
+    - source: salt://roles/opensearch/opensearch/files/internal_users.yml.jinja
+    - user: opensearch
+    - group: opensearch
+    - template: jinja
+    - context:
+        users:
+          {% for user, credential in config['users'].items() %}
+          {{ user }}:
+            username: {{ salt['zr.get_username'](credential) }}
+            password: {{ salt['zr.get_password'](credential) }}
+          {% endfor %}
+
+opensearch_security_initialize:
+   cmd.run:
+     - name: >
+         bash /opt/opensearch/plugins/opensearch-security/tools/securityadmin.sh
+         -cacert /opt/opensearch/config/root-ca.pem
+         -cert /opt/opensearch/config/admin.pem
+         -key /opt/opensearch/config/admin.key
+         -f /opt/opensearch/plugins/opensearch-security/securityconfig/internal_users.yml
+         -nhnv -icl
+         -h {{ config['network_host'] }}
+
+         touch /opt/opensearch/plugins/opensearch-security/securityconfig/.initialized
+     - env:
+         JAVA_HOME: /opt/opensearch/jdk
+     - creates: /opt/opensearch/plugins/opensearch-security/securityconfig/.initialized
diff --git a/roles/opensearch/opensearch/files/account.conf b/roles/opensearch/opensearch/files/account.conf
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/files/account.conf
@@ -0,0 +1 @@
+{{ username }}:{{ password }}
diff --git a/roles/opensearch/opensearch/files/es-query.sh.jinja b/roles/opensearch/opensearch/files/es-query.sh.jinja
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/files/es-query.sh.jinja
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+#   -------------------------------------------------------------
+#   OpenSearch wrapper
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   Source file:    roles/opensearch/opensearch/files/es-query.sh.jinja
+#   -------------------------------------------------------------
+#
+#   <auto-generated>
+#       This file is managed by our rOPS SaltStack repository.
+#
+#       Changes to this file may cause incorrect behavior
+#       and will be lost if the state is redeployed.
+#   </auto-generated>
+
+OPENSEARCH_ACCOUNT=$(cat $HOME/.opensearch-account)
+
+COMMAND=$1
+shift
+
+curl {{ url }}/$COMMAND -u $OPENSEARCH_ACCOUNT -k "$@"
diff --git a/roles/opensearch/opensearch/files/internal_users.yml.jinja b/roles/opensearch/opensearch/files/internal_users.yml.jinja
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/files/internal_users.yml.jinja
@@ -0,0 +1,38 @@
+#   -------------------------------------------------------------
+#   OpenSearch configuration
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   Source file:    roles/opensearch/opensearch/files/internal_users.yml.jinja
+#   -------------------------------------------------------------
+#
+#   <auto-generated>
+#       This file is managed by our rOPS SaltStack repository.
+#
+#       Changes to this file may cause incorrect behavior
+#       and will be lost if the state is redeployed.
+#   </auto-generated>
+
+_meta:
+  type: "internalusers"
+  config_version: 2
+
+#   -------------------------------------------------------------
+#   Reserved users to ensure access continuity
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{{ users['admin']['username'] }}:
+  hash: {{ salt['opensearch.hash_password'](users['admin']['password']) }}
+  reserved: true
+  backend_roles:
+    - "admin"
+  description: "Alternative admin user"
+
+#   -------------------------------------------------------------
+#   Dashboards (formerly Kibana)
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{{ users['dashboards']['username'] }}:
+  hash: {{ salt['opensearch.hash_password'](users['dashboards']['password']) }}
+  reserved: true
+  description: "Dashboards to OpenSearch machine user"
diff --git a/roles/opensearch/opensearch/files/opensearch.conf b/roles/opensearch/opensearch/files/opensearch.conf
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/files/opensearch.conf
@@ -0,0 +1,40 @@
+cluster.name: {{ config['cluster_name'] }}
+node.name: {{ config['node_name'] }}
+network.host: {{ config['network_host'] }}
+http.port: 9200
+bootstrap.memory_lock: true
+
+{% if config['cluster_type'] == 'single-node' %}
+discovery.type: single-node
+{% else %}
+discovery.seed_hosts:
+{% for node in config["nodes"] %}
+  - {{ node }}
+{% endfor %}
+cluster.initial_master_nodes:
+{% for node in config["lead_nodes"] %}
+  - {{ node }}
+{% endfor %}
+{% endif %}
+
+plugins.security.allow_default_init_securityindex: true
+plugins.security.audit.type: internal_opensearch
+plugins.security.enable_snapshot_restore_privilege: true
+plugins.security.check_snapshot_restore_write_privileges: true
+plugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_access"]
+
+plugins.security.ssl.transport.pemcert_filepath: {{ config["node_name"] }}.pem
+plugins.security.ssl.transport.pemkey_filepath: {{ config["node_name"] }}.key
+plugins.security.ssl.transport.pemtrustedcas_filepath: root-ca.pem
+plugins.security.ssl.transport.enforce_hostname_verification: false
+plugins.security.ssl.transport.resolve_hostname: false
+plugins.security.ssl.http.enabled: true
+plugins.security.ssl.http.pemcert_filepath: {{ config["node_name"] }}_http.pem
+plugins.security.ssl.http.pemkey_filepath: {{ config["node_name"] }}_http.key
+plugins.security.ssl.http.pemtrustedcas_filepath: root-ca.pem
+plugins.security.nodes_dn:
+{% for node in config["nodes_certificates"] %}
+- CN={{ node["fqdn"] }},OU=Infrastructure,DC=nasqueron,DC=org
+{% endfor %}
+plugins.security.authcz.admin_dn:
+- CN=admin.infra-logs.nasqueron.org,OU=CA,DC=nasqueron,DC=org
diff --git a/roles/opensearch/opensearch/files/opensearch.service b/roles/opensearch/opensearch/files/opensearch.service
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/files/opensearch.service
@@ -0,0 +1,34 @@
+[Unit]
+Description=OpenSearch search, analytics, and visualization suite
+Documentation=https://opensearch.org/docs/latest/
+After=network.target
+
+[Service]
+RuntimeDirectory=opensearch
+PrivateTmp=true
+
+User=opensearch
+Group=opensearch
+
+LimitNOFILE=65536
+LimitMEMLOCK=infinity
+LimitNPROC=4096
+LimitAS=infinity
+LimitFSIZE=infinity
+
+Environment="KNN_LIB_DIR=/opt/opensearch/plugins/opensearch-knn/knnlib"
+WorkingDirectory=/opt/opensearch
+ExecStart=/opt/opensearch/bin/opensearch -p /opt/opensearch/opensearch.pid -q
+
+StandardOutput=journal
+StandardError=inherit
+
+# To shutdown: send SIGTERM signal to JVM, success if exit code 143
+TimeoutStopSec=0
+KillSignal=SIGTERM
+KillMode=process
+SendSIGKILL=no
+SuccessExitStatus=143
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/opensearch/opensearch/files/tlsconfig.yml.jinja b/roles/opensearch/opensearch/files/tlsconfig.yml.jinja
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/files/tlsconfig.yml.jinja
@@ -0,0 +1,55 @@
+#   -------------------------------------------------------------
+#   OpenSearch configuration
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   Source file:    roles/opensearch/opensearch/files/tlsconfig.yml.jinja
+#   -------------------------------------------------------------
+#
+#   <auto-generated>
+#       This file is managed by our rOPS SaltStack repository.
+#
+#       Changes to this file may cause incorrect behavior
+#       and will be lost if the state is redeployed.
+#   </auto-generated>
+
+#   -------------------------------------------------------------
+#   CA
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ca:
+   root:
+      dn: CN=root.ca-{{ config['cluster_name'] }}.{{ domain_name }},OU=CA,DC=nasqueron,DC=org
+      keysize: 2048
+      validityDays: 730
+      pkPassword: none
+      file: root-ca.pem
+
+defaults:
+      validityDays: 730
+      pkPassword: none
+      httpsEnabled: true
+      reuseTransportCertificatesForHttp: false
+      verifyHostnames: false
+      resolveHostnames: false
+
+#   -------------------------------------------------------------
+#   Nodes
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+nodes:
+{% for node in config['nodes_certificates'] %}
+  - name: {{ node['id'] }}
+    dn: CN={{ node['fqdn'] }},OU=Infrastructure,DC=nasqueron,DC=org
+    dns: {{ node['fqdn'] }}
+    ip: {{ node['ip'] }}
+{% endfor %}
+
+#   -------------------------------------------------------------
+#   Clients
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+clients:
+  - name: admin
+    dn: CN=admin.{{ config['cluster_name'] }}.{{ domain_name }},OU=CA,DC=nasqueron,DC=org
+    admin: True
diff --git a/roles/opensearch/opensearch/init.sls b/roles/opensearch/opensearch/init.sls
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/init.sls
@@ -0,0 +1,13 @@
+#   -------------------------------------------------------------
+#   Salt — Provision OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   -------------------------------------------------------------
+
+include:
+  - .kernel
+  - .software
+  - .config
+  - .service
+  - .wrapper
diff --git a/roles/opensearch/opensearch/kernel.sls b/roles/opensearch/opensearch/kernel.sls
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/kernel.sls
@@ -0,0 +1,20 @@
+#   -------------------------------------------------------------
+#   Salt — Provision OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   -------------------------------------------------------------
+
+#   -------------------------------------------------------------
+#   Virtual memory
+#
+#   https://opensearch.org/docs/latest/opensearch/install/important-settings/
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{% if grains['kernel'] == 'Linux' %}
+
+vm.max_map_count:
+  sysctl.present:
+    - value: 262144
+
+{% endif %}
diff --git a/roles/opensearch/opensearch/service.sls b/roles/opensearch/opensearch/service.sls
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/service.sls
@@ -0,0 +1,31 @@
+#   -------------------------------------------------------------
+#   Salt — Provision OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   -------------------------------------------------------------
+
+#   -------------------------------------------------------------
+#   systemd
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{% from "map.jinja" import services with context %}
+
+#   -------------------------------------------------------------
+#   Unit configuration
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{% if services['manager'] == 'systemd' %}
+
+opensearch_unit:
+  file.managed:
+    - name: /etc/systemd/system/opensearch.service
+    - source: salt://roles/opensearch/opensearch/files/opensearch.service
+    - mode: 0644
+  service.running:
+    - name: opensearch
+    - enable: true
+    - watch:
+      - file: opensearch_unit
+
+{% endif %}
diff --git a/roles/opensearch/opensearch/software.sls b/roles/opensearch/opensearch/software.sls
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/software.sls
@@ -0,0 +1,76 @@
+#   -------------------------------------------------------------
+#   Salt — Provision OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   -------------------------------------------------------------
+
+{% from "map.jinja" import shells with context %}
+
+#   -------------------------------------------------------------
+#   User account
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+opensearch_group:
+  group.present:
+    - name: opensearch
+    - gid: 835
+
+opensearch_user:
+  user.present:
+    - name: opensearch
+    - fullname: OpenSearch
+    - uid: 835
+    - gid: opensearch
+    - home: /opt/opensearch
+    - shell: {{ shells['bash'] }}
+
+#   -------------------------------------------------------------
+#   Download and extract tarballs
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/usr/local/dl:
+  file.directory
+
+{% if grains['kernel'] == 'Linux' and grains['cpuarch'] == 'x86_64' %}
+{% for product, info in pillar['opensearch_products'].items() %}
+
+{% set distname = product + "-" + info['version'] %}
+
+/usr/local/dl/{{ distname }}.tar.gz:
+  file.managed:
+    - source: https://artifacts.opensearch.org/releases/bundle/{{ product }}/{{ info['version'] }}/{{ distname }}-linux-x64.tar.gz
+    - source_hash: {{ info['hash'] }}
+
+/opt/{{ product }}:
+  file.directory:
+    - user: opensearch
+    - group: opensearch
+
+extract_opensearch_{{ product }}:
+  archive.extracted:
+    - name: /opt/{{ product }}
+    - source: /usr/local/dl/{{ distname }}.tar.gz
+    - user: opensearch
+    - group: opensearch
+    - overwrite: True
+    - enforce_toplevel: False
+    - options: --strip 1
+    - onchanges:
+       - file: /usr/local/dl/{{ distname }}.tar.gz
+
+{% endfor %}
+{% endif %}
+
+#   -------------------------------------------------------------
+#   Cleanup legacy versions
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{% for product, versions in pillar['opensearch_legacy_products'].items() %}
+{% for version in versions %}
+
+/usr/local/dl/{{ product }}-{{ version }}.tar.gz:
+  file.absent
+
+{% endfor %}
+{% endfor %}
diff --git a/roles/opensearch/opensearch/wrapper.sls b/roles/opensearch/opensearch/wrapper.sls
new file mode 100644
--- /dev/null
+++ b/roles/opensearch/opensearch/wrapper.sls
@@ -0,0 +1,32 @@
+#   -------------------------------------------------------------
+#   Salt — Provision OpenSearch
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#   Project:        Nasqueron
+#   License:        Trivial work, not eligible to copyright
+#   -------------------------------------------------------------
+
+{% set config = salt['opensearch.get_config']() %}
+
+#   -------------------------------------------------------------
+#   Wrapper for curl
+#   Admin client for OpenSearch
+#
+#   https://opensearch.org/docs/latest/opensearch/install/important-settings/
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/usr/local/bin/es-query:
+  file.managed:
+    - source: salt://roles/opensearch/opensearch/files/es-query.sh.jinja
+    - mode: 0755
+    - template: jinja
+    - context:
+        url: https://{{ config['network_host'] }}:9200
+
+/root/.opensearch-account:
+  file.managed:
+     - source: salt://roles/opensearch/opensearch/files/account.conf
+     - mode: 0600
+     - template: jinja
+     - context:
+        username: {{ salt['zr.get_username'](config['users']['admin']) }}
+        password: {{ salt['zr.get_password'](config['users']['admin']) }}
diff --git a/top.sls b/top.sls
--- a/top.sls
+++ b/top.sls
@@ -28,7 +28,7 @@
     - roles/webserver-core
     - roles/webserver-legacy
   'cloudhugger':
-    - roles/paas-kubernetes
+    - roles/opensearch
   'docker-001':
     - roles/paas-docker
   'dwellers':