Compare commits

...

1 Commits

Author SHA1 Message Date
pedrooot d4f3a7337a feat(aws): add transfer_server_pqc_ssh_kex_enabled check 2026-05-22 00:28:20 +02:00
25 changed files with 347 additions and 3 deletions
+7
View File
@@ -438,6 +438,13 @@ mainConfig:
# Minimum number of Availability Zones that an ELBv2 must be in
elbv2_min_azs: 2
# AWS Post-Quantum TLS Configuration
# aws.transfer_server_pqc_ssh_kex_enabled
transfer_pqc_ssh_allowed_policies:
- "TransferSecurityPolicy-2025-03"
- "TransferSecurityPolicy-FIPS-2025-03"
- "TransferSecurityPolicy-AS2Restricted-2025-07"
# AWS Secrets Configuration
# Patterns to ignore in the secrets checks
@@ -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 |
| `transfer_server_pqc_ssh_kex_enabled` | `transfer_pqc_ssh_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 |
| `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer |
+1
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `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)
- Sites, Additional Google services, and Marketplace checks for Google Workspace provider using the Cloud Identity Policy API [(#11281)](https://github.com/prowler-cloud/prowler/pull/11281)
- `entra_app_registration_client_secret_unused` check for M365 provider [(#11232)](https://github.com/prowler-cloud/prowler/pull/11232)
- `cloudsql_instance_cmek_encryption_enabled` check for GCP provider [(#11023)](https://github.com/prowler-cloud/prowler/pull/11023)
@@ -1151,6 +1151,7 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_ssl_listeners",
"s3_bucket_secure_transport_policy"
]
+1
View File
@@ -49,6 +49,7 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_ssl_listeners",
"elbv2_nlb_tls_termination_enabled",
"s3_bucket_secure_transport_policy",
@@ -1289,6 +1289,7 @@
"elb_ssl_listeners",
"elbv2_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_nlb_tls_termination_enabled",
"cloudfront_distributions_https_enabled",
"cloudfront_distributions_origin_traffic_encrypted",
@@ -1442,6 +1443,7 @@
"acm_certificates_with_secure_key_algorithms",
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"cloudfront_distributions_using_deprecated_ssl_protocols"
]
},
+4 -2
View File
@@ -2366,7 +2366,8 @@
}
],
"Checks": [
"elbv2_insecure_ssl_ciphers"
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled"
]
},
{
@@ -2389,7 +2390,8 @@
}
],
"Checks": [
"elbv2_insecure_ssl_ciphers"
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled"
]
},
{
@@ -1145,6 +1145,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_secure_transport_policy"
@@ -1164,6 +1165,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_secure_transport_policy"
+1
View File
@@ -487,6 +487,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"s3_bucket_secure_transport_policy"
]
@@ -266,6 +266,7 @@
"ec2_ebs_default_encryption",
"efs_encryption_at_rest_enabled",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_encryption_at_rest_enabled",
"opensearch_service_domains_node_to_node_encryption_enabled",
@@ -35,7 +35,8 @@
],
"Checks": [
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers"
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled"
]
},
{
@@ -2040,6 +2040,7 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_nlb_tls_termination_enabled",
"elbv2_ssl_listeners",
"glue_data_catalogs_connection_passwords_encryption_enabled",
@@ -3090,6 +3091,7 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_internet_facing",
"elbv2_listeners_underneath",
"elbv2_logging_enabled",
@@ -2042,6 +2042,7 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_nlb_tls_termination_enabled",
"elbv2_ssl_listeners",
"glue_data_catalogs_connection_passwords_encryption_enabled",
@@ -3093,6 +3094,7 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_internet_facing",
"elbv2_listeners_underneath",
"elbv2_logging_enabled",
@@ -653,6 +653,7 @@
"apigateway_restapi_client_certificate_enabled",
"ec2_ebs_volume_encryption",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy"
@@ -5262,6 +5262,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_secure_transport_policy"
@@ -5549,6 +5550,7 @@
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
"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",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"ec2_ebs_default_encryption",
"emr_cluster_master_nodes_no_public_ip",
@@ -474,6 +474,7 @@
"elbv2_ssl_listeners",
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
"transfer_server_pqc_ssh_kex_enabled",
"redshift_cluster_in_transit_encryption_enabled",
"elasticache_redis_cluster_in_transit_encryption_enabled",
"dynamodb_accelerator_cluster_in_transit_encryption_enabled",
+8
View File
@@ -380,6 +380,14 @@ aws:
# Minimum number of Availability Zones that an ELBv2 must be in
elbv2_min_azs: 2
# AWS Post-Quantum TLS Configuration
# aws.transfer_server_pqc_ssh_kex_enabled
# Allowed post-quantum security policies for AWS Transfer Family servers
transfer_pqc_ssh_allowed_policies:
- "TransferSecurityPolicy-2025-03"
- "TransferSecurityPolicy-FIPS-2025-03"
- "TransferSecurityPolicy-AS2Restricted-2025-07"
# AWS Elasticache Configuration
# aws.elasticache_redis_cluster_backup_enabled
# Minimum number of days that a Redis cluster must have backups retention period
@@ -0,0 +1,43 @@
{
"Provider": "aws",
"CheckID": "transfer_server_pqc_ssh_kex_enabled",
"CheckTitle": "AWS Transfer Family server uses a post-quantum hybrid SSH key exchange security policy",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "transfer",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "AwsTransferServer",
"ResourceGroup": "network",
"Description": "**AWS Transfer Family servers** (SFTP, FTPS, AS2) are assessed for use of a **post-quantum (PQ) hybrid SSH key exchange security policy**. Servers whose `SecurityPolicyName` does not include ML-KEM key exchanges (`mlkem768x25519-sha256`, `mlkem768nistp256-sha256`, `mlkem1024nistp384-sha384`) leave file-transfer sessions exposed to **harvest-now, decrypt-later** attacks.",
"Risk": "Without a PQ-ready security policy, SSH/SFTP traffic captured today can be stored and decrypted in the future once a **cryptographically relevant quantum computer** is available. This threatens long-term **confidentiality** of transferred files, credentials, and metadata.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/transfer/latest/userguide/post-quantum-security-policies.html",
"https://docs.aws.amazon.com/transfer/latest/userguide/security-policies.html",
"https://aws.amazon.com/security/post-quantum-cryptography/",
"https://csrc.nist.gov/projects/post-quantum-cryptography"
],
"Remediation": {
"Code": {
"CLI": "aws transfer update-server --server-id <server_id> --security-policy-name TransferSecurityPolicy-2025-03",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::Transfer::Server\n Properties:\n Protocols:\n - SFTP\n SecurityPolicyName: TransferSecurityPolicy-2025-03 # FIX: post-quantum hybrid SSH KEX policy\n```",
"Other": "1. In the AWS Console, go to AWS Transfer Family > Servers\n2. Select the server and choose Edit on the Additional details panel\n3. Set Cryptographic algorithm options (Security policy) to TransferSecurityPolicy-2025-03 (or another approved PQ policy)\n4. Save the changes",
"Terraform": "```hcl\nresource \"aws_transfer_server\" \"<example_resource_name>\" {\n protocols = [\"SFTP\"]\n security_policy_name = \"TransferSecurityPolicy-2025-03\" # FIX: post-quantum hybrid SSH KEX policy\n endpoint_type = \"PUBLIC\"\n}\n```"
},
"Recommendation": {
"Text": "Migrate AWS Transfer Family servers to a **post-quantum security policy** (e.g., `TransferSecurityPolicy-2025-03`, `TransferSecurityPolicy-FIPS-2025-03`, `TransferSecurityPolicy-AS2Restricted-2025-07`) that adds ML-KEM hybrid SSH key exchange. Avoid deprecated `*-PQ-SSH-Experimental-2023-04` policies. Review allowed policies regularly as AWS publishes new PQ-ready options.",
"Url": "https://hub.prowler.com/check/transfer_server_pqc_ssh_kex_enabled"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [
"transfer_server_in_transit_encryption_enabled"
],
"Notes": ""
}
@@ -0,0 +1,40 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.transfer.transfer_client import transfer_client
PQC_TRANSFER_POLICIES_DEFAULT = [
"TransferSecurityPolicy-2025-03",
"TransferSecurityPolicy-FIPS-2025-03",
"TransferSecurityPolicy-AS2Restricted-2025-07",
]
class transfer_server_pqc_ssh_kex_enabled(Check):
"""Verify that every AWS Transfer Family server uses a post-quantum security policy.
A Transfer Family server PASSES when its ``SecurityPolicyName`` is in the
configured allowlist of policies that enable hybrid ML-KEM SSH key exchange.
"""
def execute(self) -> list[Check_Report_AWS]:
findings = []
pqc_policies = transfer_client.audit_config.get(
"transfer_pqc_ssh_allowed_policies", PQC_TRANSFER_POLICIES_DEFAULT
)
for server in transfer_client.servers.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=server)
policy = server.security_policy_name or "<none>"
if server.security_policy_name in pqc_policies:
report.status = "PASS"
report.status_extended = (
f"Transfer Server {server.id} uses post-quantum security policy "
f"{policy}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Transfer Server {server.id} uses security policy {policy}, "
"which does not enable post-quantum hybrid SSH key exchange."
)
findings.append(report)
return findings
@@ -46,6 +46,9 @@ class Transfer(AWSService):
)
for protocol in server_description.get("Protocols", []):
server.protocols.append(Protocol(protocol))
server.security_policy_name = server_description.get(
"SecurityPolicyName", ""
)
server.tags = server_description.get("Tags", [])
except Exception as error:
logger.error(
@@ -65,4 +68,5 @@ class Server(BaseModel):
id: str
region: str
protocols: List[Protocol] = Field(default_factory=list)
security_policy_name: str = ""
tags: List[Dict[str, str]] = Field(default_factory=list)
@@ -0,0 +1,215 @@
from unittest import mock
from unittest.mock import patch
import botocore
from moto import mock_aws
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
SERVER_ID = "s-01234567890abcdef"
SERVER_ARN = (
f"arn:aws:transfer:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:server/{SERVER_ID}"
)
make_api_call = botocore.client.BaseClient._make_api_call
def _make_describe_server_mock(security_policy_name: str):
def _mock(self, operation_name, kwarg):
if operation_name == "ListServers":
return {
"Servers": [
{
"Arn": SERVER_ARN,
"ServerId": SERVER_ID,
}
]
}
if operation_name == "DescribeServer":
return {
"Server": {
"Arn": SERVER_ARN,
"ServerId": SERVER_ID,
"Protocols": ["SFTP"],
"SecurityPolicyName": security_policy_name,
}
}
return make_api_call(self, operation_name, kwarg)
return _mock
mock_pqc = _make_describe_server_mock("TransferSecurityPolicy-2025-03")
mock_fips_pqc = _make_describe_server_mock("TransferSecurityPolicy-FIPS-2025-03")
mock_classical = _make_describe_server_mock("TransferSecurityPolicy-2024-01")
mock_no_policy = _make_describe_server_mock("")
class Test_transfer_server_pqc_ssh_kex_enabled:
@mock_aws
def test_no_servers(self):
from prowler.providers.aws.services.transfer.transfer_service import Transfer
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled.transfer_client",
new=Transfer(aws_provider),
):
from prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled import (
transfer_server_pqc_ssh_kex_enabled,
)
check = transfer_server_pqc_ssh_kex_enabled()
result = check.execute()
assert len(result) == 0
@patch("botocore.client.BaseClient._make_api_call", new=mock_pqc)
@mock_aws
def test_pq_policy(self):
from prowler.providers.aws.services.transfer.transfer_service import Transfer
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled.transfer_client",
new=Transfer(aws_provider),
):
from prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled import (
transfer_server_pqc_ssh_kex_enabled,
)
check = transfer_server_pqc_ssh_kex_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "TransferSecurityPolicy-2025-03" in result[0].status_extended
assert result[0].resource_id == SERVER_ID
assert result[0].resource_arn == SERVER_ARN
assert result[0].region == AWS_REGION_US_EAST_1
@patch("botocore.client.BaseClient._make_api_call", new=mock_fips_pqc)
@mock_aws
def test_fips_pq_policy(self):
from prowler.providers.aws.services.transfer.transfer_service import Transfer
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled.transfer_client",
new=Transfer(aws_provider),
):
from prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled import (
transfer_server_pqc_ssh_kex_enabled,
)
check = transfer_server_pqc_ssh_kex_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "FIPS-2025-03" in result[0].status_extended
@patch("botocore.client.BaseClient._make_api_call", new=mock_classical)
@mock_aws
def test_classical_policy(self):
from prowler.providers.aws.services.transfer.transfer_service import Transfer
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled.transfer_client",
new=Transfer(aws_provider),
):
from prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled import (
transfer_server_pqc_ssh_kex_enabled,
)
check = transfer_server_pqc_ssh_kex_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "TransferSecurityPolicy-2024-01" in result[0].status_extended
assert "does not enable post-quantum" in result[0].status_extended
@patch("botocore.client.BaseClient._make_api_call", new=mock_no_policy)
@mock_aws
def test_missing_policy(self):
from prowler.providers.aws.services.transfer.transfer_service import Transfer
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled.transfer_client",
new=Transfer(aws_provider),
):
from prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled import (
transfer_server_pqc_ssh_kex_enabled,
)
check = transfer_server_pqc_ssh_kex_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "<none>" in result[0].status_extended
@patch("botocore.client.BaseClient._make_api_call", new=mock_classical)
@mock_aws
def test_configurable_allowlist(self):
from prowler.providers.aws.services.transfer.transfer_service import Transfer
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1],
audit_config={
"transfer_pqc_ssh_allowed_policies": [
"TransferSecurityPolicy-2025-03",
"TransferSecurityPolicy-2024-01",
]
},
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled.transfer_client",
new=Transfer(aws_provider),
):
from prowler.providers.aws.services.transfer.transfer_server_pqc_ssh_kex_enabled.transfer_server_pqc_ssh_kex_enabled import (
transfer_server_pqc_ssh_kex_enabled,
)
check = transfer_server_pqc_ssh_kex_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
@@ -35,6 +35,7 @@ def mock_make_api_call(self, operation_name, kwarg):
"Arn": SERVER_ARN,
"ServerId": SERVER_ID,
"Protocols": ["SFTP"],
"SecurityPolicyName": "TransferSecurityPolicy-2025-03",
"Tags": [{"Key": "key", "Value": "value"}],
}
}
@@ -83,3 +84,7 @@ class Test_transfer_service:
assert transfer.servers[SERVER_ARN].region == "us-east-1"
assert transfer.servers[SERVER_ARN].tags == [{"Key": "key", "Value": "value"}]
assert transfer.servers[SERVER_ARN].protocols[0] == Protocol.SFTP
assert (
transfer.servers[SERVER_ARN].security_policy_name
== "TransferSecurityPolicy-2025-03"
)