# AI agent infrastructure permissions for safe applies

AI agents should not receive a standing permission that means "apply infrastructure changes." They should receive a narrow, expiring authorization to apply one reviewed change to one proven target account, with a recovery procedure that an operator has read and accepted.

I have seen teams mistake a Terraform plan for a safety control and an approval button for a decision. Neither is enough on its own. A plan can be regenerated against another account. An approval can outlive a changed commit. A rollback note can say "revert" when the change destroys data or alters a dependency that no longer exists.

The useful boundary is more demanding: the agent prepares evidence, a person reviews that evidence, and the execution path refuses to proceed if the evidence no longer describes the action. That sounds fussy until the first time an assistant inherits the wrong cloud profile, reads a stale state snapshot, and aims a perfectly valid command at production.

## Permission must bind an action, not a job title

AI agent infrastructure permissions should authorize a concrete mutation, not a category such as "deploy," "Terraform," or "production operations." Broad categories feel convenient because people think in roles. Cloud APIs execute requests, however, and requests have targets, parameters, identities, and consequences.

A useful apply authorization binds at least these facts:

- The immutable source revision and the configuration directory used to make the plan.
- The reviewed plan artifact or its SHA-256 digest.
- The target cloud account or tenant identity reported by the execution credential.
- The execution identity, role, subscription, project, and permitted region where relevant.
- An expiry and a recovery reference that tells the operator what happens if the apply must stop or be reversed.

The distinction between permission to *calculate* a change and permission to *make* it matters. Let an agent inspect repositories, call read-only APIs, run validation, and propose a plan. Do not silently treat those activities as evidence that it may mutate production. Discovery access tends to expose operational detail. Mutation access changes the bill, availability, and sometimes the evidence available after an incident.

A role named `InfrastructureDeployer` is not an approval object. It is an implementation detail. If it can apply anything in every account whenever a process obtains it, then your approval process depends on people remembering to use it correctly. Agents are fast enough to turn that memory test into an incident.

## A reviewed plan must be the exact artifact that applies

A reviewed plan has value only if the apply command consumes that same plan. Re-running `terraform plan` after someone approves the output creates a new artifact, even when the files look unchanged.

Terraform's command documentation makes this distinction clear. `terraform plan -out=FILE` writes a plan file intended for `terraform apply FILE`; `terraform apply` without a saved plan creates a new plan before it asks for confirmation. The second form is fine for an interactive human session. It is the wrong form for an agent workflow that claims a human reviewed the proposed changes.

Use a saved plan and render it to JSON for review. A minimal shell sequence looks like this:

```sh
set -euo pipefail

git rev-parse HEAD > evidence/source-revision.txt
terraform init -lockfile=readonly
terraform plan -out=evidence/change.tfplan
terraform show -json evidence/change.tfplan > evidence/change.json
shasum -a 256 evidence/change.tfplan > evidence/change.tfplan.sha256
terraform providers lock -platform=darwin_arm64
```

The output digest has a simple shape:

```text
f1f5f2...a94c  evidence/change.tfplan
```

The approval record should copy that full digest, not merely attach a screenshot of the terminal. The apply runner then verifies it before execution:

```sh
shasum -a 256 -c evidence/change.tfplan.sha256
terraform apply -input=false evidence/change.tfplan
```

This prevents a common failure: the agent opens a pull request, produces a plan, gets an approval comment, then fetches the latest main branch and runs a fresh plan. A colleague may have merged a different change between those actions. The later plan can add a deletion, switch an image version, or point at another provider alias. The reviewer approved yesterday's proposed graph, not whatever the runner sees now.

A binary Terraform plan can contain sensitive values. Do not paste it into tickets or chat. The JSON representation can also reveal sensitive material depending on provider schemas and values, so redact only through a review process you understand. Redaction must not remove resource addresses, actions, target identity, or dependency changes. Those are exactly the facts an approver needs.

The saved-plan pattern is strongest when the execution environment is also pinned. Use the same provider lock file, Terraform version, variables, backend configuration, and workspace selection that created the plan. If any of those inputs change, discard the plan and generate a fresh review package. Trying to salvage an old approval is how teams convert a sensible control into theatre.

## The named target must come from the credential, not a label

A target named `prod` proves almost nothing. Repositories copy directory names. Workspaces drift. Environment variables linger in shells. An agent can faithfully follow a configuration called production while authenticated to a sandbox, or worse, a production account selected by an inherited profile.

Require the execution identity to ask the cloud control plane who it is before planning and again immediately before applying. Store the result in the evidence package and compare it against the approved target.

For an AWS-based action, a check can be as plain as this:

```sh
aws sts get-caller-identity --output json > evidence/caller-identity.json
cat evidence/caller-identity.json
```

The result identifies the account and principal:

