
Security Hub control EC2.13 fails when a group allows ingress from 0.0.0.0/0 or ::/0 to port 22. I opened the EC2 controls page. Severity is High. The Config rule is restricted-ssh.
The control is a route table with no internet gateway on the app subnet, a group that denies inbound until you name a source, a closed SSH rule, and endpoints so private instances do not need world egress to reach AWS APIs.
Pair this page with Ubuntu host hardening for sshd and the local firewall on the AMI. Pair it with IAM roles for the instance profile that calls SSM and S3. Keep the secure coding checklist for the app the instance runs.
Private subnets, not the default VPC
A default VPC in a Region already has an internet gateway and a public subnet in each AZ. Launching i-app there with a public IPv4 address puts SSH and the app port on the internet the moment a group allows them. Build vpc-app yourself.
Two subnet types. Public: default route to an internet gateway. Attach that route only to subnets that hold NAT gateways or load balancers. Private: default route to a NAT gateway in the public subnet if the instance must call the public internet. Prefer no default route at all when VPC endpoints cover the APIs you use.
aws ec2 create-vpc --cidr-block 10.20.0.0/16 --tag-specifications \
'ResourceType=vpc,Tags=[{Key=Name,Value=vpc-app}]'
# public: 10.20.0.0/24 + igw-app
# private: 10.20.10.0/24 + optional nat-app, never igw-app
# i-app: subnet-private-a, AssociatePublicIpAddress=false
Map public IP on launch to false for i-app and for the database. An instance in a private subnet with a public IP is a confused object. The subnet route is the reachability story. The address is how scanners find it.
NACLs are the coarse subnet filter. Leave the default NACL unless you have a named deny you can explain. Do not use a NACL as a second SSH allow from the world. Security Hub EC2.21 fails a NACL that allows world ingress to 22 or 3389. Groups are the instance filter. They are stateful. NACLs are not. Start with groups.
Security groups start at deny inbound
I opened Control traffic to your AWS resources using security groups. A new custom group has no inbound rules. No inbound rule means deny. Outbound on a new group is still allow-all. Tighten egress when you know the destinations. Do not start by opening inbound “to make the demo work.”
The default group is a different object. I opened Default security groups for your VPCs. You cannot delete it. Inbound default allows all traffic from other members of that same group. Outbound default allows all IPv4. AWS says create specific groups instead. If a launch never passed --security-group-ids, it inherited default. Move it.
aws ec2 create-security-group --vpc-id vpc-0aaa \
--group-name sg-alb --description "alb to internet 443"
aws ec2 create-security-group --vpc-id vpc-0aaa \
--group-name sg-app --description "app from alb only"
aws ec2 create-security-group --vpc-id vpc-0aaa \
--group-name sg-data --description "postgres from app only"
aws ec2 authorize-security-group-ingress --group-id sg-alb \
--ip-permissions 'IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0,Description=https}]'
aws ec2 authorize-security-group-ingress --group-id sg-app \
--ip-permissions 'IpProtocol=tcp,FromPort=8080,ToPort=8080,UserIdGroupPairs=[{GroupId=sg-alb,Description=from-alb}]'
aws ec2 authorize-security-group-ingress --group-id sg-data \
--ip-permissions 'IpProtocol=tcp,FromPort=5432,ToPort=5432,UserIdGroupPairs=[{GroupId=sg-app,Description=from-app}]'
Source is another group id, not a CIDR, for east-west traffic. When sg-alb scales, sg-app does not need a new rule. World CIDR on 443 belongs on the balancer only if that hostname is a public site. It does not belong on sg-app. It does not belong on sg-data.
Empty the default group inbound and outbound once nothing uses it. Security Hub EC2.2 is the control that wants that emptiness. I am citing the control name.
Close SSH to the internet
The groups page says: when you add inbound 22 or 3389, authorize only specific address ranges. 0.0.0.0/0 and ::/0 let anyone use that protocol. EC2.13 is the automated version of that sentence for SSH. EC2.14 is the same for 3389. EC2.53 and EC2.54 widen the check to other remote-admin ports.
Revoke the open rule. Do not “limit it later.”
aws ec2 revoke-security-group-ingress --group-id sg-app \
--ip-permissions 'IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges=[{CidrIp=0.0.0.0/0}],Ipv6Ranges=[{CidrIpv6=::/0}]'
Prefer no inbound 22 at all. Session Manager uses the SSM agent and IAM. The instance keeps outbound 443 to the SSM endpoints, or to those endpoints privately. Humans assume a role. The role is allowed ssm:StartSession on i-app. That is the IAM page applied to a shell.
aws ssm start-session --target i-app
If you still run sshd for a break-glass, bind it to the instance security group from a named prefix list you own, not from the world, and keep the daemon off the public subnet. The Ubuntu page is how you lock sshd itself: keys, no root password, default deny on the host filter.
INTERNET
|
v
igw-app ---- subnet-public-a
alb sg-alb:443
|
v
subnet-private-a
i-app sg-app:8080 from sg-alb
no public IP
no 22 from 0.0.0.0/0
|
+-- sg-data:5432 from sg-app
+-- vpce s3 / ssm / ssmmessages / ec2messages
|
SSM session via IAM, not SSH
VPC endpoints keep S3 and SSM off the internet
I opened AWS PrivateLink concepts. Interface endpoints put a private ENI in your subnet. Traffic to the service stays on the AWS network. Gateway endpoints are the other type: they add a prefix-list route for S3 or DynamoDB. They do not use PrivateLink. Both are “VPC endpoints” in the CLI.
Without them, i-app in a private subnet needs a NAT gateway to reach s3.amazonaws.com or the SSM API. NAT is egress to the internet. That is a larger blast radius and a bill. Put the endpoints in subnet-private-a and a second AZ.
aws ec2 create-vpc-endpoint --vpc-id vpc-0aaa \
--vpc-endpoint-type Gateway \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids rtb-private
for svc in ssm ssmmessages ec2messages; do
aws ec2 create-vpc-endpoint --vpc-id vpc-0aaa \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.us-east-1.$svc \
--subnet-ids subnet-private-a subnet-private-b \
--security-group-ids sg-vpce \
--private-dns-enabled
done
sg-vpce allows 443 from sg-app. That is inbound on the endpoint ENI, not inbound on i-app. Attach AmazonSSMManagedInstanceCore to the instance role, or a slimmer policy you wrote. Do not put an AKIA key on the box so it can call S3 through NAT.
An endpoint policy can deny s3:ListBucket from principals you do not intend. The default policy is allow all principals all actions on the service through that endpoint. Tighten it when the bucket policy on app-uploads already names AppReadRole.
Prove the groups and the routes
You are reading your own account. You are not scanning other people.
describe-instancesoni-app. Public IP must be empty. Subnet must besubnet-private-a.describe-route-tablesfiltered to that subnet. Noigw-target on the default route.describe-security-groupsonsg-app. Inbound is 8080 fromsg-albonly. No 22 from the world CIDR.describe-vpc-endpointsinvpc-app. Expect the S3 gateway and the three SSM interface endpoints.- From a role that may use SSM,
start-sessiontoi-app. From a role that may not, expect AccessDenied.
aws ec2 describe-instances --instance-ids i-app \
--query 'Reservations[].Instances[].{Pub:PublicIpAddress,Sub:SubnetId,Sgs:SecurityGroups}'
aws ec2 describe-security-groups --group-ids sg-app \
--query 'SecurityGroups[].IpPermissions'
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=vpc-0aaa \
--query 'VpcEndpoints[].{Svc:ServiceName,Type:VpcEndpointType,State:State}'
A public IP on i-app is a failed read even if the group looks tight. Fix the subnet and the launch template, not the group first. A leftover 22/tcp from the world CIDR on any group in the VPC is an EC2.13 fail. Revoke it even if the group is detached. Someone will attach it again.
Flow logs on vpc-app to a bucket you own are how you see the reject. Turn them on after the groups are right, not instead of the groups. I could not confirm a single first-party date that “default VPCs will stop being public.” Treat every inherited default VPC as public until the route table says otherwise.
Questions we keep getting
Is a bastion still required if I have Session Manager?
No, not for a shell on i-app. SSM plus the three interface endpoints replaces inbound 22. Keep a bastion only if a leftover tool cannot speak SSM and you have a ticket to replace that tool. Do not keep 22/tcp from the world “until the ticket lands.”
Can the ALB security group stay open on 443?
Yes, if the site is public. The app group behind it must not copy that CIDR. sg-app allows 8080 from sg-alb. If the site is internal, replace the world CIDR with your VPN prefix list.
Do I still encrypt inside a private subnet?
Yes. Private means no internet route. It does not mean only friendly processes can attach a volume, dump memory, or sit on a span port. TLS to the database and HTTPS on the balancer stay on. The 2021 PCI shortcut on this URL is the thing you undo.



