Compute

EC2 read-only inventory without exposing instance user data

An explicit list of EC2 Describe actions for an inventory or CMDB tool, with the calls that return user data or console output explicitly denied.

Published · Updated .

The problem

Inventory, CMDB and cost tools need to see what runs in an account. The usual grant is ec2:Describe*, which is also what the AWS managed policy AmazonEC2ReadOnlyAccess uses. "Describe" sounds harmless, but the wildcard includes calls that return instance user data. DescribeInstanceAttribute with the userData attribute returns the base64-encoded script an instance was launched with, and anyone can decode it. DescribeLaunchTemplateVersions returns each version's launch template data, which also has a userData field. Bootstrap scripts regularly carry database passwords, API tokens or registration keys, so a read-only inventory role can turn into a way to collect secrets.

The wildcard also grows by itself. The Service Authorization Reference listed roughly 190 ec2:Describe actions when this page was written, and every new one is granted automatically. This example lists the calls a typical inventory needs and adds an explicit Deny for the user data and console calls, so attaching a broader read-only policy later does not undo the protection.

The policy

Inventory reads with a user-data guard

IAM identity-based policy. Attach to: The role your inventory, CMDB or reporting tool assumes (for a SaaS vendor, a cross-account role with an external ID)

Inventory reads with a user-data guard · IAM identity-based policyOpen in Inline Policy Generator
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DescribeInventory",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "ec2:DescribeAvailabilityZones",
        "ec2:DescribeInstances",
        "ec2:DescribeInstanceStatus",
        "ec2:DescribeInstanceTypes",
        "ec2:DescribeImages",
        "ec2:DescribeVolumes",
        "ec2:DescribeSnapshots",
        "ec2:DescribeVpcs",
        "ec2:DescribeSubnets",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSecurityGroupRules",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DescribeAddresses",
        "ec2:DescribeLaunchTemplates",
        "ec2:DescribeTags"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyUserDataAndConsoleReads",
      "Effect": "Deny",
      "Action": [
        "ec2:DescribeInstanceAttribute",
        "ec2:DescribeLaunchTemplateVersions",
        "ec2:GetLaunchTemplateData",
        "ec2:GetConsoleOutput",
        "ec2:GetConsoleScreenshot",
        "ec2:GetPasswordData"
      ],
      "Resource": "*"
    }
  ]
}

Statement by statement

  • DescribeInventory: the listed Describe calls have no resource type in the Service Authorization Reference, which is why EC2's own examples use Resource "*" for them; a narrower ARN would make them fail. The compensating control is the explicit action list. DescribeLaunchTemplates returns template names and version numbers, but not their contents. Add a StringEquals condition on aws:RequestedRegion if the tool should see only certain Regions.
  • DenyUserDataAndConsoleReads: DescribeInstanceAttribute can return user data, DescribeLaunchTemplateVersions and GetLaunchTemplateData return launch template data that includes user data, the two console calls show what an instance printed or displayed while booting, and GetPasswordData returns a Windows instance's encrypted administrator password. An explicit Deny wins over any Allow, so this statement still holds if someone later attaches AmazonEC2ReadOnlyAccess to the same role. It also blocks harmless attributes such as disableApiTermination; if the tool needs them, see the pitfalls.

Before you start

  • A role for the inventory tool in account 111122223333, and a list of the EC2 calls the tool makes, from its documentation or from CloudTrail.
  • AWS CLI v2. The checks use --dry-run, which asks EC2 whether a call would be authorized without performing it.

