AWS Integration
Zenfra connects to Amazon Web Services by assuming an IAM role in your account with STS
AssumeRole. No long-lived AWS keys are stored in Zenfra: every run receives short-lived
session credentials that expire on their own.
An integration works in one of two credential modes, chosen by a single switch:
| Control plane (default) | Worker | |
|---|---|---|
Who calls AssumeRole |
Zenfra, using its own IAM principal | Your private worker, using the AWS identity the worker host already has |
| Trust-policy principal | Zenfra's principal (shown in the UI) | Your worker's role |
| External ID | Generated by Zenfra | Generated by Zenfra, or a value you choose |
| Where the credentials exist | Zenfra's control plane hands them to the worker for the run | Only on your worker and inside the run; the control plane never receives them |
| Worker pools | Public and private | Private only |
| Verify button | Available | Not applicable; the role is verified by running |
Choose worker mode when your workers run inside your own AWS account and you do not want Zenfra's principal in your trust chain.
Worker mode changed on 2026-09-15. Integrations created with Generate credentials in worker enabled before that date used to be bootstrapped by Zenfra's control plane. They now mean what this page describes: your worker's own AWS identity assumes the role. If you have such an integration, update the role's trust policy to name the worker principal instead of Zenfra's role (see Worker mode), make sure every stack that uses it runs on a private worker pool, then validate with a Plan Only run (read) followed by a Plan & Apply run (write).
How it works
- You create an IAM role in your AWS account with the permissions your infrastructure needs and a trust policy that allows the right principal to assume it.
- You register the role in Zenfra as an AWS integration and attach it to stacks.
- When a run starts, the role is assumed with an external ID (a generated one identifies
your organization and the stack, and the current version-2 format also the integration;
see External ID formats below), and the resulting credentials are placed in
the run environment as
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKENandAWS_REGION. - Terraform or OpenTofu picks them up from the environment, so no credentials go into the
provider configuration. On a stack with customer-managed S3 state,
the same credentials serve the
s3backend.
The session name is zenfra-<run id>, so every change in CloudTrail maps back to one run.
Creating an AWS integration
- Navigate to Integrations in the sidebar.
- Under Cloud Providers, click Amazon Web Services.
- Click New Integration and fill in the form:
| Field | Description |
|---|---|
| Name | A descriptive name (e.g. production-aws) |
| Space | The space the integration belongs to; stacks in that space and its child spaces can attach it |
| Role ARN | The IAM role to assume (e.g. arn:aws:iam::123456789012:role/ZenfraTerraformRole) |
| Generate credentials in worker (private worker pools only) | Off: control-plane mode. On: worker mode, see below |
| External ID (worker mode only, optional) | Your own external ID. Leave empty to use the generated one |
| Tag sessions | Adds session tags (zenfra:org_id, zenfra:stack_id, zenfra:run_id, zenfra:permission) to the assumed session for CloudTrail. Requires an sts:TagSession statement in the trust policy |
| Duration | Lifetime of the session credentials, 900 to 3600 seconds (default 3600) |
| Region | The AWS region used for the STS call and exported as AWS_REGION |
| Labels | Optional labels, including an auto-attach label |
- Open the Trust Policy tab, copy the policy, and set it as the trust relationship of your IAM role.
- In control-plane mode, click Verify to confirm Zenfra can assume the role. In worker mode there is nothing to verify from Zenfra's side; run a proposed run instead.
Attaching to stacks and permissions
An attachment binds one integration to one stack with a permission level:
| Permission | Used for |
|---|---|
| Read | The planning phase of proposed and tracked runs |
| Write | Applying, destroying and tasks (includes read) |
The permission is selected per phase, not per run: a tracked run plans with read
credentials and, after approval, applies with write credentials. The external ID carries
the permission as its last segment, so a trust policy that matches only @read lets the
role be assumed for planning but not for applying. Both phases assume the same role, so
what the credentials can do is set by that role's IAM permissions, not by the phase.
Attach from the stack side (the wizard's Bundles step or Cloud integrations when editing
the stack) or from the integration's Attachments tab, where Attach Stack asks for the
stack and the permission. The tab lists each attachment's stack, permissions and type:
Manual, or Auto for stacks attached through the integration's auto-attach label. Detaching
an auto-attached stack warns that it may be re-attached while the stack still carries the
label; remove the label from the stack or clear it on the integration to detach for good.
Attached integrations are also visible on the stack's Configuration tab under
Cloud Integrations.
Two refusals come from the permission model. A run whose phase has no attachment with the
required permission fails with no_matching_permission (no cloud integration attachment
with the permission this phase requires): a write-only attachment cannot plan. A credential
request that does not fit the run's type and status is refused with phase_not_allowed.
Trust policy and external IDs
The Trust Policy tab always shows the policy that matches the saved integration. Changing the credential mode, the external ID or session tagging changes the policy; update the role in AWS after saving.
External ID formats
| Integrations | Format |
|---|---|
| Created before September 2026 | zenfra@<org id>@<stack slug>@<read\|write> |
| Created since | zenfra@<org id>@<integration id>@<stack slug>@<read\|write> |
| Worker mode with a custom external ID | Your value, used verbatim for every stack and phase |
The trust policy uses a pattern, so one role can serve every stack of an organization
(zenfra@<org id>@*) or every stack of one integration
(zenfra@<org id>@<integration id>@*). Copy the exact IDs from the External ID
condition section of the tab. The Overview tab shows which generated format the
integration uses.
Control-plane mode
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "<zenfra principal, shown in the UI>" },
"Action": "sts:AssumeRole",
"Condition": {
"StringLike": { "sts:ExternalId": "zenfra@<org id>@*" }
}
},
{
"Effect": "Allow",
"Principal": { "AWS": "<zenfra principal, shown in the UI>" },
"Action": "sts:TagSession"
}
]
}
The sts:TagSession statement is present only when Tag sessions is on. It must stay
a separate statement: the sts:ExternalId condition applies to sts:AssumeRole only.
Worker mode
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "<your-worker-role-arn>" },
"Action": "sts:AssumeRole",
"Condition": {
"StringLike": { "sts:ExternalId": "zenfra@<org id>@*" }
}
}
]
}
Replace <your-worker-role-arn> with the IAM principal your worker runs as. With a custom
external ID the condition becomes "StringEquals": { "sts:ExternalId": "<your value>" }.
Zenfra's principal is not needed in worker mode and can be removed from the role once a
run has succeeded.
Worker mode: what the worker needs
In worker mode the worker process calls STS itself through the AWS SDK's default credential chain (the Private Worker Setup guide covers running the worker itself). Give it an identity in one of these ways:
- EC2 instance profile: attach a role to the instance. When the worker runs in Docker,
raise the IMDSv2 hop limit to 2 so the container can reach the instance metadata service:
aws ec2 modify-instance-metadata-options --instance-id <id> --http-endpoint enabled --http-put-response-hop-limit 2 - Kubernetes: IRSA or EKS Pod Identity; the SDK picks them up on its own.
- Static keys as
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYin the worker's environment, or a mounted shared credentials file selected withAWS_PROFILE.
The SDK takes the first source that answers, environment variables first. Static keys left in the worker's environment override an instance profile or IRSA and make the worker a different principal than the trust policy expects.
The worker identity needs one permission, sts:AssumeRole on the roles it should assume,
plus sts:TagSession when Tag sessions is on. To find the principal to trust, run
aws sts get-caller-identity in the worker's own credential context (inside the worker
container, or with the same profile, IRSA or Pod Identity), since the host's identity can
differ: an ARN like
arn:aws:sts::<account>:assumed-role/<RoleName>/<instance id> means the trust policy must
name arn:aws:iam::<account>:role/<RoleName>.
Worker mode is refused on public worker pools: a stack on a public pool with a worker-mode
integration fails at the start of the run with
worker credential mode requires a private worker pool.
Reading the run log
In worker mode, each phase records the outcome of the credential step as one line (control-plane mode writes no such lines):
cloud credentials assumed <role ARN> as zenfra-<run id> (permission=read)on success.cloud credentials failed: worker identity <ARN> could not assume <role ARN> (<STS error code>)when the worker's own identity was refused. The ARN in that line is the principal the trust policy has to allow.
Verifying an integration
In control-plane mode, Verify asks Zenfra to assume the role once with a test external ID. It confirms that the role exists, that the trust policy allows Zenfra's principal, and that the external ID condition matches.
In worker mode the button is hidden and the overview shows "Role assumption is verified on your worker at run time", because only your worker can test that trust. Editing the mode, the external ID, session tagging or the role ARN clears the last verification.
Managing integrations
The AWS Integrations list shows each integration's name, status (Active, Error or
Disabled) and role ARN, with View, Edit, Verify and Delete in its menu.
Creating, editing, verifying, deleting and attaching require the Admin role.
Delete is refused while stacks are attached (This integration has active stack
attachments.) unless you tick Force delete (removes all attachments); those stacks then
stop receiving credentials from this integration on their next run.
Security
- No long-lived credentials: only temporary session tokens are used.
- External ID protection: prevents confused-deputy attacks; generated IDs are distinct per stack and phase (a custom ID is used as given).
- Phase-scoped assumption: planning presents the
readexternal ID and changes thewriteone, so a read-only attachment can plan but not apply. - Worker mode keeps Zenfra out of the trust chain: the control plane never receives the credentials. This does not stop a run from printing its own short-lived credentials into its log, exactly as with any CI system; treat run logs accordingly.
- Session tagging (optional) records organization, stack, run and permission on every assumed session for CloudTrail.
Worker mode changed meaning on 2026-09-15; the September 2026 Upgrade Notes list what to do for integrations created before then.