Skip to content

Connecting to OpenSearch when the security plugin is on

A default distribution serves HTTPS with a self-signed certificate on the same port, and no custom CA can be supplied on this transport.

When you connect to OpenSearch over HTTPS and the security plugin is on, the certificate is what stops you, not the credentials. The username and password go through as an HTTP Basic header the moment you fill them in. The handshake in front of them either verifies against the platform’s certificate store or it fails, and there is no field on the form that changes that answer.

What a default distribution serves, and where

The distribution ships the security plugin enabled. A node started that way serves HTTPS, presents a certificate it generated for itself, and requires an admin password. That is the state of a cluster nobody has configured yet.

The connection form has four fields: host, port, user, password. Host is required - validate() throws “OpenSearch requires a host”. Port defaults to 9200. User and password are sent as HTTP Basic only when a user is set, which is what the security plugin wants. There is no database field, and it is ignored if you set one: an index has no namespace above it, and this product’s own SQL agrees - SHOW TABLES LIKE % answers TABLE_SCHEM null. There is no connection string either, so there is no URL to paste a scheme into.

Behind those four fields there is no driver at all. Statements go to POST /_plugins/_sql, the SQL plugin that ships bundled with the distribution. Schema and monitoring come from the REST APIs instead - GET /_cat/indices, GET /<index>/_mapping, _cluster/health, _cluster/stats. Every one of those is the runtime’s own fetch, with no connection pool, no sniffing and no retry underneath it. One statement is one request to one host. Connecting proves the cluster with a single SELECT 1, which needs no index and so also succeeds against a cluster holding nothing yet.

Why the port does not change under TLS

9200 is the default for both schemes. A TLS deployment serves HTTPS on that same number. The engine grid names this engine’s transport http and its port 9200 for exactly that reason: there is one port, and the scheme in front of it is a separate question.

Setting the connection’s ssl mode to anything other than disable switches the transport from http to https. It changes the scheme and it changes nothing else. It does not shift the port, because there is no second well-known number to shift to. Inventing one would mean sending a Basic header carrying an admin password to a port nothing is listening on.

If you have seen 9201 attached to OpenSearch somewhere, it came from a container fixture. The studio repo’s compose file publishes the node on host port 9201 because the Elasticsearch service in the same file already claims 9200. That is a collision on one machine, not a fact about the product. A real node is on 9200, TLS or not.

The security plugin certificate problem this transport cannot solve

fetch verifies the chain against the platform’s certificate store, and it cannot be told otherwise. Carrying a custom CA, presenting a client certificate or relaxing verification all require an undici Agent as the dispatcher, and undici is not a dependency here.

So the three fields you would reach for do nothing:

Field What it would do elsewhere What happens here
ssl.caCert pin against a pasted CA not honoured
ssl.clientCert present a client certificate not honoured
ssl.rejectUnauthorized accept an unverified chain not honoured

Custom CA certificates, client certificates and relaxed verification are not honoured on this transport. A default distribution’s self-signed certificate therefore fails verification, and such a cluster needs a publicly trusted certificate, a terminating proxy in front of it, or the security plugin disabled. There is no fourth option and no toggle that produces one.

One consequence worth stating flatly: of the four non-disable TLS modes, verify-system is the only one whose name describes what this transport actually does. require here does not mean “encrypt without checking” the way it does on the driver-based engines, because nothing in this code path can skip a check, and verify-ca and verify-full cannot pin against a pasted CA. Nothing in the code branches on which mode you picked. That is the kind of boundary the security page exists to publish.

Three deployment shapes that work

A publicly trusted certificate on the cluster. The node presents a certificate the machine running the container already trusts, from a CA in the platform store. Nothing has to be pasted anywhere because nothing needs to be told about it.

A terminating proxy in front of the cluster. The proxy holds the trusted certificate and speaks to the cluster behind it on whatever terms the two of them have arranged, including a self-signed certificate the proxy is configured to accept. The connection points at the proxy’s host and port, and the proxy is where the certificate problem gets solved by software that has the knobs for it.

The security plugin disabled, on a private network. This is what the container fixture does:

services:
  opensearch:
    image: opensearchproject/opensearch:3.8.0
    environment:
      discovery.type: single-node
      DISABLE_SECURITY_PLUGIN: 'true'
      OPENSEARCH_JAVA_OPTS: '-Xms512m -Xmx512m'

The plugin is installed rather than licensed, so it is switched off by name. Measured against a node in that state, a bogus Basic header is ignored outright - HTTP 200 - which is why credentials are genuinely optional there. It is also why this is the shape with a condition attached: a cluster with the plugin off has no authentication and no transport encryption, so it belongs on a private network beside the tool that reads it, and nowhere else.

Whichever shape you land in, an authentication failure is legible when it happens. 401 and 403 are the one category this provider classifies on the HTTP status rather than on the response body, because HTTP itself fixes their meaning and no 401 body could be captured from a cluster running with the plugin disabled.

System indices your cluster shows that a naming rule misses

Once you are connected, the sidebar raises a second question the security plugin also touches. Measured on a stock 3.8.0 node with two hand-made probe indices, _cat/indices listed four:

.plugins-ml-config              engine bookkeeping, dot-prefixed
probe_orders                    yours
probe_shapes                    yours
top_queries-2026.08.18-74305    engine bookkeeping, no leading dot

On an empty cluster, two of every three indices are the engine’s own. The familiar leading-dot convention catches the first and misses the last, so the transport carries a second rule for the date-suffixed query-insights shape, /^top_queries-\d{4}\.\d{2}\.\d{2}-\d+$/. That makes hiding them a judgement rather than a rule, which is why it is exposed as a flag the caller decides about and not a filter applied on the wire. Hidden is the default; an operator debugging ML inference wants .plugins-ml-config in the tree and a developer writing a query does not.

If you do turn them on, note that top_queries-2026.08.18-74305 carries hyphens and dots, so it is a name SQL needs quoted - and on this product the identifier quote is a backtick:

SELECT * FROM `top_queries-2026.08.18-74305` LIMIT 10

Double quotes will not do it. They are a string literal in this grammar, so SELECT "customer" FROM probe_orders answers HTTP 200 with the literal word customer in every row, and a predicate written that way compares two literals and returns zero rows with no error at all.

The security plugin shows up here too, in a quieter way. It grants index privileges per index, so a role that can list twenty indices and describe nineteen of them is an ordinary configuration rather than a fault. A mapping read refused for authorisation costs that one index its column list and leaves the other nineteen intact. The tree degrades by one entry instead of going blank, which is the only reading of that situation that matches what the cluster actually told us.

Everything measured above was verified against OpenSearch 3.8.0, image opensearchproject/opensearch:3.8.0, on 2026-08-19.