Connect your AWS account without CloudFormation
Create the Wring IAM role in your own infrastructure-as-code (or the console) instead of launching the CloudFormation stack, then register it from the dashboard.
The usual way to connect an account is a CloudFormation quick-create link. Some organizations do not run third-party stacks — everything in their AWS accounts is managed by their own Terraform, Pulumi or CDK. That works for us: the stack is nothing more than one IAM role, one deny policy and a completion ping back to Wring. The ping is a convenience you do not need. This page has everything required to create the role yourself and hand it to Wring.
The result is byte-for-byte the same grant as the stack creates — the dashboard builds the documents from the same source as the CloudFormation template.
1. Get the documents from the dashboard
Go to Linked AWS accounts → Add account. On the step that offers Launch in AWS Console, choose I created the role myself instead. The panel shows, for this account:
- the External ID — a UUID Wring generates per connected account. It is the only secret in the setup and belongs in the trust policy below;
- Wring's AWS account ID,
266735827728, the principal the role trusts; - the three IAM documents with your external ID already filled in (copy each, or download all three as one JSON file);
- the form that registers the finished role (step 3).
The documents are shown generically below so you can prepare your IaC before opening the dashboard.
2. Create the role
Create the role in the management (payer) account: Cost Explorer there covers every member account, so one role is enough for the whole organization. IAM is global; the Region does not matter. Connecting a member account on its own (its own row in the wizard)? Then the role goes into that member account — the ARN you register must belong to the account the row is for.
Trust policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::266735827728:root" },
"Action": ["sts:AssumeRole", "sts:TagSession"],
"Condition": { "StringEquals": { "sts:ExternalId": "{YOUR_EXTERNAL_ID}" } }
}
]
}Permission policy (inline, name WringAccess)
Every action maps to one function of the platform; there is nothing
speculative in it. Cost Explorer for cost and usage, Organizations to list the
accounts under your payer, billing:GetCredits to show AWS credits in the
dashboard, and s3:GetObject on Cost and Usage Report objects. The
full permission list explains each one.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BillingAndCostAccess",
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"billing:GetCredits",
"organizations:DescribeOrganization",
"organizations:ListAccounts"
],
"Resource": "*"
},
{
"Sid": "CURBucketAccess",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": [
"arn:aws:s3:::*-cur-*", "arn:aws:s3:::*-cur-*/*",
"arn:aws:s3:::*-cost-*", "arn:aws:s3:::*-cost-*/*",
"arn:aws:s3:::cur-*", "arn:aws:s3:::cur-*/*"
]
}
]
}You may replace the three bucket-name patterns with the ARN of your actual
Cost and Usage Report bucket. If you do, apply the same ARN in the
NotResource of the deny policy below — otherwise the deny wins and Wring
cannot read the report.
Deny policy (managed policy, name WringDataProtectionPolicy-{id})
This is the part we ask you to keep even if you trim everything else. It is an explicit deny on top of the role, so even a mistake in the allow policy above cannot reach secrets, database contents, logs, code, identities, sessions or any S3 object outside the report bucket.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDataAccess",
"Effect": "Deny",
"Action": [
"secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret",
"ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath",
"rds-data:ExecuteStatement", "rds-data:BatchExecuteStatement",
"dynamodb:GetItem", "dynamodb:BatchGetItem", "dynamodb:Query", "dynamodb:Scan",
"logs:GetLogEvents", "logs:FilterLogEvents", "logs:GetLogRecord", "logs:StartQuery", "logs:GetQueryResults",
"codecommit:GetFile", "codecommit:GetBlob", "codecommit:GetDifferences", "codecommit:GetCommit", "codecommit:GitPull",
"codeartifact:GetPackageVersionAsset", "codeartifact:ReadFromRepository",
"cognito-idp:AdminGetUser", "cognito-idp:ListUsers", "cognito-idp:AdminListGroupsForUser", "cognito-sync:QueryRecords",
"iam:GetUser", "iam:ListUsers", "iam:ListAccessKeys",
"ec2-instance-connect:SendSSHPublicKey", "ssm:StartSession", "ssm:SendCommand",
"sqs:ReceiveMessage", "kinesis:GetRecords", "kinesis:GetShardIterator", "firehose:GetRecord", "kafka:FetchTopic"
],
"Resource": "*"
},
{
"Sid": "DenyS3ObjectReadNonCur",
"Effect": "Deny",
"Action": ["s3:GetObject"],
"NotResource": [
"arn:aws:s3:::*-cur-*", "arn:aws:s3:::*-cur-*/*",
"arn:aws:s3:::*-cost-*", "arn:aws:s3:::*-cost-*/*",
"arn:aws:s3:::cur-*", "arn:aws:s3:::cur-*/*"
]
}
]
}Naming and tags
- The role name is yours to choose. Our default is
WringCostAccess-{first 8 characters of the external ID}; anything works, Wring stores the ARN. - Tags are optional. The stack sets
ManagedBy=WringandPurpose=CostOptimization.
Terraform sketch
resource "aws_iam_role" "wring_cost_access" {
name = "WringCostAccess"
assume_role_policy = file("${path.module}/wring-trust.json")
}
resource "aws_iam_role_policy" "wring_access" {
name = "WringAccess"
role = aws_iam_role.wring_cost_access.id
policy = file("${path.module}/wring-access.json")
}
resource "aws_iam_policy" "wring_data_protection" {
name = "WringDataProtectionPolicy"
policy = file("${path.module}/wring-deny.json")
}
resource "aws_iam_role_policy_attachment" "wring_data_protection" {
role = aws_iam_role.wring_cost_access.name
policy_arn = aws_iam_policy.wring_data_protection.arn
}3. Register the role
Back in the panel, enter the role ARN and the 12-digit AWS account ID the role lives in, then choose Verify and connect. Wring assumes the role once with your external ID — nothing else — and, if that works, the account is connected and the first import starts immediately: 12 months of Cost Explorer history, then daily. Dashboards and the savings estimate are usually populated within the hour.
If the verification fails, the panel shows the reason. The usual causes:
- the external ID in the trust policy is not the one shown in the panel (each connected account has its own);
- the trust policy names a different principal than
arn:aws:iam::266735827728:root; - the ARN is a typo, or the role was created in a different account than the one you typed;
- an IAM change that has not propagated yet — wait a minute and try again.
Disconnecting
With no stack to delete, disconnect the account in Wring (Linked AWS accounts → Disconnect) and delete the role in your IaC. Either one on its own ends the access: Wring can assume nothing once the role is gone, and keeps nothing to assume with once the account is disconnected.
Flexible commitments
Wring's commitment partner Archera uses a second, separate role
(ReservedAI-Read or ReservedAI-Write) in the management account. Its
template also contains exactly one resource — the role — so the same approach
works, but the role name, trust policy and external ID must match Archera's
template exactly, and a read-only connection cannot be upgraded to write
later. Ask your Wring contact for the template when you are ready for the
first commitment.