What Strays installs
Strays needs a role in your AWS account, so you should be able to read every line of it first, check it hasn't changed, and remove it in one click whenever you like. Nothing here is a summary: these are the files themselves.
Removing Strays is deleting one CloudFormation stack (strays-audit, or strays-audit-organization for a whole organisation). Strays can't delete the role. You remove it by deleting the stack, and access stops at once. Signed-in customers get the console link and commands for each account under Remove Strays from AWS. More on the security page.
What Strays cannot do
The role is read-only. With it, Strays cannot:
- Read what is inside your S3 buckets: no object, file or backup contents, only bucket settings such as public-access blocks and lifecycle rules.
- Read the values of your secrets or parameters (Secrets Manager values, Parameter Store values), or decrypt anything with your KMS keys.
- Read the contents of your databases or queues (RDS, DynamoDB, SQS), or your log messages.
- Create, change, stop, start or delete anything in your account: no instances, volumes, snapshots, users, keys, policies or settings.
- Change its own permissions, or be used by anyone except two named Strays roles (the scanner and the web app) with your external ID.
This isn't only a promise: our tests fail if any action in the template creates, changes or deletes something, or reads the contents of your data. The one action that isn't a pure read is iam:GenerateCredentialReport, which asks IAM to prepare the credential report it then reads. One optional extra is described under the permissions: it exists only if you choose to give Strays your Terraform state bucket.
One thing to know: for three of its listing calls, AWS sends back more than Strays uses: Lambda environment variables (lambda:ListFunctions) and instance user data (ec2:DescribeLaunchTemplateVersions, autoscaling:DescribeLaunchConfigurations). Strays reads only names, runtimes and image IDs from them and never keeps the rest; our tests scan an account that has secrets in both places and check they are stored nowhere. Those three actions are what the outdated-runtime, public-Lambda-URL and unused-image checks need; you can remove them from the template, and those checks then can't run. Keep secrets in Secrets Manager or Parameter Store rather than in environment variables or user data in any case.
Who can use the role
The role's trust policy lets exactly two things assume it: Strays' scanner role (strays-prod-worker) and Strays' web role (strays-prod-web, which checks the connection), and only when the request carries your external ID, a random value unique to your organisation. Anyone else, including another Strays customer, is refused. This is the protection AWS recommends against the "confused deputy" problem: even if someone learned your role's name, they could not use it without the external ID. Naming the two roles, not the whole account, means no other identity in Strays' AWS account (another function, a user or a future service) can assume it; the hourly scheduler, for one, has no way to. The role names are parameters of the template, with those defaults. The trust policy in the template is:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS:
- arn:aws:iam::<Strays' account ID>:role/strays-prod-worker
- arn:aws:iam::<Strays' account ID>:role/strays-prod-web
Action: sts:AssumeRole
Condition:
StringEquals:
sts:ExternalId: <your external ID>
Every time Strays uses the role, AWS records an AssumeRole event in your CloudTrail, so you can see when it was used.
Every permission, and why
These 89 actions are read from the role template below each time this page is built, so the list can't drift from the role. All of them describe or list things, or read settings and metrics.
| Action | Why Strays needs it |
|---|---|
| acm (2) | |
| acm:DescribeCertificate | Reads a certificate's expiry date and use. |
| acm:ListCertificates | Lists TLS certificates, to warn before they expire. |
| autoscaling (1) | |
| autoscaling:DescribeLaunchConfigurations | Finds old launch configurations that nothing uses. AWS includes the user data in this response; Strays reads only the image ID and never stores the rest. |
| backup (3) | |
| backup:ListBackupPlans | Lists backup plans (settings only). |
| backup:ListBackupVaults | Lists backup vaults, to check important data is backed up. |
| backup:ListTags | Reads backup tags, to find the owner. |
| budgets (1) | |
| budgets:ViewBudget | Reads AWS Budgets you already have, to compare with spend. |
| ce (3) | |
| ce:GetCostAndUsage | Reads your spend totals for budgets and cost by tag. |
| ce:GetSavingsPlansPurchaseRecommendation | Reads AWS's own savings suggestions, so Strays can show you them. |
| ce:ListCostAllocationTags | Checks which cost tags are active, for cost by tag. |
| cloudtrail (3) | |
| cloudtrail:DescribeTrails | Checks that CloudTrail logging is switched on. |
| cloudtrail:GetTrailStatus | Checks CloudTrail is actually logging. |
| cloudtrail:LookupEvents | Finds who created a resource, from CloudTrail's event history. |
| cloudwatch (2) | |
| cloudwatch:DescribeAlarms | Checks that important resources have alarms. |
| cloudwatch:GetMetricData | Reads usage numbers (CPU, connections, traffic) to tell idle from busy. |
| ec2 (14) | |
| ec2:DescribeAddresses | Finds Elastic IP addresses that aren't attached (they cost money). |
| ec2:DescribeImages | Finds machine images (AMIs) nobody uses, and ones shared publicly. |
| ec2:DescribeInstances | Finds stopped, idle or unowned servers and who launched them. |
| ec2:DescribeLaunchTemplateVersions | Checks which launch template versions are still referenced. AWS includes each version's user data in this response; Strays reads only the image ID and never stores the rest. |
| ec2:DescribeLaunchTemplates | Checks which launch templates are still referenced. |
| ec2:DescribeNatGateways | Finds NAT gateways, which cost money every hour, and checks they are used. |
| ec2:DescribeNetworkInterfaces | Finds network interfaces that are unused or exposed. |
| ec2:DescribeRegions | Lists the regions your account uses, so every region is scanned. |
| ec2:DescribeSecurityGroups | Finds firewall rules open to the whole internet. |
| ec2:DescribeSnapshotAttribute | Checks whether a disk snapshot is shared publicly. |
| ec2:DescribeSnapshots | Finds old and orphaned disk snapshots, and their size. |
| ec2:DescribeVolumes | Finds disks (EBS volumes) not attached to anything, a common source of waste. |
| ec2:DescribeVpcEndpoints | Finds VPC endpoints, which cost money every hour. |
| ec2:GetEbsEncryptionByDefault | Checks that new disks are encrypted by default. |
| ecr (4) | |
| ecr:DescribeImages | Finds old, large container images. |
| ecr:DescribeRepositories | Lists container repositories, for the asset register. |
| ecr:GetLifecyclePolicy | Checks a repository cleans up old images. |
| ecr:ListTagsForResource | Reads repository tags, to find the owner. |
| eks (2) | |
| eks:DescribeCluster | Checks a cluster's settings, such as a public endpoint. |
| eks:ListClusters | Finds Kubernetes clusters, which cost money every hour. |
| elasticloadbalancing (5) | |
| elasticloadbalancing:DescribeListeners | Checks load balancers for unencrypted (HTTP) listeners. |
| elasticloadbalancing:DescribeLoadBalancers | Finds load balancers, which cost money, and whether they are public. |
| elasticloadbalancing:DescribeTags | Reads load balancer tags, to find the owner. |
| elasticloadbalancing:DescribeTargetGroups | Finds load balancers with nothing behind them. |
| elasticloadbalancing:DescribeTargetHealth | Checks whether anything healthy is behind a load balancer. |
| guardduty (2) | |
| guardduty:GetDetector | Checks the GuardDuty detector is enabled. |
| guardduty:ListDetectors | Checks that GuardDuty threat detection is on. |
| iam (21) | |
| iam:GenerateCredentialReport | Asks IAM to prepare its credential report. The only action that isn't a pure read, and it changes nothing in your account. |
| iam:GetAccessKeyLastUsed | Shows when an access key was last used, to find stale keys. |
| iam:GetAccountPasswordPolicy | Checks the password policy is strong enough. |
| iam:GetAccountSummary | Checks account-level security facts, such as root user MFA. |
| iam:GetCredentialReport | Reads IAM's credential report: users, MFA status and key dates, never passwords or keys. |
| iam:GetLoginProfile | Shows whether a user has a console password (never the password). |
| iam:GetRole | Reads a role's trust policy and last use. |
| iam:GetUser | Reads a user's details and tags, for leaver checks. |
| iam:ListAccessKeys | Lists access key IDs and ages (never the secret keys). |
| iam:ListAttachedGroupPolicies | Shows which policies a group has. |
| iam:ListAttachedRolePolicies | Shows which policies a role has, to spot admin access. |
| iam:ListAttachedUserPolicies | Shows which policies a user has, to spot admin access. |
| iam:ListGroupsForUser | Shows a user's groups, to spot inherited admin access. |
| iam:ListMFADevices | Shows whether a user has MFA. |
| iam:ListPolicies | Lists policies in the account, for the asset register. |
| iam:ListPolicyTags | Reads policy tags, to find the owner. |
| iam:ListRoleTags | Reads role tags, to find the owner. |
| iam:ListRoles | Lists roles, to find ones that are unused or too trusting. |
| iam:ListSSHPublicKeys | Lists SSH public keys a user has uploaded (public keys only). |
| iam:ListUserTags | Reads user tags, to find the owner. |
| iam:ListUsers | Lists IAM users, for access reviews and leaver checks. |
| kms (3) | |
| kms:DescribeKey | Reads a key's settings, such as rotation and whether it is scheduled for deletion. |
| kms:ListKeys | Lists encryption keys (it never uses them), for the asset register. |
| kms:ListResourceTags | Reads key tags, to find the owner. |
| lambda (3) | |
| lambda:ListFunctionUrlConfigs | Finds functions exposed on a public URL without authentication. |
| lambda:ListFunctions | Lists functions and their settings. AWS includes environment variables in this response; Strays ignores them and never stores them. |
| lambda:ListTags | Reads function tags, to find the owner. |
| logs (2) | |
| logs:DescribeLogGroups | Finds log groups that keep data forever (never the log messages). |
| logs:ListTagsForResource | Reads log group tags, to find the owner. |
| rds (7) | |
| rds:DescribeDBClusterSnapshotAttributes | Checks whether a cluster snapshot is shared publicly. |
| rds:DescribeDBClusterSnapshots | Finds old database cluster snapshots. |
| rds:DescribeDBClusters | Finds database clusters and their encryption and backup settings. |
| rds:DescribeDBInstanceAutomatedBackups | Finds automated backups left behind by deleted databases. |
| rds:DescribeDBInstances | Finds idle or unencrypted databases and public access settings. |
| rds:DescribeDBSnapshotAttributes | Checks whether a database snapshot is shared publicly. |
| rds:DescribeDBSnapshots | Finds old database snapshots (it lists them, never their contents). |
| route53domains (1) | |
| route53domains:ListDomains | Lists registered domains, for the asset register and expiry warnings. |
| s3 (8) | |
| s3:GetAccountPublicAccessBlock | Checks the account-wide block on public buckets. |
| s3:GetBucketAcl | Checks whether a bucket's access list makes it public. |
| s3:GetBucketLocation | Finds which region a bucket is in. |
| s3:GetBucketPolicyStatus | Checks whether a bucket policy makes it public. |
| s3:GetBucketTagging | Reads bucket tags, to find the owner. |
| s3:GetLifecycleConfiguration | Checks whether a bucket cleans up old data (a setting, not the data). |
| s3:ListAllMyBuckets | Lists bucket names, for the asset register and public-bucket checks. |
| s3:ListBucketMultipartUploads | Finds abandoned partial uploads that still cost money. |
| secretsmanager (1) | |
| secretsmanager:ListSecrets | Lists secret names and dates, to find unused ones. It cannot return a secret's value. |
| sts (1) | |
| sts:GetCallerIdentity | Confirms which account the role belongs to, so scans never run against the wrong one. |
Extra, in the management account only (AWS Organizations)
If you connect a whole organisation, the stack adds these to the role in the management account so Strays can list your accounts:
- organizations:DescribeOrganization: Confirms the account is the organization's management account.
- organizations:ListAccounts: Lists your organization's accounts (ID, name, status), so each can be connected.
Optional: your Terraform state bucket
Off unless you type a bucket name when you install the role. Then, and only then, the role can read that one bucket (and decrypt with the one key you name), so Strays can tell resources made in code from ones made by hand. State files can contain sensitive values, so leave this empty if you'd rather not.
- kms:Decrypt: Decrypts those state files, only if you give a key for that bucket.
- s3:GetObject: Reads the Terraform state files in that one bucket, to tell resources made in code from ones made by hand.
- s3:ListBucket: Lists the files in your Terraform state bucket, that bucket only.
The templates, word for word
Exactly what is installed in your account, shown as served and escaped, so nothing is hidden. Each can be downloaded, and its SHA-256 is below so you can check your copy.
The role (CloudFormation)
Creates the read-only strays-audit role in one AWS account. This is the template the one-click link opens.
Download strays-audit-role.yaml (7876 bytes)
SHA-256: e6c70fb8eb6cf15cf6f72a4395cac018c545dca2dd95654acc5fef4b1accc324
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Read-only IAM role for a Strays AWS audit (waste and security). It can only describe resources,
read metrics and read the IAM credential report; it cannot change or delete anything.
Delete this stack when the audit is finished.
Parameters:
TrustedAccountId:
Type: String
Description: AWS account ID of the auditor who will assume this role.
AllowedPattern: "^[0-9]{12}$"
ConstraintDescription: A 12-digit AWS account ID.
ExternalId:
Type: String
Description: The external ID the auditor gave you. Stops anyone else using this role.
MinLength: 8
MaxLength: 1224
AllowedPattern: "^[\\w+=,.@:/-]+$"
WorkerRoleName:
Type: String
Default: strays-prod-worker
Description: >-
Name of the auditor's scanning role in the auditor's account. Only this role and the web
role below can assume this one. Leave the default unless the auditor says otherwise.
AllowedPattern: "^[\\w+=,.@-]{1,64}$"
WebRoleName:
Type: String
Default: strays-prod-web
Description: >-
Name of the auditor's web role (connection checks) in the auditor's account.
AllowedPattern: "^[\\w+=,.@-]{1,64}$"
RoleName:
Type: String
Default: strays-audit
Description: Name for the role.
TerraformStateBucket:
Type: String
Default: ""
Description: >-
Optional. S3 bucket holding Terraform state, so the audit can tell managed resources from
ones created by hand. Leave empty to skip.
TerraformStateKmsKeyArn:
Type: String
Default: ""
Description: Optional. KMS key ARN if that bucket uses a customer-managed key.
Conditions:
HasStateBucket: !Not [!Equals [!Ref TerraformStateBucket, ""]]
HasStateKmsKey: !Not [!Equals [!Ref TerraformStateKmsKeyArn, ""]]
Resources:
AuditRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Ref RoleName
Description: Read-only access for a Strays AWS audit (waste and security)
MaxSessionDuration: 3600
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS:
- !Sub "arn:${AWS::Partition}:iam::${TrustedAccountId}:role/${WorkerRoleName}"
- !Sub "arn:${AWS::Partition}:iam::${TrustedAccountId}:role/${WebRoleName}"
Action: sts:AssumeRole
Condition:
StringEquals:
sts:ExternalId: !Ref ExternalId
Policies:
- PolicyName: costwatch-read-only
PolicyDocument:
Version: "2012-10-17"
Statement:
# `costwatch policy` plus strays.risks.RISK_ACTIONS; tests/test_onboarding.py keeps them in sync
- Sid: CostwatchReadOnly
Effect: Allow
Action:
- sts:GetCallerIdentity
- ec2:DescribeRegions
- ec2:DescribeVolumes
- ec2:DescribeSnapshots
- ec2:DescribeImages
- ec2:DescribeAddresses
- ec2:DescribeInstances
- ec2:DescribeNatGateways
- ec2:DescribeLaunchTemplates
- ec2:DescribeLaunchTemplateVersions
- autoscaling:DescribeLaunchConfigurations
- elasticloadbalancing:DescribeLoadBalancers
- elasticloadbalancing:DescribeTargetGroups
- elasticloadbalancing:DescribeTargetHealth
- elasticloadbalancing:DescribeTags
- rds:DescribeDBSnapshots
- rds:DescribeDBClusterSnapshots
- rds:DescribeDBInstances
- rds:DescribeDBInstanceAutomatedBackups
- cloudwatch:GetMetricData
- logs:DescribeLogGroups
- cloudtrail:LookupEvents
# Security risk checks
- iam:GetAccountSummary
- iam:GenerateCredentialReport
- iam:GetCredentialReport
- ec2:DescribeSecurityGroups
- ec2:DescribeNetworkInterfaces
- ec2:DescribeSnapshotAttribute
- rds:DescribeDBSnapshotAttributes
- rds:DescribeDBClusterSnapshotAttributes
- s3:GetAccountPublicAccessBlock
- s3:ListAllMyBuckets
- s3:GetBucketLocation
- s3:GetBucketPolicyStatus
- s3:GetBucketAcl
- ec2:GetEbsEncryptionByDefault
- cloudtrail:DescribeTrails
- cloudtrail:GetTrailStatus
- guardduty:ListDetectors
- guardduty:GetDetector
- rds:DescribeDBClusters
- lambda:ListFunctions
- eks:ListClusters
- eks:DescribeCluster
- ec2:DescribeVpcEndpoints
- iam:ListRoles
- iam:GetRole
- iam:ListUsers
- iam:ListAttachedUserPolicies
- iam:ListGroupsForUser
- iam:ListAttachedGroupPolicies
- iam:GetAccountPasswordPolicy
- lambda:ListFunctionUrlConfigs
- elasticloadbalancing:DescribeListeners
- acm:ListCertificates
- acm:DescribeCertificate
- route53domains:ListDomains
- budgets:ViewBudget
- cloudwatch:DescribeAlarms
- s3:GetLifecycleConfiguration
- s3:ListBucketMultipartUploads
- kms:ListKeys
- kms:DescribeKey
- secretsmanager:ListSecrets
- ecr:DescribeRepositories
- ecr:GetLifecyclePolicy
- ecr:DescribeImages
- ce:GetSavingsPlansPurchaseRecommendation
- ce:GetCostAndUsage
- ce:ListCostAllocationTags
- iam:ListAttachedRolePolicies
- iam:GetUser
- iam:ListAccessKeys
- iam:GetAccessKeyLastUsed
- iam:GetLoginProfile
- iam:ListMFADevices
- iam:ListSSHPublicKeys
# Inventory and tagging (strays.tagging)
- lambda:ListTags
- kms:ListResourceTags
- ecr:ListTagsForResource
- logs:ListTagsForResource
- s3:GetBucketTagging
- backup:ListBackupVaults
- backup:ListBackupPlans
- backup:ListTags
- iam:ListUserTags
- iam:ListRoleTags
- iam:ListPolicies
- iam:ListPolicyTags
Resource: "*"
Tags:
- Key: purpose
Value: strays-audit
- Key: managed-by
Value: strays
TerraformStateRead:
Type: AWS::IAM::RolePolicy
Condition: HasStateBucket
Properties:
RoleName: !Ref AuditRole
PolicyName: terraform-state-read
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: s3:ListBucket
Resource: !Sub "arn:${AWS::Partition}:s3:::${TerraformStateBucket}"
- Effect: Allow
Action: s3:GetObject
Resource: !Sub "arn:${AWS::Partition}:s3:::${TerraformStateBucket}/*"
- !If
- HasStateKmsKey
- Effect: Allow
Action: kms:Decrypt
Resource: !Ref TerraformStateKmsKeyArn
- !Ref AWS::NoValue
Outputs:
RoleArn:
Description: Send this to the auditor.
Value: !GetAtt AuditRole.Arn
The organization template (CloudFormation)
For a whole AWS Organization: the same role in the management account, permission to list your accounts, and a StackSet that creates the same role in the member accounts.
Download strays-audit-organization.yaml (6181 bytes)
SHA-256: 7f39a3cd6b5809e5a6bc59510d1aaba70394a81f2f0453049ca07d75f23a1aa8
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Connects a whole AWS Organization to a Strays audit. Run it once, in the management account.
It creates the read-only strays-audit role in this account, lets Strays list the
organization's accounts, and creates a service-managed StackSet that puts the same role in
every member account of the organizational units you choose, now and as accounts are added.
Delete this stack when the audit is finished.
Parameters:
TrustedAccountId:
Type: String
Description: AWS account ID of the auditor who will assume the roles.
AllowedPattern: "^[0-9]{12}$"
ConstraintDescription: A 12-digit AWS account ID.
ExternalId:
Type: String
Description: The external ID the auditor gave you. Stops anyone else using the roles.
MinLength: 8
MaxLength: 1224
AllowedPattern: "^[\\w+=,.@:/-]+$"
OrganizationalUnitIds:
Type: CommaDelimitedList
Description: >-
Where the role goes: organizational unit IDs (ou-xxxx-xxxxxxxx) separated by commas, or the
root ID (r-xxxx) for every account in the organization.
AllowedPattern: "^(r-[0-9a-z]{4,32}|ou-[0-9a-z]{4,32}-[0-9a-z]{8,32})$"
ConstraintDescription: Organizational unit IDs (ou-...) or the root ID (r-...).
RoleTemplateUrl:
Type: String
Description: >-
Address of the Strays role template (strays-audit-role.yaml), used for this account and
for every member account, so all of them get exactly the same role.
AllowedPattern: "^https://.+"
WorkerRoleName:
Type: String
Default: strays-prod-worker
Description: Name of the auditor's scanning role; with the web role, the only roles that can assume these.
AllowedPattern: "^[\\w+=,.@-]{1,64}$"
WebRoleName:
Type: String
Default: strays-prod-web
Description: Name of the auditor's web role (connection checks).
AllowedPattern: "^[\\w+=,.@-]{1,64}$"
RoleName:
Type: String
Default: strays-audit
Description: Name for the role in every account.
ManagementRole:
Type: String
Default: create
AllowedValues: [create, existing]
Description: >-
"existing" if this management account was connected to Strays on its own first, so it
already has the role (from the strays-audit-role stack); the role is then left as it is.
TerraformStateBucket:
Type: String
Default: ""
Description: Optional. S3 bucket holding Terraform state. Leave empty to skip.
TerraformStateKmsKeyArn:
Type: String
Default: ""
Description: Optional. KMS key ARN if that bucket uses a customer-managed key.
Conditions:
CreateManagementRole: !Equals [!Ref ManagementRole, create]
Resources:
# StackSets with service-managed permissions never deploy to the management account, so this
# account gets the same role from the same template as a nested stack
ManagementAuditRole:
Type: AWS::CloudFormation::Stack
Condition: CreateManagementRole
Properties:
TemplateURL: !Ref RoleTemplateUrl
Parameters:
TrustedAccountId: !Ref TrustedAccountId
ExternalId: !Ref ExternalId
WorkerRoleName: !Ref WorkerRoleName
WebRoleName: !Ref WebRoleName
RoleName: !Ref RoleName
TerraformStateBucket: !Ref TerraformStateBucket
TerraformStateKmsKeyArn: !Ref TerraformStateKmsKeyArn
Tags:
- Key: purpose
Value: strays-audit
- Key: managed-by
Value: strays
# Added to this account's role: the one created above, or the one already there
ListAccounts:
Type: AWS::IAM::RolePolicy
Properties:
RoleName: !If
- CreateManagementRole
- !Select [1, !Split ["/", !GetAtt ManagementAuditRole.Outputs.RoleArn]]
- !Ref RoleName
PolicyName: organization-read
PolicyDocument:
Version: "2012-10-17"
Statement:
# Only the list of accounts (ID, name, status); nothing about policies or contents
- Sid: OrganizationRead
Effect: Allow
Action:
- organizations:DescribeOrganization
- organizations:ListAccounts
Resource: "*"
MemberAuditRoles:
Type: AWS::CloudFormation::StackSet
Properties:
StackSetName: strays-audit-members
Description: Read-only strays-audit role for a Strays audit, in every member account
# Carried to every member account's stack, so it's clear in each account who made it
Tags:
- Key: purpose
Value: strays-audit
- Key: managed-by
Value: strays
PermissionModel: SERVICE_MANAGED
AutoDeployment:
Enabled: true
RetainStacksOnAccountRemoval: false
Capabilities:
- CAPABILITY_NAMED_IAM
ManagedExecution:
Active: true
OperationPreferences:
RegionConcurrencyType: PARALLEL
MaxConcurrentPercentage: 100
FailureTolerancePercentage: 10
TemplateURL: !Ref RoleTemplateUrl
Parameters:
- ParameterKey: TrustedAccountId
ParameterValue: !Ref TrustedAccountId
- ParameterKey: ExternalId
ParameterValue: !Ref ExternalId
- ParameterKey: WorkerRoleName
ParameterValue: !Ref WorkerRoleName
- ParameterKey: WebRoleName
ParameterValue: !Ref WebRoleName
- ParameterKey: RoleName
ParameterValue: !Ref RoleName
- ParameterKey: TerraformStateBucket
ParameterValue: !Ref TerraformStateBucket
- ParameterKey: TerraformStateKmsKeyArn
ParameterValue: !Ref TerraformStateKmsKeyArn
StackInstancesGroup:
- DeploymentTargets:
OrganizationalUnitIds: !Ref OrganizationalUnitIds
Regions:
- !Ref AWS::Region
Outputs:
ManagementAccountId:
Description: Enter this in Strays as the management account.
Value: !Ref AWS::AccountId
RoleArn:
Description: The role in this (management) account.
Value: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${RoleName}"
StackSetName:
Description: The StackSet that creates the role in the member accounts.
Value: !Ref MemberAuditRoles
The Terraform module (main.tf)
The same role for teams that use Terraform, with the same actions and the same trust policy (the two Strays roles named above).
Download strays-audit-terraform-main.tf (5195 bytes)
SHA-256: ab3d18c792787da044e0daf5e9bb575c6df83a3737188ec7e2c2d93b4cf14cb4
data "aws_partition" "current" {}
locals {
# `costwatch policy` plus strays.risks.RISK_ACTIONS; tests/test_onboarding.py keeps them in sync
read_only_actions = [
"sts:GetCallerIdentity",
"ec2:DescribeRegions",
"ec2:DescribeVolumes",
"ec2:DescribeSnapshots",
"ec2:DescribeImages",
"ec2:DescribeAddresses",
"ec2:DescribeInstances",
"ec2:DescribeNatGateways",
"ec2:DescribeLaunchTemplates",
"ec2:DescribeLaunchTemplateVersions",
"autoscaling:DescribeLaunchConfigurations",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:DescribeTags",
"rds:DescribeDBSnapshots",
"rds:DescribeDBClusterSnapshots",
"rds:DescribeDBInstances",
"rds:DescribeDBInstanceAutomatedBackups",
"cloudwatch:GetMetricData",
"logs:DescribeLogGroups",
"cloudtrail:LookupEvents",
# Security risk checks
"iam:GetAccountSummary",
"iam:GenerateCredentialReport",
"iam:GetCredentialReport",
"ec2:DescribeSecurityGroups",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSnapshotAttribute",
"rds:DescribeDBSnapshotAttributes",
"rds:DescribeDBClusterSnapshotAttributes",
"s3:GetAccountPublicAccessBlock",
"s3:ListAllMyBuckets",
"s3:GetBucketLocation",
"s3:GetBucketPolicyStatus",
"s3:GetBucketAcl",
"ec2:GetEbsEncryptionByDefault",
"cloudtrail:DescribeTrails",
"cloudtrail:GetTrailStatus",
"guardduty:ListDetectors",
"guardduty:GetDetector",
"rds:DescribeDBClusters",
"lambda:ListFunctions",
"eks:ListClusters",
"eks:DescribeCluster",
"ec2:DescribeVpcEndpoints",
"iam:ListRoles",
"iam:GetRole",
"iam:ListUsers",
"iam:ListAttachedUserPolicies",
"iam:ListGroupsForUser",
"iam:ListAttachedGroupPolicies",
"iam:GetAccountPasswordPolicy",
"lambda:ListFunctionUrlConfigs",
"elasticloadbalancing:DescribeListeners",
"acm:ListCertificates",
"acm:DescribeCertificate",
"route53domains:ListDomains",
"budgets:ViewBudget",
"cloudwatch:DescribeAlarms",
"s3:GetLifecycleConfiguration",
"s3:ListBucketMultipartUploads",
"kms:ListKeys",
"kms:DescribeKey",
"secretsmanager:ListSecrets",
"ecr:DescribeRepositories",
"ecr:GetLifecyclePolicy",
"ecr:DescribeImages",
"ce:GetSavingsPlansPurchaseRecommendation",
"ce:GetCostAndUsage",
"ce:ListCostAllocationTags",
"iam:ListAttachedRolePolicies",
"iam:GetUser",
"iam:ListAccessKeys",
"iam:GetAccessKeyLastUsed",
"iam:GetLoginProfile",
"iam:ListMFADevices",
"iam:ListSSHPublicKeys",
# Inventory and tagging (strays.tagging)
"lambda:ListTags",
"kms:ListResourceTags",
"ecr:ListTagsForResource",
"logs:ListTagsForResource",
"s3:GetBucketTagging",
"backup:ListBackupVaults",
"backup:ListBackupPlans",
"backup:ListTags",
"iam:ListUserTags",
"iam:ListRoleTags",
"iam:ListPolicies",
"iam:ListPolicyTags",
]
}
data "aws_iam_policy_document" "trust" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = [
"arn:${data.aws_partition.current.partition}:iam::${var.trusted_account_id}:role/${var.worker_role_name}",
"arn:${data.aws_partition.current.partition}:iam::${var.trusted_account_id}:role/${var.web_role_name}",
]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
}
}
resource "aws_iam_role" "audit" {
name = var.role_name
description = "Read-only access for a Strays AWS audit (waste and security)"
assume_role_policy = data.aws_iam_policy_document.trust.json
max_session_duration = 3600
tags = var.tags
}
data "aws_iam_policy_document" "read_only" {
# checkov:skip=CKV_AWS_356:An account-wide audit must read every role, user, bucket and certificate; every action is read-only (enforced by tests)
statement {
sid = "CostwatchReadOnly"
actions = local.read_only_actions
resources = ["*"]
}
}
resource "aws_iam_role_policy" "read_only" {
name = "costwatch-read-only"
role = aws_iam_role.audit.id
policy = data.aws_iam_policy_document.read_only.json
}
data "aws_iam_policy_document" "state" {
count = var.terraform_state_bucket == null ? 0 : 1
statement {
actions = ["s3:ListBucket"]
resources = ["arn:${data.aws_partition.current.partition}:s3:::${var.terraform_state_bucket}"]
}
statement {
actions = ["s3:GetObject"]
resources = ["arn:${data.aws_partition.current.partition}:s3:::${var.terraform_state_bucket}/*"]
}
dynamic "statement" {
for_each = var.terraform_state_kms_key_arn == null ? [] : [var.terraform_state_kms_key_arn]
content {
actions = ["kms:Decrypt"]
resources = [statement.value]
}
}
}
resource "aws_iam_role_policy" "state" {
count = var.terraform_state_bucket == null ? 0 : 1
name = "terraform-state-read"
role = aws_iam_role.audit.id
policy = data.aws_iam_policy_document.state[0].json
}
Check the files yourself
Download a template and work out its SHA-256. It should match the value above. The one-click link loads the same files.
macOS or Linux:
shasum -a 256 strays-audit-role.yamlWindows (Command Prompt):
certutil -hashfile strays-audit-role.yaml SHA256Once installed, see exactly what the role in your account allows, straight from AWS:
aws iam get-role --role-name strays-audit
aws iam get-role-policy --role-name strays-audit --policy-name costwatch-read-only