Skip to content

Connect a cloud account

Ingest a real cloud estate so Costs, Hunters, Governance, and the Savings Register populate with your data. leancosts is read-only: connecting grants read access only; it never writes to or mutates your cloud. For the full posture (encryption, tenant isolation, and sub-processors), see Security and data handling.

All connectors live on Admin → Connections: one list, grouped by provider. Add connection opens a searchable picker with one row per connector, grouped into Clouds, AI platforms and SaaS tools: choose yours, and its setup form opens in the same window. A row states how many services that connector’s hunters cover and expands to the full list, so you can see what connecting it buys before you connect it. (OpenAI and Anthropic are two rows, not one “LLM” row: they are separate connectors you enable and connect independently.)

All four Azure agreements connect the same way. The agreement decides only what you can unlock on top, which is why nothing asks for it until you need it.

1. Grant read-only access (every agreement)

Section titled “1. Grant read-only access (every agreement)”

Go to Admin → Connections → Add connection and pick Microsoft Azure.

One-Click Connect is the default path, and it asks nothing about your agreement. It is a device-code OAuth flow that registers the service principal, creates its secret, assigns the three required roles, validates the result, and starts the first sync: you never handle a credential by hand. It is available whenever the deployment has AZURE_PORTAL_CLIENT_ID configured.

Manual credentials performs the same grant yourself, and asks nothing about your agreement either. After the first sync, each subscription on the connection row shows the offer Azure reports for it (for example Enterprise Agreement or Enterprise Dev/Test), so you never have to declare it.

Setup guide is the one place that asks which Azure agreement do you have?, because the guide and its Download PDF go to someone else before a connection exists. The answer decides which optional billing block they print and which sentence describes negotiated pricing. It never changes the required grant:

AgreementPick this when
Pay-as-you-go / StandardYou pay Microsoft or a reseller at list, with no enrollment. This is the default.
Enterprise Agreement (EA)You hold an enrollment number and negotiated rates.
Microsoft Customer Agreement (MCA)Your billing account has billing profiles and invoice sections.
Cloud Solution Provider (CSP)A partner owns the subscriptions and invoices you.

On Manual credentials: register an app, create a client secret, then assign three roles at each subscription scope. The wizard prints them as one ready-to-run az loop, and Setup guide / Download PDF carry the same block for someone else to run.

The connection will not validate without all three:

  • Reader → inventory: resource groups, resources, tags.
  • Cost Management Reader → the 13-month cost-history backfill.
  • Log Analytics Reader → lets the ingestion hunter read Basic and Auxiliary tables through /search. Plain Reader already runs /query; this role adds the /search action those plans require.

Paste the display name, tenant id, client id and client secret into the wizard, hit Test, then save. The first healthy connection starts auto-discovery.

Record when the secret expires. One-Click reads the expiry back from Azure and stores it, so leancosts counts down for you; that date renders labelled from Azure. On the manual path the expiry is an optional field you fill in yourself, and it renders labelled as entered: it is your word, not something we read from Azure. The connection’s Settings tab shows the date at all times (Admin → Connections → Open → Settings → Credential), not only when it is close, and reads Client secret expiry not recorded with an Add date action when none is stored; adding or changing a date never asks you for the secret and never rotates it. Inside 30 days a badge appears on the connection row too: it warns at 14 days and turns urgent at 3 days. An expired secret is named as an expired secret rather than surfacing a raw AADSTS… string.

Not the cloud admin? The connect step carries a Send setup instructions panel. It mints one scoped, single-use link and, if you give it an address, emails it to whoever holds Azure Owner or User Access Administrator. They open the link, run the grant in their own cloud, and paste the credential straight into leancosts over TLS: the secret never travels through email or chat. This works for every workspace, trial and paid alike.

Everything in this section is optional and safe to skip; the connection works without it. Each grant unlocks one family of findings and names what stays dark without it. They also live on the connection’s Access tab after the first sync (Admin → Connections → Open), where leancosts already knows from the evidence which ones the service principal holds. Each missing one has a How to grant with its command, pre-filled for your connection: you run it, leancosts never does, and the next sync re-reads the evidence. So you can grant them when you want the findings, rather than before you can connect at all.

Per subscription or tenant:

  • Reservations Reader at tenant scope: reservation utilization and expiry. Lives outside subscription RBAC, so it needs a Reservations Administrator or a Global Admin with elevated access. On an Enterprise Agreement the Enrollment Reader billing role below reads reservations too, so this role is not needed there. Skip → under-used and expiring reservations stay invisible.
  • Savings plan Reader at tenant scope (/providers/Microsoft.BillingBenefits): savings-plan utilization. Reservations Reader does not cover savings plans. Needs a User Access Administrator (elevated access). Skip → under-used savings plans stay invisible.
  • leancosts Data Factory Debug Session Reader at subscription scope, a custom role holding only Microsoft.DataFactory/factories/read and Microsoft.DataFactory/factories/queryDataFlowDebugSessions/action, so the Data Factory hunter can see how long Mapping Data Flow debug clusters have been alive. Reader does not include that query action, and we never ask for Data Factory Contributor. The CLI snippet creates the definition once and assigns it per subscription. Skip → the long-lived debug-cluster finding stays dark, and the connection’s Access tab lists this role under Refused once a sync has asked.

Four more settings are applied on individual resources, not once per subscription, so a tenant that granted every role above can still be missing all four. Run them whenever you want the findings they unlock.

Container Insights, on each AKS cluster. A monitoring setting, not a role: leancosts reads pods, persistent volumes and CPU/memory requests from the Log Analytics workspace Container Insights writes to, with the Log Analytics Reader role above. Nothing runs inside the cluster and no AKS role is asked. (Running a command inside a cluster needs runcommand/action, which only roles that can also change the cluster carry, so leancosts never asks for it.)

It is opt-in, one cluster at a time. The command below collects only the three streams the AKS findings read (KubePodInventory for idle clusters and pools, KubePVInventory for stranded disks, Perf for node-pool requests), in every namespace, every 15 minutes, into a Log Analytics workspace you name. Always pass --workspace-resource-id: without it Azure creates a DefaultResourceGroup-* resource group and a default workspace.

Terminal window
cat > dataCollectionSettings.json <<'JSON'
{
"interval": "15m",
"namespaceFilteringMode": "Off",
"streams": [
"Microsoft-KubePodInventory",
"Microsoft-KubePVInventory",
"Microsoft-Perf"
]
}
JSON
az aks enable-addons --addons monitoring \
--resource-group "<RESOURCE_GROUP>" --name "<CLUSTER_NAME>" \
--workspace-resource-id "<WORKSPACE_RESOURCE_ID>" \
--data-collection-settings dataCollectionSettings.json

What it costs: Log Analytics bills ingestion per GB, about US$2.76 per GB for Analytics Logs at pay-as-you-go (Azure retail price, East US 2, September 2026, after the first 5 GB a month). Container logs are left out on purpose: on the production clusters we measured they were over 90% of Container Insights ingestion.

leancosts also reads the cluster’s data collection rule (with Reader) to know what it collects. A namespace filter hides pods and volumes, so on a filtered cluster, or one whose rule cannot be read, idle findings and stranded-PV delete claims are withheld.

Skip → idle-cluster and idle-node-pool findings stay silent and stranded-PV disks ship review-only. The AKS findings say so: the coverage note names the cluster and carries the command above, already scoped to it. If the workspace refuses the read, the note names the workspace and the Log Analytics Reader assignment it needs.

Azure Managed Prometheus, on each AKS cluster. A monitoring setting, not a role: leancosts reads each container’s requests and measured usage from the Azure Monitor workspace Managed Prometheus writes to, with the access the connection already holds. Nothing runs inside the cluster and no AKS role is asked. If the workspace refuses the read, the note names the workspace and the Monitoring Data Reader assignment it needs.

It is opt-in, one cluster at a time, and it is not free: Azure Monitor bills Managed Prometheus ingestion on the workspace you name, and enabling it creates a data collection rule and endpoint in your subscription.

Terminal window
az aks update --enable-azure-monitor-metrics \
--resource-group "<RESOURCE_GROUP>" --name "<CLUSTER_NAME>" \
--azure-monitor-workspace-resource-id "<AZURE_MONITOR_WORKSPACE_RESOURCE_ID>"

Skip → the request-rightsizing finding (aks_workload_requests_oversized) stays silent. The AKS findings say so: the coverage note names the cluster and carries the command above, already scoped to it. A workload that scales (a changing replica count, or any HorizontalPodAutoscaler in its namespace) is never graded, because a smaller request raises the utilization its scaler reads.

lastAccessTimeTrackingPolicy, on each storage account. This one is a blob-service setting, not a role assignment. It makes Azure stamp a per-blob lastAccessTime, the prerequisite for proposing a Hot → Cool → Cold → Archive lifecycle policy off real access history rather than an account-level transaction ratio.

