Rich Gibbs

EC2 read-only hardening audit: what Inspector misses, and what to check by hand (2026)

aws · ec2 · hardening · audit · inspector · imdsv2 · indie-founder · saas

Direct answer

A read-only EC2 hardening audit should inspect the instance from the host outward: users, SSH, packages, firewall, exposed services, Docker, backups, logs, IMDSv2, disk encryption, and security groups without making changes.

You turned on AWS Inspector, you wired up IAM Access Analyzer, and the consoles are mostly green. Your EC2 fleet is two-to-five instances, the team is one-to-five people, and you’d like to believe the AWS-native tooling is enough.

This post is the honest answer to the question “is an Inspector scan the same thing as an EC2 read-only hardening audit?” — no. They overlap on maybe 30 % of the surface that actually gets indie SaaS owned. The other 70 % is instance-level configuration that AWS-native tooling, by design, doesn’t look at.

What follows is the read-only EC2 hardening audit a solo founder or small ops team can actually run in about an hour on a single instance: five categories of checks, each runnable from inside the box with no agent, no third-party SaaS, and no write access. None of it replaces Inspector. It is the other layer.

What AWS Inspector and Access Analyzer actually cover

It is worth being precise about what the native tools do well, because the gap is what we’re going to walk.

Amazon Inspector is a managed vulnerability-management service. It does three things, per the AWS docs: (1) continuously scans EC2 instances for software vulnerabilities and unintended network exposure, (2) scans container images in ECR, and (3) scans Lambda functions and layers. The EC2 scan is essentially CVE-package matching against the OS package inventory plus a network-reachability layer powered by the same engine as VPC Reachability Analyzer. See What is Amazon Inspector? for the canonical scope statement.

IAM Access Analyzer is a policy-and-resource analyzer. Per the Access Analyzer guide, it identifies resources in your account shared with external principals, validates IAM policies against best practices, and (in the newer “unused access” findings) flags unused permissions and roles. It does not look at anything inside an EC2 instance.

Both are valuable. Neither was designed to answer “is sshd accepting password auth on this box right now?”, because that’s not a control-plane question — it’s an instance-level configuration question, and the AWS control plane has no opinion about the contents of /etc/ssh/sshd_config.

The AWS Well-Architected Framework — Security Pillar is explicit that defense in depth requires both: the protect compute design principle calls out reducing attack surface, hardening operating systems, and enforcing service-level configuration as distinct from identity and detective controls.

That instance-level layer is what the rest of this post is about.

The five-check read-only EC2 hardening audit

Each of the five sections below is something a non-root SSH session can answer in under ten minutes. Read-only means: no apt install, no agent, no IAM changes, no Inspector activation toggles. You are just observing.

If you’d rather skip ahead and have somebody else run this against one host, that’s what the VPS/EC2 Hardening QuickCheck at the end of this post exists for. Otherwise, keep reading.

1. IMDSv2 enforcement (the SSRF gate Inspector won’t fail you on)

The Instance Metadata Service version 1 lets any process on the box — including any application code with an SSRF bug — fetch the instance’s IAM role credentials over plain HTTP with no token. IMDSv2 requires a session token obtained via PUT, which an SSRF attacker generally cannot mint.

AWS publishes the migration story at Configure the Instance Metadata Service for an existing instance. The instance attribute you want is HttpTokens=required. The instance attribute you want to also lock down is HttpPutResponseHopLimit=1 so that container workloads on the host can’t use the metadata service as a confused deputy. The full option reference is in Use IMDSv2.

Read-only checks from inside the box:

# Should 401 without a token (IMDSv2 enforced):
curl -s -o /dev/null -w "%{http_code}\n" \
  http://169.254.169.254/latest/meta-data/

# Should succeed with a token:
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/info

If the first call returns 200, IMDSv1 is still accepted on this instance and any SSRF in your app stack is one HTTP request away from your role credentials. Inspector will not fail this for you; it shows up as a “finding” only if you’ve enabled the relevant network-reachability rule package and the instance is publicly reachable on the metadata port (which it never is). The check you want is the local one, above.

