AWS: Increase instance security by allowing SSH only from your IP

Closing down ports enhances security. Learn how to block SSH and still access your instance

Do you have challenges with AWS? Click here

Block SSH

By blocking port 22 on your instance attackers can not brute force it or exploit eventual vulnerabilities. But then how do you access it? With some shell scripting, you can allow access specifically from your IP, thwarting attacks against the SSH server.

The usual process is to keep port 22 always open to the world, especially when you have no fixed-IP corporate network, just in case you need to SSH into it. While SSH is quite secure, closing the port prevents the bad guys from even trying to get into. And when you need to get into the instance, you can open port 22 to your specific IP address so that you can do so without friction.

Let’s see what scripting is required to implement this solution!

Process

Here is the full script, you just need to input the instance-id. Run this before you SSH in normally and it will make sure port 22 is opened for you.

You need jq to be installed, which you can do usually with a simple sudo apt install jq.

INSTANCE_ID="..."

SG=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID --query "Reservations[].Instances[].SecurityGroups[].GroupId" --no-paginate | jq -r '.[0]')

while : ; do
	MYIP=$(curl -s ifconfig.me)
	[ -z "$MYIP" ] || break
done

CIDRS=$(aws ec2 describe-security-groups --group-ids $SG | jq -r '.SecurityGroups[].IpPermissions[] | select(.FromPort == 22 and .ToPort == 22) | .IpRanges[].CidrIp')

for ip in $CIDRS; do
	[ "$MYIP/32" != "$ip" ] && aws ec2 revoke-security-group-ingress --group-id $SG --protocol tcp --port 22 --cidr $ip
done

[ -z $(echo "$CIDRS" | grep "$MYIP/32") ] && aws ec2 authorize-security-group-ingress --group-id $SG --protocol tcp --port 22 --cidr "$MYIP/32"

After you’re finished, you can clean up:

aws ec2 revoke-security-group-ingress --group-id $SG --protocol tcp --port 22 --cidr "$MYIP/32"

Let’s see how each part is working together to keep port 22 secure!

Get the Security Group

The first step is to get the security group ID associated with the instance. This is the string with the format of sg-xxxxxx. To do this, you need to query the instance, either by the instance-id or some tags, such as the name of the instance.

To get the security group ID by name, use this:

SG=$(aws ec2 describe-instances --filter "Name=tag:Name,Values=<name>" \
--query "Reservations[].Instances[].SecurityGroups[].GroupId" \
--no-paginate | jq -r '.[0]')

And when you have the instance-id, use this:

SG=$(aws ec2 describe-instances --instance-ids <instance-id> \
--query "Reservations[].Instances[].SecurityGroups[].GroupId" \
--no-paginate | jq -r '.[0]')

The [0] part gets the first security group (from the first instance, if the query returns multiple). If you have more than one and want to use a different one than the first, you need to filter for that. Fortunately, jq is more than capable to do so.

After this line is run SG will hold the security group ID.

Get the current IP

The second task is to get the current external IP of the machine you are connecting from. This may be the IP your device reports or maybe your router supports returning it, but the most reliable solution is to use an external service. There are several such services each with its own level of reliability.

One service that you can use is ifconfig.me, and I’ll use that in this solution. To get the IP, simply use curl -s ifconfig.me.

But I’ve realized that in some cases it fails to return the IP, especially when the WiFi is connecting. So I needed to put a check that retries automatically.

With a while loop, it looks like this:

while : ; do
	MYIP=$(curl -s ifconfig.me)
	[ -z "$MYIP" ] || break
done

The MYIP will hold the current external IP.

Get the allowed IPs

The next step is to query the security group and see what IPs it currently allows:

CIDRS=$(aws ec2 describe-security-groups --group-ids $SG \
| jq -r '.SecurityGroups[].IpPermissions[]
| select(.FromPort == 22 and .ToPort == 22) | .IpRanges[].CidrIp')

The allowed IPs are associated with port ranges. In this situation I only care about exact matches to port 22 (FromPort == 22 and ToPort == 22), but there can be rules that allow a wider range including port 22. You need to take care of those if you have such rules.

The CIDRS will contain the allowed CIDR ranges.

CIDR

What is a CIDR that is referenced here? It is an address range instead of being a specific one. In our case, the /32 suffix defines a single address.

Remove unnecessary ones

The next step is some housekeeping, as you should remove unneeded rules. These might be remnants of previous runs or rules allowed separately. The only address we need to connect to the instance is “$MYIP/32”, so let’s remove all the others:

for ip in $CIDRS; do
	[ "$MYIP/32" != "$ip" ] && aws ec2 revoke-security-group-ingress \
		--group-id $SG --protocol tcp --port 22 --cidr $ip
done

Allow the IP

And finally, make sure to authorize your own IP. In case if it is already present in the $CIDRS then this step is skipped. This can happen when you reconnect to the instance without changing your IP.

[ -z $(echo "$CIDRS" | grep "$MYIP/32") ] && aws ec2 authorize-security-group-ingress \
		--group-id $SG --protocol tcp --port 22 --cidr "$MYIP/32"

SSH in!

By this time, everything is set up, and you can SSH into the instance without problems. As housekeeping, you can also remove your address after you’re done with this script:

aws ec2 revoke-security-group-ingress --group-id $SG \
	--protocol tcp --port 22 --cidr "$MYIP/32"

Conclusion

While key-based SSH is considered to be secure even if it is open to the world, minimizing the attack surface is a good practice nevertheless. Fortunately, with a minimal amount of scripting you can lock down port 22 on your instance but still be able to SSH into it whenever needed.

02 April 2019