OpenSearch is the search and analytics engine behind full-text search, faceted navigation, and log analytics on modern web stacks. It’s also become the go-to replacement for Elasticsearch on many self-hosted platforms as licensing changes have pushed projects to migrate away from it. If you need fast, relevant search over large datasets, OpenSearch is the piece that sits behind the scenes doing the heavy lifting.
This guide walks through the whole thing from zero: what each command actually does, why it’s there, and — because this trips people up constantly — how to make sure OpenSearch is never reachable by anyone except the application running on the same machine. We’ll go slower than a typical install guide on purpose, so you can follow along even if you’ve never installed a search engine before.
Why Security Is the Main Character of This Post
Before touching a single command, it’s worth understanding why so much of this guide is about locking things down rather than just “getting it running.”
Search and database engines like OpenSearch, Elasticsearch, MongoDB, and Redis have a long, well-documented history of being left open to the public internet — usually because a tutorial somewhere said “just bind to 0.0.0.0 so it’s easy to connect to,” or because a cloud firewall rule was left too permissive. Automated bots constantly scan the entire internet for exactly these ports (9200 for OpenSearch/Elasticsearch, 27017 for MongoDB, 6379 for Redis, and so on). The moment one of these services responds on a public IP without authentication, it gets found — usually within hours, sometimes minutes. What follows is one of a few outcomes, all bad: the entire dataset is copied out silently, the entire dataset is deleted and replaced with a ransom note demanding payment for its return (this has happened to tens of thousands of exposed Elasticsearch and MongoDB instances at once, in automated, indiscriminate sweeps), or the server itself gets used as a foothold to attack other things on your network.
None of that requires the attacker to be sophisticated or to target you specifically. It just requires the port to be open. That’s the entire reason this guide spends real time on two things: making sure OpenSearch never listens anywhere except 127.0.0.1 (your own machine, talking to itself), and making sure it has a real password instead of a well-known default. Do both, and the entire class of attack described above simply doesn’t apply to you — there’s no route in.
Prerequisites
- A server running Ubuntu, with
aptandcurlavailable - A non-root user with
sudoprivileges - At least 1–2 GB of RAM free for OpenSearch’s JVM heap (more if you’re indexing a large catalog)
- No separate Java install needed — OpenSearch ships with its own bundled JDK, so you don’t need to install or manage a Java version yourself
Step 1: Import the OpenSearch Signing Key
What this step does and why it exists: every package your system installs via apt comes from a repository — basically a folder of software hosted on someone’s server. Nothing stops a network in the middle, a compromised mirror, or a malicious actor from tampering with those files in transit and slipping in a modified package. To stop that, projects like OpenSearch cryptographically sign their release metadata with a private key that only they hold. Your system then needs the matching public key so apt can check: “does this signature actually match what OpenSearch’s real private key would have produced?” If yes, the files are authentic and untampered. If no — or if there’s no key to check against at all — apt refuses to install anything from that repository. That refusal is a safety feature working as intended, not a bug to route around.
curl -sSL -o /tmp/opensearch.pgp https://artifacts.opensearch.org/publickeys/opensearch.pgp
sudo gpg --dearmor --batch --yes -o /usr/share/keyrings/opensearch-keyring.gpg /tmp/opensearch.pgp
rm -f /tmp/opensearch.pgpBreaking down what each line does:
curl ... -o /tmp/opensearch.pgpdownloads OpenSearch’s official public key from their own HTTPS domain and saves it to a temporary file.gpg --dearmorconverts the key from “ASCII-armored” format (the human-readable-----BEGIN PGP PUBLIC KEY BLOCK-----text block you’d see if you opened the file) into the compact binary formataptexpects for a keyring file. Think of it as changing the file’s packaging, not its contents.- The output is saved specifically to
/usr/share/keyrings/opensearch-keyring.gpg, a file dedicated to this one repository — rather than the old-style globalapt-key add, which dumped a key into one big trust store shared by every repo on the system. That older approach meant a key added for one piece of software was implicitly trusted to sign packages for anything else too — a much bigger blast radius if that one key were ever misused. Keeping a separate keyring file per repository (which we’ll reference explicitly in Step 2) avoids that.
Deep Dive: What Happens When apt Says “NO_PUBKEY”
Here’s a real problem we ran into, and it’s common enough with the OpenSearch repo that you should expect it: after adding the repository in the next step and running apt update, you may see an error like this:
W: OpenPGP signature verification failed: ... InRelease: The following signatures
couldn't be verified because the public key is not available: NO_PUBKEY 4E9275EE6BA2427F
E: The repository '...' is not signed.Let’s unpack this properly, because “just fetch the missing key” isn’t very satisfying without understanding what a key ID even is.
What is a “key ID”? A GPG key isn’t one single key — it’s a keypair (public + private) that has a long, unique fingerprint, similar in spirit to a serial number. A “key ID” is just a shorter, more convenient stand-in for that fingerprint — typically the last 16 hexadecimal characters of it — used so humans and tools can reference a specific key without typing out the whole fingerprint every time. In the error above, 4E9275EE6BA2427F is that ID. It’s telling you: “the repository’s metadata file was signed by a key with this exact ID, and I don’t have a copy of that public key to check the signature against.”
Why didn’t Step 1 already cover this? Projects often sign releases with more than one key or subkey over time — for example, a primary key plus one or more “release signing” subkeys that get rotated periodically. The single key file published at OpenSearch’s public-keys URL doesn’t necessarily include every subkey that has ever signed a package. So apt is checking a real signature against a real key that does exist — it’s just not the one you already downloaded.
Important: you do not “generate” this key yourself. This isn’t about creating your own new keypair — the key already exists; it belongs to the OpenSearch project. What you’re doing is fetching a copy of a public key that already exists, the same way you’d look up someone’s public phone number rather than inventing one for them. That copy is stored on public keyservers — essentially open, shared directories that anyone can upload a public key to, or query by key ID or fingerprint. keyserver.ubuntu.com is one such directory, commonly used for exactly this kind of lookup.
Here’s how to actually fetch it, using the real key ID from the error above as a worked example:
gpg --no-default-keyring --keyring /tmp/os-extra.gpg \
--keyserver keyserver.ubuntu.com --recv-keys 4E9275EE6BA2427F
gpg --no-default-keyring --keyring /tmp/os-extra.gpg --export 4E9275EE6BA2427F \
| sudo tee -a /usr/share/keyrings/opensearch-keyring.gpg > /dev/null
rm -f /tmp/os-extra.gpg /tmp/os-extra.gpg~What each part is actually doing:
--no-default-keyring --keyring /tmp/os-extra.gpgtells GPG “don’t touch my personal keyring, use this disposable temporary file instead.” This keeps the fetch isolated and easy to clean up — we’re not trying to permanently trust this key for GPG operations in general, only to extract it and hand it to apt for one specific repository.--keyserver keyserver.ubuntu.com --recv-keys 4E9275EE6BA2427Fis the actual lookup: “go ask this keyserver for the public key matching this ID, and download it into the keyring I specified.”--export 4E9275EE6BA2427Fthen reads that key back out of the temporary keyring in the binary format apt needs, and pipes it straight into the same keyring file from Step 1 (tee -aappends rather than overwrites, so the original key is preserved alongside this one).- The final line just deletes the temporary keyring files — they’ve done their job once the key is copied into the real one.
A honest caveat worth knowing: pulling a key from a public keyserver by ID alone is standard, widely-used practice across the Debian/Ubuntu ecosystem for third-party repositories — but it is a slightly weaker guarantee than, say, verifying a fingerprint published over HTTPS directly by the project. Keyservers will happily store a key uploaded by anyone, for any name — the ID itself doesn’t prove who controls it. In our case, we already trust the OpenSearch project because we fetched their primary key directly from their own HTTPS domain in Step 1, and this subkey is only being used to complete verification of packages coming from that same official OpenSearch repository. For extra assurance in a stricter environment, you can cross-check the fingerprint of a fetched key against one published in OpenSearch’s own official documentation before trusting it.
Step 2: Add the OpenSearch APT Repository
Why: Ubuntu’s default package repositories don’t carry OpenSearch, so we need to tell apt about an additional one — OpenSearch’s own official repository — and, using signed-by, explicitly tie it to the keyring file we just built. This is the modern, scoped alternative to the old global apt-key approach mentioned earlier: this specific key is only trusted for this specific repository, nothing else.
echo "deb [signed-by=/usr/share/keyrings/opensearch-keyring.gpg] https://artifacts.opensearch.org/releases/bundle/opensearch/3.x/apt stable main" \
| sudo tee /etc/apt/sources.list.d/opensearch-3.x.list
sudo apt-get updateThe first command writes a single line describing the repository into its own file under /etc/apt/sources.list.d/ (each third-party repo typically gets its own file here, rather than being crammed into one shared list — makes it easy to remove later if needed). apt-get update then refreshes apt’s local knowledge of what packages are available, downloading and — this time successfully — verifying that repository’s metadata against the keys we imported.
Confirm the package is visible and see which version you’ll get:
apt-cache policy opensearchStep 3: Set a Real Admin Password Before Installing
Why this step exists: earlier versions of OpenSearch (and Elasticsearch before it) would happily start up with a well-known default login like admin/admin if you didn’t configure anything. Combined with instances left open to the internet (see the security section above), this was a huge contributor to real-world breaches — no hacking skill required, just try the documented default password on any exposed instance. Since version 2.12, OpenSearch’s security plugin refuses to fall back to that default at all. You’re required to set a real password up front, via the OPENSEARCH_INITIAL_ADMIN_PASSWORD environment variable, before the package finishes installing — its post-install script reads this variable and configures the built-in admin account with it.
sudo env OPENSEARCH_INITIAL_ADMIN_PASSWORD='your-strong-password-here' \
apt-get install -y opensearchUse a genuinely strong, unique password here (a long random string from a password manager is ideal) — this account has full administrative access to every index and every document OpenSearch holds.
Notice the use of env VAR=value command rather than sudo -E. sudo -E only preserves environment variables that already exist in your current shell — it won’t inject a brand-new one that wasn’t already exported beforehand, and depending on your system’s sudo configuration it may silently be ignored entirely (you’ll actually see a warning to that effect). It’s an easy trap: the command appears to succeed, but the password was never actually passed through. env VAR=value command instead explicitly sets that variable for the one command being run, regardless of sudo’s environment policy. Afterwards, double-check it actually took effect by looking for this exact line in the install log:
sudo grep "Admin password set successfully" /var/log/opensearch/install_demo_configuration.logIf that line isn’t there, the account may have ended up in a state where you’ll need to reset it manually with OpenSearch’s securityadmin.sh tool before it’s safe to use — don’t skip this check.
Step 4: Check the vm.max_map_count Kernel Setting
Why: under the hood, OpenSearch stores its indexes using Lucene, which relies heavily on memory-mapped files — a technique where a file on disk is mapped directly into a process’s memory space for fast access, rather than being read through slower normal file I/O. Every index segment needs its own memory mapping, and a busy OpenSearch node can easily need hundreds of thousands of them. Linux caps how many memory mappings a single process is allowed to have, and the out-of-the-box default is far too low for this. If it’s too low, OpenSearch will fail one of its built-in “bootstrap checks” and simply refuse to start — a deliberate safety check rather than a crash.
sysctl vm.max_map_countIt needs to be at least 262144. If it’s lower, raise it — and make sure the change survives a reboot by writing it into /etc/sysctl.conf rather than only setting it for the current session:
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
sudo sysctl -pStep 5: Restricting Network Access — the Local-Only Setup, Explained Properly
This is the step that actually determines whether everything in the security section above applies to your server or not, so let’s slow down here.
What does “binding” even mean? When a program like OpenSearch starts a network service, it has to choose which network interface(s) it will accept incoming connections on — this is called the bind address. A server typically has several: a loopback interface (always 127.0.0.1, meaning “this same machine talking to itself”), and one or more real network interfaces with the machine’s actual LAN or public IP address. Whatever address a service binds to determines who is even allowed to attempt a connection — this is decided before any password or permission check ever happens.
0.0.0.0means “accept connections arriving on any network interface this machine has” — including its public IP, if it has one. Many tutorials default to this because it’s the path of least resistance for getting something to “just work” from another machine. It is also exactly what the scanning bots described earlier are searching for.127.0.0.1(the loopback address, akalocalhost) means “only accept connections that originate from this exact same machine.” A connection attempt from anywhere else — another computer on the same office Wi-Fi, let alone the open internet — physically never reaches this interface. It’s not a permissions check that could theoretically be bypassed; the packets simply have nowhere else to go. This is the strongest, simplest guarantee available, and it’s all a single-server setup like this one needs, since whatever application talks to OpenSearch runs on that very same machine.
With that in mind, here’s how we actually configure it. Add these lines to OpenSearch’s main config file:
sudo tee -a /etc/opensearch/opensearch.yml > /dev/null <<'EOF'
network.host: 127.0.0.1
http.port: 9200
discovery.type: single-node
cluster.name: my-opensearch-cluster
node.name: node-1
EOFWhat each setting does:
network.host: 127.0.0.1— the actual lockdown. This is the setting that makes OpenSearch bind only to the loopback interface, as explained above.http.port: 9200— the port your application will connect to (this is the default; stated explicitly here for clarity).discovery.type: single-node— OpenSearch is designed to run as a multi-machine cluster that automatically finds its peers (“discovery”). On a single server, there are no peers to find, so without this setting OpenSearch will sit there waiting to discover other nodes that don’t exist, and never fully initialize. This tells it, explicitly, “you are the only node, don’t wait for company.”cluster.name/node.name— just human-readable labels, useful once you’re looking at logs or multiple environments and need to tell them apart. They have no security implication.
Why do this at the application level if a firewall could also block the port? Because it’s a second, independent layer of protection — not a replacement for a firewall, a complement to it. If the server also runs a firewall like ufw, keep its default policy of denying incoming connections, and simply don’t add any rule that opens port 9200 or 9300. That way, even if someone accidentally changes network.host back to 0.0.0.0 in the future, the firewall still blocks external access. And conversely, even if a firewall rule is ever misconfigured or temporarily disabled for troubleshooting, OpenSearch itself still isn’t listening anywhere reachable. Two independent locks on the same door — a mistake in one doesn’t undo the other. This same reasoning is why we also generated a real admin password in Step 3 even though the network is locked down: defense in depth means no single mistake is enough to cause a breach.
If you ever do have a legitimate need for a second server to reach this OpenSearch instance — say, a separate application server — the right approach is still not to open it to 0.0.0.0. Prefer a private network / VPN between the two machines, or an SSH tunnel, or, at minimum, a firewall rule that allows only that one specific server’s IP address rather than the entire internet.
Step 6: Enable and Start the Service
Why: installing the package doesn’t start it or set it up to run automatically — that’s deliberate, so you have a chance to configure it (as we just did) before it ever accepts connections.
sudo systemctl daemon-reload
sudo systemctl enable opensearch.service
sudo systemctl start opensearch.service
sudo systemctl status opensearch.servicedaemon-reloadtells systemd to re-read unit files, picking up the one the package just installed.startruns it right now, for this session.enableis the one people forget — it’s what makes OpenSearch come back up automatically the next time the server reboots (for a kernel update, a power loss, anything). Without it,startalone means the service is running today but silently won’t be there after the next restart, which usually gets discovered at the worst possible time.
Step 7: Verify It’s Running — and Only Locally
First, confirm it actually responds, using the admin credentials from Step 3:
curl -sk -u admin:'your-strong-password-here' https://127.0.0.1:9200A few things worth explaining in that single line: it’s https, not http, because OpenSearch’s security plugin encrypts traffic by default — even traffic that never leaves the machine, since defense in depth applies to encryption too, not just network access. The -k flag tells curl to skip certificate validation, which is fine and expected here: the certificate is a self-signed “demo” certificate generated automatically at install time, meant only for this kind of local/internal use, not for proving identity to the public internet. You should get back a small JSON response containing the cluster name and OpenSearch version — that’s confirmation the service is up, the password works, and TLS is functioning.
Then, separately, confirm where it’s actually listening:
ss -tlnp | grep -E '9200|9300'This command lists all TCP ports currently listening for connections (-t for TCP, -l for listening sockets only, -n to show raw addresses/ports instead of resolving names, -p to show which process owns each socket), filtered down to just the two ports OpenSearch uses: 9200 for the HTTP API your application talks to, and 9300 for internal node-to-node cluster communication.
Both lines should show 127.0.0.1 as the bound address — not 0.0.0.0 or your server’s public IP. Here’s precisely why that distinction matters, spelled out: if that column instead showed 0.0.0.0, it would mean the socket is listening on every interface, so anyone who can send a packet to your server’s public IP address — meaning literally anyone on the internet — can attempt a TCP connection to port 9200. From that point on, whether they can actually do anything harmful depends entirely on the security plugin and password holding up perfectly with zero misconfiguration, forever. That’s a much weaker position than “the connection can’t even reach the port to begin with,” which is what 127.0.0.1 guarantees. This one line of ss output is, in effect, the final proof that the security work from Step 5 actually took effect — always check it after any config change to OpenSearch, not just the first time.
Real Issue: “No Alive Nodes Found” When an Application Connects
Everything above gets OpenSearch itself installed, secured, and running. But the first time we actually pointed a real application at it — one whose installer validates the search engine connection as part of setup — it failed with an error that had nothing to do with anything covered so far:
Could not validate a connection to the OpenSearch. No alive nodes found in your clusterThis is worth walking through because the error message is misleading — OpenSearch was alive and perfectly healthy the whole time. Here’s what was actually happening, and it comes straight back to two things this guide already covered: TLS and network binding.
The actual cause: the security plugin’s demo installer (Step 3) enables TLS on the HTTP layer by default, which is why every curl example in this guide uses https://127.0.0.1:9200, never plain http://. Port 9200 only speaks TLS — a plain HTTP request to it doesn’t get an error response, it just gets nothing, because there’s no unencrypted listener there to answer. The application’s installer, when only given a bare host like 127.0.0.1 with no scheme, defaults to building a plain http:// connection string. That connection attempt doesn’t fail loudly with something like “wrong protocol” — the underlying search client library just can’t complete a handshake, gives up, and reports it as “no alive nodes,” which sounds like a cluster health problem rather than what it actually is: a protocol mismatch.
We verified this directly before touching any config, which is a good habit whenever a client library’s error message doesn’t quite match reality — go straight to the protocol level and check both possibilities yourself:
curl -s -m 5 http://127.0.0.1:9200 -o /dev/null -w "http status: %{http_code}\n"
curl -sk -m 5 https://127.0.0.1:9200 -o /dev/null -w "https status: %{http_code}\n"The first came back 000 (no response at all — connection refused/reset at the protocol level); the second came back 200. That one comparison confirmed the theory in seconds, before spending any time second-guessing OpenSearch’s own health or config.
The fix, part one — force the client to actually use HTTPS. Most tools that accept a bare “host” setting will build the connection URL themselves, defaulting to plain HTTP unless the scheme is explicit. The fix is simply to give it the scheme up front rather than a bare hostname — in our case, that meant passing https://127.0.0.1 as the host value instead of 127.0.0.1. This generalizes to any application you connect to a TLS-enabled OpenSearch: check whether its “host” setting wants a bare hostname or a full URL, and don’t assume it infers the scheme correctly.
The fix, part two — get past the self-signed certificate. Forcing HTTPS alone isn’t quite enough. The certificate Step 3’s installer generated is self-signed (that’s what made the -k flag necessary in every curl example in this guide — it tells curl to skip verifying who signed the certificate). A browser or a quick manual curl check can get away with skipping that check; a real application’s HTTP client usually verifies certificates properly by default, and it should — that’s not a setting you want to casually switch off, since it’s the thing that stops something else from silently impersonating your OpenSearch endpoint. The correct fix isn’t to tell the application to stop verifying certificates — it’s to make the machine actually trust the certificate that’s already there, by registering OpenSearch’s own CA as a trusted authority system-wide:
sudo cp /etc/opensearch/root-ca.pem /usr/local/share/ca-certificates/opensearch-local-ca.crt
sudo update-ca-certificatesThis copies the CA certificate that Step 3’s demo installer generated (the same file listed in the paths table below) into the system’s trusted-CA directory and rebuilds the OS-wide certificate bundle. From that point on, any properly-behaved HTTPS client on this machine — PHP’s curl extension included — verifies the certificate successfully instead of rejecting it, without disabling verification anywhere. You can confirm it worked with a quick PHP one-liner before touching the application at all:
php -r '
$ch = curl_init("https://127.0.0.1:9200");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, "admin:your-strong-password-here");
$result = curl_exec($ch);
echo $result === false ? "CURL ERROR: " . curl_error($ch) : "SUCCESS";
'A path we deliberately did not take: the quickest-looking “fix” here would have been to just turn the security plugin off entirely (plugins.security.disabled: true), removing TLS and authentication from the equation altogether. On a machine that’s already loopback-only and never exposed to the internet, that’s a tempting shortcut — and it’s exactly the kind of shortcut worth resisting on principle, because “this box happens to be safe today” is a fragile reason to remove a real security control, and the fix above (trusting a certificate you already generated yourself) is barely more work and leaves the security plugin, TLS, and authentication fully intact. When a security feature is inconvenient, the better question is usually “how do I work correctly with this” rather than “how do I turn this off” — the CA-trust approach here is a small, concrete example of choosing the former.
Key Paths to Know
| Path | Purpose |
|---|---|
/etc/opensearch/opensearch.yml | Main configuration file |
/etc/opensearch/root-ca.pem | CA certificate for the self-signed demo TLS certs |
/var/lib/opensearch/ | Index data |
/var/log/opensearch/ | Logs, including the install/demo-cert log |
/etc/opensearch/jvm.options | JVM heap size (-Xms / -Xmx, default 1 GB) |
Quick Security Checklist
Before considering this done, confirm every one of these:
- ☐
network.hostinopensearch.ymlis127.0.0.1, not0.0.0.0or a public IP - ☐
ss -tlnp | grep -E '9200|9300'confirms this in practice, not just in the config file - ☐ The install log confirms
Admin password set successfullywith a real, unique, strong password — never the default - ☐ Any server firewall (
ufwor equivalent) has no rule allowing inbound 9200 or 9300 from outside - ☐
systemctl is-enabled opensearchreportsenabled, so it survives a reboot
With all five checked, OpenSearch is installed, running under its own unprivileged system user, completely unreachable from outside the machine, and set to come back up automatically after a reboot. From here, point your application (a custom search feature, an e-commerce platform, whatever’s consuming it) at https://127.0.0.1:9200 with the admin credentials, and you’re good to go.
Leave a Reply