OpenSearch domains should be in a VPC
An OpenSearch domain outside a VPC gets a public endpoint reachable from the internet. Even with restrictive access policies, that exposure is real: IP-based policies can be misconfigured, and a single policy change can expose sensitive index data. VPC placement adds a network-layer boundary that limits connectivity to resources within the VPC or connected networks, which shrinks the blast radius of any policy mistake.
Public OpenSearch endpoints also show up in DNS enumeration and internet scanning tools, making them visible targets. Note that VPC placement alone doesn't guarantee private access. This control checks VPC membership, not route-level reachability.
Retrofit consideration
Migrating an existing public OpenSearch domain into a VPC requires creating a new domain, reindexing all data, and updating all client connection strings. AWS does not support in-place migration from public to VPC endpoints. Plan for downtime or a blue-green cutover.
Implementation
Choose the approach that matches how you manage Terraform.
Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.
module "opensearch" {
source = "pcidss.compliance.tf/terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
module "opensearch" {
source = "hipaa.compliance.tf/terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
module "opensearch" {
source = "nist800171.compliance.tf/terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
module "opensearch" {
source = "nydfs23.compliance.tf/terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
module "opensearch" {
source = "rbiitfnbfc.compliance.tf/terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
module "opensearch" {
source = "hipaasecurity2003.compliance.tf/terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
module "opensearch" {
source = "nistcsfv11.compliance.tf/terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
If you use terraform-aws-modules/opensearch/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.
module "opensearch" {
source = "terraform-aws-modules/opensearch/aws"
version = ">=2.0.0,<3.0.0"
advanced_security_options = {
enabled = true
internal_user_database_enabled = true
master_user_options = {
master_user_name = "admin"
master_user_password = "P0fix-Test-2026!"
}
}
auto_tune_options = {
desired_state = "DISABLED"
}
cluster_config = {
dedicated_master_enabled = false
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
domain_endpoint_options = {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "abc123"
ebs_options = {
ebs_enabled = true
volume_size = 20
volume_type = "gp3"
}
engine_version = "OpenSearch_2.11"
}
Use AWS provider resources directly. See docs for the resources involved: aws_opensearch_domain.
resource "aws_opensearch_domain" "this" {
advanced_security_options {
enabled = true
internal_user_database_enabled = true
master_user_options {
master_user_name = "admin"
master_user_password = "ChangeMe123!"
}
}
auto_tune_options {
desired_state = "DISABLED"
}
cluster_config {
instance_count = 1
instance_type = "t3.small.search"
zone_awareness_enabled = false
}
cognito_options {
enabled = true
identity_pool_id = "us-east-1:12345678-1234-1234-1234-123456789012"
role_arn = "arn:aws:iam::123456789012:role/example-role"
user_pool_id = "us-east-1_AbCdEfGhI"
}
domain_endpoint_options {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
domain_name = "pofix-abc123"
ebs_options {
ebs_enabled = true
volume_size = 10
volume_type = "gp3"
}
encrypt_at_rest {
enabled = true
}
engine_version = "OpenSearch_2.11"
log_publishing_options {
cloudwatch_log_group_arn = local.es_log_group_arn
log_type = "AUDIT_LOGS"
}
log_publishing_options {
cloudwatch_log_group_arn = local.es_log_group_arn
log_type = "ES_APPLICATION_LOGS"
}
log_publishing_options {
cloudwatch_log_group_arn = local.es_log_group_arn
log_type = "SEARCH_SLOW_LOGS"
}
log_publishing_options {
cloudwatch_log_group_arn = local.es_log_group_arn
log_type = "INDEX_SLOW_LOGS"
}
node_to_node_encryption {
enabled = true
}
vpc_options {
security_group_ids = ["sg-12345678"]
subnet_ids = ["subnet-12345678"]
}
}
What this control checks
The control checks that the aws_opensearch_domain resource includes a vpc_options block with at least one entry in subnet_ids. Omitting vpc_options entirely means the domain gets a public endpoint and fails. To pass, define a vpc_options block with subnet_ids pointing into your VPC and security_group_ids referencing groups that restrict inbound access. The security_group_ids argument is technically optional, but skipping it leaves the domain on the VPC default security group, which is rarely appropriate. When zone awareness is enabled, supply subnets matching the configured AZ count (typically two or three).
Common pitfalls
VPC placement does not guarantee private access
A domain in a VPC can still be reachable from the internet if the associated subnets have routes to an internet gateway and the security groups allow
0.0.0.0/0inbound. This control only checks VPC membership, not subnet routing or security group rules. Pair VPC placement with private subnets and restrictiveaws_security_groupingress rules.Subnet count must match Availability Zone count
If
zone_awareness_enabledis true in thecluster_configblock, you must supply subnets in two or three AZs depending onavailability_zone_count. Providing a single subnet with zone awareness enabled causes a hard deployment error, not a soft failure. Terraform will surface this as an API error on apply.Cannot convert public domain to VPC in place
AWS does not support switching an existing public OpenSearch domain to VPC mode via
UpdateDomainConfig. Terraform will require resource replacement, which means data loss without a migration plan. Take manual snapshots before you start, or build an automated snapshot strategy into the cutover runbook.VPC endpoint limits per domain
Each OpenSearch domain in a VPC provisions an ENI in each specified subnet. Those ENIs consume private IP addresses and count against the per-subnet ENI limit. Large clusters in small subnets (
/28) can exhaust available IPs, causing node provisioning failures that are easy to misread as an OpenSearch issue rather than a networking one.
Audit evidence
Auditors will look for AWS Config results for the opensearch-in-vpc-only managed rule showing compliant evaluations across all in-scope OpenSearch domains. Console screenshots of each domain's "Networking" tab showing VPC, subnet, and security group assignments work as supporting point-in-time evidence. CloudTrail CreateDomain and UpdateDomainConfig events confirm that VPCOptions was specified at creation and has not since been removed.
For continuous compliance, a Security Hub report showing the opensearch_domain_in_vpc check passing across all accounts and regions rounds out the evidence package.
Framework-specific interpretation
PCI DSS v4.0: Requirement 1 calls for network controls that isolate cardholder data environment components from untrusted networks. A public OpenSearch endpoint means your CDE has an internet-facing component, which fails that isolation test. VPC placement helps meet Requirement 1, but it doesn't stand alone. Security groups and NACLs need to restrict traffic further before you can claim full coverage.
HIPAA Omnibus Rule 2013: 45 CFR 164.312 requires access controls and transmission security for ePHI. OpenSearch indexes can hold ePHI directly or serve queries against it, so the domain needs the same network boundary you'd apply to any other ePHI datastore. VPC placement is one way to satisfy that boundary requirement under the access control and transmission security provisions.
Related controls
OpenSearch domains should have fine-grained access control enabled
Elasticsearch domain node-to-node encryption should be enabled
Redshift Serverless workgroups should prohibit public access
ElastiCache clusters should not use the default subnet group
Tool mappings
Use these identifiers to cross-reference this control across tools, reports, and evidence.
Compliance.tf Control:
opensearch_domain_in_vpcAWS Config Managed Rules:
ELASTICSEARCH_IN_VPC_ONLY,OPENSEARCH_IN_VPC_ONLYCheckov Check:
CKV_AWS_137Powerpipe Control:
aws_compliance.control.opensearch_domain_in_vpcProwler Check:
opensearch_service_domains_not_publicly_accessibleAWS Security Hub Control:
Opensearch.2
Last reviewed: 2026-03-09