This check flags Elasticsearch or OpenSearch domains that run with a public endpoint instead of inside a VPC. A public domain is exposed to the internet and relies entirely on access policies for protection, which is fragile. The fix is to launch the domain inside a VPC, and since VPC placement cannot be changed after creation, you usually create a new domain and migrate.
Amazon OpenSearch Service (formerly Elasticsearch Service) gives you two networking models when you create a domain: public access or VPC access. The elasticsearch_novpc check catches domains running with the public option. On paper a public domain is convenient, you get an endpoint, you point your app at it, done. In practice it puts a search cluster that often holds logs, customer records, and application data directly on the public internet, guarded by nothing but an access policy that is easy to get wrong.
This post walks through what the check looks at, why a public OpenSearch domain is a real risk, and how to move to VPC access without losing data.
What this check detects
The check inspects each OpenSearch/Elasticsearch domain in your account and looks at its VPCOptions configuration. A domain placed in a VPC has subnet and security group IDs attached. A public domain has none, and instead exposes a resolvable endpoint reachable from anywhere on the internet.
You can spot the difference quickly from the CLI:
aws opensearch describe-domain \
--domain-name my-domain \
--query 'DomainStatus.VPCOptions'
If the result is null or empty, the domain is public and this check will fail. A domain inside a VPC returns something like:
{
"VPCId": "vpc-0a1b2c3d4e5f67890",
"SubnetIds": ["subnet-0123456789abcdef0", "subnet-0fedcba9876543210"],
"AvailabilityZones": ["us-east-1a", "us-east-1b"],
"SecurityGroupIds": ["sg-0abc123def4567890"]
}
Note: Public versus VPC is decided at creation time and is permanent for the life of the domain. There is no API call to flip an existing public domain into a VPC. This single fact shapes the entire remediation, which is why it comes up repeatedly below.
Why it matters
A public OpenSearch endpoint is internet-facing by definition. The only thing standing between an attacker and your data is the domain access policy, and access policies for OpenSearch are notoriously easy to misconfigure. A single overly broad principal turns a private cluster into an open one.
Here is an access policy that looks intentional but is wide open:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "*" },
"Action": "es:*",
"Resource": "arn:aws:es:us-east-1:111122223333:domain/my-domain/*"
}
]
}
With a public domain and a policy like this, anyone who discovers the endpoint can read and write every index. OpenSearch and Elasticsearch clusters have been a recurring source of large breaches precisely because they end up exposed without strong authentication. Search clusters tend to accumulate sensitive data: application logs full of tokens and PII, copies of database records for fast querying, and analytics events. Losing the contents of a single index can mean disclosing millions of records.
The risks with a public domain break down into a few concrete scenarios:
- Policy drift to wildcard. A developer debugging a connectivity issue widens the policy to
"AWS": "*"to make things work, then forgets to revert it. - Credential exposure. If you rely on IAM-signed requests but an access key leaks, the public endpoint is reachable from anywhere the key is used.
- No network choke point. Because the endpoint resolves to public IPs, you cannot use security groups or NACLs as a backstop. There is no second layer.
- Scanning and enumeration. Public OpenSearch endpoints are actively scanned. An open cluster will be found, often within hours.
VPC placement removes the entire class of "exposed to the internet" problems. The domain endpoint resolves to private IPs inside your VPC, and reaching it requires being on the network or going through a controlled path like a VPN, Direct Connect, or a bastion. Access policy mistakes still matter, but they are no longer a direct path from the internet to your data.
How to fix it
Because you cannot convert a public domain to a VPC domain in place, remediation means creating a VPC-backed domain and migrating data into it. The good news is the process is well-trodden and OpenSearch has snapshot tooling built in.
Step 1: Create a new domain inside your VPC
Pick at least two private subnets in different Availability Zones for resilience, and a security group that allows traffic only from your application tier.
aws opensearch create-domain \
--domain-name my-domain-vpc \
--engine-version "OpenSearch_2.11" \
--cluster-config InstanceType=r6g.large.search,InstanceCount=2,ZoneAwarenessEnabled=true \
--ebs-options EBSEnabled=true,VolumeType=gp3,VolumeSize=100 \
--vpc-options SubnetIds=subnet-0123456789abcdef0,subnet-0fedcba9876543210,SecurityGroupIds=sg-0abc123def4567890 \
--node-to-node-encryption-options Enabled=true \
--encryption-at-rest-options Enabled=true \
--domain-endpoint-options EnforceHTTPS=true
Note: When zone awareness is enabled, your subnet count should match your AZ count and your instance count should be even, so shards distribute cleanly across zones.
Step 2: Configure a least-privilege security group
The security group on the domain is your network gate. Allow HTTPS only from the application security group, not from CIDR ranges that span the office or the whole VPC.
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc123def4567890 \
--protocol tcp \
--port 443 \
--source-group sg-0appserver12345678
Step 3: Snapshot the old domain and restore into the new one
Register a manual snapshot repository backed by S3 on the old domain, take a snapshot, then register the same repository on the new domain and restore. Both domains need an IAM role that can write to and read from the S3 bucket.
# On the source domain, register the repo (run against the OpenSearch API)
curl -XPUT "https://OLD-DOMAIN-ENDPOINT/_snapshot/migration-repo" \
-H "Content-Type: application/json" \
-d '{
"type": "s3",
"settings": {
"bucket": "my-opensearch-snapshots",
"region": "us-east-1",
"role_arn": "arn:aws:iam::111122223333:role/OpenSearchSnapshotRole"
}
}'
# Take a snapshot of all indices
curl -XPUT "https://OLD-DOMAIN-ENDPOINT/_snapshot/migration-repo/snap-1?wait_for_completion=true"
Register the same repository on the new VPC domain and restore. Since the new domain is private, run these requests from an instance inside the VPC or through a tunnel.
# On the new VPC domain
curl -XPUT "https://NEW-VPC-ENDPOINT/_snapshot/migration-repo" \
-H "Content-Type: application/json" \
-d '{
"type": "s3",
"settings": {
"bucket": "my-opensearch-snapshots",
"region": "us-east-1",
"role_arn": "arn:aws:iam::111122223333:role/OpenSearchSnapshotRole"
}
}'
curl -XPOST "https://NEW-VPC-ENDPOINT/_snapshot/migration-repo/snap-1/_restore"
Warning: Running two domains in parallel during migration doubles your OpenSearch cost for the cutover window. Plan the migration to be short, and tear down the old domain as soon as you have verified data integrity and repointed your applications.
Step 4: Repoint applications and verify
Update your application configuration and any connection strings to use the new VPC endpoint, deploy, and confirm reads and writes work. Compare document counts on a sample of indices before you trust the cutover:
curl -s "https://NEW-VPC-ENDPOINT/_cat/indices?v&h=index,docs.count"
Step 5: Delete the public domain
Danger: Deleting a domain permanently destroys it and all its data. Confirm your new domain is serving production traffic and that you have a verified snapshot in S3 before running this. There is no undo.
aws opensearch delete-domain --domain-name my-domain
How to prevent it from happening again
Manual fixes do not stick. The reliable way to keep domains in a VPC is to make public domains impossible to create in the first place, using infrastructure as code and policy gates.
Define domains in Terraform with VPC options always set
resource "aws_opensearch_domain" "main" {
domain_name = "my-domain-vpc"
engine_version = "OpenSearch_2.11"
cluster_config {
instance_type = "r6g.large.search"
instance_count = 2
zone_awareness_enabled = true
}
vpc_options {
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
security_group_ids = [aws_security_group.opensearch.id]
}
encrypt_at_rest {
enabled = true
}
node_to_node_encryption {
enabled = true
}
domain_endpoint_options {
enforce_https = true
}
}
Block public domains in CI with policy-as-code
A Checkov or OPA gate in your pipeline can fail any plan that defines an OpenSearch domain without vpc_options. Here is a Conftest/OPA rule for Terraform plan JSON:
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_opensearch_domain"
not resource.change.after.vpc_options
msg := sprintf("OpenSearch domain '%s' must be deployed in a VPC", [resource.address])
}
Tip: Pair the CI gate with a Service Control Policy at the AWS Organizations level that denies es:CreateDomain and es:CreateElasticsearchDomain unless the request includes VPC options. The SCP catches anything that bypasses your pipeline, including console clicks and one-off scripts.
Run the Lensix check continuously
IaC and SCPs cover new resources, but legacy domains and out-of-band changes still slip through. Keeping the elasticsearch_novpc check running on a schedule means a public domain gets flagged within minutes of appearing, rather than surfacing in an annual audit.
Best practices
VPC placement is the headline, but a well-secured OpenSearch domain layers several controls together. Treat the following as the baseline for any new domain:
- Always deploy in a VPC with private subnets across at least two Availability Zones.
- Scope the security group tightly so only the application tier can reach port 443, using source security group references rather than broad CIDRs.
- Enable encryption at rest and node-to-node encryption at creation. These also cannot be retrofitted easily, so set them from day one.
- Enforce HTTPS and require a modern TLS policy on the domain endpoint.
- Use fine-grained access control with IAM and, where needed, internal user roles, instead of relying on a single resource-based access policy.
- Avoid wildcard principals. If you ever see
"AWS": "*"in an access policy, treat it as an incident until proven otherwise. - Take regular automated snapshots to S3 so migrations and recovery are routine rather than risky.
- Log access via OpenSearch audit logs to CloudWatch so you have a record of who queried what.
None of these are exotic. The cost of getting them right is a few extra lines in your Terraform module and a CI gate. The cost of getting them wrong is a search cluster full of customer data sitting on the open internet. Put domains in a VPC, gate it in the pipeline, and let a continuous check catch the strays.

