Skip to content
Expert Cloud & AI
Menu

AWS deployment access as code: federation, permissions and account baselines

Separate human access from pipeline federation, bind deployment roles to explicit claims, and roll out AWS account baselines with controlled exceptions.

Chris Baran

· 9 min read

On this page
  1. Give people and pipelines separate access paths
  2. Share the provider within each account
  3. Bind trust to the intended service connection
  4. Review the workload’s deployment authority
  5. Govern production changes through code
  6. Separate automatic baselines from account-specific exceptions
  7. Check the configuration contract before enabling deployment

A deployment pipeline needs authority to change infrastructure. An engineer investigating that workload needs a different kind of access. Treating both as “AWS access” hides the decisions that matter: which identity is trusted, what it can change, which accounts receive it and who approves its use. Combined with permission guardrails, this separation can serve as a governance control: production infrastructure changes go through reviewed code and approved pipelines.

For an oil and gas company, the engineering work included federated deployment-role templates, scoped workforce permission sets and an onboarding checklist connecting AWS configuration to Azure DevOps. Separating the shared identity-provider baseline from workload roles made each proposed grant of authority easier to review: its trust, permissions and target accounts could be examined together.

The fictional example here is a telemetry service. Operators read its logs, a pipeline releases its infrastructure, and a platform team maintains the access baseline. Those responsibilities have separate controls.

Give people and pipelines separate access paths

Human access starts with IAM Identity Center. A permission set defines the policies for a task; assignments connect users or groups to that permission set in selected accounts. Identity Center provisions the corresponding account roles. The telemetry operators need a scoped log-reading permission set and an appropriate session duration. Their operational duties determine the access, rather than the permissions needed to deploy the application. See AWS’s permission-set model.

The pipeline uses workload federation. It presents a signed OIDC token to AWS Security Token Service, which can issue temporary credentials for the deployment role. The role’s trust policy governs entry; its permission policies govern subsequent AWS actions. AWS’s web-identity API describes that exchange.

Migration needs its own scope. Earlier deployment roles retained IAM-user trust during transition, while newer purpose-built roles used OIDC-only trust. Introducing federation and retiring every older credential path are separate changes, each requiring an inventory of its consumers.

100%
Engineers reach a scoped operator role through IAM Identity Center. A separately authorised pipeline exchanges its token at STS against an account-local OIDC provider and deployment-role trust policy, then uses an approved CloudFormation execution role. StackSets provisions the provider and deployment-role baselines into eligible member accounts.

Human assignments, pipeline trust, workload permissions and baseline deployment govern different parts of the access path.

Human assignments, pipeline trust, workload permissions and baseline deployment govern different parts of the access path.

Share the provider within each account

An IAM OIDC provider is an account resource describing a trusted issuer. AWS requires a unique provider URL within an account, and a role must trust a provider in that same account. A central template can therefore provision the same issuer configuration into multiple accounts, with one provider per account reused by the relevant roles. See AWS’s OIDC provider requirements.

Give the provider a central platform/security owner. Workload templates reference its account-local ARN instead of competing to create the same resource. Spreading issuer configuration across the organisation does not grant deployment permission: role trust, role permissions and Azure DevOps pipeline authorisation remain separate decisions.

Roll out one provider baseline per account

Enable all features in AWS Organizations and have a management-account administrator activate trusted access for StackSets. Service-managed StackSets then create the execution roles needed in member accounts.

The CLI implementation uses a central helper to create or update a SERVICE_MANAGED StackSet, enable automatic deployment and explicitly create stack instances. Azure DevOps invokes that helper through an AWSShellScript task using an existing central security deployment connection.

The walkthrough below uses a reusable CloudFormation equivalent: a parent stack owns the provider StackSet, its child template and inputs. Deploy the provider before the role StackSets. Replace the fictional issuer and audience defaults with the integration’s actual values before rollout:

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  EligibleOuId:
    Type: String
  IssuerUrl:
    Type: String
    Default: https://ci.example.org/provider
  Audience:
    Type: String
    Default: example-deployment-audience
