Skip to content

OpenSearch domains should have logging to CloudWatch Logs enabled

OpenSearch domains process search queries and index operations that can reveal performance bottlenecks, unauthorized access patterns, and configuration errors. Without CloudWatch Logs integration, these signals vanish after the domain's internal buffer rotates, leaving you blind during incident investigation.

Centralizing OpenSearch logs in CloudWatch also enables metric filters, alarms, and cross-account log aggregation. If an attacker modifies cluster settings or exfiltrates data through unusual query patterns, CloudWatch log data provides the forensic trail needed to scope the breach and satisfy regulatory reporting timelines.

Retrofit consideration

Existing domains require a CloudWatch log group and a resource policy granting OpenSearch permission to write to it. If your account already has the maximum number of CloudWatch Logs resource policies (10), you must consolidate policies before enabling log publishing.

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  = "soc2.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  = "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  = "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  = "acscessentialeight.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  = "cfrpart11.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  = "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"
}

module "opensearch" {
  source  = "pcidssv321.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 one or more log_publishing_options blocks. Each block requires a log_type set to one of INDEX_SLOW_LOGS, SEARCH_SLOW_LOGS, ES_APPLICATION_LOGS, or AUDIT_LOGS, and a cloudwatch_log_group_arn pointing to a valid aws_cloudwatch_log_group. The enabled argument must either be omitted (defaults to true) or explicitly set to true. A domain with no log_publishing_options blocks fails, as does one where all blocks have enabled = false.

An aws_cloudwatch_log_resource_policy must also exist, granting the OpenSearch service principal (es.amazonaws.com) permission to call logs:PutLogEvents and logs:CreateLogStream on the target log group. Without it, the domain accepts the configuration but silently fails to deliver logs.

Common pitfalls

  • CloudWatch Logs resource policy missing or too restrictive

    Terraform will successfully apply log_publishing_options on the aws_opensearch_domain, but log delivery silently fails without a matching aws_cloudwatch_log_resource_policy granting logs:PutLogEvents and logs:CreateLogStream to es.amazonaws.com. The control may pass at the configuration level while no logs actually arrive.

  • 10-policy limit on CloudWatch Logs resource policies

    AWS enforces a hard limit of 10 resource policies per region on CloudWatch Logs. With multiple OpenSearch domains or other services needing log delivery, you will need to consolidate policies before you hit the ceiling. Crossing it causes LimitExceededException on aws_cloudwatch_log_resource_policy creation, and there is no in-band warning before you get there.

  • Audit logs require fine-grained access control

    Setting log_type = "AUDIT_LOGS" in log_publishing_options only works if advanced_security_options with enabled = true and internal_user_database_enabled are already configured on the domain. Without fine-grained access control enabled, OpenSearch rejects the audit log configuration at apply time.

  • Legacy elasticsearch_domain resource

    Older Terraform configurations may still use the deprecated aws_elasticsearch_domain resource, which supports log_publishing_options with the same block structure. Migration to aws_opensearch_domain is required for engine versions after Elasticsearch 7.10. The control evaluates the live AWS resource regardless of which Terraform resource type created it, so a passing plan check does not guarantee the resource type is current.

Audit evidence

Config rule evaluation results showing the domain as COMPLIANT confirm that log publishing options are configured. Console screenshots of the domain's "Logs" tab in the OpenSearch Service dashboard, showing enabled log types and their target CloudWatch log group ARNs, confirm the active configuration directly.

For deeper verification, CloudWatch Logs Insights queries showing log events actually flowing confirm active delivery, not just configuration. CloudTrail events for UpdateDomainConfig API calls with LogPublishingOptions in the request parameters establish when logging was enabled and by whom. Where audit logs are enabled, fine-grained access control log entries in CloudWatch complete the picture.

Framework-specific interpretation

SOC 2: CC7.2 asks for monitoring to detect anomalies in system operation. Shipping OpenSearch logs to CloudWatch gives you the continuous visibility and alerting that satisfies CC7.2, and the documented response capability that CC7.3 expects.

PCI DSS v4.0: Requirement 10 covers audit logging across all in-scope system components, including the ability to detect and investigate suspicious activity. For OpenSearch domains touching cardholder data, CloudWatch Logs integration addresses the log review requirements in 10.4.x and the retention requirement in 10.5.1.1.

Tool mappings

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

  • Compliance.tf Control: opensearch_domain_logs_to_cloudwatch

  • AWS Config Managed Rules: ELASTICSEARCH_LOGS_TO_CLOUDWATCH, OPENSEARCH_LOGS_TO_CLOUDWATCH

  • Checkov Checks: CKV_AWS_317, CKV_AWS_84

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

  • Prowler Checks: opensearch_service_domains_audit_logging_enabled, opensearch_service_domains_cloudwatch_logging_enabled

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

  • Trivy Check: AWS-0042

Last reviewed: 2026-03-09