Find EC2 Instances Not Managed by Systems Manager

Bundles of blue network cables running into servers in a dimly lit server room

Photo by Brett Sayles on Pexels

To find EC2 instances not managed by SSM, list running instances with DescribeInstances and compare them with the managed nodes from Systems Manager’s DescribeInstanceInformation in the same Region. An instance that is missing, or whose PingStatus is ConnectionLost, can’t be patched, inventoried or reached with Session Manager. The usual causes are no instance profile, no agent, or no network path to the Systems Manager endpoints.

Systems Manager only helps with the instances it can see. Patch Manager skips the rest, Inventory has holes, and Session Manager can’t open a shell on them, so someone keeps port 22 open “just for that one box”. The gap is easy to miss because the console lists managed nodes, not the instances that failed to register.

This example gives you a read-only TypeScript script for the AWS SDK for JavaScript v3 that finds EC2 instances not managed by SSM in every Region and names the most likely cause for each one, so you can fix them in batches. It fits alongside the other AWS SDK v3 fleet and security audit examples.

What does an EC2 instance need to be managed by Systems Manager?

The AWS guide to troubleshooting managed node availability lists three requirements:

  • SSM Agent installed and running. Some AWS managed AMIs launch with the agent preinstalled; custom and third-party images often don’t have it.
  • Permissions. Either an IAM instance profile with the AmazonSSMManagedInstanceCore managed policy (or equivalent permissions), or Default Host Management Configuration turned on for the Region. An instance role needs nothing broader; the script to find IAM policies that grant admin access flags roles that were given admin just to make an agent work.
  • A network path. Outbound HTTPS (port 443) to ssm, ssmmessages and ec2messages endpoints for the Region, through an internet gateway or NAT, or interface VPC endpoints for a private subnet with no internet access. SSM Agent opens all connections itself, so no inbound rule is needed.

Once registered, the service checks each node’s health every five minutes. A node that stops answering shows PingStatus: ConnectionLost; after 30 days in that state, it may drop out of the Fleet Manager console list entirely.

How does Default Host Management Configuration change this?

Default Host Management Configuration (DHMC) lets Systems Manager manage EC2 instances without an instance profile. You turn it on once per Region, and it uses a role you choose, by default one with the AmazonSSMManagedEC2InstanceDefaultPolicy managed policy. It has requirements of its own:

  • The instance must use IMDSv2; DHMC doesn’t support IMDSv1.
  • SSM Agent 3.2.582.0 or later.
  • If the instance already has a profile that allows ssm:UpdateInstanceInformation, the agent uses the profile’s permissions instead of DHMC.

After you turn it on, instances can take up to 30 minutes to pick up the role. The script reads the setting with GetServiceSetting and uses it to explain gaps. If you plan to rely on DHMC, the script to find EC2 instances without IMDSv2 and require it clears that prerequisite in bulk.

What does the script do?

  1. Lists RegionsDescribeRegions, or --regions=.
  2. Reads managed nodesDescribeInstanceInformation with the paginator (50 per page, the maximum). It doesn’t return stopped or terminated nodes, so the script only compares running instances.
  3. Reads the DHMC settingGetServiceSetting for /ssm/managed-instance/default-ec2-instance-management-role.
  4. Reads running instancesDescribeInstances filtered on instance-state-name=running, keeping the instance profile, IMDS setting and Name tag.
  5. Explains each gapMatches the instance against the node list and prints a verdict. Nothing is changed.

Prerequisites

Which IAM permissions does it need?

ssm-coverage-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadFleetAndManagedNodes",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "ec2:DescribeInstances",
        "ssm:DescribeInstanceInformation"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ReadDhmcSetting",
      "Effect": "Allow",
      "Action": "ssm:GetServiceSetting",
      "Resource": "arn:aws:ssm:*:*:servicesetting/ssm/managed-instance/default-ec2-instance-management-role"
    }
  ]
}

If ssm:GetServiceSetting is denied, the script treats DHMC as off instead of failing. The IAM policy generator for TypeScript AWS SDK code gives you a draft if you extend it.

The script to find EC2 instances not managed by SSM

find-ec2-instances-not-managed-by-ssm.ts

