Ensure Kafka Cluster is Not Publicly Accessible
Overview
This check verifies that Amazon MSK (Managed Streaming for Apache Kafka) clusters are not exposed to the public internet. Public access should be disabled to keep your Kafka brokers accessible only within your VPC.
MSK clusters can be configured to allow public access, which exposes broker endpoints to the internet. While this may seem convenient for external applications, it significantly increases your security risk.
Risk
Exposing your MSK cluster to the public internet creates serious security vulnerabilities:
- Data exposure: Unauthorized users may read sensitive messages from your Kafka topics
- Data tampering: Attackers can inject malicious data or modify event streams
- Service disruption: Public endpoints are targets for denial-of-service attacks
- Costly data egress: Malicious traffic or data exfiltration can result in unexpected charges
- Compliance violations: Many frameworks (C5, KISA-ISMS-P) require private-only access for sensitive systems
Remediation Steps
Prerequisites
- AWS account access with permissions to modify MSK cluster settings
- The cluster must be in a state that allows connectivity changes (not during other updates)
AWS Console Method
- Go to the Amazon MSK Console: https://console.aws.amazon.com/msk
- Make sure you are in the us-east-1 region (or your target region)
- Click on the name of your cluster
- Select the Properties tab
- Scroll to the Networking settings section
- Click Edit next to public access settings
- Select Disabled for public access
- Click Save changes
The cluster will begin updating. This process may take several minutes.
Note: If your applications currently rely on public access, you will need to update them to connect through private networking (VPC, VPN, or AWS PrivateLink) before disabling public access.
AWS CLI (optional)
Check Current Cluster Settings
First, get the cluster ARN and current version:
# List all MSK clusters
aws kafka list-clusters --region us-east-1
# Get cluster details including current version
aws kafka describe-cluster \
--cluster-arn "arn:aws:kafka:us-east-1:123456789012:cluster/my-cluster/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--region us-east-1
Note the CurrentVersion value from the output - you will need it for the update command.
Disable Public Access
aws kafka update-connectivity \
--cluster-arn "arn:aws:kafka:us-east-1:123456789012:cluster/my-cluster/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--current-version "K1XXXXXXX" \
--connectivity-info '{"PublicAccess":{"Type":"DISABLED"}}' \
--region us-east-1
Replace the placeholder values:
arn:aws:kafka:...: Your MSK cluster ARNK1XXXXXXX: The current version string from describe-cluster output
Monitor Update Progress
# Check cluster state (will show UPDATING during the change)
aws kafka describe-cluster \
--cluster-arn "arn:aws:kafka:us-east-1:123456789012:cluster/my-cluster/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--query 'ClusterInfo.State' \
--output text \
--region us-east-1
Wait until the state returns to ACTIVE before making additional changes.
CloudFormation (optional)
When creating a new MSK cluster with CloudFormation, explicitly set public access to disabled:
AWSTemplateFormatVersion: '2010-09-09'
Description: MSK Cluster with Public Access Disabled
Parameters:
ClusterName:
Type: String
Description: Name of the MSK cluster
KafkaVersion:
Type: String
Default: '3.5.1'
Description: Apache Kafka version
InstanceType:
Type: String
Default: kafka.m5.large
Description: Instance type for broker nodes
NumberOfBrokerNodes:
Type: Number
Default: 3
Description: Number of broker nodes
Subnet1:
Type: AWS::EC2::Subnet::Id
Description: First subnet for MSK brokers
Subnet2:
Type: AWS::EC2::Subnet::Id
Description: Second subnet for MSK brokers
Subnet3:
Type: AWS::EC2::Subnet::Id
Description: Third subnet for MSK brokers
SecurityGroupId:
Type: AWS::EC2::SecurityGroup::Id
Description: Security group for MSK brokers
Resources:
MSKCluster:
Type: AWS::MSK::Cluster
Properties:
ClusterName: !Ref ClusterName
KafkaVersion: !Ref KafkaVersion
NumberOfBrokerNodes: !Ref NumberOfBrokerNodes
BrokerNodeGroupInfo:
InstanceType: !Ref InstanceType
ClientSubnets:
- !Ref Subnet1
- !Ref Subnet2
- !Ref Subnet3
SecurityGroups:
- !Ref SecurityGroupId
StorageInfo:
EBSStorageInfo:
VolumeSize: 100
ConnectivityInfo:
PublicAccess:
Type: DISABLED
EncryptionInfo:
EncryptionInTransit:
ClientBroker: TLS
InCluster: true
Outputs:
ClusterArn:
Description: ARN of the MSK cluster
Value: !Ref MSKCluster
Deploy the stack:
aws cloudformation create-stack \
--stack-name msk-private-cluster \
--template-body file://msk-cluster.yaml \
--parameters \
ParameterKey=ClusterName,ParameterValue=my-private-cluster \
ParameterKey=Subnet1,ParameterValue=subnet-xxxxxxxxx1 \
ParameterKey=Subnet2,ParameterValue=subnet-xxxxxxxxx2 \
ParameterKey=Subnet3,ParameterValue=subnet-xxxxxxxxx3 \
ParameterKey=SecurityGroupId,ParameterValue=sg-xxxxxxxxx \
--region us-east-1
Terraform (optional)
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
variable "cluster_name" {
description = "Name of the MSK cluster"
type = string
}
variable "kafka_version" {
description = "Apache Kafka version"
type = string
default = "3.5.1"
}
variable "instance_type" {
description = "Instance type for broker nodes"
type = string
default = "kafka.m5.large"
}
variable "number_of_broker_nodes" {
description = "Number of broker nodes (must be multiple of AZs)"
type = number
default = 3
}
variable "subnet_ids" {
description = "List of subnet IDs for broker nodes"
type = list(string)
}
variable "security_group_ids" {
description = "List of security group IDs for broker nodes"
type = list(string)
}
resource "aws_msk_cluster" "main" {
cluster_name = var.cluster_name
kafka_version = var.kafka_version
number_of_broker_nodes = var.number_of_broker_nodes
broker_node_group_info {
instance_type = var.instance_type
client_subnets = var.subnet_ids
security_groups = var.security_group_ids
storage_info {
ebs_storage_info {
volume_size = 100
}
}
connectivity_info {
public_access {
type = "DISABLED"
}
}
}
encryption_info {
encryption_in_transit {
client_broker = "TLS"
in_cluster = true
}
}
tags = {
Environment = "production"
}
}
output "cluster_arn" {
description = "ARN of the MSK cluster"
value = aws_msk_cluster.main.arn
}
output "bootstrap_brokers_tls" {
description = "TLS connection host:port pairs"
value = aws_msk_cluster.main.bootstrap_brokers_tls
}
Example terraform.tfvars:
cluster_name = "my-private-cluster"
subnet_ids = ["subnet-xxxxxxxxx1", "subnet-xxxxxxxxx2", "subnet-xxxxxxxxx3"]
security_group_ids = ["sg-xxxxxxxxx"]
Deploy:
terraform init
terraform plan
terraform apply
Verification
After disabling public access, verify the configuration:
- Go to the Amazon MSK Console
- Click on your cluster name
- Select the Properties tab
- In the Networking settings section, confirm Public access shows Disabled
CLI verification
# Check cluster connectivity settings
aws kafka describe-cluster \
--cluster-arn "arn:aws:kafka:us-east-1:123456789012:cluster/my-cluster/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--query 'ClusterInfo.BrokerNodeGroupInfo.ConnectivityInfo.PublicAccess.Type' \
--output text \
--region us-east-1
The output should be DISABLED. If it shows SERVICE_PROVIDED_EIPS, public access is still enabled.
Run Prowler to verify:
prowler aws --checks kafka_cluster_is_public
Additional Resources
- Amazon MSK public access documentation
- MSK networking overview
- MSK security best practices
- AWS PrivateLink for MSK
Notes
- MSK Serverless clusters: Serverless clusters are private by default and do not support public access, so they automatically pass this check.
- Application migration: Before disabling public access, ensure all clients can connect through private networking (VPC peering, VPN, Transit Gateway, or PrivateLink).
- Authentication: Even with private access, always enable authentication (IAM, SASL/SCRAM, or mTLS) for defense in depth.
- Security groups: Use security groups to further restrict which VPC resources can connect to your brokers.
- Update timing: Connectivity changes can take 15-30 minutes. Do not make other cluster modifications during this time.
- Rollback: If needed, you can re-enable public access using the same process, but this is not recommended for production workloads.