Back to Checks
aws

Confidential-workload host does not expose non-standard ports to the internet

ec2_confidential_workload_host_unrestricted_ingress
Service: ec2Severity: high
OfficialBuilt-inVerified
Categories:internet-exposed
Resource Type:AwsEc2Instance

Hosts of Nitro Enclave workloads are evaluated for unrestricted ingress (0.0.0.0/0 or ::/0) on TCP/UDP ports outside a configurable allow-list (enclave_sg_allow_ports, default [22, 80, 443]). Aggregates ingress across every security group attached to the host. Assesses the host environment only.

Risk

Overly permissive security groups expose the host to lateral movement and to attackers probing high-port services. The host's I/O path is the enclave's I/O path over vsock, so exposed proxy ports on the host translate to exposed enclave communication channels.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks ec2_confidential_workload_host_unrestricted_ingress

Recommendation

Apply least-exposure to confidential-workload hosts: restrict internet-facing ingress to the minimum set of standard ports the workload actually needs, and prefer security-group-to-security-group references for internal services.

Remediation

CLI

aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port <port> --cidr 0.0.0.0/0

Terraform

resource "aws_security_group_rule" "example_resource" {
  type = "ingress"
  from_port = 443
  to_port = 443
  protocol = "tcp"
  cidr_blocks = ["0.0.0.0/0"]  # critical: only for allow-listed ports
  security_group_id = var.host_sg_id
}

NativeIaC

Resources:
  example_resource:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: confidential-workload host ingress
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0  # critical: keep public ingress to the allow-listed ports only

Other

1Enumerate the security groups attached to each confidential-workload host.
2For each rule that allows 0.0.0.0/0 or ::/0 ingress on a port outside {22, 80, 443}, restrict the source to a private CIDR or a peer security group.
3Prefer SG-to-SG references over CIDR-based ingress for internal services.

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
from prowler.providers.aws.services.ec2.lib.enclave import (
    HOST_TRUST_MODEL_BOILERPLATE,
    UNRESTRICTED_INGRESS_SUMMARY_THRESHOLD,
    is_enclave_parent,
    rule_world_facing_port_range,
)


class ec2_confidential_workload_host_unrestricted_ingress(Check):
    """Ensure confidential-workload hosts do not expose non-standard ports.

    Security groups attached to the host are evaluated for ingress rules that
    permit ``0.0.0.0/0`` or ``::/0`` on TCP/UDP ports outside a configurable
    allow-list (``enclave_sg_allow_ports``; defaults to ``[22, 80, 443]``).
    This check assesses the host environment; it does not audit the enclave
    itself.

    - PASS: No security-group rule exposes a non-allow-listed port to the world.
    - FAIL: At least one rule opens a non-allow-listed port to the world.
    - MANUAL: SGs referenced by the instance are not observable in the service
      cache (e.g., collection failure) — no exposure verified on the visible
      SGs, but visibility is incomplete (fail-closed).
    """

    def execute(self) -> list[Check_Report_AWS]:
        """Execute the confidential-workload host unrestricted-ingress check.

        For each confidential-workload host, iterates over every ingress rule
        of every attached security group and flags rules that allow world
        ingress on TCP/UDP ports outside the configured allow-list. Rules with
        no port bounds (e.g., ``IpProtocol=-1``) are flagged as ``all``.
        Non-allow-listed ranges larger than
        ``UNRESTRICTED_INGRESS_SUMMARY_THRESHOLD`` (10) are collapsed into a
        ``from-to`` label to keep the finding message actionable.

        Returns:
            list[Check_Report_AWS]: One report per confidential-workload host.
        """
        findings = []
        allow_ports = set(
            ec2_client.audit_config.get("enclave_sg_allow_ports", [22, 80, 443])
        )
        # Rebuilt once per scan; the SG dict does not change while iterating
        # instances.
        observed_sg_ids = {sg.id for sg in ec2_client.security_groups.values()}

        for instance in ec2_client.instances:
            if not is_enclave_parent(instance):
                continue

            report = Check_Report_AWS(metadata=self.metadata(), resource=instance)
            exposed_ports = set()

            # SGs referenced by the instance but not resolvable from the
            # service layer — treated as missing visibility (MANUAL) rather
            # than "no exposure". ec2_client.security_groups is indexed by
            # ARN, so match on the SG's .id attribute.
            missing_sgs = [
                sg_id
                for sg_id in (instance.security_groups or [])
                if sg_id not in observed_sg_ids
            ]

            for sg in ec2_client.security_groups.values():
                if sg.id not in instance.security_groups:
                    continue
                for rule in sg.ingress_rules:
                    port_range = rule_world_facing_port_range(
                        rule, protocols=("tcp", "udp")
                    )
                    if port_range is None:
                        continue
                    if port_range == "all":
                        exposed_ports.add("all")
                        continue
                    from_port, to_port = port_range
                    non_allowed = set(range(from_port, to_port + 1)) - allow_ports
                    if not non_allowed:
                        continue
                    if len(non_allowed) > UNRESTRICTED_INGRESS_SUMMARY_THRESHOLD:
                        exposed_ports.add(f"{from_port}-{to_port}")
                    else:
                        exposed_ports.update(non_allowed)

            if exposed_ports:
                numeric_ports = sorted(p for p in exposed_ports if isinstance(p, int))
                labels = sorted(p for p in exposed_ports if isinstance(p, str))
                ports_str = ", ".join(str(p) for p in numeric_ports + labels)
                report.status = "FAIL"
                report.status_extended = (
                    f"Confidential-workload host {instance.id} has security "
                    f"groups that expose non-allow-listed ports to the "
                    f"internet: {ports_str}. {HOST_TRUST_MODEL_BOILERPLATE}"
                )
            elif missing_sgs:
                report.status = "MANUAL"
                report.status_extended = (
                    f"Confidential-workload host {instance.id} ingress "
                    f"cannot be fully verified: security group(s) "
                    + ", ".join(missing_sgs)
                    + f" referenced by the instance were not observable. "
                    f"No non-allow-listed exposure found on the visible SGs. "
                    f"{HOST_TRUST_MODEL_BOILERPLATE}"
                )
            else:
                report.status = "PASS"
                report.status_extended = (
                    f"Confidential-workload host {instance.id} exposes only "
                    f"allow-listed ports {sorted(allow_ports)} (if any) to "
                    f"the internet. {HOST_TRUST_MODEL_BOILERPLATE}"
                )
            findings.append(report)
        return findings