// find-ec2-instances-not-managed-by-ssm.ts
// Compares running EC2 instances with the managed nodes Systems Manager knows about, per Region,
// and gives the most likely reason for each gap: no instance profile, Default Host Management
// Configuration (DHMC) off or blocked by IMDSv1, agent offline, or an old SSM Agent. Read-only.
// Usage: npx tsx find-ec2-instances-not-managed-by-ssm.ts [--regions=us-east-1,eu-west-1]
import { EC2Client, DescribeRegionsCommand, paginateDescribeInstances, type Instance } from "@aws-sdk/client-ec2";
import {
  SSMClient,
  GetServiceSettingCommand,
  paginateDescribeInstanceInformation,
  type InstanceInformation,
} from "@aws-sdk/client-ssm";

const regionArg = process.argv.slice(2).find((a) => a.startsWith("--regions="))?.split("=")[1];
const DHMC_SETTING = "/ssm/managed-instance/default-ec2-instance-management-role";
const DHMC_MIN_AGENT = "3.2.582.0";

interface Row {
  Region: string;
  Instance: string;
  Name: string;
  Platform: string;
  Profile: string;
  IMDSv2: string;
  Ping: string;
  Agent: string;
  Verdict: string;
}

async function listRegions(): Promise<string[]> {
  if (regionArg) return regionArg.split(",").map((r) => r.trim()).filter(Boolean);
  const out = await new EC2Client({}).send(new DescribeRegionsCommand({}));
  return (out.Regions ?? []).map((r) => r.RegionName ?? "").filter(Boolean).sort();
}

// Compares dotted version strings such as 3.3.1142.0 and 3.2.582.0.
function olderThan(version: string, min: string): boolean {
  const a = version.split(".").map(Number);
  const b = min.split(".").map(Number);
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
    const d = (a[i] ?? 0) - (b[i] ?? 0);
    if (d !== 0) return d < 0;
  }
  return false;
}

async function dhmcRole(ssm: SSMClient): Promise<string | undefined> {
  try {
    const s = (await ssm.send(new GetServiceSettingCommand({ SettingId: DHMC_SETTING }))).ServiceSetting;
    return s?.Status !== "Default" && s?.SettingValue ? s.SettingValue : undefined;
  } catch {
    return undefined; // no permission or setting unavailable: treat as off and say so in the verdict
  }
}

function verdict(inst: Instance, node: InstanceInformation | undefined, dhmc: string | undefined): string {
  const hasProfile = inst.IamInstanceProfile?.Arn !== undefined;
  const imdsv2 = inst.MetadataOptions?.HttpTokens === "required";
  if (node?.PingStatus === "Online") {
    if (!hasProfile && dhmc && node.AgentVersion && olderThan(node.AgentVersion, DHMC_MIN_AGENT)) return "managed, agent too old for DHMC";
    return node.IsLatestVersion === false ? "managed (agent update available)" : "ok";
  }
  if (node?.PingStatus === "ConnectionLost") {
    return `CONNECTION LOST since ${node.LastPingDateTime?.toISOString().slice(0, 16) ?? "?"}: agent stopped or endpoints unreachable`;
  }
  if (!hasProfile && !dhmc) return "NOT MANAGED: no instance profile and DHMC off";
  if (!hasProfile && !imdsv2) return "NOT MANAGED: DHMC needs IMDSv2 (HttpTokens=required)";
  if (hasProfile) return "NOT MANAGED: check the profile's SSM permissions, the agent and HTTPS to the SSM endpoints";
  return "NOT MANAGED: check the agent (3.2.582.0+ for DHMC) and HTTPS to the SSM endpoints";
}