Deploy it step by step

  1. Match the action list to the tool. Vendors often document a required policy; compare it with the list above and add only read actions you can name. If the tool already runs with broader rights, IAM Access Analyzer can generate a policy from the role's CloudTrail activity. Keep the Deny statement whatever you add to the Allow.
  2. Choose explicit actions or the wildcard plus the Deny. The explicit list is the least-privilege option, at the cost of maintenance when the tool starts using a new call. The alternative is to replace the Allow list with ec2:Describe* and keep DenyUserDataAndConsoleReads. You then accept every future Describe action, while the known user-data paths stay closed. Either way, avoid ec2:Describe* without the Deny for any principal that should not read bootstrap secrets.
  3. Validate and attach. Validation will note that Resource is "*". That is expected for these actions; read the finding rather than suppressing it by habit.
    Commands (replace <placeholders>)
    aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://policy.json
    aws iam put-role-policy --role-name inventory-reader --policy-name ec2-inventory --policy-document file://policy.json

Verify it without changing anything

These checks only read state, simulate, or use dry runs, so they are safe to run in any account. Exact error wording varies by service and over time.

An allowed call, dry run

Run as the inventory role. With --dry-run, EC2 checks authorization and returns an error either way, so the real response is never sent.

Commands (replace <placeholders>)
aws ec2 describe-instances --dry-run --region eu-west-1

Expected: An error with code DryRunOperation, typically worded 'Request would have succeeded, but DryRun flag is set.' That error is the success signal. UnauthorizedOperation would mean the Allow is missing or something else denies it.

The user-data paths, dry run

Use any real instance and launch template in the Region. Neither call returns data in dry-run mode, even if the policy is wrong.

Commands (replace <placeholders>)
aws ec2 describe-instance-attribute --instance-id <instance-id> --attribute userData --dry-run --region eu-west-1
aws ec2 describe-launch-template-versions --launch-template-id <launch-template-id> --dry-run --region eu-west-1

Expected: UnauthorizedOperation for both, usually with the message 'You are not authorized to perform this operation.' and an encoded authorization message. DryRunOperation here means the Deny is missing or not attached to this role.

Confirm the Deny is what blocks them

The simulator distinguishes an explicit deny from a missing allow, which dry runs cannot. Repeat it with ec2:Describe* in the Allow list if you choose the wildcard variant.

Commands (replace <placeholders>)
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names ec2:DescribeInstances ec2:DescribeInstanceAttribute ec2:DescribeLaunchTemplateVersions ec2:GetConsoleOutput

Expected: allowed for ec2:DescribeInstances and explicitDeny, with the matched statement DenyUserDataAndConsoleReads, for the other three. implicitDeny instead of explicitDeny means the Deny statement is missing from the document you tested.

Common mistakes and troubleshooting

The tool fails on one unlisted call

The symptom is UnauthorizedOperation for a specific action in the tool's log. EC2 often includes an encoded authorization message; aws sts decode-authorization-message turns it into readable JSON if the caller has sts:DecodeAuthorizationMessage. Add that single action rather than switching to Describe*.

Needing harmless instance attributes

Unlike most Describe calls, DescribeInstanceAttribute is authorized against instance ARNs, but IAM has no condition key for the attribute name, so you cannot allow disableApiTermination and deny userData in the same call. If the tool needs those attributes, drop DescribeInstanceAttribute from the Deny only after checking that no instance in scope has secrets in its user data.

Other places user data appears

Spot Instance request launch specifications also have a userData field, and Auto Scaling launch configurations and CloudFormation templates can contain the same scripts. This policy covers the EC2 paths listed above, not every service that stores bootstrap data.

Limitations and production considerations

  • Read-only is not harmless. Security group rules map the network, tags sometimes hold secrets, and instance profile ARNs show which roles exist. Treat the inventory role's credentials as sensitive.
  • The Deny protects this role only, and anyone who can edit the role's policies can remove it. To block user-data reads across an organization, use an SCP for principals that have no reason to read them.
  • The real fix for secrets in user data is to keep them out: have the instance fetch them at boot from Secrets Manager or Parameter Store using its instance profile, which leaves nothing worth reading in DescribeInstanceAttribute.

Official AWS documentation

Account IDs, bucket and organization names are AWS documentation placeholders. Test in a non-production account before use (see the methodology).