# AI agents multiple cloud accounts: bind every action

AI agents multiple cloud accounts safely only when every request names one immutable account boundary and the executor refuses every mismatch. An agent that can say "deploy to production" but cannot identify the AWS account, Azure subscription, or Google Cloud project behind that word has been given ambiguous authority.

Teams often frame this as an IAM cleanup exercise. It is an execution design problem. IAM can correctly grant access to two accounts while the agent, its wrapper, or a shell profile still chooses the wrong one. Once that happens, a perfectly valid credential makes a perfectly valid change in the wrong place.

The fix is less glamorous than a clever permissions model. Define executable targets ahead of time, bind each one to provider-issued identifiers and a dedicated execution identity, make the agent request a target by handle, and verify the caller immediately before a state-changing call. Human-readable environment names remain useful, but they do not decide where code runs.

## Ambiguous targets create real authority

An environment label is not an authority boundary. "Production" tells a person how a team intends to use resources. It does not tell an API which AWS account, Azure subscription, Google Cloud project, tenant, region, or assumed role must receive a request.

That distinction sounds pedantic until a company has `prod`, `production`, `prod-old`, and `production-sandbox` scattered across separate organizations. I have seen account aliases copied during migrations, stale shell profiles point at retired accounts, and a harmless-looking read operation land in the account that incident responders were trying to preserve. Agents make this worse because they take natural-language labels literally and can act far faster than the person who notices the ambiguity.

A target must answer all of these questions before execution:

- Which provider owns the resource?
- Which immutable account boundary receives the call?
- Which environment does the organization assign to that boundary?
- Which execution identity may act there?
- Which geographic or organizational limits apply?

The order matters. The account boundary comes before the environment label. If a request says `environment: production` but lacks an account ID or subscription ID, it is incomplete. Reject it rather than asking the agent to infer the missing detail from a repository name, a ticket title, or its current terminal configuration.

A common bad recommendation says that account naming conventions solve this problem. They help people scan a list, and they fail as a control. Names are mutable strings. An account ID, subscription ID, tenant ID, project number, cluster UID, and role ARN are provider-issued references with a much stronger claim on identity.

## An environment label must not choose an account

Treat environment as controlled metadata attached to a target record, not as input that resolves a target at execution time. This prevents a request such as "apply this production fix" from searching a live inventory and selecting whichever account happens to match a tag.

The distinction has a practical consequence. Your deployment catalog may contain many targets with `environment: production`: payments, internal tools, regional services, and acquisition-era accounts. Each remains a separate destination. An agent must select one approved handle, such as `aws-prod-payments`, rather than submit a broad selector such as `environment=production`.

Use the provider identifiers that establish the actual boundary:

| Provider | Bind to | Do not rely on |
| --- | --- | --- |
| AWS | Account ID, partition, role ARN, permitted region | Account alias, profile name, role display name |
| Azure | Tenant ID, subscription ID, resource group scope where needed | Subscription display name, portal directory selection |
| Google Cloud | Project number and project ID, organization or folder where relevant | Project display name, active local configuration |

The project ID in Google Cloud is usually stable enough to be useful in requests, while the project number gives you an additional immutable check. Azure needs both tenant and subscription because a subscription alone does not describe the identity context that issued a token. In AWS, an account number alone does not tell you which role the action will use. Bind both.

Do not bury this information in prompt text. A sentence in an agent instruction that says "never touch production" cannot stop a credential that already permits the action. The trusted executor must have the target record and must decide whether the requested identity and destination match it.

## A target registry should contain facts, not guesses

Keep a small, reviewed registry of executable cloud targets. This is not a second IAM system and should not attempt to restate every cloud permission. Cloud IAM still decides whether a role can call an API. The registry answers a narrower question: where may this request go when it uses this target handle?

This example uses fictional identifiers, but the shape is deliberate:

```yaml
targets:
  aws-prod-payments:
    provider: aws
    environment: production
    account_id: "482901736154"
    partition: aws
    role_arn: "arn:aws:iam::482901736154:role/agent-payments-deploy"
    regions:
      - us-east-1
      - us-west-2

  azure-prod-fulfillment:
    provider: azure
    environment: production
    tenant_id: "2f34c630-1ce8-4ed3-a7ce-8a3286e799a1"
    subscription_id: "841742bf-9db5-4cf8-9f6b-f1778d5b7511"
    scopes:
      - "/subscriptions/841742bf-9db5-4cf8-9f6b-f1778d5b7511/resourceGroups/fulfillment-prod"

  gcp-prod-catalog:
    provider: gcp
    environment: production
    project_id: "catalog-prod-417"
    project_number: "548201736915"
    parent: "organizations/193847561029"
```

The registry must be owned like production configuration. Give each target an accountable team, a review path, and a retirement date or status when an account leaves service. Otherwise, old targets quietly become a map of places an agent can still reach.

Do not build records from free-form discovery and immediately permit execution. Cloud inventory APIs are useful for finding accounts that need an owner. They are not authorization decisions. A discovered account might be a forensic copy, a vendor-managed subscription, an acquisition remnant, or a testing tenant that shares confusing labels with production.

The registry also prevents a subtle drift problem. A person can rename an Azure subscription or an AWS role without changing the immutable ID. The display label can update for readability while the binding stays intact. If the immutable ID changes, treat it as a new target that requires review, even when the name remains the same.

## The agent should request a handle, never compose a destination

An agent should send intent and a target handle. It should not send a role ARN copied from a command, an arbitrary cloud profile name, or a shell command that includes a user-chosen subscription. Those fields give the agent control over the most sensitive part of the request.

A request can look as plain as this:

```json
{
  "request_id": "chg-8a31f6",
  "target": "aws-prod-payments",
  "operation": "aws.ec2.reboot_instances",
  "region": "us-east-1",
  "arguments": {
    "InstanceIds": ["i-0abc123def4567890"]
  },
  "reason": "Recover the failed checkout worker after approved release"
}
```

The executor resolves `aws-prod-payments` to the record it already holds. It assumes only the listed role, rejects `eu-west-1` because that region is absent from the record, and sends the request with the selected account identity. The agent never receives a reusable cloud credential as part of this exchange.

The failure this prevents is easy to recognize. An agent runs a command with `--profile prod`, but a developer's local `prod` profile points at the shared services account. The command is syntactically correct, the API accepts it, and the team discovers the mistake only when the expected payment worker is still running. A target handle that resolves outside the agent, followed by an identity check, stops the command before the reboot request leaves the executor.

Do not accept both a handle and a caller-supplied destination "for flexibility." That creates two sources of truth. If a request contains `target: aws-prod-payments` and a different role ARN, the executor must reject the request. It should never choose whichever field looks more specific.

## Verify the live caller before a write

A fixed target record is necessary, but it does not prove that the active credential is the one you intended. Tokens expire, role assumptions fail over, local profiles leak into subprocesses, and cloud SDKs can use unexpected credential provider chains. Check the live identity immediately before a state-changing call.

For AWS, AWS documents `GetCallerIdentity` as returning the account, ARN, and user ID of the calling identity. Its documentation also notes that the call returns these details even when an explicit deny would otherwise block it. That makes it a good diagnostic and preflight primitive. It does not grant permission to do anything else, and it does not replace a comparison against an expected record.

A preflight command has an output shape like this:

```bash
aws sts get-caller-identity --output json
```

```json
{
  "UserId": "AROAEXAMPLEID:agent-run-8a31f6",
  "Account": "482901736154",
  "Arn": "arn:aws:sts::482901736154:assumed-role/agent-payments-deploy/agent-run-8a31f6"
}
```

Compare `Account` with the target's `account_id`. Parse the ARN and compare the assumed role with the configured role. Do not use a substring test such as "ARN contains payments." Reject a different partition, account, or role before the write call.

Apply the same pattern elsewhere. For Azure, query the active tenant and subscription, then compare both with the selected target. For Google Cloud, query the active project and the authenticated principal, then validate the project against the target record before a modifying API call. These checks need to run in the same process context that sends the action. Checking one terminal and executing in another only provides comfort.

There is a limit to preflight checks: a role can be correct but over-permitted. The cloud role itself still needs least privilege for its assigned target. Binding stops a request from crossing into an unintended account. IAM limits what the correct account identity can do once it gets there. You need both controls.

## Broad cross-account roles hide the mistake until it hurts

One administrator role that can assume into every account is popular because it reduces setup work. It also turns target selection into an irreversible high-risk decision. If the agent or executor selects the wrong account, the same broad identity often has enough access to complete the error.

Separate execution roles by account and purpose. A production deployment role for payments should not also have a trust path into analytics, identity, or disaster recovery accounts. Where an agent needs read access across many accounts, use separate read roles and make the target binding visible for each call. Read access can expose customer data, infrastructure topology, and credentials stored in poorly chosen places. Calling it read-only does not make selection errors harmless.

Use cloud-native trust conditions to narrow how a role may be assumed. AWS role trust policies can constrain the trusted principal and can require an external ID where that pattern fits the caller. Azure workload identities can be limited by federated credential claims and resource role assignments. Google Cloud service account impersonation can constrain who may mint an access token. The exact mechanism differs, but the design stays the same: one execution identity should map to one reviewed destination and purpose.

Do not confuse a central broker identity with broad authority. A broker may coordinate requests for many targets while each request obtains a target-specific identity. This takes more setup than a universal administrator role. It also makes the blast radius legible when a process, prompt, or integration goes wrong.

## Approval must show the destination a person can verify

A person cannot approve a safe action if the approval hides the account boundary. "Restart checkout worker in production" asks the reviewer to trust invisible resolution logic. The approval needs the target handle, environment label, immutable account identifier, active role or service identity, operation, and the resource scope that the API will affect.

For an AWS instance reboot, a useful approval reads like this:

```text
Target: aws-prod-payments
Environment: production
Account: 482901736154
Identity: agent-payments-deploy
Action: ec2:RebootInstances
Region: us-east-1
Resource: i-0abc123def4567890
Reason: Recover failed checkout worker after approved release
```

That detail is not approval theater. Reviewers often know the account number or target handle of their own service, while an agent's prose description can be plausible and wrong. Make the destination large enough to scan before the action description. People notice a surprising account number more readily than a subtle mismatch buried in a command payload.

Do not ask for blanket approval of "all work in this session" when the session spans multiple production accounts. A session can have a legitimate boundary, such as repeated changes to one deployment target, but it must not quietly widen. Require a fresh approval when the target handle changes, when an operation moves from read to write, or when a request touches a more sensitive scope than the reviewer approved.

Sallyport's per-session authorization identifies the connecting process, and its per-call credential setting can require an approval for each use. That is useful only when the action request carries a target that a reviewer can recognize and the executor has already bound it to one destination.

## Logs must connect intent, identity, and provider evidence

Cloud audit logs tell you what identity called an API, but they rarely explain the agent's instruction, target selection, or reviewer decision. Agent-side records can explain intent, but they do not prove the cloud received the expected call. Keep both records and join them with a request ID and the immutable target identifiers.

For every action, record these fields before and after execution:

- The agent process or session that made the request
- The selected target handle and its resolved immutable identifiers
- The execution identity observed during preflight
- The operation, resource scope, result, and request ID
- The provider audit event reference when the provider supplies one

AWS CloudTrail, Azure Activity Log, and Google Cloud Audit Logs each capture provider-side activity, subject to the services and logging configuration in use. Preserve those records in their own security boundary. Do not treat an agent activity log as a substitute for provider evidence, because a compromised local process could lie about a request it never completed.

Sallyport records agent runs and individual calls in separate journals projected from an encrypted, hash-chained audit log. Its `sp audit verify` command can verify that chain offline over ciphertext, which helps when you need to check whether local action records were altered after an incident.

The practical test is an investigation drill. Pick one approved change and ask an engineer to answer four questions without guessing: which agent process requested it, which target it selected, which cloud identity executed it, and which provider-side event confirms it. If any answer requires correlating timestamps by hand across three consoles, your records are too weak.

## A wrong-account incident usually begins before the API call

The visible failure is often a production change in the wrong account. The earlier failure is usually an uncontrolled resolution path. Treat the incident as a binding defect, not merely as a person who "used the wrong profile."

Consider a plausible sequence. A repository holds a script that accepts `ENV=prod`. Its wrapper translates that to an AWS profile named `prod`. A developer changed that profile during a migration to point at a shared services account, because the old payments account had no longer needed direct access. Later, an agent receives a request to restart a payment worker. It finds the script, sets `ENV=prod`, and runs it. The script's profile resolution picks shared services. The role has broad EC2 permissions because the team used it for operational convenience. The reboot succeeds in the wrong account.

No part of the cloud API was confused. The script made a valid request with a valid identity. A post-incident fix that merely tells agents to "double-check the account" will fail again because it leaves the same resolution path in place.

Repair this class of failure in a specific order:

1. Disable the affected session or role path and preserve the agent and cloud audit records.
2. Identify the exact selector that chose the wrong destination, such as a profile name, default subscription, environment tag, or caller-supplied role ARN.
3. Replace that selector with an approved target handle resolved by the executor.
4. Add a live identity comparison that rejects any mismatch before writes.
5. Split the broad role if it allowed unrelated account actions.

Then test the rejection path on purpose. Point a staging target at a credential for a different test account and confirm that the executor stops before the modifying API call. Teams often test success paths and never prove that their account binding fails closed.

## Rollout works best when you start with the riskiest writes

Do not wait for a complete cloud inventory before putting explicit binding around dangerous actions. Start with the operations that can change or expose production state: deployments, identity changes, network edits, data exports, secret rotation, and destructive infrastructure commands. Read-only discovery can help build the registry, but it should not silently grant execution access to newly found accounts.

First, list the accounts that can receive those actions and assign each a clear environment label and owner. Next, create a target record for each account-purpose pair. Then make one executor path resolve handles, obtain target-specific credentials, verify the live identity, and write the action record. Finally, remove direct agent access to ambient profiles, default subscriptions, and credential files.

Expect resistance from engineers who are used to switching accounts from a terminal with one variable. That habit is fast because it pushes context into a person's memory. An autonomous agent does not have that memory, and people reviewing its actions do not have it either. Put the context in the request, the target registry, and the execution evidence.

If you cannot state the account number, execution identity, and resource scope for an action before it runs, the agent does not yet have enough information to act safely. Make it gather the missing facts, or keep the action with a human.
