Skip to content
KostrixBeta, home

Read-only by construction: the permission to break something was never granted.

Most AWS cost tools ask for write access to your infrastructure eventually, to stop an instance or delete a snapshot on your behalf. Kostrix does not, because it cannot: its AWS cost optimization runs on read-only access, with no mutating AWS call anywhere in it. It recommends. You act, with your own hands and your own audit trail.
Granted permissions334 actions
  • 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*
Every action is a read. Most are Describe, List or Get; the 12 that are not are named below the policy, each with the scope it is limited to.

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

There is no Deny statement, and this page does not claim one. The policy is an allow list and nothing outside it is granted, which is a plainer guarantee than a deny would be. What you are checking when you read it is that the allow list contains no verb that writes.

Verifiable from your side

Because every call is a read, CloudTrail on your account is an independent record of what Kostrix has done: management events by default, and bucket-level S3 reads such as ListBucket once you enable S3 data events. You do not have to take the guarantee on our word.
kostrix-scan-readonlymanaged policy, both modes
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": "*"
    }
  ]
}
Read-only compute, storage, database, analytics, ML and billing access for Kostrix (1 of 2).
kostrix-scan-readonly-2managed policy, both modes
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": "*"
    }
  ]
}
Read-only network, integration, observability and security-service access for Kostrix (2 of 2).
kostrix-self-inspectioninline policy, CrossAccountRole mode
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": "*"
    }
  ]
}
Lets Kostrix ask IAM, at link time, whether THIS role could change anything - and refuse the link if it could. iam:SimulatePrincipalPolicy is a read of IAM's own evaluation; it is scoped to this role and cannot simulate, let alone alter, any other principal. The Get/List entries are the fallback for reading the same policies by hand.
kostrix-self-inspectioninline policy, AccessKey mode
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": "*"
    }
  ]
}
Same self-inspection grant as the role's, for the access-key path.

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.

  1. 01Strictly read-only against AWS. Never call a mutating API. Recommend, never act.
  2. 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.
  3. 03Anything per month rests on a trailing 30-day run rate, never month-to-date.
  4. 04An absent measurement is not a measurement of zero. A failed or empty call means skip and log loudly.
  5. 05Estimates must reconcile. Every term is printed in the evidence, and multiplying the printed terms lands exactly on the saving shown.
  6. 06Findings are keyed by account, region, rule and resource. Resource ids are not globally unique.
  7. 07Demo data never mixes with real data.
  8. 08Empty is not unknown, in both the interface and the report.
  9. 09Scan every enabled region unless the operator pinned the list.
  10. 10Never guess pricing.

Send the policy to your security team. It is the short conversation.