Skip to content

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

  1. 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.
  2. You register the role in Zenfra as an AWS integration and attach it to stacks.
  3. 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_TOKEN and AWS_REGION.
  4. 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 s3 backend.

The session name is zenfra-<run id>, so every change in CloudTrail maps back to one run.

Creating an AWS integration

  1. Navigate to Integrations in the sidebar.
  2. Under Cloud Providers, click Amazon Web Services.
  3. 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
  1. Open the Trust Policy tab, copy the policy, and set it as the trust relationship of your IAM role.
  2. 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_ID and AWS_SECRET_ACCESS_KEY in the worker's environment, or a mounted shared credentials file selected with AWS_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 read external ID and changes the write one, 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.