Skip to content

Elasticsearch domain error logging to CloudWatch Logs should be enabled

Elasticsearch error logs carry diagnostic information that's difficult to reconstruct after the fact: index mapping conflicts, deprecated API warnings, query failures, and JVM garbage collection issues. Without routing these to CloudWatch Logs, operators have no durable, searchable record of domain-level errors and are left querying cluster health APIs after something has already gone wrong.

CloudWatch integration also unlocks metric filters and alarms on error patterns, so teams can catch misconfigurations or degraded nodes before they cascade into data loss or an availability incident.

Retrofit consideration

Enabling log publishing on an existing domain does not cause downtime, but the target CloudWatch log group must already exist with a resource policy granting es.amazonaws.com the actions logs:PutLogEvents, logs:CreateLogStream, and logs:CreateLogGroup. Terraform will apply cleanly without it, but no logs will flow.

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  = "nistcsf.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 for a log_publishing_options block on aws_elasticsearch_domain or aws_opensearch_domain with log_type = "ES_APPLICATION_LOGS" and cloudwatch_log_group_arn set to a valid log group ARN. Setting enabled = false within that block fails, as does omitting the block entirely. A companion aws_cloudwatch_log_resource_policy must grant es.amazonaws.com write access to the target log group; without it, the domain appears configured in Terraform but produces no log data.

Common pitfalls

  • Missing CloudWatch Logs resource policy

    Log delivery silently fails if no aws_cloudwatch_log_resource_policy grants es.amazonaws.com the required actions on the target log group: logs:PutLogEvents, logs:CreateLogStream, and logs:CreateLogGroup. The domain shows as configured in Terraform but produces no log data. Manage the resource policy in the same module as the domain so the dependency is explicit.

  • Confusing log_type values

    There are four valid log_type values: INDEX_SLOW_LOGS, SEARCH_SLOW_LOGS, ES_APPLICATION_LOGS, and AUDIT_LOGS. This control requires ES_APPLICATION_LOGS specifically. Configuring only slow log types won't pass the check, even if multiple log_publishing_options blocks are present.

  • Using aws_opensearch_domain vs aws_elasticsearch_domain

    After migrating to aws_opensearch_domain, the log_publishing_options block arguments are identical, but double-check the service principal in the CloudWatch Logs resource policy. In most regions it should still be es.amazonaws.com, not opensearchservice.amazonaws.com. Verify against your region's service endpoint before applying.

  • Explicitly setting enabled to false

    Setting enabled = false in a log_publishing_options block fails this control even when cloudwatch_log_group_arn is present. Terraform won't error, but log delivery is off and nothing in the plan output will flag it as a problem. Always set enabled = true explicitly rather than relying on the default.

Audit evidence

An auditor expects the AWS Config rule elasticsearch-logs-to-cloudwatch (or equivalent Security Hub check) evaluating each Elasticsearch domain as COMPLIANT. A populated log group with recent ES application entries confirms the pipeline is active. The aws es describe-elasticsearch-domain-config output should show LogPublishingOptions with an ES_APPLICATION_LOGS key, Enabled: true, and a valid CloudWatchLogsLogGroupArn.

For deeper assurance, auditors may also request the CloudWatch Logs resource policy confirming es.amazonaws.com write access, and a sample Logs Insights query showing error events are actually flowing.

Framework-specific interpretation

NIST Cybersecurity Framework v2.0: DE.CM (Continuous Monitoring) and DE.AE (Adverse Event Analysis) both call for sustained visibility into events that could affect system integrity or availability. Routing Elasticsearch error logs to CloudWatch covers that directly: operators get a queryable record of index errors, deprecated API usage, and JVM issues, and can build metric filters to alert on anomalous patterns before they escalate.

Tool mappings

Use these identifiers to cross-reference this control across tools, reports, and evidence.

  • Compliance.tf Control: es_domain_error_logging_enabled

  • AWS Config Managed Rule: ELASTICSEARCH_LOGS_TO_CLOUDWATCH

  • Checkov Check: CKV_AWS_84

  • Powerpipe Controls: aws_compliance.control.es_domain_error_logging_enabled, aws_compliance.control.es_domain_logs_to_cloudwatch

  • AWS Security Hub Controls: ES.4, Opensearch.4

  • KICS Query: acb6b4e2-a086-4f35-aefd-4db6ea51ada2

  • Trivy Check: AWS-0042

Last reviewed: 2026-03-09