2. SSH posture (the line sshd_config actually applies)

Inspector does not parse sshd_config. The CIS Benchmarks do — see the CIS Amazon Web Services Foundations Benchmark and CIS AWS / Ubuntu / Amazon Linux benchmarks for the canonical control list. For a read-only audit you don’t need to map every CIS control; you need the five that account for most real-world EC2 compromises.

sudo sshd -T | grep -Ei \
  '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractiveauthentication|permitemptypasswords|x11forwarding|clientaliveinterval|maxauthtries|allowusers|allowgroups) '

The sshd -T form dumps the effective runtime configuration, including Include files and Match blocks, which is the only honest way to audit SSH. Grepping /etc/ssh/sshd_config by hand will miss the cloud-init drop-in that re-enables password auth on Ubuntu AMIs, or the Match Address block a contractor added six months ago.

What you want to see, roughly:

  • permitrootlogin no (or prohibit-password, never yes).
  • passwordauthentication no.
  • kbdinteractiveauthentication no (the new name for challengeresponseauthentication; defaults flipped on several distributions in 2024).
  • permitemptypasswords no.
  • A clientaliveinterval and clientalivecountmax pair that actually evicts dead sessions.
  • An explicit allowusers or allowgroups line so a freshly-created system user can’t SSH by default.

3. Security-group surface (from the host’s perspective, not the console’s)

You can absolutely list security groups via the AWS Console. The console will show you the configured rules. It will not show you which of those rules the local box’s own listening sockets actually answer on, which is the surface that matters when somebody is scanning your public IP.

sudo ss -tulpenH | awk '{print $1, $5, $7}'

Cross-reference every listening 0.0.0.0: or [::]: socket with the SG inbound rules attached to the instance’s primary ENI. Three patterns to flag:

  1. A daemon listening on 0.0.0.0 for a service that should be loopback only (Redis on :6379, Postgres on :5432, an internal admin endpoint on :9000). Loopback-bind in the daemon config; do not rely on the SG alone.
  2. An SG rule open to 0.0.0.0/0 for a port no process is currently listening on. That is drift — somebody opened it during an incident and never closed it.
  3. A daemon listening on a port that is covered by an open SG rule but should not be (a dev-mode HTTP server on :3000, a Jupyter on :8888).

The Well-Architected Security Pillar’s Protect networks design principle calls this out as “minimize the attack surface” — and the read-only version is just ss plus the SG rule list.

4. Patch state and unattended-upgrades reality check

Inspector will tell you which packages have known CVEs. It will not tell you whether the box is actually applying security updates on a schedule, which is the difference between “we patched on Monday” and “we have not rebooted since the 2024 OpenSSL CVE.”

# When did the kernel last reboot into the running version?
uptime -s
uname -r

# Is unattended-upgrades actually installed and running? (Debian/Ubuntu)
systemctl is-enabled unattended-upgrades 2>/dev/null
systemctl is-active  unattended-upgrades 2>/dev/null
grep -E '^(APT::Periodic::Update-Package-Lists|APT::Periodic::Unattended-Upgrade)' \
  /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null

# Amazon Linux 2023 / RHEL family:
systemctl is-enabled dnf-automatic.timer 2>/dev/null
systemctl is-active  dnf-automatic.timer 2>/dev/null

An instance whose last boot was 11 months ago and whose unattended-upgrades was disabled the day the founder hit a noisy apt upgrade is the most common ‘we never got owned because we got lucky’ setup in indie SaaS. The fix is two lines of config. The audit step is just running the four commands above.

5. Docker socket exposure

If the box runs Docker, the single most consequential misconfiguration is exposing the Docker daemon socket — either by mounting /var/run/docker.sock into a container that handles untrusted input, or by listening on a TCP port without TLS. Docker’s own security documentation calls this out as effectively root on the host. The CIS Docker Benchmark §2 covers the daemon-configuration controls in detail.

