AWS Network ACL Restricts SSH


Description

SSH access should only be granted from protected network CIDR ranges.

Query · python

import ipaddress

GLOBAL_IPV6 = ipaddress.IPv6Network("::/0")
# Choose an arbitrary sentinel value that isn't equivalent to the GLOBAL_IPV6 value.
IPV6_SENTINEL = ipaddress.IPv6Network("::1/128")


def policy(resource):
    # Enumerate the entries in the network ACL, in evaluation order.
    ingress_entries = sorted(
        (entry for entry in resource["Entries"] if not entry["Egress"]),
        key=lambda x: x["RuleNumber"],
    )
    for entry in ingress_entries:
        # Look for SSH ingress rules from wildcard IPs.
        if (
            entry.get("CidrBlock") == "0.0.0.0/0"
            # Handle non-standard representations like `"0::/0"`.
            or ipaddress.IPv6Network(entry.get("Ipv6CidrBlock") or IPV6_SENTINEL) == GLOBAL_IPV6
        ) and (
            not entry.get("PortRange")
            or entry["PortRange"]["From"] <= 22 <= entry["PortRange"]["To"]
        ):
            # If this is a deny rule, then the ACL has an explicit deny rule with a lower (more
            # important) precedence than any rule that would allow SSH from arbitrary IPs. If it's
            # an allow rule, then the opposite is true. Either way, this rule determines the
            # entire outcome of the policy evaluation.
            #
            # Another way to read this: pass the policy check if the SSH rule here is a deny.
            return entry["RuleAction"] == "deny"

    # Found no SSH ingress rules from wildcard IPs.
    return True

Analyst notes

Remove the NACL rule granting unprotected SSH access.

Raw source AWS Network ACL Restricts SSH · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: policy
Filename: aws_network_acl_restricted_ssh.py
PolicyID: "AWS.NetworkACL.RestrictedSSH"
DisplayName: "AWS Network ACL Restricts SSH"
Enabled: true
ResourceTypes:
  - AWS.EC2.NetworkACL
Tags:
  - AWS
  - Panther
  - Lateral Movement:Remote Services
Reports:
  MITRE ATT&CK:
    - TA0008:T1021
Severity: High
Description: >
  SSH access should only be granted from protected network CIDR ranges.
