Multi-Account Setup
Onboard every account in an AWS Organization at once — by Organization Discovery or a CloudFormation StackSet — using read-only cross-account roles and no long-lived keys.
Overview
KubeSense collects inventory and CloudWatch metrics from an AWS account by assuming a read-only IAM role inside it. For one or two accounts you add each by hand — see Single Account Setup. When your estate spans an AWS Organization, multi-account setup onboards them all in a single rollout — and keeps up with accounts that join, get suspended, or leave later.
There are two ways to run it. Pick based on one question: will you let KubeSense list your organization's accounts?
| Organization Discovery | CloudFormation StackSet | |
|---|---|---|
| How accounts are found | KubeSense lists the organization and enrolls what it finds | Each account registers itself as its stack is created |
| Extra permission needed | A read-only role that can call organizations:ListAccounts | None |
| Reviewable preview before anything is enrolled | Yes | No |
| Scope by Organizational Unit | Yes, in KubeSense | Yes, via StackSet deployment targets |
| Accounts that leave or are suspended | Paused automatically on the next cycle | De-registered when the stack instance is deleted |
| Choose it when | You can grant organization read access — recommended | Listing the organization is off the table |
Both use read-only cross-account roles and store no long-lived AWS keys. Every account is verified against its own identity before any data is ingested, so a misconfigured role fails closed rather than silently attributing one account's resources to another.
Architecture
Controller account (the KubeSense controller runs here)
│ sts:AssumeRole — read-only, nothing stored
│
├── KubeSenseOrgListerRole (management / delegated-admin account)
│ └─ organizations:ListAccounts → the account list [Approach A only]
│
├── KubeSenseCollectorRole → Account A ──┐
├── KubeSenseCollectorRole → Account B ├─ inventory + CloudWatch metrics
└── KubeSenseCollectorRole → Account C ──┘Controller account — the AWS account your KubeSense controller runs in. It holds one
IAM identity, the controller role, named kubesense-controller by default, and that
identity is what reaches into every other account.
Monitored account — any account you want observed. Each contains an identical
read-only role, KubeSenseCollectorRole by default, whose trust policy names your
controller role. Nothing else is installed in a monitored account.
Management / delegated-administrator account — organizations:ListAccounts only
succeeds from one of these. Approach A needs one; a delegated administrator is strongly
preferred, since it keeps the lister role out of your most sensitive account.
The collector role name must be identical in every account: KubeSense never stores a per-account role ARN. It derives one for each account as arn:aws:iam::ACCOUNT-ID:role/KubeSenseCollectorRole, and that single convention is what makes enrollment work with no per-account configuration. You may use a different name, but change it in every account and tell KubeSense the new name — otherwise the derived ARN is wrong and that account collects nothing.
Prerequisites
Before you begin, ensure you have:
- A KubeSense controller running in your controller account with an identity of its own — an EKS IRSA role or an EC2 instance profile
- Permission to create IAM roles in the controller account and in your management account
- Permission to create CloudFormation StackSets from your management or delegated-administrator account
- Trusted access enabled between CloudFormation and AWS Organizations — a one-time action under CloudFormation → StackSets → Enable trusted access. Without it the service-managed permission model is unavailable
- Administrator access to the KubeSense dashboard
Step 1 — Prepare the controller account
Do this once, in the controller account, whichever approach you choose.
1.1 Give the controller its own IAM identity
Skip this if it already has one. A pod with no identity falls back to whatever the node has, which is not something to build organization-wide access on.
Name the role kubesense-controller. Do not name it KubeSenseCollectorRole —
that name is reserved for the collector role created in every account, possibly including
the controller account, and two roles cannot share a name.
The controller role needs no AWS read permissions of its own. All collection happens
through the collector roles it assumes, so the only permission it ever gets is the
sts:AssumeRole grant in Step 1.3.
On EKS, confirm the cluster has an IAM OIDC provider — IRSA does not work without one:
aws eks describe-cluster --name <cluster> \
--query identity.oidc.issuer --output text
# if that printed None, associate one (once per cluster)
eksctl utils associate-iam-oidc-provider --cluster <cluster> --approveThen create the role and bind it to the controller's ServiceAccount:
kubectl get pod <controller-pod> -n kubesense \
-o jsonpath='{.spec.serviceAccountName}'
eksctl create iamserviceaccount \
--cluster <cluster> \
--namespace kubesense \
--name <controller-serviceaccount> \
--role-name kubesense-controller \
--approve \
--override-existing-serviceaccounts
kubectl rollout restart deployment/<controller-deployment> -n kubesenseThe restart matters: EKS injects credentials at pod startup, so an annotation added to a running pod has no effect until it is replaced. If the ServiceAccount is managed by Helm, add the annotation to your values file too, or the next upgrade removes it.
On EC2, create the role with an EC2 trust policy instead and attach it to the instance as an instance profile. Its ARN is what the rest of this page calls the controller role.
1.2 Confirm the identity the controller actually uses
Every template and trust policy below names this identity, so getting it right here saves the most common class of failure:
kubectl exec -n kubesense <controller-pod> -- \
aws sts get-caller-identity --query Arn --output textIf it prints an assumed-role ARN such as
arn:aws:sts::111111111111:assumed-role/kubesense-controller/..., convert it to the plain
IAM role form — arn:aws:iam::111111111111:role/kubesense-controller — and use that
everywhere below.
Check this even if you are sure: If the command returns something like assumed-role/CLUSTER-node-role/i-0abc..., the controller is falling back to the EC2 node instance role because its own identity is not reaching the pod. Do not use the node role in your trust policies — it is shared by every pod on that node, so trusting it grants organization-wide read access to anything scheduled there. Fix the controller's identity instead, then restart the pod so the credentials are injected.
1.3 Allow the controller to assume the collector roles
Attach an inline policy to the controller's role. In the console: IAM → Roles → the
controller's role → Add permissions → Create inline policy → JSON, paste the policy,
and name it KubeSenseAssumeCollectorRoles.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::*:role/KubeSenseCollectorRole",
"arn:aws:iam::*:role/KubeSenseOrgListerRole"
]
}]
}Or with the CLI:
aws iam put-role-policy \
--role-name kubesense-controller \
--policy-name KubeSenseAssumeCollectorRoles \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::*:role/KubeSenseCollectorRole",
"arn:aws:iam::*:role/KubeSenseOrgListerRole"
]
}]
}'The second ARN is only used by Approach A. Leaving it in costs nothing — it permits
assuming a role that does not exist unless you create it — and saves editing this policy
if you switch approaches. Narrow the * to explicit account IDs if you prefer; the grant
is already bounded by role name.
Cross-account access needs both halves: Assuming a role across accounts requires two permissions granted in different places — this identity policy on the controller, and a trust policy on the target role naming the controller. Miss either and the assume fails with AccessDenied. The templates below write the trust side for you; this step is the half you own.
Approach A — Organization Discovery
You create one extra read-only role that permits organizations:ListAccounts. KubeSense
assumes it, enumerates the organization, shows you a plan, and enrolls the accounts you
approve. Five steps: roll out the collector role, create the lister role, configure, review,
activate.
Step A1 — Roll the collector role out to every account
In the KubeSense dashboard, go to Settings → Integrations → AWS → Add New AWS Account,
open the Multi Account tab and choose Organization Discovery. Under Deploy the
collector role, click View setup instructions — the modal offers the CloudFormation
template (kubesense-monitored-account-role.yaml) as a download and shows the four
parameter values ready to copy.
Then, signed in to your management or delegated-administrator account:
- Go to CloudFormation → StackSets → Create StackSet.
- Under Permissions, keep the default Service-managed permissions. This lets CloudFormation create roles across the organization without pre-provisioning anything per account.
- Choose Upload a template file and select the downloaded template.
- Name the StackSet — for example
kubesense-collector-role— and fill in the parameters:
| Parameter | Value |
|---|---|
ControllerAccountId | Your 12-digit controller account ID, from the ARN confirmed in Step 1.2. |
ControllerRoleName | The controller role's name only, not its ARN — the part after role/. Default kubesense-controller. |
CollectorRoleName | Leave as KubeSenseCollectorRole unless you have a naming standard. It must match what you tell KubeSense in Step A3. |
OrgId | Optional but recommended: your o-xxxx id. The trust policy then additionally requires the calling principal to belong to this organization, so a leaked controller ARN alone is not enough to assume the role. |
- On Configure StackSet options, tick "I acknowledge that AWS CloudFormation might
create IAM resources with custom names" (
CAPABILITY_NAMED_IAM). It is required because the template pins the role name, which is deliberate. - Under Deployment targets, choose Deploy to organizational units and select the OUs holding the accounts you want observed. Select the organization root to cover everything.
- Enable automatic deployment. This is what gives accounts added to those OUs later the role with no action from you, and removes it cleanly when an account leaves. Skipping it is the single most common reason a later-created account never collects.
- Choose a single region. IAM is global, so extra regions only create redundant stack instances.
- Submit, and wait for every stack instance to reach
CURRENT/SUCCEEDED.
Count the stack instances before moving on: A service-managed StackSet never deploys to your organization's management account, even when you target the organization root. AWS excludes it silently — no stack instance and no error — so an organization of N accounts produces N−1 instances when the management account is one of them. That is usually fine, because "Skip management account" is on by default. If you do want that account observed, deploy the same template into it as a plain stack (CloudFormation → Create stack, not Create StackSet) with the same parameters, and turn "Skip management account" off in Step A3. Turning the setting off without deploying the role leaves the account enrolled with no role to assume, failing every collection cycle.
About the role's permissions: This template attaches the AWS-managed ReadOnlyAccess policy, which is broad — it includes read access to object contents in S3, which KubeSense never uses. If your security review requires least privilege, replace it with the read-only permissions policy on the Single Account Setup page, which grants only the API calls KubeSense actually makes. The StackSet template used by Approach B already attaches a least-privilege policy inline.If you built a least-privilege policy from an earlier version, check it against that page. Recent additions are tag:GetResources for resource tags, pi:ListAvailableResourceMetrics and pi:GetResourceMetrics for RDS Database Insights metrics, and dynamodb:DescribeTable for DynamoDB table size and capacity. Leaving one out does not stop collection; that feature just stays empty.
Step A2 — Create the organization lister role
This is the one role Approach B does not need. It lets KubeSense enumerate the organization, and it lives in your management or delegated-administrator account — nowhere else, and only once.
Create a role there named KubeSenseOrgListerRole with this trust policy, using the
exact controller ARN from Step 1.2:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<CONTROLLER-ACCOUNT-ID>:role/kubesense-controller" },
"Action": "sts:AssumeRole"
}]
}and this permissions policy — read-only organization metadata, nothing else:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"organizations:ListAccounts",
"organizations:ListAccountsForParent",
"organizations:ListParents",
"organizations:ListOrganizationalUnitsForParent",
"organizations:DescribeOrganization"
],
"Resource": "*"
}]
}These five actions are exactly what discovery calls, and no more. There is no write permission and no access to account contents. Note the role's ARN — KubeSense asks for it next.
Why a second role at all: Your controller runs in an ordinary member account, where ListAccounts is refused no matter what permissions you grant locally — the call only succeeds from the management or delegated-admin account. So KubeSense makes two hops: it assumes this lister role to find the accounts, then assumes each monitored account's collector role to collect from them.
Step A3 — Configure the enrollment in KubeSense
Back on the Organization Setup screen, fill in:
| Field | What to enter |
|---|---|
| Organization Lister Role ARN | The ARN from Step A2. |
| Regions | The regions each enrolled account is queried in. An account collects nothing from a region that is not selected. The first is also used to call the Organizations API. |
| Include Organization Unit IDs | Optional. Restrict enrollment to specific OUs (ou-xxxx-xxxxxxxx); their descendants are included. Leave empty to cover the whole organization. |
| Exclude Account IDs | Optional. Accounts to always skip. This wins over the OUs above. |
| Skip management account | On by default. Leave it on unless you run workloads in the management account. |
| Collector Role Name | Fixed to KubeSenseCollectorRole — it has to match the role deployed in Step A1. |
| Resource Types | Which services each newly enrolled account collects. An account enrolled with none selected is created and then collects nothing. |
Save the configuration. Nothing is collected yet — a new enrollment starts in preview mode.
Resource types apply at first enrollment only: These defaults apply at the moment an account is first enrolled. Changing an individual account's settings afterwards is safe — enrollment never overwrites your per-account choices on a later cycle. Equally, editing these defaults later affects only accounts enrolled from that point on.
Step A4 — Review the preview
KubeSense lists your organization and shows what it would do with each account, without creating anything. Discovery takes 10–30 seconds.
| Outcome | Meaning |
|---|---|
| Would enroll | The account will be onboarded, using the derived collector role ARN. |
| Skipped — management account | It is the organization's management account and skip management account is on. |
| Skipped — excluded | You listed it under excluded accounts. |
| Skipped — inactive | AWS reports it as suspended or closed. |
Read the list carefully — it is the last checkpoint before collection starts, and the only place a misconfiguration is cheap to fix:
- An account you expected is missing entirely — it is outside the OUs you selected. Check the OU list.
- Nothing is listed at all, or the preview errors — almost always the lister role: either the controller cannot assume it, or it does not live in the management or delegated-administrator account.
- An account is listed that should not be — add it to excluded accounts and review again.
What the preview cannot tell you: It confirms which accounts KubeSense will enroll — not that each one's collector role actually exists. The role ARN is derived by convention and never called at this stage, so an account whose Step A1 stack instance failed still shows as "Would enroll" and then fails once collection begins. If Step A1 reported any failed stack instances, resolve those first.
Step A5 — Activate
Choose Enable Enrollment. KubeSense creates one integration per approved account and begins collecting within a few minutes. (Finish without enrolling leaves it in preview — nothing is created, and you can return and enable it later.)
From here the enrollment maintains itself. On a recurring cycle — roughly every 15 minutes — it re-reads your organization and reconciles:
- New accounts in the covered OUs are enrolled automatically, with your configured defaults.
- Accounts that leave the organization or the selected OUs, become suspended, or are added to your exclude list are paused. Pausing stops collection and retains everything already collected.
- Accounts that return are resumed automatically.
- Accounts you added by hand are never touched, whatever else changes.
Approach B — CloudFormation StackSet
Use this when you will not grant organizations:ListAccounts. Each account registers
itself as its stack deploys, so KubeSense never enumerates your organization.
Do these steps in order. Each account calls KubeSense the moment its stack is created, so the KubeSense side has to be ready before the StackSet rolls out — otherwise every stack instance fails and must be retried.
Step B1 — Prepare KubeSense
Go to Settings → Integrations → AWS → Add New AWS Account, open the Multi Account tab and choose CloudFormation StackSet. On the Setup step provide the Controller Role ARN from Step 1.2, the regions to collect from, any OU or account filters, and the resource types to enable on each account. No lister role is required. Save to continue.
On the Deploy step, copy the Template URL and click Generate Token. That
completes the parameter set: ControllerRoleArn, KubeSenseEndpoint, PushToken,
PushPath and CollectorRoleName, each with a copy button.
The push token is shown once: Copy it before you leave the screen — it cannot be recovered afterwards, only replaced. Generating a new token stops any StackSet still carrying the previous one from registering, until that StackSet is updated with the new value.
Step B2 — Create the StackSet
Signed in to your management or delegated-administrator account, go to CloudFormation → StackSets → Create StackSet, keep Service-managed permissions, paste the template URL from Step B1, and fill in:
| Parameter | Value |
|---|---|
ControllerRoleArn | The full ARN from Step 1.2. Note this template takes the ARN, unlike Approach A's which takes an account id and role name separately. |
KubeSenseEndpoint | The HTTPS base URL of KubeSense, from Step B1. Must be reachable from AWS. |
PushToken | The registration token from Step B1. Marked no-echo, so it does not appear in stack events or the console. |
PushPath | The registration path on that endpoint. The default /api/cloud-org-enrollments/push is correct when KubeSenseEndpoint is your dashboard, which is the usual case. Enter it with no trailing slash — a wrong value makes every stack instance fail with HTTP 404 or 405. |
CollectorRoleName | Must match what you configured in KubeSense. |
CaBundle | Only if your endpoint's certificate is issued by a private CA, or its chain omits an intermediate. |
InsecureSkipTlsVerify | Defaults to true so enrollment works before the certificate chain is sorted out. Set it to false once the chain verifies. |
Acknowledge CAPABILITY_NAMED_IAM as in Step A1, target your OUs, and enable automatic deployment — that is what onboards future accounts and cleanly removes departing ones.
Network requirement: The registration function runs in AWS, outside your VPCs, so the KubeSense endpoint must be reachable over public HTTPS from the accounts you deploy to. If KubeSense is only reachable internally, expose the registration path through a public endpoint. Only that one path needs to be reachable — it authenticates its own token and ignores any session headers.
Step B3 — Deploy the management account as a plain stack
Skip this only if you do not want your organization's management account observed. A service-managed StackSet never deploys to it, silently, so it registers nothing.
- Sign in to the management account and go to CloudFormation → Create stack → With new resources. This is Create stack, not Create StackSet.
- Use the same template URL.
- Enter the same parameter values as the StackSet — particularly
CollectorRoleName,PushPathandPushToken, since KubeSense derives this account's role ARN by the same convention as every other. Reuse the same push token; generating a new one invalidates the StackSet's. - Acknowledge
CAPABILITY_NAMED_IAMand create the stack.
Because this is a plain stack it is not covered by the StackSet's automatic deployment. It is the one account you maintain by hand: if you later update the StackSet's parameters, update this stack too.
Step B4 — Verify the rollout
Watch the StackSet's Stack instances tab until every instance reports SUCCEEDED. Each
successful instance means that account has created its read-only role and registered itself
with KubeSense. A failed instance carries the HTTP status in its status reason — see
Troubleshooting. Registration is idempotent, so failed instances can be
retried safely once the cause is fixed.
Then confirm from the KubeSense side: the enrolled account count should be your successful stack instances, plus one if you deployed the management account in Step B3. If the dashboard is one short and every instance succeeded, the missing account is almost always the management account.
What the StackSet deploys
Two resources per account — no agents, no persistent compute:
- The read-only collector role, trusting your controller. Unlike Approach A's
template, this one uses a least-privilege inline policy listing only the API calls
KubeSense makes, rather than the broad AWS-managed
ReadOnlyAccess. - A registration function, which runs only at stack create and stack delete. On create
it tells KubeSense "this account is now available", supplying nothing but its own
account ID; on delete it says the reverse. It also calls
iam:ListAccountAliasesin its own account so the account appears under its alias rather than a bare twelve-digit number — best-effort, and an account that denies it still registers.
The function cannot choose which role KubeSense uses — KubeSense derives that itself from the account ID and your configured role name, so a registration request cannot point KubeSense at an arbitrary role even if the token leaks. Deleting a stack instance de-registers that account and stops collection; data already collected is retained, and a KubeSense outage never blocks a stack teardown.
Managing enrolled accounts
Seeing what is enrolled. The AWS integration page lists every automatically enrolled account, whether it is collecting or paused, and how it was registered. Manually added integrations are listed separately — enrollment never claims them.
Changing what one account collects. Adjust that account's resource types as usual. Enrollment will not overwrite the change on a later cycle.
Changing the defaults for future accounts. Edit the resource types on the enrollment. Existing accounts keep their current settings; only accounts enrolled from that point on pick up the new defaults.
Stopping collection for one account. With Organization Discovery, add it to Exclude Account IDs — it is paused on the next cycle and its data is retained; removing it from the list resumes it. With the StackSet, delete that account's stack instance; redeploying re-registers it.
Pausing enrollment entirely. With Organization Discovery, choose Return to preview on the Organization Setup screen and save: discovery keeps running so the plan stays current, but no account is enrolled, paused, or resumed automatically. Choose Enable Enrollment on the Review screen to resume. With the StackSet, generate a new push token without redeploying — accounts whose stacks are created after that cannot register until the StackSet is updated.
Accounts already enrolled keep collecting either way; tearing those down is a separate, deliberate action, so pausing can never silently stop collection.
Troubleshooting
| Symptom | Likely cause | Action |
|---|---|---|
| An account is listed but is not collecting | KubeSenseCollectorRole missing in that account, or named differently | Confirm the role exists with the exact configured name. If the logs report the account resolving to your controller account id, that account has no role of its own and the controller fell back to its own identity — re-run its stack instance |
| Preview is empty or errors | The lister role is not in the management or delegated-admin account, or lacks the five organizations:* permissions | ListAccounts returns nothing from an ordinary member account however it is permissioned. Recheck Step A2 |
Preview reports AccessDenied on sts:AssumeRole | The identity in the message is the principal actually used | If it is not the controller role from Step 1.2, the controller is not running as the identity you configured. If it is, one half is missing: the identity policy (Step 1.3) or the lister role's trust policy (Step A2) |
| Push registration fails with 404 or 405 | PushPath does not match where your endpoint serves registration | Fix PushPath, with no trailing slash. A 405 returned by nginx as an HTML page means the request fell through to the dashboard's static file handler |
| Push registration fails with 403 | The push token is wrong or has been replaced | Redeploy the StackSet with the current token |
| Push registration fails with 409 | The enrollment is disabled in KubeSense | Enable it and retry the failed instances |
| Push registration fails with 401 | A gateway in front of KubeSense rejected the call | Allow-list the registration path at your gateway |
CERTIFICATE_VERIFY_FAILED on stack instances | AWS could not build a trust chain to your endpoint's certificate | Run openssl s_client -connect <host>:443 -showcerts. Private CA → paste its PEM into CaBundle. Public CA but only one certificate returned → your server is not sending its intermediate; reinstall the certificate with the full chain |
| A new account was not onboarded | Automatic deployment is off on the StackSet, or the account was created outside the targeted OUs | Turn automatic deployment on, or move the account into a covered OU |
| One account is missing and every stack instance succeeded | It is the management account, which service-managed StackSets skip by design | Deploy to it as a plain stack (Step B3). Confirm which account it is with aws organizations describe-organization --query Organization.MasterAccountId |
| An account still shows after its stack was deleted | De-registration is best-effort so a KubeSense outage cannot block a stack delete | Pause the account from the dashboard |
KubeSenseCollectorRole already exists on stack create | That account already has the role from an earlier attempt | IAM role names are unique per account and CloudFormation will not adopt a resource it did not create. Delete the stack that owns it, or the role, and retry |
Best practices
- Use a delegated administrator for the lister role rather than the management account, so organization read access lives outside your most sensitive account.
- Set
OrgIdon the collector role template — the trust policy then also requires the caller to belong to your organization, so a leaked controller ARN is not enough on its own. - Always enable automatic deployment on the StackSet. It is what onboards accounts created later, and its absence is the commonest cause of a missing account.
- Select regions deliberately. Every enrolled account is queried in every selected region on each cycle, so unused regions cost API calls for nothing.
- Rotate the push token once a rollout finishes, and again before onboarding a new batch — updating the StackSet in the same change.
- Prefer Organization Discovery unless organization listing is genuinely off the table: the preview, OU filtering and automatic pause/resume have no equivalent in the push path.