Short version: Prefer SSM Session Manager for human access. If you must use SSH, use per-user keys, a bastion or tight source CIDRs, and a hardened sshd_config
. No shared logins. No password auth.
1) Choose the access pattern
Pattern | When to use | Notes |
---|---|---|
SSM Session Manager | Default for most estates | No SSH ports open; IAM-controlled; full session logs. Requires SSM Agent + instance profile. |
SSH via bastion | When SSM isn’t viable | Security Group allows SSH from bastion only. Audit on the bastion; rotate keys. |
EC2 Instance Connect | Occasional break-glass | Ephemeral keys via IAM; still needs 22/TCP exposure to trusted sources. |
2) Prefer SSM: minimal setup
- Launch with the SSM Agent AMI (most Amazon Linux/Ubuntu images already include it).
- Attach an instance profile with the managed policy
AmazonSSMManagedInstanceCore
. - Block SSH on Security Groups (no 22/TCP from the internet). Use Session Manager to connect.
Bonus: stream session logs to CloudWatch/S3 for audit. Set explicit log retention.
3) If using SSH: per-user keys and tight SGs
Security Group (example)
# Allow SSH only from bastion (10.0.10.5) and office CIDR
Inbound: 22/TCP Source: 10.0.10.5/32, 203.0.113.0/24
Outbound: restrict as per policy (don't leave 0.0.0.0/0 if you can avoid it)
Create an admin user (cloud-init)
At launch, inject a minimal cloud-init to create a named user with your public key:
#cloud-config
users:
- name: kevin
gecos: Kevin Wells
groups: sudo
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys:
- ssh-ed25519 AAAA... your_key_comment
packages:
- fail2ban
runcmd:
- sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
- sed -i 's/^#*PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config
- systemctl restart ssh || systemctl restart sshd
4) Harden sshd_config
(baseline)
# /etc/ssh/sshd_config (add/ensure these)
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
LoginGraceTime 30
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
# Optional: limit which users can SSH
# AllowUsers kevin opsuser
Reload SSH safely: sshd -t
then systemctl reload ssh
(or sshd
on some distros).
5) Bastion pattern (recommended over exposing 22 on every host)
- Lock inbound SSH on app/DB instances to the bastion’s private IP only.
- Harden the bastion: 2FA on admin workstations, fail2ban, audit, no agent forwarding.
- Jump through the bastion explicitly:
ssh -J admin@bastion.example.com admin@10.0.20.17
6) IMDSv2 + Instance Profile (no static keys)
- Require IMDSv2 on all EC2 instances; disable/limit hop count.
- Attach an instance profile for apps to assume roles; never bake long-lived AWS keys onto servers.
7) Logging and review
- Ship
/var/log/auth.log
(Debian/Ubuntu) or/var/log/secure
(RHEL) to CloudWatch Logs. - Alarm on bursts of failed logins to
sshd
, and on any successful login from unexpected IP ranges. - Quarterly: rotate user keys; disable accounts not used in 90 days.
8) Break-glass access (document it)
- Who can enable SSH on a locked-down host; for how long; and how this is recorded.
- Rollback steps: remove temporary SG rules, rotate temporary keys, and confirm logs captured access.
Security gaps in Linux and cloud systems risk downtime, data compromise, lost business — and compliance failures.
With 20+ years’ experience and active UK Security Check (SC) clearance, I harden Linux and cloud platforms for government, corporate, and academic sectors — ensuring secure, compliant, and resilient infrastructure.