```json
{
  "UserId": "AROAXXXXX:apply-run",
  "Account": "123456789012",
  "Arn": "arn:aws:sts::123456789012:assumed-role/InfraApply/apply-run"
}
```

For Google Cloud, record the active account and project separately. For Azure, record the subscription ID and tenant ID from the credential context. Do not rely on a cloud account display name, since people can duplicate those names. Numeric or globally unique identifiers make review less pleasant to read but much safer to execute.

The pre-apply check must use the same credential path as the apply. That sounds obvious, yet wrappers often violate it. A planning script might use a short-lived assumed role while a later command inherits a developer's default profile. An agent that calls a remote runner can plan locally but apply remotely under a service identity it never inspected.

Put a machine-checkable target assertion in the runner. This example refuses an unexpected AWS account:

```sh
expected_account="123456789012"
actual_account="$(aws sts get-caller-identity --query Account --output text)"

if [ "$actual_account" != "$expected_account" ]; then
  printf 'refusing apply: expected account %s, got %s\n' \
    "$expected_account" "$actual_account" >&2
  exit 1
fi
```

That check does not replace human review. It catches an entire class of mistakes before any API call mutates infrastructure. Pair it with provider configuration that declares the allowed account or subscription where the provider supports that guard. A provider-level assertion and a runner-level assertion fail independently, which is exactly what you want.

## State drift makes old approvals unsafe

A plan describes the desired change relative to a particular observed state. It does not promise that state will still exist an hour later.

Another engineer may deploy. An autoscaler may add or remove resources. A cloud service may rotate an attachment, replace a node, or finish an asynchronous operation. Terraform will often notice some of this when it refreshes state, but the safe response to a material difference is not to press ahead because the plan looked small earlier. Generate a new artifact and review the new diff.

Expiry handles much of this. Make an approval short enough that a human can still remember why they accepted it. The right duration varies with your release process, but it should cover a defined execution window rather than an entire workday by default. Once the window closes, force a new plan, repeat target verification, and require a fresh approval.

You also need an invalidation rule. Reject a plan when any of these changes occur after review:

- The source revision, variable set, module reference, or provider lock file changes.
- The workspace, backend, account, subscription, tenant, project, or region differs from the approved evidence.
- The plan digest does not match the approval record.
- A relevant state lock, refresh, or precondition reports a conflict that changes the proposed action.

Do not use "no changes in the pull request" as a substitute for this rule. External data sources, provider defaults, current state, and credentials sit outside the diff. Infrastructure work has too many inputs for a source-only approval to carry the entire decision.

Some teams try to solve drift by allowing the agent to automatically replan until the plan becomes clean. That recommendation is popular because it reduces queue time. It is wrong for consequential accounts. Automatic replanning can turn a reviewed update into an unreviewed replacement. Allow automatic replanning for a read-only preview if you want, but require human review again before mutation.

## Rollback must describe a recoverable state

A clear rollback path says how operators return the service to an acceptable condition, who can do it, what data it risks, and when they should stop. "Run Terraform destroy" and "revert the commit" rarely meet that standard.

Infrastructure changes fall into different recovery classes. Treating them alike creates false confidence.

A reversible configuration change, such as a security-group rule or a load-balancer weight, may support a direct inverse apply from a known good revision. A replacement of an instance group may need capacity checks before reversal. A database migration may be irreversible after it writes data, so the recovery path may be a forward migration, a restore from a verified backup, or a feature flag that stops new writes.

Write the rollback path in operational language. A useful record answers these questions:

1. What observable condition tells us to roll back, such as failed health checks, rising error rates, or a failed smoke test?
2. Which exact revision, parameter set, or command returns service to the known good configuration?
3. What prerequisite must exist first, such as a backup restore point, spare capacity, or an approved maintenance window?
4. Who has authority to act if the agent session has ended or the original approver is unavailable?
5. What cannot be restored automatically, including records, secrets, public addresses, or manual cloud changes?

A recovery path needs testing before the incident, not an optimistic sentence during it. If a team calls a change reversible, run the reversal in a representative environment and record the conditions that made it work. Providers may retain a deleted resource name, quotas may block recreation, and dependent services may cache an old endpoint. These details show up when the change is already under pressure.

For destructive changes, require a separate decision. Deleting a resource after a plan review is not morally equivalent to updating it. The agent should surface the deletion addresses, replacement actions, retention settings, and backup evidence in a form that cannot disappear inside hundreds of harmless updates. If the change includes a database, object store, identity binding, network boundary, or DNS zone, insist that the recovery owner has read the plan.

## The apply runner should refuse ambiguity

The runner that performs the mutation must enforce the approval facts itself. A bot that can receive a chat message saying "go ahead" has no dependable way to distinguish a confirmed plan from a casual instruction.

Use a structured approval record. It can live in a signed deployment system, a protected repository record, or another controlled store. The storage choice matters less than the fields and the verification. This illustrative JSON shows the shape:

```json
{
  "change_id": "infra-2025-041",
  "source_revision": "4ad7d2f",
  "plan_sha256": "f1f5f2...a94c",
  "target": {
    "cloud": "aws",
    "account_id": "123456789012",
    "region": "us-east-1",
    "workspace": "production"
  },
  "approved_by": "operator-id",
  "expires_at": "2025-04-18T15:30:00Z",
  "rollback_ref": "runbook: payments-api capacity revert"
}
```

The agent can assemble this request, but it should not write `approved_by` or extend `expires_at`. The approval service should add those facts after a person sees the rendered change. The apply runner reads the record, recomputes the plan digest, checks source revision and target identity, then marks the authorization consumed before it sends the first write request.

Consuming an approval matters. Without it, an agent can retry an approved action later after the environment has changed. A failed apply also needs an explicit status. Do not mark it complete simply because the runner issued a command. Record whether Terraform returned success, whether a cloud API operation remains pending, and whether an operator accepted the resulting state.

Keep the agent's write surface narrow. It may need HTTP calls for deployment APIs or SSH access to a controlled runner, but it should never receive a reusable cloud secret in its context. Sallyport keeps API and SSH credentials in its encrypted vault while it performs the requested action and returns the result to the agent. That setup helps prevent credential copying, but it does not make a vague apply request safe.

## Per-call approval belongs around the dangerous boundary

Per-call approval has a place in infrastructure work, but it cannot replace artifact review. If an agent asks for permission on every cloud API request, operators will approve a long sequence without understanding the aggregate result. That is approval fatigue, and it trains people to click through the only control they have.

Place human intervention where it decides something meaningful: approving a plan tied to a target and then allowing the bounded apply run. Reserve individual confirmations for actions with unusual blast radius, such as secret rotation, deletion of a protected object, break-glass access, or a command outside the expected runner contract.

Sallyport's per-session authorization can establish that a specific agent process may use an action channel for its current run, while per-call keys can require a separate confirmation for sensitive credentials. That is a clean credential boundary. Your deployment workflow still needs to define which call counts as an approved apply and which credentials deserve per-call friction.

A human should see enough evidence to make a decision without reading raw provider traffic. Show resource addresses grouped by action, replacements and deletions separately, target identity, source revision, digest, expiry, and the rollback reference. Then provide access to the full plan for the person who needs it. Hiding a destructive change under a hundred updates is a presentation failure, not an operator failure.

## A failed apply needs a different decision than a successful one

Terraform can return a failure after it has changed several resources. Cloud control planes can also accept a request and finish it later. Treating any nonzero exit status as "nothing happened" is one of the more dangerous habits in automated operations.

When an apply fails, freeze automatic retries. Capture the runner output, state lock information, target identity, and the subset of resources that completed. Then inspect the actual environment before choosing a response. A blind retry can compound a partial failure, while an immediate rollback can remove an intermediate resource that the provider is still creating.

Use this decision sequence:

1. Confirm the current cloud identity and collect the actual status of every resource named in the failed operation.
2. Determine whether the desired state is safe to complete, safe to reverse, or requires a repair plan.
3. Generate a fresh plan from the current state and have an operator review it as a new change.
4. Record the incident decision beside the original approval, including any manual actions that Terraform cannot represent.

This is where audit records earn their keep. Keep the source revision, plan digest, approval identity, target evidence, command transcript, resulting state, and follow-up decision together. A tamper-evident event trail is better than screenshots distributed across chat threads, because responders need to establish sequence, not reconstruct intent from memory.

Do not promise that automation will undo every failed apply. Some changes require a human who understands the service dependency, data durability, and customer impact. The agent can gather evidence quickly. It should not invent a recovery operation because the pipeline expects a green result.

## Make the first production rollout deliberately boring

The first agent-mediated production apply should change something small, reversible, and observable. Pick a known configuration adjustment with an already tested recovery procedure, not a migration, a networking redesign, or a secret rotation that touches multiple consumers.

Run the complete workflow under normal conditions: generate the evidence package, verify the exact account, review the rendered plan, approve the digest, apply the saved artifact, inspect the result, and consume the authorization. Then run a controlled failure drill. Expire an approval, alter the source revision, or point the runner at a nonapproved account and confirm that the runner refuses to act.

Those refusal tests matter more than a successful happy-path deployment. Every tool looks disciplined when the account, plan, and state happen to agree. The control proves itself when a rushed operator, stale artifact, or confused agent asks it to do the wrong thing and it stops.

Do not expand the permission model because the first rollout feels slow. Measure where review time goes. If people spend time comparing account identifiers, improve the evidence display. If plans contain too much unrelated churn, fix module ownership or state boundaries. If rollback notes are weak, require service teams to write and test them. Broad standing access is not a cure for an awkward release process; it only hides the weakness until an agent reaches it.
