Outcome
By the end of this lesson, you will be able to configure the AWS IAM role for ZopNight and explain which permissions enable read-only discovery versus scoped-write execution.
| Tier | Operator |
| JTBD | ”Get an AWS account into ZopNight in under 10 minutes with the right policy.” |
| Personas | Platform Engineer · Security/Compliance |
| Prerequisites | L1 |
| Time | 10 minutes |
| Bloom verb | Configure (Apply) and Explain (Understand) |
1. Concept
You do not give ZopNight a password. You create an IAM role in your AWS account, which is a named set of permissions, and you allow ZopNight’s account to borrow it. AWS calls that assuming a role, and it is the standard way one AWS account lets another do work on its behalf.
You keep the role. You decide what is in it, and you can change or delete it at any time without involving anyone.
The read-only part
Finding resources, reading their usage, pulling the bill and producing recommendations all need permission to look and nothing more. That part of the role looks like this:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "DiscoveryReadOnly", "Effect": "Allow", "Action": [ "ec2:Describe*", "rds:Describe*", "eks:Describe*", "eks:List*", "ecs:Describe*", "ecs:List*", "lambda:Get*", "lambda:List*", "s3:Get*", "s3:List*", "elasticloadbalancing:Describe*", "autoscaling:Describe*", "cloudfront:Get*", "cloudfront:List*", "dynamodb:Describe*", "dynamodb:List*", "route53:Get*", "route53:List*" ], "Resource": "*" }, { "Sid": "ResourceExplorer", "Effect": "Allow", "Action": [ "resource-explorer-2:Search", "resource-explorer-2:List*", "resource-explorer-2:Get*" ], "Resource": "*" }, { "Sid": "Metrics", "Effect": "Allow", "Action": [ "cloudwatch:GetMetricStatistics", "cloudwatch:GetMetricData", "cloudwatch:ListMetrics" ], "Resource": "*" }, { "Sid": "Pricing", "Effect": "Allow", "Action": [ "pricing:DescribeServices", "pricing:GetProducts" ], "Resource": "*" }, { "Sid": "BillingReadOnly", "Effect": "Allow", "Action": [ "ce:Get*", "ce:List*", "ce:Describe*" ], "Resource": "*" }, { "Sid": "ActivityLogs", "Effect": "Allow", "Action": [ "cloudtrail:LookupEvents" ], "Resource": "*" } ]}Read the verbs and you can check this yourself. Every one is Describe, Get, List or Lookup. None of them changes anything.
Deploy only this and you get the full picture: every resource found, every report, every recommendation. What you do not get is anything happening. No schedules, no automatic fixes. This is what “monitor mode” means, and it is a perfectly reasonable place to stop.
The parts that can change something
If you want the schedules and the automatic fixes, you add specific extra permissions. Each one is separate, and each one is your decision:
For scheduling EC2 start/stop:
{ "Sid": "EC2StartStop", "Effect": "Allow", "Action": ["ec2:StartInstances", "ec2:StopInstances"], "Resource": "*", "Condition": { "StringEqualsIfExists": {"aws:ResourceTag/environment": ["dev", "test", "stage"]} }}Read the Condition block. It says this permission applies only to machines tagged dev, test or stage.
That is worth dwelling on. If somebody sets up a ZopNight schedule badly and it tries to stop a production machine, AWS refuses the request. The protection lives in your account, not in ours, which is the only place it is worth having.
For EBS termination (auto-remediation of orphaned volumes):
{ "Sid": "EBSOrphanCleanup", "Effect": "Allow", "Action": ["ec2:DeleteVolume", "ec2:CreateSnapshot"], "Resource": "*"}For autoscaling policy management:
{ "Sid": "AutoscalerPolicyMgmt", "Effect": "Allow", "Action": [ "autoscaling:UpdateAutoScalingGroup", "autoscaling:PutScalingPolicy", "autoscaling:DeletePolicy", "autoscaling:PutScheduledUpdateGroupAction", "autoscaling:DeleteScheduledAction", "application-autoscaling:Put*", "application-autoscaling:Delete*", "application-autoscaling:Register*", "application-autoscaling:Deregister*" ], "Resource": "*"}The shape is the same every time: a named list of actions, optionally narrowed by tag, and your policy is what decides. Anything ZopNight asks for passes through AWS’s own permission check before it reaches a resource.
Saying who may borrow the role
The role also carries a short document naming who is allowed to assume it:
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::<zopnight-account>:role/discovery-role"}, "Action": "sts:AssumeRole", "Condition": { "StringEquals": {"sts:ExternalId": "<unique-external-id>"} } }]}The external ID is not optional. ZopNight generates a different one for every organisation, and the role will not be assumed without it.
It exists to stop a specific problem. Without it, knowing ZopNight’s account number would be enough for one customer to talk ZopNight into reaching into another customer’s account. The shared name for that problem is the confused deputy: the deputy has the authority, and somebody else decides how it gets used. The external ID means every request has to prove which account it is for.
Why it is not simply read-only
A purely read-only role means no schedules, no automatic fixes, no scaling changes. That is a reporting tool, and reporting is the thing you already have.
So the honest description is read-mostly. Reading is the floor. Every ability to change something sits above it and is added one at a time, by you.
Most customers start in monitor mode and turn on the write permissions gradually, as they get comfortable with what the product does. That order is deliberate, and it is the same pattern the Engineer track calls progressive autonomy.
When you have many AWS accounts
Large companies run an AWS Organization with dozens or hundreds of accounts under it. You do not set each one up by hand.
- The role is rolled out to every member account using a CloudFormation StackSet, which is AWS’s way of applying one template across many accounts at once
- Every copy carries the same role name and the same external ID for your ZopNight organisation
- ZopNight lists each member account separately, all under one organisation
This works at 100 accounts as easily as at 3, and ZopNight gives you the template.
2. Demo
A typical AWS connection walkthrough:
T+0 Customer opens ZopNight → Cloud Accounts → Add AWS AccountT+1 min ZopNight displays the IAM trust policy + permissions templateT+1 min Customer copies the CloudFormation template URLT+2 min Customer launches in AWS CloudFormation ConsoleT+3 min Stack creates the IAM roleT+4 min Customer pastes role ARN + external ID back to ZopNightT+4 min ZopNight validates via STS AssumeRoleT+5 min Account marked Active; discovery preview beginsFive minutes from start to active. The CloudFormation template eliminates manual IAM policy editing.
3. Hands-on (6 min)
Connect an AWS account (or audit one already connected):
1. In ZopNight, navigate to Cloud Accounts → Add AWS Account.2. Note the offered CloudFormation template URL.3. In AWS Console (a sandbox account, not production), launch the stack.4. Inspect the IAM role created. Verify: - Every Action ends with Describe, Get, List, or Lookup (unless you elected to enable scoped writes) - The trust policy includes the external ID - The role name matches the ZopNight-published convention5. Paste the role ARN back to ZopNight; verify connection in <60 seconds.6. After connection, open Permission Visibility (covered in L4) and confirm Granted across the expected service categories.4. Knowledge check
Q1
The AWS IAM role for ZopNight discovery is described as “read-mostly” because:
A. The baseline is read-only across all services needed for discovery, metrics, billing, and recommendations
B. It uses an old API
C. It only has around 50% of the permissions that it actually needs in order to function correctly
D. It can only read every other request
Show answer
Correct: A. Scoped writes (StartInstances, StopInstances, DeleteVolume, etc.) are opt-in extensions added by the customer to unlock execution. Read-mostly = read-only baseline + opt-in scoped writes. The customer controls which extensions to grant.
Q2
The sts:ExternalId condition in the trust policy primarily defends against:
A. SQL injection through the role name, which the condition escapes before the whole policy statement is evaluated
B. Phishing
C. The “confused deputy” attack: where one customer’s account could trick ZopNight into accessing another customer’s role
D. DoS
Show answer
Correct: C. The unique external ID per organization makes this structurally impossible. External ID is the canonical defense against the confused deputy pattern in cross-account assume-role. AWS documentation calls this out explicitly.
Q3
A team wants ZopNight to schedule EC2 instances tagged environment=dev but never anything tagged environment=prod. The defensible IAM configuration:
A. Grant ec2:StartInstances and ec2:StopInstances with a Condition requiring aws:ResourceTag/environment to be in [“dev”, “test”, “stage”]
B. Don’t grant EC2 start/stop permissions
C. Rely on ZopNight never requesting anything in production, since the product does not target production resources by default
D. Use a separate AWS account for dev
Show answer
Correct: A. The IAM denies any prod-tagged request before ZopNight even attempts the call. Tag-based IAM conditions are the right tool for this. The defense is at the cloud provider’s policy layer, not at the application layer. Even a buggy ZopNight call cannot stop a prod instance if the IAM forbids it.
5. Apply
ZopNight publishes the current policy and the CloudFormation template at:
- Cloud Accounts → Add AWS Account in the product
- Documentation for offline review
For organizations adopting StackSet deployment across many accounts:
- The CloudFormation template is StackSet-compatible
- The external ID is the same across all member accounts in one ZopNight organization
- Best practice: deploy via StackSet to a defined OU rather than per-account manual setup
Related lessons
- L3: GCP & Azure credentials (next)
- L4: Permission Visibility
- T2.M2.5: Adopt-or-replace existing cloud scaling
Glossary terms touched
IAM role · Assume-role · External ID · Confused deputy · CloudFormation StackSet