async function main(): Promise<void> {
  const rows: Row[] = [];
  for (const region of await listRegions()) {
    const ec2 = new EC2Client({ region });
    const ssm = new SSMClient({ region });
    try {
      const nodes = new Map<string, InstanceInformation>();
      for await (const page of paginateDescribeInstanceInformation({ client: ssm }, { MaxResults: 50 })) {
        for (const n of page.InstanceInformationList ?? []) if (n.InstanceId) nodes.set(n.InstanceId, n);
      }
      const dhmc = await dhmcRole(ssm);
      const filters = [{ Name: "instance-state-name", Values: ["running"] }];
      for await (const page of paginateDescribeInstances({ client: ec2 }, { Filters: filters })) {
        for (const inst of (page.Reservations ?? []).flatMap((r) => r.Instances ?? [])) {
          const id = inst.InstanceId ?? "?";
          const node = nodes.get(id);
          rows.push({
            Region: region,
            Instance: id,
            Name: inst.Tags?.find((t) => t.Key === "Name")?.Value ?? "-",
            Platform: inst.PlatformDetails ?? "?",
            Profile: inst.IamInstanceProfile?.Arn?.split("/").pop() ?? (dhmc ? "- (DHMC on)" : "-"),
            IMDSv2: inst.MetadataOptions?.HttpTokens === "required" ? "required" : "optional",
            Ping: node?.PingStatus ?? "not registered",
            Agent: node?.AgentVersion ?? "-",
            Verdict: verdict(inst, node, dhmc),
          });
        }
      }
    } catch (err) {
      rows.push({ Region: region, Instance: "?", Name: "-", Platform: "?", Profile: "?", IMDSv2: "?", Ping: "?", Agent: "?", Verdict: `error: ${err instanceof Error ? err.name : String(err)}` });
    }
  }

  console.table(rows);
  const unmanaged = rows.filter((r) => r.Verdict.startsWith("NOT MANAGED") || r.Verdict.startsWith("CONNECTION LOST"));
  console.log(`${rows.length} running instance(s) checked; ${unmanaged.length} not reachable by Systems Manager.`);
  console.log("Read-only: nothing was changed.");
  if (unmanaged.length) process.exitCode = 2;
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-ec2 @aws-sdk/client-ssm
npm install --save-dev tsx typescript

AWS_PROFILE=readonly npx tsx find-ec2-instances-not-managed-by-ssm.ts

# Only the Regions you run workloads in
AWS_PROFILE=readonly npx tsx find-ec2-instances-not-managed-by-ssm.ts --regions=us-east-1,eu-west-1

Sample output

Output

┌─────────┬─────────────┬───────────────────────┬─────────────┬──────────────┬───────────────┬────────────┬──────────────────┬──────────────┬──────────────────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ Region      │ Instance              │ Name        │ Platform     │ Profile       │ IMDSv2     │ Ping             │ Agent        │ Verdict                                                                                      │
├─────────┼─────────────┼───────────────────────┼─────────────┼──────────────┼───────────────┼────────────┼──────────────────┼──────────────┼──────────────────────────────────────────────────────────────────────────────────────────────┤
│ 0       │ 'eu-west-1' │ 'i-0a12b34c56d78e901' │ 'web-1'     │ 'Linux/UNIX' │ 'web-ssm'     │ 'required' │ 'Online'         │ '3.3.1142.0' │ 'ok'                                                                                         │
│ 1       │ 'eu-west-1' │ 'i-0b23c45d67e89f012' │ 'batch-2'   │ 'Linux/UNIX' │ '-'           │ 'optional' │ 'not registered' │ '-'          │ 'NOT MANAGED: no instance profile and DHMC off'                                              │
│ 2       │ 'us-east-1' │ 'i-0c34d56e78f90a123' │ 'bastion'   │ 'Linux/UNIX' │ '- (DHMC on)' │ 'optional' │ 'not registered' │ '-'          │ 'NOT MANAGED: DHMC needs IMDSv2 (HttpTokens=required)'                                       │
│ 3       │ 'us-east-1' │ 'i-0d45e67f89a01b234' │ 'win-build' │ 'Windows'    │ 'build-agent' │ 'required' │ 'ConnectionLost' │ '3.3.987.0'  │ 'CONNECTION LOST since 2026-09-20T14:05: agent stopped or endpoints unreachable'             │
│ 4       │ 'us-east-1' │ 'i-0e56f78a90b12c345' │ 'worker-7'  │ 'Linux/UNIX' │ 'worker'      │ 'required' │ 'not registered' │ '-'          │ "NOT MANAGED: check the profile's SSM permissions, the agent and HTTPS to the SSM endpoints" │
└─────────┴─────────────┴───────────────────────┴─────────────┴──────────────┴───────────────┴────────────┴──────────────────┴──────────────┴──────────────────────────────────────────────────────────────────────────────────────────────┘
5 running instance(s) checked; 3 not reachable by Systems Manager.
Read-only: nothing was changed.

The IDs and names are illustrative. In us-east-1, DHMC is on, yet bastion still isn’t registered because it allows IMDSv1. win-build was managed until a week ago, which usually means the agent service stopped or a routing change cut it off from the endpoints.

How do you fix each verdict?

Verdict Most likely fix
No instance profile and DHMC off Attach a profile with AmazonSSMManagedInstanceCore, or turn on DHMC for the Region to cover every instance at once.
DHMC needs IMDSv2 Set HttpTokens to required after checking the software on the instance supports IMDSv2.
Profile attached, not registered Check the profile’s policies, then the agent (installed, running, 3.2.582.0 or later for DHMC), then HTTPS to the endpoints or the VPC endpoints.
Connection lost Restart the agent, and look for recent changes to route tables, NAT gateways, VPC endpoints or proxy settings.
Agent update available Update SSM Agent; IsLatestVersion is only reported for Linux nodes.

For a single stubborn instance, SSM Agent 3.1.501.0 and later include ssm-cli get-diagnostics, which runs the connectivity checks from the instance itself. You run it on the instance itself, over SSH or the EC2 serial console, which is exactly the access Systems Manager was meant to replace. Once Session Manager covers your fleet, the script to find unused EC2 key pairs across Regions shows which SSH keys nothing references any more.

Private subnets and endpoint cost

Instances in private subnets without a NAT gateway need interface VPC endpoints for ssm, ssmmessages and ec2messages. SSM Agent 3.3.40.0 and later use ssmmessages instead of ec2messages where available, but AWS still lists all three. The endpoints’ security group must allow inbound 443 from the instances. Interface endpoints are billed for every hour they’re provisioned in each Availability Zone, so share one set per VPC rather than one per team. The scripts to find unused VPC interface endpoints and find idle NAT gateways costing you money show which of the two paths you’re already paying for.

Troubleshooting the script

  • Every instance shows “not registered” in one Region. You may be querying a different Region than the instances run in; the SSM node list is Regional. Pass --regions= explicitly.
  • AccessDeniedException on DescribeInstanceInformation. Add the action from the policy above. The guide to troubleshoot AWS IAM access denied errors step by step covers SCP denies.
  • An instance you just fixed still shows as unmanaged. Registration isn’t instant, and DHMC can take up to 30 minutes. Run the script again later.
  • Stopped instances are missing. That’s deliberate. Stopped instances aren’t returned by DescribeInstanceInformation; the script to find EC2 instances stopped for weeks deals with those separately.

If the goal is shell access rather than patching, the guide to troubleshoot why you can’t SSH into an EC2 instance covers the SSH path and when to switch to Session Manager. For a full inventory of what runs where, the script to report EC2 instances by type, launch time and Region complements this one. To keep a record of how those instances’ profiles and security groups change over time, check AWS Config is recording in every Region.

Ask ChatWithCloud instead

You can ask ChatWithCloud “Which running EC2 instances aren’t managed by Systems Manager?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your AWS profile and explains the result, one profile and Region per session. It runs that code without a confirmation step, so connect ChatWithCloud to your AWS account with a read-only profile. The guide to troubleshoot AWS infrastructure with an AI CLI shows how to dig into one instance, and the ChatWithCloud security model explains what is sent where.

Frequently asked questions

How do I find EC2 instances not managed by SSM?

Compare the running instance IDs from DescribeInstances with the node IDs from DescribeInstanceInformation in each Region. Anything missing, or in ConnectionLost, isn’t managed.

Why is my EC2 instance not showing in Systems Manager?

Usually one of three reasons: SSM Agent isn’t installed or running, the instance has no permissions (no profile and DHMC off), or it can’t reach the Systems Manager endpoints over HTTPS.

Do I still need an instance profile with Default Host Management Configuration?

No. DHMC provides the permissions through a role for the whole Region, as long as the instance uses IMDSv2 and SSM Agent 3.2.582.0 or later.

Does Systems Manager need inbound ports open?

No. SSM Agent makes outbound HTTPS connections to the service, so no inbound security group rule is required on the instance.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud