feat(aws): add cloudfront_distributions_pqc_tls_enabled check (#11317)

Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
Pedro Martín
2026-06-22 17:44:51 +02:00
committed by GitHub
parent 6826422a6a
commit 10d9fc35e6
23 changed files with 296 additions and 0 deletions
+3
View File
@@ -439,6 +439,9 @@ mainConfig:
elbv2_min_azs: 2
# AWS Post-Quantum TLS Configuration
# aws.cloudfront_distributions_pqc_tls_enabled
cloudfront_pqc_min_protocol_versions:
- "TLSv1.3_2025"
# aws.apigateway_domain_name_pqc_tls_enabled
apigateway_pqc_tls_allowed_policies:
- "SecurityPolicy_TLS13_1_2_FIPS_PFS_PQ_2025_09"
@@ -55,6 +55,7 @@ The following list includes all the AWS checks with configurable variables that
| `elasticache_redis_cluster_backup_enabled` | `minimum_snapshot_retention_period` | Integer |
| `elb_is_in_multiple_az` | `elb_min_azs` | Integer |
| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer |
| `cloudfront_distributions_pqc_tls_enabled` | `cloudfront_pqc_min_protocol_versions` | List of Strings |
| `apigateway_domain_name_pqc_tls_enabled` | `apigateway_pqc_tls_allowed_policies` | List of Strings |
| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean |
| `iam_user_access_not_stale_to_sagemaker` | `max_unused_sagemaker_access_days` | Integer |
+1
View File
@@ -44,6 +44,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642)
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Cloudflare provider, mapping existing Cloudflare edge/network checks across the applicable DORA pillars [(#11645)](https://github.com/prowler-cloud/prowler/pull/11645)
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the AlibabaCloud provider, mapping existing AlibabaCloud checks across the applicable DORA pillars [(#11646)](https://github.com/prowler-cloud/prowler/pull/11646)
- `cloudfront_distributions_pqc_tls_enabled` check for AWS provider to verify CloudFront distributions enforce a post-quantum TLS 1.3 security policy [(#11317)](https://github.com/prowler-cloud/prowler/pull/11317)
- `apigateway_domain_name_pqc_tls_enabled` check for AWS provider to verify API Gateway custom domain names use a post-quantum TLS security policy [(#11316)](https://github.com/prowler-cloud/prowler/pull/11316)
- `transfer_server_pqc_ssh_kex_enabled` check for AWS provider to verify Transfer Family servers use a post-quantum hybrid SSH key exchange security policy [(#11315)](https://github.com/prowler-cloud/prowler/pull/11315)
@@ -1151,6 +1151,7 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_ssl_listeners",
+1
View File
@@ -49,6 +49,7 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_ssl_listeners",
@@ -2367,6 +2367,7 @@
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
]
@@ -2392,6 +2393,7 @@
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
]
@@ -1145,6 +1145,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
@@ -1166,6 +1167,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
+1
View File
@@ -487,6 +487,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
@@ -266,6 +266,7 @@
"ec2_ebs_default_encryption",
"efs_encryption_at_rest_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
@@ -36,6 +36,7 @@
"Checks": [
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
]
@@ -2040,6 +2040,7 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_nlb_tls_termination_enabled",
@@ -3092,6 +3093,7 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_internet_facing",
@@ -2042,6 +2042,7 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_nlb_tls_termination_enabled",
@@ -3095,6 +3096,7 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_internet_facing",
@@ -653,6 +653,7 @@
"apigateway_restapi_client_certificate_enabled",
"ec2_ebs_volume_encryption",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"opensearch_service_domains_node_to_node_encryption_enabled",
@@ -5262,6 +5262,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
@@ -5551,6 +5552,7 @@
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners"
@@ -40,6 +40,7 @@
"ec2_instance_public_ip",
"efs_encryption_at_rest_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
@@ -474,6 +474,7 @@
"elbv2_ssl_listeners",
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"redshift_cluster_in_transit_encryption_enabled",
+4
View File
@@ -381,6 +381,10 @@ aws:
elbv2_min_azs: 2
# AWS Post-Quantum TLS Configuration
# aws.cloudfront_distributions_pqc_tls_enabled
# Allowed CloudFront MinimumProtocolVersion values that enable post-quantum hybrid key exchange
cloudfront_pqc_min_protocol_versions:
- "TLSv1.3_2025"
# aws.apigateway_domain_name_pqc_tls_enabled
# Allowed post-quantum TLS security policies for API Gateway custom domain names
apigateway_pqc_tls_allowed_policies:
@@ -0,0 +1,43 @@
{
"Provider": "aws",
"CheckID": "cloudfront_distributions_pqc_tls_enabled",
"CheckTitle": "CloudFront distributions enforce a post-quantum TLS 1.3 security policy",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "cloudfront",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "AwsCloudFrontDistribution",
"ResourceGroup": "network",
"Description": "**CloudFront distributions** are assessed for use of a **TLS 1.3-only security policy** (`TLSv1.3_2025`). CloudFront's quantum-safe key exchanges (`X25519MLKEM768`, `SecP256r1MLKEM768`) only work with TLS 1.3. Distributions that allow TLS 1.2 (or older) fallback to classical key exchanges and are exposed to `harvest-now, decrypt-later` attacks.",
"Risk": "Without a TLS 1.3-only policy, viewer traffic captured today can be decrypted once a **cryptographically relevant quantum computer** is available. Distributions using the default CloudFront certificate (`*.cloudfront.net`) cannot enable a post-quantum policy because they are pinned to the legacy `TLSv1` policy.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/secure-connections-supported-viewer-protocols-ciphers.html",
"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html#DownloadDistValues-security-policy",
"https://aws.amazon.com/security/post-quantum-cryptography/",
"https://csrc.nist.gov/projects/post-quantum-cryptography"
],
"Remediation": {
"Code": {
"CLI": "aws cloudfront update-distribution --id <distribution_id> --distribution-config '{...\"ViewerCertificate\":{\"MinimumProtocolVersion\":\"TLSv1.3_2025\",...}}'",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::CloudFront::Distribution\n Properties:\n DistributionConfig:\n ViewerCertificate:\n AcmCertificateArn: <example_certificate_arn>\n SslSupportMethod: sni-only\n MinimumProtocolVersion: TLSv1.3_2025 # FIX: enforces TLS 1.3 + post-quantum KEX\n```",
"Other": "1. In the AWS Console, go to CloudFront > Distributions\n2. Select the distribution and open the General tab\n3. Choose Edit on Settings\n4. Set Custom SSL certificate (do not use the default *.cloudfront.net certificate)\n5. Set Security policy to TLSv1.3_2025\n6. Save changes",
"Terraform": "```hcl\nresource \"aws_cloudfront_distribution\" \"<example_resource_name>\" {\n # ...\n viewer_certificate {\n acm_certificate_arn = \"<example_certificate_arn>\"\n ssl_support_method = \"sni-only\"\n minimum_protocol_version = \"TLSv1.3_2025\" # FIX: enforces TLS 1.3 + post-quantum KEX\n }\n}\n```"
},
"Recommendation": {
"Text": "Use a **custom SSL certificate** with **SNI** support and set `MinimumProtocolVersion` to `TLSv1.3_2025` so CloudFront refuses TLS 1.2 handshakes and uses the hybrid ML-KEM key exchange. Distributions still using the default CloudFront certificate must be migrated to a custom certificate to enable post-quantum TLS.",
"Url": "https://hub.prowler.com/check/cloudfront_distributions_pqc_tls_enabled"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [
"cloudfront_distributions_using_deprecated_ssl_protocols"
],
"Notes": ""
}
@@ -0,0 +1,56 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudfront.cloudfront_client import (
cloudfront_client,
)
PQC_CLOUDFRONT_POLICIES_DEFAULT = [
"TLSv1.3_2025",
]
class cloudfront_distributions_pqc_tls_enabled(Check):
"""Verify that every CloudFront distribution enforces TLS 1.3 with post-quantum key exchange.
Quantum-safe key exchanges (``X25519MLKEM768``, ``SecP256r1MLKEM768``) are
only available on TLS 1.3 viewer connections. A distribution PASSES when
its ``MinimumProtocolVersion`` belongs to the configured allowlist of
TLS 1.3-only policies. Distributions that rely on the default CloudFront
certificate are pinned to the legacy ``TLSv1`` policy and therefore FAIL.
"""
def execute(self) -> list[Check_Report_AWS]:
"""Execute the CloudFront post-quantum TLS policy check.
Returns:
A list of reports containing each CloudFront distribution's
post-quantum TLS compliance status.
"""
findings = []
pqc_policies = cloudfront_client.audit_config.get(
"cloudfront_pqc_min_protocol_versions", PQC_CLOUDFRONT_POLICIES_DEFAULT
)
for distribution in cloudfront_client.distributions.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=distribution)
policy = distribution.minimum_protocol_version or "<none>"
if distribution.default_certificate:
report.status = "FAIL"
report.status_extended = (
f"CloudFront Distribution {distribution.id} uses the default "
"CloudFront certificate, which pins the security policy to "
"TLSv1 and cannot enable post-quantum TLS."
)
elif distribution.minimum_protocol_version in pqc_policies:
report.status = "PASS"
report.status_extended = (
f"CloudFront Distribution {distribution.id} uses post-quantum "
f"TLS policy {policy}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"CloudFront Distribution {distribution.id} uses TLS policy "
f"{policy}, which is not in the post-quantum allowlist."
)
findings.append(report)
return findings
@@ -48,6 +48,9 @@ class CloudFront(AWSService):
"SSLSupportMethod", "static-ip"
)
)
minimum_protocol_version = item["ViewerCertificate"].get(
"MinimumProtocolVersion", ""
)
origins = []
for origin in item.get("Origins", {}).get("Items", []):
origins.append(
@@ -79,6 +82,7 @@ class CloudFront(AWSService):
ssl_support_method=ssl_support_method,
default_certificate=default_certificate,
certificate=certificate,
minimum_protocol_version=minimum_protocol_version,
)
self.distributions[distribution_id] = distribution
@@ -268,3 +272,4 @@ class Distribution(BaseModel):
origin_failover: Optional[bool] = None
ssl_support_method: Optional[SSLSupportMethod] = None
certificate: Optional[str] = None
minimum_protocol_version: str = ""
@@ -0,0 +1,158 @@
import sys
from unittest import mock
from prowler.providers.aws.services.cloudfront.cloudfront_service import (
Distribution,
Origin,
)
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
DISTRIBUTION_ID = "E27LVI50CSW06W"
DISTRIBUTION_ARN = (
f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}"
)
REGION = "us-east-1"
CHECK_MODULE = "prowler.providers.aws.services.cloudfront.cloudfront_distributions_pqc_tls_enabled.cloudfront_distributions_pqc_tls_enabled"
CLIENT_MODULE = "prowler.providers.aws.services.cloudfront.cloudfront_client"
def _clear_cloudfront_modules():
sys.modules.pop(CHECK_MODULE, None)
sys.modules.pop(CLIENT_MODULE, None)
def _build_distribution(
*,
minimum_protocol_version: str,
default_certificate: bool = False,
):
return Distribution(
arn=DISTRIBUTION_ARN,
id=DISTRIBUTION_ID,
region=REGION,
origins=[
Origin(
id="o1",
domain_name="origin.example.com",
origin_protocol_policy="https-only",
origin_ssl_protocols=["TLSv1.2"],
)
],
origin_failover=False,
minimum_protocol_version=minimum_protocol_version,
default_certificate=default_certificate,
)
def _build_client(distributions: dict, audit_config: dict | None = None):
cloudfront_client = mock.MagicMock()
cloudfront_client.distributions = distributions
cloudfront_client.audit_config = audit_config or {}
return cloudfront_client
def _execute_check(cloudfront_client):
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
_clear_cloudfront_modules()
try:
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront",
return_value=cloudfront_client,
),
):
from prowler.providers.aws.services.cloudfront.cloudfront_distributions_pqc_tls_enabled.cloudfront_distributions_pqc_tls_enabled import (
cloudfront_distributions_pqc_tls_enabled,
)
check = cloudfront_distributions_pqc_tls_enabled()
return check.execute()
finally:
_clear_cloudfront_modules()
class Test_cloudfront_distributions_pqc_tls_enabled:
def test_no_distributions(self):
cloudfront_client = _build_client({})
result = _execute_check(cloudfront_client)
assert len(result) == 0
def test_pq_policy_tls13_2025(self):
cloudfront_client = _build_client(
{
DISTRIBUTION_ID: _build_distribution(
minimum_protocol_version="TLSv1.3_2025"
)
}
)
result = _execute_check(cloudfront_client)
assert len(result) == 1
assert result[0].status == "PASS"
assert "TLSv1.3_2025" in result[0].status_extended
assert result[0].resource_id == DISTRIBUTION_ID
assert result[0].resource_arn == DISTRIBUTION_ARN
def test_classical_tls12_2021(self):
cloudfront_client = _build_client(
{
DISTRIBUTION_ID: _build_distribution(
minimum_protocol_version="TLSv1.2_2021"
)
}
)
result = _execute_check(cloudfront_client)
assert len(result) == 1
assert result[0].status == "FAIL"
assert "TLSv1.2_2021" in result[0].status_extended
assert "not in the post-quantum allowlist" in result[0].status_extended
def test_default_cloudfront_certificate(self):
cloudfront_client = _build_client(
{
DISTRIBUTION_ID: _build_distribution(
minimum_protocol_version="TLSv1",
default_certificate=True,
)
}
)
result = _execute_check(cloudfront_client)
assert len(result) == 1
assert result[0].status == "FAIL"
assert "default CloudFront certificate" in result[0].status_extended
def test_configurable_allowlist(self):
cloudfront_client = _build_client(
{
DISTRIBUTION_ID: _build_distribution(
minimum_protocol_version="TLSv1.2_2021"
)
},
audit_config={
"cloudfront_pqc_min_protocol_versions": [
"TLSv1.3_2025",
"TLSv1.2_2021",
]
},
)
result = _execute_check(cloudfront_client)
assert len(result) == 1
assert result[0].status == "PASS"
@@ -64,6 +64,7 @@ def example_distribution_config(ref):
"ViewerCertificate": {
"SSLSupportMethod": "static-ip",
"Certificate": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
"MinimumProtocolVersion": "TLSv1.3_2025",
},
"Comment": "an optional comment that's not actually optional",
"Enabled": False,
@@ -234,6 +235,7 @@ class Test_CloudFront_Service:
]
SSL_SUPPORT_METHOD = SSLSupportMethod.sni_only
CERTIFICATE = "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
MINIMUM_PROTOCOL_VERSION = "TLSv1.3_2025"
cloudfront = mock.MagicMock
cloudfront.distributions = {
@@ -249,6 +251,7 @@ class Test_CloudFront_Service:
tags=TAGS,
ssl_support_method=SSL_SUPPORT_METHOD,
certificate=CERTIFICATE,
minimum_protocol_version=MINIMUM_PROTOCOL_VERSION,
)
}
@@ -288,6 +291,10 @@ class Test_CloudFront_Service:
== DEFAULT_CACHE_CONFIG.field_level_encryption_id
)
assert cloudfront.distributions[DISTRIBUTION_ID].tags == TAGS
assert (
cloudfront.distributions[DISTRIBUTION_ID].minimum_protocol_version
== MINIMUM_PROTOCOL_VERSION
)
def test_get_log_delivery_sources_with_active_delivery(self):
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER