Skip to content

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_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.

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