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
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_options to an existing aws_elasticsearch_domain and Terraform will destroy and recreate it. Data goes with it unless you snapshot first. There's no in-place migration path. Define vpc_options before the resource is ever applied.
Subnet count must match AZ requirements
With zone_awareness_enabled = true, each Availability Zone in availability_zone_count needs its own subnet ID. Pass a single subnet and domain creation fails immediately. The number of entries in subnet_ids must match the availability_zone_count value in the zone_awareness_config block.
Service-linked role must exist before VPC domain creation
Domain creation fails if the AWSServiceRoleForAmazonElasticsearchService service-linked role doesn't exist in the account. Elasticsearch needs it to provision ENIs in the VPC. Add aws_iam_service_linked_role with aws_service_name = "es.amazonaws.com" to the module and declare depends_on on the domain resource.
Confusing legacy resource name with OpenSearch
When migrating from aws_elasticsearch_domain to aws_opensearch_domain, the vpc_options block 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_vpc - AWS Config Managed Rule:
ELASTICSEARCH_IN_VPC_ONLY - Checkov Check:
CKV_AWS_137 - Powerpipe Control:
aws_compliance.control.es_domain_in_vpc - AWS Security Hub Control:
ES.2
Last reviewed: 2026-03-09