ES domains should be in a VPC
A public Elasticsearch endpoint exposes the search API and Kibana dashboard to the internet, where only IAM and resource-based policies stand between it and unauthorized access. VPC deployment adds network-layer isolation, limiting reachability to sources inside the VPC, peered VPCs, or on-premises networks over VPN or Direct Connect. Elasticsearch clusters commonly hold sensitive aggregated log and application data; a misconfigured access policy on a public domain leaks that data with nothing to catch it at the network layer.
VPC placement also unlocks security groups for port-level traffic control and VPC Flow Logs for traffic auditing. Neither is available on public domains.
Retrofit consideration
AWS does not support migrating an existing public Elasticsearch domain into a VPC in place. You must create a new VPC-enabled domain, snapshot the indices from the old domain, restore them to the new one, update all client endpoints, then delete the original. Plan for downtime or a dual-write period.
Implementation
Choose the approach that matches how you manage Terraform.
Use AWS provider resources directly. See docs for the resources involved: aws_elasticsearch_domain.
resource "aws_elasticsearch_domain" "this" {
advanced_security_options {
enabled = true
internal_user_database_enabled = true
master_user_options {
master_user_name = "admin"
master_user_password = "ChangeMe123!"
}
}
cluster_config {
dedicated_master_count = 3
dedicated_master_enabled = true
dedicated_master_type = "m5.large.elasticsearch"
instance_count = 3
instance_type = "m5.large.elasticsearch"
zone_awareness_config {
availability_zone_count = 3
}
zone_awareness_enabled = true
}
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"
}
elasticsearch_version = "7.10"
encrypt_at_rest {
enabled = true
}
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", "subnet-12345678", "subnet-12345678"]
}
}
What this control checks
The control checks that the Elasticsearch domain resource includes a vpc_options block with at least one entry in subnet_ids. Both aws_elasticsearch_domain and aws_opensearch_domain support this block; the check applies to both. A domain without vpc_options, or with an empty subnet_ids list, gets a public endpoint and fails. For multi-AZ deployments, subnet_ids must span at least two Availability Zones to match availability_zone_count in the zone_awareness_config block. The security_group_ids argument is not evaluated by this control, but omitting it leaves the domain on the VPC's default security group, which typically allows all inbound traffic.
Common pitfalls
VPC placement is immutable after creation
Try to add
vpc_optionsto an existingaws_elasticsearch_domainand Terraform will destroy and recreate it. Data goes with it unless you snapshot first. There's no in-place migration path. Definevpc_optionsbefore the resource is ever applied.Subnet count must match AZ requirements
With
zone_awareness_enabled = true, each Availability Zone inavailability_zone_countneeds its own subnet ID. Pass a single subnet and domain creation fails immediately. The number of entries insubnet_idsmust match theavailability_zone_countvalue in thezone_awareness_configblock.Service-linked role must exist before VPC domain creation
Domain creation fails if the
AWSServiceRoleForAmazonElasticsearchServiceservice-linked role doesn't exist in the account. Elasticsearch needs it to provision ENIs in the VPC. Addaws_iam_service_linked_rolewithaws_service_name = "es.amazonaws.com"to the module and declaredepends_onon the domain resource.Confusing legacy resource name with OpenSearch
When migrating from
aws_elasticsearch_domaintoaws_opensearch_domain, thevpc_optionsblock doesn't carry itself over. The argument names are identical across both resources, which makes it easy to miss during a refactor. Audit the new resource block explicitly before applying.
Audit evidence
The managed Config rule elasticsearch-in-vpc-only is the primary evidence artifact: it returns COMPLIANT for domains with a VPC endpoint and NON_COMPLIANT for public ones. Console screenshots of the domain's VPC configuration panel (VPC ID, subnet IDs, security group IDs) work as point-in-time evidence.
For continuous coverage over the audit period, export Security Hub findings or a Config conformance pack filtered to this rule, showing a sustained compliant state. CloudTrail CreateElasticsearchDomain and UpdateElasticsearchDomainConfig events both carry the VPCOptions parameter, documenting that VPC placement was configured at creation or update time.
Framework-specific interpretation
SOC 2: CC6.1 and CC6.6 ask for logical access controls and protection from threats outside system boundaries. VPC deployment addresses the network layer of CC6.6 and restricts access paths per CC6.1, though IAM and domain access policies still need to cover the identity layer.
PCI DSS v4.0: Requirement 1 calls for network security controls between trusted and untrusted zones. For environments storing cardholder data in Elasticsearch, VPC placement with security groups is the practical implementation. A public endpoint backed only by IAM policies won't satisfy an examiner asking for network segmentation evidence.
HIPAA Omnibus Rule 2013: Elasticsearch clusters indexing ePHI need to be off the public internet. That's the practical implication of 45 CFR 164.312(a)(1) (access control) and 164.312(e)(1) (transmission security). VPC deployment limits reachability to authorized network paths and is one way to satisfy both safeguards.
NIST SP 800-53 Rev 5: Security groups and NACLs are the managed interfaces SC-7 and AC-4 call for. Both controls require that communications at external and internal boundaries pass through controlled interfaces. VPC deployment is what gives you those interfaces on an Elasticsearch domain.
FedRAMP Moderate Baseline Rev 4: SC-7 (Boundary Protection) is the applicable control. FedRAMP Moderate requires federal system components to sit behind managed network boundaries, not exposed to the public internet. A VPC-deployed domain satisfies that requirement; a public endpoint does not.
Related controls
Tool mappings
Use these identifiers to cross-reference this control across tools, reports, and evidence.
Compliance.tf Control:
es_domain_in_vpcAWS Config Managed Rule:
ELASTICSEARCH_IN_VPC_ONLYCheckov Check:
CKV_AWS_137Powerpipe Control:
aws_compliance.control.es_domain_in_vpcAWS Security Hub Control:
ES.2
Last reviewed: 2026-03-09