# Is the daemon listening on TCP anywhere?
sudo ss -tlpn | grep -E 'docker|2375|2376'

# Which running containers have the socket mounted?
docker ps --format '{{.ID}} {{.Image}} {{.Names}}' 2>/dev/null | \
  while read id image name; do
    if docker inspect "$id" 2>/dev/null | \
         grep -q '"/var/run/docker.sock"'; then
      echo "SOCKET MOUNT: $name ($image)"
    fi
  done

A container that mounts the socket can start a new container that mounts the host’s / and chroot into it. That is not a CVE — Inspector will never flag it — but it is an instant root-equivalent path the moment that container has a code-execution bug.

What this audit deliberately does not do

A read-only EC2 hardening audit is one layer. It is not:

  • A penetration test. Nothing here exploits anything. We are reading config and listening sockets, not attacking the application. The audit-vs-pentest framing for indie founders is in a separate post.
  • A CVE scan. Inspector is the right tool for that, and you should leave it on. The point of the post is the other layer, not a replacement for the package-vulnerability layer.
  • A compliance attestation. SOC 2 / HIPAA / ISO 27001 require documented policies, evidence collection, vendor management, and access reviews — none of which this audit produces. A hardened instance is a part of those, not a substitute.
  • A continuous control. This is a point-in-time read. The continuous version is your patch cadence, your unattended-upgrades, your SG-drift detection, and your IMDSv2-required default in the launch template.

The CIS AWS Foundations Benchmark and the AWS Well-Architected Security Pillar both treat instance-level hardening and IAM-level controls as distinct layers, and there is a reason for that: they fail differently, and one rarely catches the other.

When the read-only audit isn’t enough

If you’ve run the five checks above on one instance and it lit up in two or more sections, the bottleneck is usually not the audit — it is the time to do the remediation. At that point you have three options:

  1. DIY the fixes in a maintenance window. Most of the items above are 10–30-minute changes. The Ubuntu / Debian EC2 hardening checklist is the matching to-do list.
  2. Hire someone to run the audit on more than one instance. A multi-host audit is mostly cross-referencing the same five categories with the launch templates and AMI lineage that produced them.
  3. Stop here and accept the residual risk. Sometimes the right answer.

There is no fourth option where AWS-native tooling alone closes this gap. Inspector, Access Analyzer, GuardDuty, and Security Hub are all great at what they’re designed to do; none of them parse sshd_config, none of them check whether the metadata service still accepts unauthenticated requests, and none of them tell you that a container is mounting the Docker socket.

Further reading on this site

Two pieces in the same cluster, both prerequisites or natural follow-ons to this audit:


If you’ve run the five checks above and would rather hand a single instance to somebody else for a written, prioritized fix list — IMDSv2 enforcement, SSH posture, SG surface, patch cadence, Docker exposure, plus the five other categories the QuickCheck covers — that is exactly the VPS/EC2 Hardening QuickCheck we offer. $149, one host, read-only, no agents installed, 24-hour turnaround. No managed retainers, no exploitation, no surprise add-ons. Multi-host setups upgrade to the $249 tier; everything stays read-only.

If you’d rather DIY the externally observable subset first, the free QuickCheck Mini script runs the DNS / ports / TLS / headers / public-IMDS checks against your own host in about a minute.

14-day refund on the paid QuickCheck, no questions asked.

Frequently asked questions

What does a read-only EC2 hardening audit check?

It checks host users, SSH posture, patch state, open ports, firewall rules, services, Docker exposure, backups, logs, IMDSv2, disk encryption, and nearby AWS security group assumptions.

Why is AWS Inspector not enough for EC2 hardening?

Inspector is useful for CVEs and AWS-side findings, but it can miss host-level operational risks such as stale users, exposed local files, weak service config, and backup artifacts.

Can EC2 hardening be audited without changing the instance?

Yes. A read-only pass can inspect configuration, logs, package state, and cloud metadata without changing files, firewall rules, users, or services.