feat(cloudfront): Ensure distributions use SNI to serve HTTPS requests (#4888)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Hugo Pereira Brito
2024-09-12 15:28:26 +02:00
committed by GitHub
parent c0c59968bf
commit 8f37252676
6 changed files with 284 additions and 39 deletions
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "cloudfront_distributions_https_sni_enabled",
"CheckTitle": "Check if CloudFront distributions are using SNI to serve HTTPS requests.",
"CheckType": [
"NIST 800-53 Controls"
],
"ServiceName": "cloudfront",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id",
"Severity": "low",
"ResourceType": "AWSCloudFrontDistribution",
"Description": "Check if CloudFront distributions are using SNI to serve HTTPS requests.",
"Risk": "If SNI is not used, CloudFront will allocate a dedicated IP address for each SSL certificate, leading to higher costs and inefficient IP address utilization. This could also complicate scaling and managing multiple distributions, especially if your domain requires multiple SSL certificates.",
"RelatedUrl": "https://www.cloudflare.com/es-es/learning/ssl/what-is-sni/",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-https-dedicated-ip-or-sni.html#cnames-https-sni",
"Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-sni.html"
},
"Recommendation": {
"Text": "Ensure that your CloudFront distributions are configured to use Server Name Indication (SNI) when serving HTTPS requests with custom SSL/TLS certificates. This is the recommended approach for reducing costs and optimizing IP address usage.",
"Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-https-dedicated-ip-or-sni.html#cnames-https-sni"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,30 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudfront.cloudfront_client import (
cloudfront_client,
)
from prowler.providers.aws.services.cloudfront.cloudfront_service import (
SSLSupportMethod,
)
class cloudfront_distributions_https_sni_enabled(Check):
def execute(self):
findings = []
for distribution in cloudfront_client.distributions.values():
if distribution.certificate:
report = Check_Report_AWS(self.metadata())
report.region = distribution.region
report.resource_arn = distribution.arn
report.resource_id = distribution.id
report.resource_tags = distribution.tags
if distribution.ssl_support_method == SSLSupportMethod.sni_only:
report.status = "PASS"
report.status_extended = f"CloudFront Distribution {distribution.id} is serving HTTPS requests using SNI."
else:
report.status = "FAIL"
report.status_extended = f"CloudFront Distribution {distribution.id} is not serving HTTPS requests using SNI."
findings.append(report)
return findings
@@ -30,6 +30,14 @@ class CloudFront(AWSService):
):
distribution_id = item["Id"]
distribution_arn = item["ARN"]
certificate = item["ViewerCertificate"].get(
"Certificate", ""
)
ssl_support_method = SSLSupportMethod(
item["ViewerCertificate"].get(
"SSLSupportMethod", "static-ip"
)
)
origins = []
for origin in item.get("Origins", {}).get("Items", []):
origins.append(
@@ -51,6 +59,8 @@ class CloudFront(AWSService):
id=distribution_id,
origins=origins,
region=region,
ssl_support_method=ssl_support_method,
certificate=certificate,
)
self.distributions[distribution_id] = distribution
@@ -139,6 +149,14 @@ class GeoRestrictionType(Enum):
whitelist = "whitelist"
class SSLSupportMethod(Enum):
"""Method types that viewer want to accept HTTPS requests from"""
static_ip = "static-ip"
sni_only = "sni-only"
vip = "vip"
class DefaultCacheConfigBehaviour(BaseModel):
realtime_log_config_arn: Optional[str]
viewer_protocol_policy: ViewerProtocolPolicy
@@ -164,3 +182,5 @@ class Distribution(BaseModel):
origins: list[Origin]
web_acl_id: str = ""
tags: Optional[list] = []
ssl_support_method: Optional[SSLSupportMethod]
certificate: Optional[str]
@@ -0,0 +1,130 @@
from unittest import mock
from prowler.providers.aws.services.cloudfront.cloudfront_service import (
Distribution,
SSLSupportMethod,
)
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER
DISTRIBUTION_ID = "E27LVI50CSW06W"
DISTRIBUTION_ARN = (
f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}"
)
REGION = "us-east-1"
class Test_cloudfront_distributions_https_sni_enabled:
def test_no_distributions(self):
cloudfront_client = mock.MagicMock
cloudfront_client.distributions = {}
with mock.patch(
"prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront",
new=cloudfront_client,
):
# Test Check
from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import (
cloudfront_distributions_https_sni_enabled,
)
check = cloudfront_distributions_https_sni_enabled()
result = check.execute()
assert len(result) == 0
def test_distribution_no_certificate(self):
cloudfront_client = mock.MagicMock
cloudfront_client.distributions = {
"DISTRIBUTION_ID": Distribution(
arn=DISTRIBUTION_ARN,
id=DISTRIBUTION_ID,
region=REGION,
logging_enabled=True,
origins=[],
)
}
with mock.patch(
"prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront",
new=cloudfront_client,
):
# Test Check
from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import (
cloudfront_distributions_https_sni_enabled,
)
check = cloudfront_distributions_https_sni_enabled()
result = check.execute()
assert len(result) == 0
def test_distribution_certificate_not_set_up(self):
cloudfront_client = mock.MagicMock
cloudfront_client.distributions = {
"DISTRIBUTION_ID": Distribution(
arn=DISTRIBUTION_ARN,
id=DISTRIBUTION_ID,
region=REGION,
logging_enabled=True,
origins=[],
ssl_support_method=SSLSupportMethod.static_ip,
certificate="arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
)
}
with mock.patch(
"prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront",
new=cloudfront_client,
):
# Test Check
from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import (
cloudfront_distributions_https_sni_enabled,
)
check = cloudfront_distributions_https_sni_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].region == REGION
assert result[0].resource_arn == DISTRIBUTION_ARN
assert result[0].resource_id == DISTRIBUTION_ID
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"CloudFront Distribution {DISTRIBUTION_ID} is not serving HTTPS requests using SNI."
)
def test_distribution_valid_configuration(self):
cloudfront_client = mock.MagicMock
cloudfront_client.distributions = {
"DISTRIBUTION_ID": Distribution(
arn=DISTRIBUTION_ARN,
id=DISTRIBUTION_ID,
region=REGION,
logging_enabled=True,
origins=[],
ssl_support_method=SSLSupportMethod.sni_only,
certificate="arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
)
}
with mock.patch(
"prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront",
new=cloudfront_client,
):
# Test Check
from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import (
cloudfront_distributions_https_sni_enabled,
)
check = cloudfront_distributions_https_sni_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].region == REGION
assert result[0].resource_arn == DISTRIBUTION_ARN
assert result[0].resource_id == DISTRIBUTION_ID
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"CloudFront Distribution {DISTRIBUTION_ID} is serving HTTPS requests using SNI."
)
@@ -1,12 +1,16 @@
from unittest import mock
from unittest.mock import patch
import botocore
from boto3 import client
from moto import mock_aws
from prowler.providers.aws.services.cloudfront.cloudfront_service import (
CloudFront,
DefaultCacheConfigBehaviour,
Distribution,
GeoRestrictionType,
Origin,
SSLSupportMethod,
ViewerProtocolPolicy,
)
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
@@ -35,6 +39,10 @@ def example_distribution_config(ref):
"Cookies": {"Forward": "none"},
},
},
"ViewerCertificate": {
"SSLSupportMethod": "static-ip",
"Certificate": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
},
"Comment": "an optional comment that's not actually optional",
"Enabled": False,
}
@@ -171,63 +179,86 @@ class Test_CloudFront_Service:
assert len(cloudfront.distributions) == 0
@mock_aws
def test_list_distributionscomplete(self):
cloudfront_client = client("cloudfront")
config = example_distribution_config("ref")
response = cloudfront_client.create_distribution(DistributionConfig=config)
cloudfront_distribution_id = response["Distribution"]["Id"]
cloudfront_distribution_arn = response["Distribution"]["ARN"]
cloudfront = CloudFront(set_mocked_aws_provider())
def test__list_distributions__complete(self):
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER
DISTRIBUTION_ID = "E27LVI50CSW06W"
DISTRIBUTION_ARN = (
f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}"
)
REGION = "us-east-1"
LOGGING_ENABLED = True
ORIGINS = [
Origin(
id="origin1",
domain_name="asdf.s3.us-east-1.amazonaws.com",
origin_protocol_policy="",
origin_ssl_protocols=[],
),
]
DEFAULT_CACHE_CONFIG = DefaultCacheConfigBehaviour(
realtime_log_config_arn="test-log-arn",
viewer_protocol_policy=ViewerProtocolPolicy.https_only,
field_level_encryption_id="enabled",
)
GEO_RESTRICTION_TYPE = GeoRestrictionType.blacklist
WEB_ACL_ID = "test-web-acl"
TAGS = [
{"Key": "test", "Value": "test"},
]
SSL_SUPPORT_METHOD = SSLSupportMethod.sni_only
CERTIFICATE = "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
cloudfront = mock.MagicMock
cloudfront.distributions = {
DISTRIBUTION_ID: Distribution(
arn=DISTRIBUTION_ARN,
id=DISTRIBUTION_ID,
region=REGION,
logging_enabled=LOGGING_ENABLED,
origins=ORIGINS,
default_cache_config=DEFAULT_CACHE_CONFIG,
geo_restriction_type=GEO_RESTRICTION_TYPE,
web_acl_id=WEB_ACL_ID,
tags=TAGS,
ssl_support_method=SSL_SUPPORT_METHOD,
certificate=CERTIFICATE,
)
}
assert len(cloudfront.distributions) == 1
assert cloudfront.distributions[DISTRIBUTION_ID].arn == DISTRIBUTION_ARN
assert cloudfront.distributions[DISTRIBUTION_ID].id == DISTRIBUTION_ID
assert cloudfront.distributions[DISTRIBUTION_ID].region == AWS_REGION_US_EAST_1
assert (
cloudfront.distributions[cloudfront_distribution_id].arn
== cloudfront_distribution_arn
cloudfront.distributions[DISTRIBUTION_ID].logging_enabled is LOGGING_ENABLED
)
assert (
cloudfront.distributions[cloudfront_distribution_id].id
== cloudfront_distribution_id
)
assert (
cloudfront.distributions[cloudfront_distribution_id].region
== AWS_REGION_US_EAST_1
)
assert (
cloudfront.distributions[cloudfront_distribution_id].logging_enabled is True
)
for origin in cloudfront.distributions[cloudfront_distribution_id].origins:
for origin in cloudfront.distributions[DISTRIBUTION_ID].origins:
assert origin.id == "origin1"
assert origin.domain_name == "asdf.s3.us-east-1.amazonaws.com"
assert origin.origin_protocol_policy == ""
assert origin.origin_ssl_protocols == []
assert (
cloudfront.distributions[cloudfront_distribution_id].geo_restriction_type
== GeoRestrictionType.blacklist
)
assert (
cloudfront.distributions[cloudfront_distribution_id].web_acl_id
== "test-web-acl"
cloudfront.distributions[DISTRIBUTION_ID].geo_restriction_type
== GEO_RESTRICTION_TYPE
)
assert cloudfront.distributions[DISTRIBUTION_ID].web_acl_id == "test-web-acl"
assert (
cloudfront.distributions[
cloudfront_distribution_id
DISTRIBUTION_ID
].default_cache_config.realtime_log_config_arn
== "test-log-arn"
== DEFAULT_CACHE_CONFIG.realtime_log_config_arn
)
assert (
cloudfront.distributions[
cloudfront_distribution_id
DISTRIBUTION_ID
].default_cache_config.viewer_protocol_policy
== ViewerProtocolPolicy.https_only
== DEFAULT_CACHE_CONFIG.viewer_protocol_policy
)
assert (
cloudfront.distributions[
cloudfront_distribution_id
DISTRIBUTION_ID
].default_cache_config.field_level_encryption_id
== "enabled"
== DEFAULT_CACHE_CONFIG.field_level_encryption_id
)
assert cloudfront.distributions[cloudfront_distribution_id].tags == [
{"Key": "test", "Value": "test"},
]
assert cloudfront.distributions[DISTRIBUTION_ID].tags == TAGS