Terminal window
az storage account blob-service-properties update \
-g <resource-group> --account-name <storage-account> \
--enable-last-access-tracking true

Skip → tier_demote_cool still fires, but review-only: no lifecycle change request is drafted for the account, and the finding carries an enable-tracking pre-flight instead: with the command above already filled in for that account (subscription, resource group, name), under evidence.enableTrackingRunbook.

Two things to know before enabling it. Azure stamps lastAccessTime only from the moment tracking is on, so let a window of access history accumulate before the signal means anything. And the tracking updates bill as ordinary blob operations: small per blob, but not zero on an account with many objects.

AKS cost analysis add-on, on each AKS cluster whose cost you want split by Kubernetes namespace. A cluster setting, not a role: it needs the Standard or Premium tier, and no extra grant: the connector reads the result with the roles above. The setup guide’s CLI snippet and PDF carry this command in a per-cluster loop.

Terminal window
az aks update --ids "<cluster-resource-id>" --tier standard --enable-cost-analysis

Skip → get_kubernetes_costs and the Costs page’s Kubernetes tab list the cluster with allocation off and no namespace rows; cluster and node-pool findings are unaffected. Full walkthrough: See Kubernetes costs by namespace.

Savings that compare two prices (a resize, a move to a cheaper tier) are priced from Azure’s public pay-as-you-go rates unless leancosts has read your negotiated price sheet. Azure has no fallback in between: its cost data carries no list price, so no discount can be derived from it the way it is for AWS and GCP (there, your on-demand rate per service is measured from the last three closed months of billed versus list cost). Savings measured from your own bill (an idle or orphaned resource) are always at what you pay. Until a price sheet is read, the Opportunities page and the Savings Report say so beside the figures. What you can do about it depends on your agreement.

Pay-as-you-go / Standard. There is no price sheet to read: you pay list, so list prices are exact. Nothing to grant.

Enterprise Agreement (EA). Grant Enrollment Reader on the enrollment and set the enrollment number in the connection’s Settings tab, and the sync pulls your exact negotiated rates from the EA Price Sheet, once a day. Azure prepares the sheet in the background, which took about 30 minutes on the enrollment we measured. leancosts checks every 10 minutes and applies the rates as soon as the sheet is ready. Meanwhile the Access tab says the price sheet is being prepared. They are applied as a discount per service (negotiated price over Microsoft’s list price), not as individual meter prices. Until the grant lands, the connection’s Access tab lists Enrollment Reader under Refused, with the date the price sheet refused.

Enrollment Reader is an EA billing role, not an Azure RBAC role, so az role assignment create cannot assign it: it is a PUT against the billing API. Run this as a user who holds EA Enrollment Writer; EA roles do not show in the portal IAM blade, and a service principal can hold exactly one of them. The How to fix command on the Access tab pre-fills the enrollment number and client id for you.

