Technical deep dive
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
- Give people and pipelines separate access paths
- Share the provider within each account
- Bind trust to the intended service connection
- Review the workload’s deployment authority
- Govern production changes through code
- Separate automatic baselines from account-specific exceptions
- 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.
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: falseIn 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.comThat 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:
| Boundary | Acceptance check |
|---|---|
| Human access | The assigned permission set permits the operator’s log-reading task and excludes deployment actions. |
| Deployment authority | Stack, execution-role and any CDK role permissions stay within the workload’s approved scope. |
| Baseline targeting | OU-wide baselines and selected-account roles have distinct automatic-deployment settings. |
| Release control | Only 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.