Resources:
  SharedProviderStackSet:
    Type: AWS::CloudFormation::StackSet
    Properties:
      StackSetName: shared-pipeline-provider
      PermissionModel: SERVICE_MANAGED
      CallAs: SELF
      Capabilities: [CAPABILITY_IAM]
      AutoDeployment:
        Enabled: true
        RetainStacksOnAccountRemoval: false
      ManagedExecution:
        Active: true
      OperationPreferences:
        FailureToleranceCount: 0
        MaxConcurrentCount: 1
      StackInstancesGroup:
        - DeploymentTargets:
            OrganizationalUnitIds: [!Ref EligibleOuId]
          Regions: [ap-southeast-2]
      Parameters:
        - ParameterKey: IssuerUrl
          ParameterValue: !Ref IssuerUrl
        - ParameterKey: Audience
          ParameterValue: !Ref Audience
      TemplateBody: |
        AWSTemplateFormatVersion: "2010-09-09"
        Parameters:
          IssuerUrl: {Type: String}
          Audience: {Type: String}
        Resources:
          PipelineProvider:
            Type: AWS::IAM::OIDCProvider
            Properties:
              Url: !Ref IssuerUrl
              ClientIdList: [!Ref Audience]

Supply an approved pilot OU; its child OUs are included. For organisation-wide member-account rollout, supply the organisation root ID instead. StackInstancesGroup covers existing accounts; automatic deployment covers future arrivals. Use one home region, here ap-southeast-2: IAM providers are global within an account, so deploying the same URL from multiple regional stacks creates an ownership collision.

The example deletes the provider stack when an account leaves the target scope. Retire dependent roles before that move, or deliberately retain the stack with an owner and retirement plan. The management account needs a separate, directly deployed provider stack because service-managed StackSets exclude it.

Protect the central Azure DevOps connection

The control connection must reach the management account or a registered delegated-administrator account with StackSets authority. The example uses management-account CallAs: SELF; use DELEGATED_ADMIN for a registered delegate. Bootstrap this connection with existing authorised access: a new federation path cannot deploy its own missing provider. Treat this as privileged organisation-wide administration; selecting an OU does not confine a delegated administrator’s available deployment authority.

The central identity needs permission to operate the parent CloudFormation stack and to create or update the StackSet, manage its instances and read operation results. If the parent stack uses a CloudFormation execution role, put the StackSets permissions there and restrict iam:PassRole to that role. Scope grants to the baseline and its home region where supported. Application deployment roles should have no baseline-administration authority.

Save the parent template above as templates/shared-provider-stackset.yaml. Use a Test stage for cfn-lint, followed by a Deploy stage requiring success and refs/heads/main. This manually queued skeleton declares all three inputs, lints the parent template and uses the Create/Update Stack task to update its control stack:

trigger: none
parameters:
  - name: eligibleOuId
    type: string
  - name: issuerUrl
    type: string
    default: https://ci.example.org/provider
  - name: audience
    type: string
    default: example-deployment-audience
pool:
  vmImage: ubuntu-latest
stages:
  - stage: Test
    jobs:
      - job: Lint
        steps:
          - script: python -m pip install cfn-lint
          - script: cfn-lint templates/shared-provider-stackset.yaml
  - stage: Deploy
    dependsOn: Test
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - job: Provider
        steps:
          - task: CloudFormationCreateOrUpdateStack@1
            inputs:
              awsCredentials: central-baseline-control
              regionName: ap-southeast-2
              stackName: shared-provider-control
              templateSource: file
              templateFile: templates/shared-provider-stackset.yaml
              templateParametersSource: inline
              templateParameters: |
                - ParameterKey: EligibleOuId
                  ParameterValue: '${{ parameters.eligibleOuId }}'
                - ParameterKey: IssuerUrl
                  ParameterValue: '${{ parameters.issuerUrl }}'
                - ParameterKey: Audience
                  ParameterValue: '${{ parameters.audience }}'
              capabilityIAM: true
              capabilityNamedIAM: false