Terminal window
BILLING_ACCOUNT="<ENROLLMENT_NUMBER>" # pre-filled when the connection has one
APP_ID="<CLIENT_ID>" # pre-filled from the connection
SP_ID=$(az ad sp show --id "$APP_ID" --query id -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)
ASSIGNMENT_ID=$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)
az rest --method put \
--url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$BILLING_ACCOUNT/billingRoleAssignments/$ASSIGNMENT_ID?api-version=2019-10-01-preview" \
--headers "Content-Type=application/json" \
--body "{\"properties\":{\"principalId\":\"$SP_ID\",\"principalTenantId\":\"$TENANT_ID\",\"roleDefinitionId\":\"/providers/Microsoft.Billing/billingAccounts/$BILLING_ACCOUNT/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e\"}}"

Skip → savings that compare two prices use Azure’s public pay-as-you-go rates instead of your negotiated ones.

Microsoft Customer Agreement (MCA). The Price Sheet read leancosts performs today is the EA one, scoped to a billing account. MCA publishes its price sheet per billing profile, and we do not read it yet, so there is no grant to run, and MCA savings that compare two prices use Azure’s public pay-as-you-go rates. The wizard says this plainly rather than printing a command that unlocks nothing.

Cloud Solution Provider (CSP). Same position as MCA, with one extra reason: your partner sets your prices, not Microsoft, and the list the partner holds is what the partner pays, not what you are invoiced.

  1. Go to Admin → Connections → Add connection and pick Amazon Web Services. leancosts pre-fills your ExternalId and offers Launch stack in AWS: signed in to your payer account, it opens CloudFormation with the template and your ExternalId filled in, so you only acknowledge IAM and create it. The alternatives are Download CloudFormation template: leancosts-payer-role.json, with that ExternalId already in it, so you never hand-invent one. A Download PDF button gives a branded setup doc to hand to whoever owns the AWS account (it names the template and its SHA-256, so they can check the file they deploy), and a collapsible permissions guide explains why each read-only grant is requested.

  2. In your payer (management) account, open CloudFormation → Stacks → Create stack → With new resources, upload the template, name the stack leancosts, acknowledge that it creates IAM resources with custom names, and create it. From a terminal instead: aws cloudformation deploy --stack-name leancosts --template-file leancosts-payer-role.json --capabilities CAPABILITY_NAMED_IAM. It creates the read-only cross-account IAM role leancosts-readonly: AWS-managed ReadOnlyAccess, a small inline Cost Explorer / Organizations read policy (plus the three AWS Marketplace agreement reads, which show your contract renewals), and reads of your data denied (except your Cost and Usage Report’s files). Nothing in it can mutate a resource, and deleting the stack removes it. Or hand it to Claude Code. Copy prompt for Claude, beside Download CloudFormation template, copies a prompt that has Claude Code deploy the same stack with your AWS CLI. It assumes the CLI is installed and signed in to the payer account. The agent first shows you aws sts get-caller-identity and waits for your yes, then deploys only this stack, checks it with aws cloudformation describe-stacks, and changes nothing else. If the CLI path fails, it asks before switching to the CloudFormation console steps. The member role form has the same button for the StackSet.

  3. When the stack reads CREATE_COMPLETE, its RoleArn output is the role ARN; leancosts fills it in from your payer account id. Create connection and it is tested, with the ExternalId already filled in. (An access-key path exists for quick evaluation, but the cross-account role is the recommended posture.) A role name must start with leancosts-: it is the only name leancosts can assume.

    If the payer has more than one account, add a member role. The payer role reads the payer account only. Open Member role on the connection and Download CloudFormation template: it creates leancosts-aws-member, trusting the same leancosts account with the same ExternalId, with AWS-managed ReadOnlyAccess, the three AWS Marketplace agreement reads and an explicit deny on reads of your data (S3 objects, DynamoDB items, log events, queue messages, parameters). Deploy it from the management account as a service-managed StackSet targeting the organization root, or only the OUs that hold your workloads, with automatic deployment on. A StackSet never deploys into the management account, and that is expected: the payer account keeps its own role. Then save the one ARN the form prefills, arn:aws:iam::{accountId}:role/leancosts-aws-member, for every member account: leancosts puts each account’s id where {accountId} is, so nothing is typed per account. It applies on the next sync. Until the member role is set, leancosts scans the payer account only and says so on the connection; an account the StackSet did not reach is listed as unread.

  4. Point leancosts at a Cost & Usage Report (recommended). Cost Explorer (the feed step 3 connects) reports cost per service, not per resource, and carries no list price. Two consequences, both stated on the connection itself: individual resources show no cost, and savings claims are priced at public list rather than your negotiated rate, because the discount can only be derived from a list-vs-billed comparison a CUR provides. With a CUR, leancosts measures your on-demand rate per service over the last three closed months and prices savings claims at it. Commitment-covered and Spot usage is left out of that rate, because it is not what new on-demand usage costs you.

    Open Configure CUR on the connection. If you already have a CUR, Data Export (CUR 2.0) or FOCUS export, paste its bucket and prefix: all five layouts parse, and the format selector only labels which one you have. If you don’t, the same panel carries a one-command AWS CLI block that creates the bucket, grants the billing service write access, and defines a daily report including resource IDs. You run it; leancosts only ever reads the bucket.

    AWS writes the first report within ~24 hours. Nothing goes dark while you wait: Cost Explorer keeps covering every period the export hasn’t delivered, and once it does, the CUR takes over for those periods so nothing is double-counted.

    A CUR also states each line’s cost net of any negotiated discount (EDP, private rate, SPP), and leancosts stores that net cost, so your stored cost matches the invoice and your negotiated rate is what prices the savings claims.

  5. To slice AWS cost by your tags, activate them as cost-allocation tags in your AWS management account (Billing → Cost allocation tags). AWS cost data carries no per-resource tags, so until a key is activated, AWS spend shows as untagged under Costs → Cost by tag: Cost Explorer can only group cost by activated cost-allocation tags. Activation applies going forward (up to ~24h to populate) and leancosts picks the keys up automatically on the next sync. The connector setup panel and the Download PDF both repeat this step. To see which keys are activated, open the connection in Admin → Connections and read Data → Cost allocation tags: the activated keys with the share of the last closed month’s spend that carries a value, and the keys on your resources that are not activated yet. The connection row shows a warning only while no key is activated at all.

  6. To split EKS cost by Kubernetes namespace, enable Split cost allocation data under Billing and Cost Management → Cost management preferences and include it in the CUR export this connection reads. It adds pod-level lines the connector aggregates per namespace; AWS exposes no API for the preference, so leancosts cannot report whether it is on. Skip → the cluster appears with no namespace rows. Full walkthrough: See Kubernetes costs by namespace.

AWS Bedrock needs no extra step. The models you run on AWS Bedrock (Claude, Nova, Titan and the rest) appear on AI usage under its AWS Bedrock source, read from this connection’s cost data: money per model, account and endpoint, and token counts wherever a Cost & Usage Report states them. The source is being validated with real workspaces, so it may not be open in yours yet.

Go to Admin → Connections → Add connection, pick Google Cloud and follow the connector wizard. The Setup guide and Download PDF carry the exact gcloud steps as copy-paste blocks; the key points:

  1. Enable the BigQuery billing export first (console only, one-time): GCP Console → Billing → Billing export → BigQuery export → Standard usage cost, pointed at a project + dataset. This needs Billing Account Administrator on the billing account. If that’s not you, the PDF is the handoff doc to send to whoever owns it. GCP does not backfill: spend accrues only from the day the export is enabled, so do this before anything else. Without it the connection validates and inventory syncs, but the row shows Cost ingest is off and no spend flows.

  2. Create the read-only service account, enable the APIs, and grant the roles with the snippet: the service account, then gcloud services enable for the APIs the reads call (Cloud Asset, Monitoring, Cloud Billing, BigQuery; IAM roles alone 403 on a project with the API disabled), then the required roles (billing, asset inventory, monitoring, and the BigQuery pair that reads the export), then the optional enrichment viewers, each annotated with what you lose by skipping it. One of them is roles/compute.viewer: read-only and optional: it lets leancosts re-check a VM’s live status (compute.instances.get) during each sync, so “idle while RUNNING” findings verify the VM is actually still running instead of trusting the inventory snapshot (which can lag reality): a VM found already stopped is dropped, and a verified finding carries the check’s timestamp. Skip it and those findings still fire at reduced confidence: their evidence names the sync-time status as its boundary instead of a live read. The same read is the only one that says which managed instance group owns a VM, so without it an off-hours schedule on a VM is withheld and a hot VM’s size-up lists no cost-add. Another is roles/artifactregistry.reader, also read-only and optional: it reads each project’s Artifact Registry settings to confirm gcr.io is redirected to Artifact Registry, so a legacy Container Registry bucket (artifacts.<project>.appspot.com) nothing reads is flagged at higher confidence. Skip it and those findings still fire at a lower confidence, with a step asking you to confirm the redirect yourself. The predefined role can also download your images and packages; to grant only the one permission leancosts reads, create a custom role instead:

    Terminal window
    gcloud iam roles create leancostsRegistrySettings --project=PROJECT_ID --title="leancosts registry settings" --permissions=artifactregistry.projectsettings.get
    gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:SA_EMAIL" --role="projects/PROJECT_ID/roles/leancostsRegistrySettings"

    When the billing account bills more than one project (nearly always), the inventory and monitoring roles must reach every billed project: grant them once at the organization (gcloud organizations add-iam-policy-binding, step 2b of the snippet). Granted on the export project only, leancosts reads that one project’s utilization and none of the others, and the connection’s Access tab lists the projects it cannot read. API enablement has no org-level equivalent: repeat it per billed project. A project you miss does not fail the sync: it is skipped and named on the connection row (see Some projects weren’t scanned under Troubleshooting). One optional viewer, roles/recommender.viewer, reads Google’s own recommendations for the project (machine types, idle VMs, disks and addresses, committed-use purchases, Cloud SQL sizing and Cloud Run cost). It is read-only: it sees recommendations and insights, never a resource’s configuration. If you granted the three narrower recommender viewers this replaced, they keep working for everything except Cloud Run’s cost recommendations. Another optional viewer, roles/recommender.billingAccountCudViewer, is granted on the billing account itself, not a project: it reads GCP’s committed-use purchase recommendations at billing-account scope, which can catch a recommendation the per-project read alone misses. Skip it and leancosts still reads the per-project recommendations; you only lose the extra billing-account-wide informational row.

  3. Paste back into leancosts: the billing account ID + the service-account JSON key in the connect modal, then the export’s project / dataset / table under Connect export (on the connection row while cost ingest is off, or in the connection’s Settings → Billing export).

  4. To split GKE cost by Kubernetes namespace, turn on cost allocation per cluster: gcloud container clusters update <name> --location <loc> --project <p> --enable-cost-allocation. Optional, and already on for Autopilot clusters. It labels the billing export with k8s-namespace, which the connector rolls up. Skip → the cluster appears with no namespace rows. Full walkthrough: See Kubernetes costs by namespace.

GCP hunters read the same canonical cost model as Azure/AWS; remediation ships as guided gcloud kits.

The moment a connection is healthy, auto-discovery ingests, with no “pick your subscriptions” prompt:

  1. Subscriptions / accounts the connector can see.
  2. Resources (Azure Resource Graph / AWS & GCP inventory APIs).
  3. Resource groups (derived counts).
  4. Tags: populated onto each resource; tag governance reads them directly.
  5. Cost data: daily granularity for the current month + 12 prior months.

Watch progress on the connection in Admin → Connections: its status pill on the row, and under Open the progress strip and the Console tab, which tails the sync log. You only ever opt subscriptions out (a toggle), never in.

While the first sync runs, the Costs page carries a notice that the view may be incomplete until the sync finishes, and refreshes itself when it ends.

If you’re on a trial, the first completed sync of a cloud account opens the entire product for 7 days: every finding and every surface, no card. The window applies once per cloud account (ever), and access stays read-only by construction. When it ends, the savings total stays visible and subscribing brings everything back. Under the Micro monitored-spend band, leancosts is simply free while you stay under it, no window needed.

  • Two meters on each connection row (Admin → Connections) show where an Azure, AWS or GCP connection stands. The connection’s sheet (Open) repeats both at the top, each with a legend that spells out every count.

    • Hunters N of M live counts the cloud’s hunters in the last sweep across your organization, judged against this connection’s grants: live, impaired, blocked, or not run yet. The row adds how many are blocked and how many are impaired when any are.
    • Grants has one segment per grant leancosts documents for that cloud, in the order the setup guide lists them. The row says how many are identified and, when any are, how many are refused. Hover a segment to see the grant, its state and what it does. Click a segment to open the sheet on the Access tab at that grant’s row, where How to fix says who runs the fix, the command, and to click Sync after. Click anywhere else on the meter to open the Access tab.
  • The connection’s Access tab shows, per grant, what leancosts has already observed and what the grant unlocks. It is derived from data already ingested, never a live probe of your cloud, so it costs you nothing to open. Grants are grouped worst first, and there is deliberately no “disabled”:

    • Refused: we have positive proof the grant is missing. Your bill shows commitments the connection cannot read (Azure), a project refused a read for a missing role (GCP), or a service read or the AWS Organizations enumeration was denied (AWS). A setting that is off on some AKS clusters, such as Container Insights, lands here too: the group then reads Refused or off, and the row says how many clusters it is off on.
    • Not observed: no evidence either way. This is not a warning. An empty estate and a missing grant look identical from the outside, so we say so rather than guess. When the inventory shows your estate holds nothing the grant would read (no Data Factory, no AKS cluster; on GCP no running VM, no BigQuery dataset), the row ends with Nothing to fix: this estate has no Data Factory (or whatever it lacks), and there is no How to fix block. On GCP that line appears only once every project was read, so a project the service account could not reach never reads as an empty estate. A GCP BigQuery read refused although a permission check shows the permissions held (a deny policy or VPC Service Controls) also has no How to fix: granting the role again changes nothing. On GCP, a grant is identified by a permission check only when every project asked answered: a project whose check got no answer keeps the row here, named, until a sync gets one.
    • Not measured: leancosts does not check this grant, so it says nothing about it either way.
    • Identified: we have seen it work, with the evidence and the date.

    Where a command comes in both shells (the Azure ones), its block has Bash and PowerShell tabs beside Copy. Pick the one your Azure Cloud Shell or terminal runs. The choice is remembered on this device and every command block on the page follows it, and Copy copies the one on show. Copy prompt for Claude always carries the Bash command.

    Every How to fix also offers Copy prompt for Claude: a prompt that has Claude Code run the fix with your cloud CLI (az, gcloud or aws, installed and signed in). It carries this connection’s own identity (the client id, the service account or the role ARN) and its subscriptions or projects where leancosts knows them, and tells the agent how to look up anything it does not. On GCP the command is bound to the connection’s own service account and billing account. It binds the role once at your organization, and falls back to a loop over only the projects that need it when the project sits under no organization or you cannot bind there. Nothing it prints enables the Compute Engine API, which would create a default network in the project. The agent shows you who it is signed in as and waits for your yes, then takes the recommended CLI path. If that fails, it says why and asks which alternative you want: the portal steps, Azure PowerShell where it applies, or a Terraform hint. It never switches on its own. The prompt also says which permission you need (for example Owner or User Access Administrator on the subscription). It changes only that grant or setting, verifies it, and asks you to click Sync. The row turns green only when the next sync’s read succeeds.

    Each row also says what the grant does to the hunters:

    • Blocks N hunters or Weakens N hunters on a refused grant, naming the hunters it stops or narrows. They are the same hunters the row’s meter shows blocked or impaired.
    • Used by N hunters on any other grant. A grant that is not identified names them.
    • Affects no hunter on a grant no hunter reads, with what it feeds instead, such as the Enterprise Agreement price sheet.

    By hunter on the Access tab lists the hunters one by one, with the refused grant that blocks or weakens each. Go to grant jumps to that grant’s row.

  • Costs shows non-zero trend/daily data.

  • Coverage shows your real resources under each required tag.

  • Opportunities starts surfacing optimization findings as hunters fan out.

The connection name is just a label: nothing in your cost, resource or opportunity data depends on it, so you can change it whenever the estate is reorganised. In Admin → Connections, click Open on the connection, then in Settings click the pencil beside its name, type the new one and Save. Two connections of the same cloud can’t share a name inside a workspace; if the name is taken, the field tells you and keeps your edit so you can adjust it.

To rename the workspace itself, go to Admin → Workspace → Organization profile, edit the company / workspace name and save: the sidebar picks it up straight away. Both actions need workspace-admin rights.

If you run several leancosts workspaces (onboarding a set of tenants, or promoting a configured workspace to another environment), you can move connectors as a file instead of running the connect steps again. Admin → Connectors has Export and Import above the cloud tabs.

Export lets you tick the connectors you want and asks for a passphrase (there is a Generate button). It downloads one .json file. Read the warning on that screen before you do it:

  • The file contains your cloud credentials, encrypted with that passphrase. Anyone who has both the file and the passphrase can connect to those clouds.
  • The passphrase is not stored and cannot be recovered. Save it somewhere safe as you create it.
  • Rotating a credential inside leancosts does not invalidate a copy that already left in a file. Each exported connector is stamped with who exported it and when, shown on the connector row.
  • Exporting requires its own permission, separate from managing connectors. If you do not see the Export button, ask a workspace owner to grant it.

Import asks for the file and the passphrase, then shows you exactly what it will do before writing anything: one row per connector, ticked when it is ready to import, and greyed out with a reason when it is not: the name is already in use, that cloud account is already connected, or you are at your connection limit. Untick anything you do not want. The button tells you the real count, “Import 3 of 5 connectors”.

A few things worth knowing:

  • Nothing is overwritten. A connector that already exists is skipped, never updated. To change a credential on an existing connector, edit it directly.
  • Imported connectors start untested and are checked on their next sync. Use Test on the row if you want to confirm immediately.
  • A trial connection cannot be exported: its access is scoped differently, and copying it would quietly widen it.
  • AWS keeps its original ExternalId. Your IAM trust policy already contains that value, so the import keeps it and asks you to confirm. Do not rebuild the role from the ExternalId this workspace shows elsewhere.
  • If the passphrase is wrong, or the file was edited after it was exported, the import refuses it outright rather than importing part of it.
  • Connection won’t validate: re-check the role assignments (Azure) or the ExternalId + trust policy (AWS). The Test button surfaces the failing call.

  • No cost rows after sync: Cost Management/CUR data lags; the resource and tag phases complete first, costs follow. Open the connection’s Console tab to see the cost phase progress.

  • Some projects weren’t scanned (GCP): the connection’s Data tab shows a warn callout, “N projects could not be scanned”, while still reading Connected. One 403 in one project does not fail the sync, so this is deliberate: the rest of the estate synced fine. It matters because the billing export covers the whole billing account: those projects’ spend still counts in your totals, but nothing in them is inventoried, so no savings can be found there and your savings total is understated by whatever is sitting in them. Two causes, and either one produces the same 403: cloudasset.googleapis.com isn’t enabled in that project, or the service account doesn’t hold roles/cloudasset.viewer there. The callout lists the affected project ids and Show the commands gives you the exact gcloud lines for your service account: run them, then Sync now. The callout disappears on the next clean sync. Nothing already collected is discarded while a project is denied: its previously-inventoried resources are held, not retired. One 403 is deliberately not listed here: a Marketplace project Google generated to carry a third-party SaaS subscription refuses every read, and no grant of yours can change that. When a project’s entire bill is SaaS from a named non-Google seller, the sync says so in the connection log (“Marketplace procurement project…”) and leaves it out of the callout: there is no GCP resource in it to inventory, and its spend is already counted from the billing export.

  • Only the payer account is scanned (AWS): the connection row reads Accounts: Only the payer is scanned under its meters, and the Data tab shows a warn callout, on an Organizations payer that has no member role set. Click the Accounts line to open the Member role form. The other accounts’ spend still counts in your totals, but nothing they own is inventoried, so no savings can be found there. Set the Member role as described above. If a role is set but leancosts could not assume it in some accounts, the row reads N could not be read and the callout lists them with AWS’s own message: fix the role’s trust policy or ExternalId in those accounts, then Sync now. Their previous inventory is kept meanwhile.

  • Some services weren’t scanned (AWS): the connection’s Data tab shows a warn callout, “N services could not be scanned”, while still reading Connected. A denied read in one region of one service does not fail the sync, so this is deliberate: the rest of the estate synced fine. It matters because Cost Explorer reports spend per service either way: that service’s spend still counts in your totals, but nothing it owns in the listed regions and accounts is inventoried, so no savings can be found there and your savings total is understated by whatever is running there. The callout names the cause for each gap, because a denied read is not always the role’s doing: an SCP or resource policy denied the read (changing the role will not help: ask the owner of the organization or of the resource to allow it), access was denied with no policy named (a custom policy in place of ReadOnlyAccess, a permission boundary or an SCP), or the read was lost to throttling (it clears on the next sync). It lists each unscanned service with its account and regions, and Show what AWS returned prints AWS’s own message, which names the exact missing action (e.g. dynamodb:ListTables). For a denial that is the role’s to fix, attach the AWS-managed ReadOnlyAccess policy to the role, or grant that action, then Sync now. The callout disappears on the next clean sync.

    Two sentences, depending on what the region holds. After each sync, leancosts checks every region a read was denied in against the last 3 months of your bill and the resources it already holds there. A region with spend or resources keeps the sentence above: no savings can be found there. A denied region with no spend and no resources is not a problem, so it is not listed as one: a quiet line under the callout says not used, no action needed, which is what an SCP that closes regions you never use looks like. When a connection has nothing but unused denied regions, the callout does not appear. A sync that has not yet judged a region (an older sync) keeps the first sentence. Nothing already collected is discarded while a service is denied: its previously-inventoried resources are held, not retired. If no read succeeds at all, that is a broken role rather than a coverage gap and the sync fails loudly instead: the connection shows Last sync failed, and the error card names the missing ReadOnlyAccess and carries the one command that attaches it to your role, with a Copy button. Run it in the payer account, then Sync now.

  • GCP connection shows an error: a failed GCP connection renders a plain-language card instead of the raw provider error: a headline, a one-sentence cause, and (when it’s something you can fix) a How to fix → link that opens the setup guide scrolled to the relevant step, creating the service account, configuring the BigQuery billing export, or re-adding the connection. When Google refuses the billing account’s project list, the card shows the one command that fixes it instead: the Billing Account Viewer grant on your billing account for your service account, with a Copy button. A Billing Account Administrator runs it; a role granted on a project does not cover it. When the costs cannot be read from the billing export (a project saved by its display name instead of its ID, a missing table, the pricing export), the card says so and names the cause; saving such a pointer is refused with the same sentence. A throttled or temporarily-unavailable call from Google, including a quota or rate-limit refusal, retries automatically and never asks you to change anything. The raw error string is always available behind Show details.