Runbook: >
  Remove the NACL rule granting unprotected SSH access.
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-recommended-nacl-rules.html
Tests:
  - Name: Secure VPC SSH Access
    ExpectedResult: true
    Resource:
      {
        "AccountId": "123456789012",
        "Region": "ap-southeast-2",
        "ARN": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ID": "acl-111222333",
        "Tags": {},
        "ResourceID": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ResourceType": "AWS.EC2.NetworkACL",
        "TimeCreated": null,
        "Associations":
          [
            {
              "NetworkAclAssociationId": "aclassoc-111222333",
              "NetworkAclId": "acl-111222333",
              "SubnetId": "subnet-111222333",
            },
          ],
        "Entries":
          [
            {
              "CidrBlock": "8.8.8.8/32",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 22, "To": 22 },
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 99,
            },
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": true,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 22, "To": 22 },
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 100,
            },
          ],
        "IsDefault": true,
        "NetworkAclId": "acl-111222333",
        "OwnerId": "123456789012",
        "VpcId": "vpc-6aa60b12",
      }
  - Name: Global VPC SSH Access
    ExpectedResult: false
    Resource:
      {
        "AccountId": "123456789012",
        "Region": "ap-southeast-2",
        "ARN": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ID": "acl-111222333",
        "Tags": {},
        "ResourceID": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ResourceType": "AWS.EC2.NetworkACL",
        "TimeCreated": null,
        "Associations":
          [
            {
              "NetworkAclAssociationId": "aclassoc-111222333",
              "NetworkAclId": "acl-111222333",
              "SubnetId": "subnet-111222333",
            },
          ],
        "Entries":
          [
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 22, "To": 22 },
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 100,
            },
          ],
        "IsDefault": true,
        "NetworkAclId": "acl-111222333",
        "OwnerId": "123456789012",
        "VpcId": "vpc-6aa60b12",
      }
  - Name: Global IPv6 VPC SSH Access
    ExpectedResult: false
    Resource:
      {
        "AccountId": "123456789012",
        "Region": "ap-southeast-2",
        "ARN": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ID": "acl-111222333",
        "Tags": {},
        "ResourceID": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ResourceType": "AWS.EC2.NetworkACL",
        "TimeCreated": null,
        "Associations":
          [
            {
              "NetworkAclAssociationId": "aclassoc-111222333",
              "NetworkAclId": "acl-111222333",
              "SubnetId": "subnet-111222333",
            },
          ],
        "Entries":
          [
            {
              "CidrBlock": null,
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": "::/0",
              "PortRange": { "From": 22, "To": 22 },
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 100,
            },
          ],
        "IsDefault": true,
        "NetworkAclId": "acl-111222333",
        "OwnerId": "123456789012",
        "VpcId": "vpc-6aa60b12",
      }
  - Name: Global VPC SSH Access From Range
    ExpectedResult: false
    Resource:
      {
        "AccountId": "123456789012",
        "Region": "ap-southeast-2",
        "ARN": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ID": "acl-111222333",
        "Tags": {},
        "ResourceID": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ResourceType": "AWS.EC2.NetworkACL",
        "TimeCreated": null,
        "Associations":
          [
            {
              "NetworkAclAssociationId": "aclassoc-111222333",
              "NetworkAclId": "acl-111222333",
              "SubnetId": "subnet-111222333",
            },
          ],
        "Entries":
          [
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 0, "To": 1024 },
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 100,
            },
          ],
        "IsDefault": true,
        "NetworkAclId": "acl-111222333",
        "OwnerId": "123456789012",
        "VpcId": "vpc-6aa60b12",
      }
  - Name: Global VPC SSH Access From Unrestricted Port Range
    ExpectedResult: false
    Resource:
      {
        "AccountId": "123456789012",
        "Region": "ap-southeast-2",
        "ARN": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ID": "acl-111222333",
        "Tags": {},
        "ResourceID": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ResourceType": "AWS.EC2.NetworkACL",
        "TimeCreated": null,
        "Associations":
          [
            {
              "NetworkAclAssociationId": "aclassoc-111222333",
              "NetworkAclId": "acl-111222333",
              "SubnetId": "subnet-111222333",
            },
          ],
        "Entries":
          [
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 100,
            },
          ],
        "IsDefault": true,
        "NetworkAclId": "acl-111222333",
        "OwnerId": "123456789012",
        "VpcId": "vpc-6aa60b12",
      }
  - Name: Selective SSH Access
    ExpectedResult: true
    Resource:
      {
        "AccountId": "123456789012",
        "Region": "ap-southeast-2",
        "ARN": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ID": "acl-111222333",
        "Tags": {},
        "ResourceID": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ResourceType": "AWS.EC2.NetworkACL",
        "TimeCreated": null,
        "Associations":
          [
            {
              "NetworkAclAssociationId": "aclassoc-111222333",
              "NetworkAclId": "acl-111222333",
              "SubnetId": "subnet-111222333",
            },
          ],
        "Entries":
          [
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": null,
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 100,
            },
            {
              "CidrBlock": "1.1.1.1/32",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 22, "To": 22 },
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 30,
            },
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 22, "To": 22 },
              "Protocol": "-1",
              "RuleAction": "deny",
              "RuleNumber": 50,
            },
          ],
        "IsDefault": true,
        "NetworkAclId": "acl-111222333",
        "OwnerId": "123456789012",
        "VpcId": "vpc-6aa60b12",
      }
  - Name: Selective SSH Access From Port Range
    ExpectedResult: true
    Resource:
      {
        "AccountId": "123456789012",
        "Region": "ap-southeast-2",
        "ARN": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ID": "acl-111222333",
        "Tags": {},
        "ResourceID": "arn:aws:ec2:ap-southeast-2:123456789012:network-acl/acl-111222333",
        "ResourceType": "AWS.EC2.NetworkACL",
        "TimeCreated": null,
        "Associations":
          [
            {
              "NetworkAclAssociationId": "aclassoc-111222333",
              "NetworkAclId": "acl-111222333",
              "SubnetId": "subnet-111222333",
            },
          ],
        "Entries":
          [
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": null,
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 100,
            },
            {
              "CidrBlock": "1.1.1.1/32",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 0, "To": 1024 },
              "Protocol": "-1",
              "RuleAction": "allow",
              "RuleNumber": 30,
            },
            {
              "CidrBlock": "0.0.0.0/0",
              "Egress": false,
              "IcmpTypeCode": null,
              "Ipv6CidrBlock": null,
              "PortRange": { "From": 22, "To": 22 },
              "Protocol": "-1",
              "RuleAction": "deny",
              "RuleNumber": 50,
            },
          ],
        "IsDefault": true,
        "NetworkAclId": "acl-111222333",
        "OwnerId": "123456789012",
        "VpcId": "vpc-6aa60b12",
      }


# ------ paired body: aws_network_acl_restricted_ssh.py ------

import ipaddress

GLOBAL_IPV6 = ipaddress.IPv6Network("::/0")
# Choose an arbitrary sentinel value that isn't equivalent to the GLOBAL_IPV6 value.
IPV6_SENTINEL = ipaddress.IPv6Network("::1/128")


def policy(resource):
    # Enumerate the entries in the network ACL, in evaluation order.
    ingress_entries = sorted(
        (entry for entry in resource["Entries"] if not entry["Egress"]),
        key=lambda x: x["RuleNumber"],
    )
    for entry in ingress_entries:
        # Look for SSH ingress rules from wildcard IPs.
        if (
            entry.get("CidrBlock") == "0.0.0.0/0"
            # Handle non-standard representations like `"0::/0"`.
            or ipaddress.IPv6Network(entry.get("Ipv6CidrBlock") or IPV6_SENTINEL) == GLOBAL_IPV6
        ) and (
            not entry.get("PortRange")
            or entry["PortRange"]["From"] <= 22 <= entry["PortRange"]["To"]
        ):
            # If this is a deny rule, then the ACL has an explicit deny rule with a lower (more
            # important) precedence than any rule that would allow SSH from arbitrary IPs. If it's
            # an allow rule, then the opposite is true. Either way, this rule determines the
            # entire outcome of the policy evaluation.
            #
            # Another way to read this: pass the policy check if the SSH rule here is a deny.
            return entry["RuleAction"] == "deny"

    # Found no SSH ingress rules from wildcard IPs.
    return True

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.