In this walkthrough, the parent CloudFormation stack owns StackSet updates; do not mix in direct CLI StackSet changes.

Replace the illustrative connection name with the protected central connection. Route proposed baseline changes through pull requests and security review. Restrict connection administration and authorise only the baseline pipeline. For production, configure approvals and checks on the connection, outside editable pipeline YAML; the main-branch condition alone does not establish those controls.

Application pipelines use separate AWS OIDC service connections, one per target account. Bind each workload role to its connection’s exact subject and authorise individual pipelines. Protect production workload connections with the same approval and administration controls. Reusing the account’s provider gives the telemetry workload no access to the central control connection or its StackSets authority.

Confirm readiness before releasing role StackSets

Wait for the provider operation to succeed, then inspect per-account operation results and confirm successful provisioning in every intended account before releasing role StackSets. An accepted API request is not completed provisioning.

For the telemetry example, configure any automatically deployed role StackSet’s automatic-deployment dependencies to include the provider StackSet ARN for future account onboarding. Check both resulting instances before authorising pipeline use. Managed execution queues operations; it does not itself declare that dependency.

Bind trust to the intended service connection

The issuer identifies who signed the token. The audience identifies its intended recipient. The subject identifies the workload identity being presented. For the AWS Toolkit’s Azure DevOps federation flow, that subject identifies a service connection. Accepting the issuer alone leaves the role insufficiently tied to the intended deployment path.

The following trust document shares the fictional provider above. Apply CloudFormation’s Fn::Sub substitution when assigning it to the role’s AssumeRolePolicyDocument, so the provider ARN uses the destination account:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/ci.example.org/provider"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "ci.example.org/provider:aud": "example-deployment-audience",
        "ci.example.org/provider:sub": "sc://example-team/telemetry/production"
      }
    }
  }]
}

Replace the issuer, audience and subject with the values issued for the actual integration. The provider URL includes https://; its ARN suffix and condition-key prefix omit the scheme. Use exact claim matching. AWS’s Azure DevOps federation walkthrough explains the AWS Toolkit flow. Microsoft’s Azure Resource Manager federation guidance documents a different service-connection setup and issuer changes specific to it; claim formats need to be checked for the connection type in use.

A matching subject establishes which connection the job used. An authorised job can still contain harmful instructions. Protect pipeline code, build inputs and connection administration, then limit what the resulting AWS session can do.

Review the workload’s deployment authority

For the telemetry service, separate the pipeline’s deployment role from the execution role CloudFormation uses to change resources. Scope stack operations to the intended workload, and give the execution role only the service actions and resources that workload requires.

One statement in the deployment role’s policy permits passing the designated execution role to CloudFormation:

Effect: Allow
Action: iam:PassRole
Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/TelemetryExecution
Condition:
  StringEquals:
    iam:PassedToService: cloudformation.amazonaws.com

That statement covers delegation; stack-operation permissions and the execution role’s permissions still need their own definitions. AWS documents scoping PassRole by role and service. Once a service role is associated with a stack, other principals allowed to operate that stack can use it without holding iam:PassRole themselves. Review stack access with the CloudFormation service-role behaviour in mind.

CDK adds another layer to trace: deployment, publishing and lookup roles created by bootstrapping. Follow every permitted role assumption through to the final execution permissions. Similarly, aws:CalledVia conditions apply to supported forwarded requests; AWS says the key is absent when a service uses a service role. Choose and verify the delegation model before relying on that condition.

An additional workload permission should arrive as a reviewable change: required action, resource scope, target accounts, owner and review point. Keep shared deployment identities under platform control. The SCP guardrails article explains the outer policy layer; the controlled experimentation article explains how to agree a bounded workload’s scope.

