---
title: Security group not used
description: Datadog, the leading service for cloud-scale monitoring.
breadcrumbs: >-
  Docs > Datadog Security > Code Security > Infrastructure as Code (IaC)
  Security > IaC Security Rules > Security group not used
---

> For the complete documentation index, see [llms.txt](https://docs.datadoghq.com/llms.txt).

# Security group not used

{% callout %}
# Important note for users on the following Datadog sites: app.ddog-gov.com, us2.ddog-gov.com

{% alert level="danger" %}
This product is not supported for your selected [Datadog site](https://docs.datadoghq.com/getting_started/site.md). ({% placeholder "user-datadog-site-name" /%}).
{% /alert %}

{% /callout %}

## Metadata{% #metadata %}

**Id:** `terraform-aws-security-groups-not-used` 

**Provider:** AWS

**Platform:** Terraform

**Severity:** Low

**Category:** Access Control

#### Learn More{% #learn-more %}

- [Provider Reference](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group)

### Description{% #description %}

This check ensures that AWS load balancers are associated with appropriate security groups, which control network traffic to and from the resource. When the `security_groups` attribute is omitted from an `aws_lb` resource, as shown below, the load balancer may become exposed to unrestricted network access, increasing the risk of unauthorized access or attacks:

```gdscript3
resource "aws_lb" "test" {
  name = "test"
  load_balancer_type = "application"
  subnets = [aws_subnet.subnet1.id, aws_subnet.subnet2.id]
  internal = true
}
```

By explicitly defining `security_groups`, you can restrict inbound and outbound traffic to only trusted sources:

```gdscript3
resource "aws_lb" "test" {
  name = "test"
  load_balancer_type = "application"
  subnets = [aws_subnet.subnet1.id, aws_subnet.subnet2.id]
  internal = true
  security_groups = [aws_security_group.allow_tls.id]
}
```

## Compliant Code Examples{% #compliant-code-examples %}

```terraform
resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  description = "Allow TLS inbound traffic"
  vpc_id      = aws_vpc.main.id

  ingress {
    description      = "TLS from VPC"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = [aws_vpc.main.cidr_block]
    ipv6_cidr_blocks = [aws_vpc.main.ipv6_cidr_block]
  }

  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  tags = {
    Name = "allow_tls"
  }
}

resource "aws_lb" "test" {
  name = "test"
  load_balancer_type = "application"
  subnets = [aws_subnet.subnet1.id, aws_subnet.subnet2.id]
  internal = true
  security_groups = [aws_security_group.allow_tls.id]
}
```

```terraform
# orphan lives inside a child module and is attached there too, via an
# aws_security_group_rule in the SAME module - the module-relative
# reference (aws_security_group.orphan, never "module.child.aws_security
# _group.orphan") must be qualified with the child module's own scope
# before it can match orphan's flattened resource key
# ("module.child.orphan") in plan-JSON. See query.rego's
# reference_targets_resource-based fresh-create is_used() body.
module "child" {
  source = "./child"
}
```

```terraform
# shared is declared inside module "parent" and used only as an input
# argument to a SECOND module nested one level inside "parent" - never
# referenced by any resource directly. The reference itself
# ("aws_security_group.shared") is relative to "parent"'s own scope, not
# the nested module's, and "parent"'s own module_calls entry for the
# nested module is only reachable via
# root_module.module_calls["parent"].module.module_calls["nested"], not
# root_module.module_calls directly.
module "parent" {
  source = "./parent"
}
```

## Non-Compliant Code Examples{% #non-compliant-code-examples %}

```terraform
resource "aws_lb" "test" {
  name = "test"
  load_balancer_type = "application"
  subnets = [aws_subnet.subnet1.id, aws_subnet.subnet2.id]
  internal = true
}

resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  description = "Allow TLS inbound traffic"
  vpc_id      = aws_vpc.main.id

  ingress {
    description      = "TLS from VPC"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = [aws_vpc.main.cidr_block]
    ipv6_cidr_blocks = [aws_vpc.main.ipv6_cidr_block]
  }

  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  tags = {
    Name = "allow_tls"
  }
}
```

```terraform
provider "aws" {
  region                      = "us-east-1"
  access_key                  = "fake"
  secret_key                  = "fake"
  skip_credentials_validation = true
  skip_requesting_account_id  = true
  skip_metadata_api_check     = true
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "attached" {
  name   = "attached"
  vpc_id = aws_vpc.main.id
}

# orphan is never actually attached to anything - it is only mentioned in
# this single ingress block's description text, alongside a real
# attachment (via security_groups) to a different, unrelated security
# group. Both the description and security_groups arguments are unknown
# pre-apply, so after_unknown alone cannot say which of the two
# references in the block's flat references list came from
# security_groups and which came from description.
resource "aws_security_group" "orphan" {
  name   = "orphan"
  vpc_id = aws_vpc.main.id
}

resource "aws_security_group" "user" {
  name   = "user"
  vpc_id = aws_vpc.main.id

  ingress {
    description     = "note: orphan is ${aws_security_group.orphan.id}"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    security_groups  = [aws_security_group.attached.id]
  }
}
```

```terraform
provider "aws" {
  region                      = "us-east-1"
  access_key                  = "fake"
  secret_key                  = "fake"
  skip_credentials_validation = true
  skip_requesting_account_id  = true
  skip_metadata_api_check     = true
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "attached" {
  name   = "attached"
  vpc_id = aws_vpc.main.id
}

# orphan_cidr is never actually attached to anything - it is only
# referenced from an unrelated attribute (cidr_blocks) whose after_unknown
# shape is a per-element array ([true]) rather than a bare true. That
# per-element-array field must still count as "unknown" for the ambiguity
# check, alongside security_groups (a real attachment to a different
# security group): both are unresolved pre-apply, so after_unknown alone
# cannot say which of the two references in the block's flat references
# list came from security_groups and which came from cidr_blocks.
resource "aws_security_group" "orphan_cidr" {
  name   = "orphan_cidr"
  vpc_id = aws_vpc.main.id
}

resource "aws_security_group" "user" {
  name   = "user"
  vpc_id = aws_vpc.main.id

  ingress {
    cidr_blocks     = [aws_security_group.orphan_cidr.id]
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.attached.id]
  }
}
```
