Read-only by construction: the permission to break something was never granted.
- allowkostrix-scan-readonly (166)
- allowkostrix-scan-readonly-2 (153)
- allowkostrix-self-inspection ยท role (7)
- allowkostrix-self-inspection ยท access key (11)
- absentCreate* Delete* Modify* Put* Update* Terminate* Attach* Detach*
The read-only IAM policy, published in full for your security team.
Kostrix ships one CloudFormation stack. It creates two managed policies (IAM caps one policy document at 6,144 characters) and a small self-inspection policy on the role or user, and the application serves that template for download from the page where you link the account. Together they grant 334 actions. Nothing in them can create, modify or delete a resource, and no statement grants access to the contents of a bucket, table, queue, secret or log stream. Almost every action is a Describe, a List or a Get; the few reads that use another verb are listed by name under the policy, with the scope each is held to.
What is not in it
Verifiable from your side
Show the full policy (166 actions)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ComputeReadOnly",
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"ec2:GetReservedInstancesExchangeQuote",
"ec2:SearchTransitGatewayRoutes",
"elasticloadbalancing:Describe*",
"application-autoscaling:Describe*",
"ecs:Describe*",
"ecs:List*",
"eks:Describe*",
"eks:List*",
"lambda:List*",
"lambda:GetPolicy",
"lambda:GetFunctionRecursionConfig",
"batch:Describe*",
"lightsail:GetBundles",
"lightsail:GetDisks",
"lightsail:GetDiskSnapshots",
"lightsail:GetInstances",
"lightsail:GetInstanceMetricData",
"lightsail:GetInstanceSnapshots",
"lightsail:GetLoadBalancers",
"lightsail:GetOperationsForResource",
"lightsail:GetStaticIps",
"outposts:List*",
"workspaces:Describe*",
"appstream:DescribeFleets",
"gamelift:Describe*",
"gamelift:ListFleets",
"savingsplans:DescribeSavingsPlans"
],
"Resource": "*"
},
{
"Sid": "StorageReadOnly",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:GetBucketLocation",
"s3:GetBucketPolicy",
"s3:GetBucketVersioning",
"s3:GetBucketObjectLockConfiguration",
"s3:GetEncryptionConfiguration",
"s3:GetLifecycleConfiguration",
"s3:GetReplicationConfiguration",
"s3:GetIntelligentTieringConfiguration",
"s3:GetMetricsConfiguration",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts",
"s3:ListJobs",
"s3:DescribeJob",
"s3:ListStorageLensConfigurations",
"s3:GetStorageLensConfiguration",
"elasticfilesystem:Describe*",
"fsx:Describe*",
"storagegateway:DescribeGatewayInformation",
"storagegateway:DescribeCachediSCSIVolumes",
"storagegateway:DescribeStorediSCSIVolumes",
"storagegateway:DescribeTapes",
"storagegateway:ListFileShares",
"storagegateway:ListGateways",
"storagegateway:ListTapes",
"storagegateway:ListVolumes",
"backup:Describe*",
"backup:Get*",
"backup:List*",
"transfer:DescribeServer",
"transfer:ListServers"
],
"Resource": "*"
},
{
"Sid": "DatabaseReadOnly",
"Effect": "Allow",
"Action": [
"rds:Describe*",
"rds:ListTagsForResource",
"dynamodb:Describe*",
"dynamodb:List*",
"dax:DescribeClusters",
"elasticache:Describe*",
"memorydb:Describe*",
"redshift:Describe*",
"es:Describe*",
"es:List*",
"dms:Describe*",
"timestream:DescribeEndpoints",
"timestream:ListDatabases",
"timestream:ListTables",
"timestream-influxdb:ListDbInstances",
"mq:Describe*",
"mq:List*"
],
"Resource": "*"
},
{
"Sid": "KeyspacesSchemaReadOnly",
"Effect": "Allow",
"Action": [
"cassandra:Select"
],
"Resource": "arn:aws:cassandra:*:*:/keyspace/system*"
},
{
"Sid": "AnalyticsReadOnly",
"Effect": "Allow",
"Action": [
"athena:ListWorkGroups",
"athena:GetWorkGroup",
"athena:ListQueryExecutions",
"athena:BatchGetQueryExecution",
"glue:ListJobs",
"glue:GetJob",
"glue:GetJobRuns",
"glue:ListCrawlers",
"glue:GetCrawler",
"glue:GetCrawlerMetrics",
"glue:ListCrawls",
"glue:ListDataQualityRuleRecommendationRuns",
"glue:GetDataQualityRuleRecommendationRun",
"glue:ListDataQualityRulesetEvaluationRuns",
"glue:GetDataQualityRulesetEvaluationRun",
"elasticmapreduce:Describe*",
"elasticmapreduce:List*",
"elasticmapreduce:GetManagedScalingPolicy",
"emr-serverless:GetApplication",
"emr-serverless:ListApplications",
"emr-serverless:ListJobRuns",
"kinesis:Describe*",
"kinesis:List*",
"firehose:Describe*",
"firehose:List*",
"kinesisanalytics:DescribeApplication",
"kinesisanalytics:ListApplications",
"kinesisvideo:ListStreams",
"kafka:Describe*",
"kafka:List*",
"osis:GetPipeline",
"osis:ListPipelines",
"quicksight:List*",
"quicksight:Search*",
"quicksight:DescribeAccountSettings",
"quicksight:DescribeAnalysisDefinition",
"quicksight:DescribeDashboardDefinition",
"quicksight:DescribeDataSet",
"quicksight:DescribeDataSetPermissions",
"cloudsearch:DescribeDomains",
"airflow:GetEnvironment",
"airflow:ListEnvironments",
"appflow:DescribeFlow",
"appflow:DescribeFlowExecutionRecords",
"appflow:ListFlows"
],
"Resource": "*"
},
{
"Sid": "MachineLearningReadOnly",
"Effect": "Allow",
"Action": [
"sagemaker:Describe*",
"sagemaker:List*",
"bedrock:ListProvisionedModelThroughputs",
"comprehend:ListEndpoints",
"kendra:DescribeIndex",
"kendra:ListIndices",
"rekognition:Describe*",
"personalize:Describe*",
"personalize:List*",
"qbusiness:ListApplications",
"qbusiness:ListIndices",
"qbusiness:GetIndex",
"qbusiness:ListDataSources",
"qbusiness:ListDataSourceSyncJobs",
"qbusiness:ListSubscriptions"
],
"Resource": "*"
},
{
"Sid": "CostAndCoverageReadOnly",
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"ce:GetCostAndUsageWithResources",
"ce:GetDimensionValues",
"ce:GetReservationCoverage",
"ce:GetReservationUtilization",
"ce:GetSavingsPlansCoverage",
"ce:GetSavingsPlansUtilization",
"ce:GetSavingsPlansUtilizationDetails",
"ce:GetSavingsPlansPurchaseRecommendation",
"ce:GetReservationPurchaseRecommendation",
"ce:ListCostAllocationTags",
"ce:GetAnomalies",
"ce:GetAnomalyMonitors",
"ce:ListCostCategoryDefinitions",
"cost-optimization-hub:ListEnrollmentStatuses",
"cost-optimization-hub:ListRecommendations",
"cost-optimization-hub:ListRecommendationSummaries",
"cost-optimization-hub:ListEfficiencyMetrics",
"cost-optimization-hub:GetRecommendation",
"compute-optimizer:GetEnrollmentStatus",
"compute-optimizer:GetEC2InstanceRecommendations",
"compute-optimizer:GetEBSVolumeRecommendations",
"compute-optimizer:GetLambdaFunctionRecommendations",
"compute-optimizer:GetIdleRecommendations",
"pricing:GetProducts"
],
"Resource": "*"
},
{
"Sid": "IdentityCheck",
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity",
"iam:ListAccountAliases"
],
"Resource": "*"
}
]
}Show the full policy (153 actions)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "NetworkReadOnly",
"Effect": "Allow",
"Action": [
"cloudfront:Get*",
"cloudfront:List*",
"route53:Get*",
"route53:List*",
"route53resolver:List*",
"route53domains:ListDomains",
"route53-recovery-control-config:List*",
"route53-recovery-readiness:GetReadinessCheckStatus",
"route53-recovery-readiness:ListReadinessChecks",
"globalaccelerator:List*",
"directconnect:Describe*",
"networkmanager:GetCoreNetwork",
"networkmanager:ListAttachments",
"networkmanager:ListCoreNetworks",
"vpc-lattice:GetService",
"vpc-lattice:List*",
"appsync:GetGraphqlApi",
"appsync:ListGraphqlApis",
"ram:ListResources"
],
"Resource": "*"
},
{
"Sid": "ApiGatewayRestReadOnly",
"Effect": "Allow",
"Action": [
"apigateway:GET"
],
"Resource": [
"arn:aws:apigateway:*::/restapis*",
"arn:aws:apigateway:*::/usageplans*"
]
},
{
"Sid": "IntegrationReadOnly",
"Effect": "Allow",
"Action": [
"states:Describe*",
"states:List*",
"states:GetExecutionHistory",
"sqs:GetQueueAttributes",
"sqs:ListQueues",
"sns:GetTopicAttributes",
"sns:List*",
"events:DescribeArchive",
"events:ListArchives",
"events:ListReplays",
"ses:List*",
"ses:GetConfigurationSet",
"ses:GetDedicatedIps",
"ses:GetEmailIdentity",
"workmail:ListOrganizations",
"workmail:ListUsers",
"workmail:GetMailboxDetails",
"connect:ListInstances",
"connect:ListPhoneNumbersV2",
"amplify:ListApps",
"amplify:ListBranches",
"amplify:ListDomainAssociations",
"amplify:ListJobs",
"refactor-spaces:Get*",
"refactor-spaces:List*",
"codebuild:List*",
"codebuild:BatchGetBuilds",
"codebuild:BatchGetFleets",
"codebuild:BatchGetProjects",
"codepipeline:GetPipeline",
"codepipeline:ListActionExecutions",
"codepipeline:ListPipelines",
"cloudformation:Describe*",
"cloudformation:List*",
"medialive:Describe*",
"medialive:List*",
"mediaconvert:ListQueues",
"mediatailor:ListChannels",
"mediaconnect:DescribeFlow",
"mediaconnect:ListFlows",
"ivs:GetChannel",
"ivs:ListChannels",
"iot:DescribeSecurityProfile",
"iot:GetBehaviorModelTrainingSummaries",
"iot:GetIndexingConfiguration",
"iot:ListSecurityProfiles",
"iot:ListSecurityProfilesForTarget",
"iot:ListTargetsForSecurityProfile",
"iot:ListThingGroups",
"iotsitewise:DescribeStorageConfiguration",
"iotsitewise:ListTimeSeries",
"groundstation:GetMinuteUsage",
"groundstation:ListContacts"
],
"Resource": "*"
},
{
"Sid": "ObservabilityReadOnly",
"Effect": "Allow",
"Action": [
"cloudwatch:Describe*",
"cloudwatch:Get*",
"cloudwatch:List*",
"logs:Describe*",
"logs:ListLogAnomalyDetectors",
"xray:GetIndexingRules",
"xray:GetSamplingRules",
"xray:GetSamplingStatisticSummaries",
"xray:GetTraceSegmentDestination",
"rum:GetAppMonitor",
"rum:ListAppMonitors",
"application-signals:List*",
"grafana:DescribeWorkspace",
"grafana:ListPermissions",
"grafana:ListWorkspaces"
],
"Resource": "*"
},
{
"Sid": "SecurityServicesReadOnly",
"Effect": "Allow",
"Action": [
"acm:Describe*",
"acm:List*",
"acm-pca:ListCertificateAuthorities",
"kms:DescribeKey",
"kms:GetKeyRotationStatus",
"kms:ListKeyRotations",
"kms:ListKeys",
"secretsmanager:DescribeSecret",
"secretsmanager:GetResourcePolicy",
"secretsmanager:ListSecrets",
"cloudtrail:DescribeTrails",
"cloudtrail:GetEventDataStore",
"cloudtrail:GetEventSelectors",
"cloudtrail:GetInsightSelectors",
"cloudtrail:GetTrailStatus",
"cloudtrail:ListEventDataStores",
"cloudtrail:LookupEvents",
"config:Describe*",
"config:GetComplianceDetailsByConfigRule",
"config:GetDiscoveredResourceCounts",
"guardduty:DescribeOrganizationConfiguration",
"guardduty:GetCoverageStatistics",
"guardduty:GetDetector",
"guardduty:GetUsageStatistics",
"guardduty:ListDetectors",
"securityhub:Describe*",
"securityhub:GetEnabledStandards",
"securitylake:List*",
"detective:ListDatasourcePackages",
"detective:ListGraphs",
"inspector2:BatchGetAccountStatus",
"inspector2:ListCisScanConfigurations",
"inspector2:ListCoverageStatistics",
"macie2:DescribeBuckets",
"macie2:GetAutomatedDiscoveryConfiguration",
"macie2:GetClassificationScope",
"macie2:GetMacieSession",
"macie2:GetUsageTotals",
"macie2:ListClassificationScopes",
"wafv2:GetWebACL",
"wafv2:ListResourcesForWebACL",
"wafv2:ListWebACLs",
"shield:DescribeSubscription",
"shield:GetSubscriptionState",
"shield:ListProtections",
"access-analyzer:GetAnalyzer",
"access-analyzer:ListAnalyzers",
"auditmanager:GetAssessment",
"auditmanager:GetInsightsByAssessment",
"auditmanager:ListAssessmentReports",
"auditmanager:ListAssessments",
"resiliencehub:Describe*",
"resiliencehub:List*",
"license-manager-user-subscriptions:List*"
],
"Resource": "*"
},
{
"Sid": "IdentityInventoryReadOnly",
"Effect": "Allow",
"Action": [
"iam:ListRoles",
"iam:ListUsers",
"iam:GetCredentialReport"
],
"Resource": "*"
},
{
"Sid": "CoverageTailReadOnly",
"Effect": "Allow",
"Action": [
"cloudhsm:DescribeClusters",
"ds:DescribeDirectories",
"cognito-idp:ListUserPools",
"cognito-idp:DescribeUserPool",
"devops-guru:DescribeAccountHealth",
"devops-guru:DescribeAccountOverview",
"aoss:ListCollections",
"aoss:BatchGetCollection"
],
"Resource": "*"
}
]
}Show the full policy (7 actions)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ConfirmReadOnly",
"Effect": "Allow",
"Action": [
"iam:SimulatePrincipalPolicy",
"iam:GetRole",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
"iam:GetRolePolicy"
],
"Resource": "arn:aws:iam::<your-account-id>:role/kostrix-scan-role"
},
{
"Sid": "ReadOwnManagedPolicies",
"Effect": "Allow",
"Action": [
"iam:GetPolicy",
"iam:GetPolicyVersion"
],
"Resource": "*"
}
]
}Show the full policy (11 actions)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ConfirmReadOnly",
"Effect": "Allow",
"Action": [
"iam:SimulatePrincipalPolicy",
"iam:GetUser",
"iam:ListAttachedUserPolicies",
"iam:ListUserPolicies",
"iam:GetUserPolicy",
"iam:ListGroupsForUser"
],
"Resource": "arn:aws:iam::<your-account-id>:user/kostrix-scan"
},
{
"Sid": "ReadOwnGroupAndManagedPolicies",
"Effect": "Allow",
"Action": [
"iam:ListAttachedGroupPolicies",
"iam:ListGroupPolicies",
"iam:GetGroupPolicy",
"iam:GetPolicy",
"iam:GetPolicyVersion"
],
"Resource": "*"
}
]
}Generated from kostrix-scan-policy.yaml, the exact template that travels with the release and is downloadable from the Connect page inside the application. The template's own comments explain each statement.
The 12 reads that are not Describe, List or Get
Some AWS services authorize their read calls under other verbs, so a policy limited to those three verbs would leave checks unable to run. Each of these is a read, and each is named here with its scope rather than hidden in the list above.
- aoss:BatchGetCollectionStatement CoverageTailReadOnly, on all resources.
- apigateway:GETStatement ApiGatewayRestReadOnly, scoped to arn:aws:apigateway:*::/restapis* and arn:aws:apigateway:*::/usageplans*.
- athena:BatchGetQueryExecutionStatement AnalyticsReadOnly, on all resources.
- cassandra:SelectStatement KeyspacesSchemaReadOnly, scoped to arn:aws:cassandra:*:*:/keyspace/system*.
- cloudtrail:LookupEventsStatement SecurityServicesReadOnly, on all resources.
- codebuild:BatchGetBuildsStatement IntegrationReadOnly, on all resources.
- codebuild:BatchGetFleetsStatement IntegrationReadOnly, on all resources.
- codebuild:BatchGetProjectsStatement IntegrationReadOnly, on all resources.
- ec2:SearchTransitGatewayRoutesStatement ComputeReadOnly, on all resources.
- iam:SimulatePrincipalPolicyStatement ConfirmReadOnly, scoped to arn:aws:iam::<your-account-id>:role/kostrix-scan-role and arn:aws:iam::<your-account-id>:user/kostrix-scan.
- inspector2:BatchGetAccountStatusStatement SecurityServicesReadOnly, on all resources.
- quicksight:Search*Statement AnalyticsReadOnly, on all resources.
What is stored, how it is protected, and how you revoke it
The stack offers two ways to link an account. In the default, cross-account role mode, Kostrix holds no secret of yours at all: only the role's ARN, which grants nothing without the trust policy in your account and your external id. In access-key mode it holds exactly one long-lived secret, the read-only IAM secret access key you paste in. Saying so plainly matters more than a reassuring phrase would, because it is the thing a reviewer needs to reason about.
- A role by default, a key only if you choose it
- In cross-account role mode the stack creates a role that only the Kostrix service's own task role may assume, and only with your external id. In access-key mode it creates an IAM user and hands you its key pair in the stack outputs. Either way the principal carries only the policies above, and its entire power is the allow list on this page.
- An access key is encrypted before it is stored
- In access-key mode the secret is sealed with AES-256-GCM under a key derived by scrypt from a deployment secret held outside the database combined with the identifiers of the link itself (your organisation, the AWS account the key is verified for and the key id), with a fresh random salt per save. An exported or leaked table is inert, and a ciphertext cannot be reused under another organisation, account or key id. The deployment secret still lives with the service: this protects the table when it travels without the service, not against someone who already has both.
- Fails closed, not open
- If no encryption password is configured, the application refuses to store an access key at all rather than quietly writing it in the clear. A security control that downgrades itself is a security claim, not a control.
- The account is verified, not assumed
- Linking calls sts:GetCallerIdentity and refuses outright if AWS reports a different account than the one the tenant is keyed by, and asks IAM (iam:SimulatePrincipalPolicy, scoped to the role or user itself) whether the credential could change anything, refusing the link if it could. What was typed on a form settles nothing.
- Revocable in one step
- Delete the CloudFormation stack and the role or key is gone. Nothing else needs undoing, because nothing else was ever changed.
Configuration and metrics. Never your data.
Read the action list above and you have read the whole footprint. The calls return how a resource is configured, how busy it has been and, through read-only Cost Explorer and Price List calls, what it costs. None of them return the contents of an object, a row, a message, a secret or a log line, and no statement in the policy would permit it.
What Kostrix reads
- Resource configuration: sizes, types, states, tags, attachments
- CloudWatch metrics: CPU, network, connections, invocations
- Cost Explorer: billed line items and commitment coverage
- AWS Price List: the public rate for a product in a region
- The account id and its alias, so the account can be named
- S3 object keys and sizes (never contents), to size idle or non-current data
- IAM user and role names and, if one already exists, the credential report
- CloudTrail's 90-day management-event history, never data events
What it never touches
- Object contents in S3
- Rows in a database or a DynamoDB table
- Messages in a queue or a stream
- Log events, as distinct from log group configuration
- Anything at all through a write, create, modify or delete call
Kostrix holds no security certification and this page claims none. It has not been through a SOC 2 audit or any equivalent. What is offered instead is the policy above, published in full, and a product whose entire access footprint you can read in a minute.
Ten rules the product is built around.
Each of these exists because breaking it once put a wrong number in front of a customer. They are published here because a cost tool asking for your trust should be specific about what it promises.
- 01Strictly read-only against AWS. Never call a mutating API. Recommend, never act.
- 02Every percentage carries a unit and a denominator. RI coverage is a share of eligible hours, SP coverage a share of eligible spend. Never added, never averaged.
- 03Anything per month rests on a trailing 30-day run rate, never month-to-date.
- 04An absent measurement is not a measurement of zero. A failed or empty call means skip and log loudly.
- 05Estimates must reconcile. Every term is printed in the evidence, and multiplying the printed terms lands exactly on the saving shown.
- 06Findings are keyed by account, region, rule and resource. Resource ids are not globally unique.
- 07Demo data never mixes with real data.
- 08Empty is not unknown, in both the interface and the report.
- 09Scan every enabled region unless the operator pinned the list.
- 10Never guess pricing.