Govern production changes through code

Make reviewed, versioned infrastructure as code (IaC), deployed through approved pipelines, the required path for production infrastructure changes. Keep routine human permission sets to reading and diagnosing production; exclude infrastructure mutation, deployment-role assumption and iam:PassRole. Enforce those permissions across console, CLI and API access.

OIDC and shared providers supply authentication foundations; preventing click-ops also requires least-privilege workforce permissions, scoped deployment and execution roles, and production SCP guardrails. Test guardrails against authorised deployments and attempted human changes before rollout. SCPs cap permissions without granting them; they do not constrain management-account identities or service-linked roles.

Close indirect routes: human stack-operation permissions can invoke an attached CloudFormation execution role without iam:PassRole. Reserve mutating stack operations for the approved deployment path. Protect pipeline code, service-connection administration and role trust and permission policies so neither operators nor deployment jobs can expand their own authority.

Keep an audit trail linking approved code, the authorised pipeline run and the scoped deployment. Make break-glass access narrow, time-bounded, approved and logged, then reconcile emergency changes into IaC. Use CloudFormation drift checks to detect out-of-band changes to supported, explicitly configured stack resource properties. Complement these checks with a production resource inventory and a review of API activity in CloudTrail to identify unmanaged resources and changes outside drift coverage.

Separate automatic baselines from account-specific exceptions

Automatic provider deployment onboards a baseline after an account enters an eligible OU. It does not create accounts; account creation and Account Factory are separate capabilities.

For a shared provider baseline, OU-wide eligibility can be the intended rule. An exceptional workload role may have a much narrower destination. Keep those deployment configurations separate.

An account filter does not permanently confine an automatically deployed StackSet. AWS documents that automatic deployments ignore account-level targeting filters for newly added accounts. For a role restricted to selected accounts, disable automatic deployment and explicitly manage its targets, or use a dedicated OU whose entire membership is eligible. An INTERSECTION filter combined with automatic deployment cannot establish that lasting restriction. See StackSet automatic-deployment considerations.

Treat pipeline readiness as a sequence: provider available, workload role successfully provisioned, service connection configured, then pipeline use authorised. Provision and check workforce assignments separately for the people who need account access.

Check the configuration contract before enabling deployment

The fragments can be checked locally. Parse the provider definition, confirm its audience agrees with the trust policy, and check the trust document against independently reviewed configuration. This deliberately strict Python check accepts only the approved single-statement configuration contract:

def check_trust(policy, provider_arn, issuer_key, audience, subject):
    [grant] = policy["Statement"]
    expected = {
        "Effect": "Allow",
        "Principal": {"Federated": provider_arn},
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {"StringEquals": {
            f"{issuer_key}:aud": audience,
            f"{issuer_key}:sub": subject,
        }},
    }
    if policy["Version"] != "2012-10-17" or grant != expected:
        raise ValueError("Trust policy differs from the approved contract")

Use the approved target configuration for the expected values. Exercise rejection cases by changing the audience, substituting another subject, widening it to a wildcard, changing the provider account or adding another trust statement. Those checks detect configuration changes; token signature validation and AWS authorisation remain separate runtime checks.

Review the rest of the access path just as concretely:

BoundaryAcceptance check
Human accessThe assigned permission set permits the operator’s log-reading task and excludes deployment actions.
Deployment authorityStack, execution-role and any CDK role permissions stay within the workload’s approved scope.
Baseline targetingOU-wide baselines and selected-account roles have distinct automatic-deployment settings.
Release controlOnly intended pipelines can use the connection; required checks are owned by the appropriate administrators.

Before production use, verify the rendered configuration and allowed and denied behaviours in a controlled account. A successful token exchange alone cannot demonstrate the complete boundary.

Bring one workload to an AWS Workload Foundations Assessment. We trace its human and deployment access, identify the controls it depends on and agree a practical path to a maintainable foundation.