Appearance
OIDC Federated Authentication
Purpose
This document describes the OpenID Connect (OIDC) federated identity configuration used by GitHub Actions CI/CD pipelines to authenticate to Azure and AWS without long-lived credentials.
Why OIDC
Traditional CI/CD authentication uses long-lived service account credentials (client secrets, access keys) stored in GitHub Secrets. OIDC eliminates these by establishing a trust relationship between GitHub's identity provider and Azure/AWS. Each workflow run receives a short-lived token that expires after the job completes.
Benefits:
- No credentials to rotate or manage
- No risk of credential leakage from GitHub Secrets
- Each workflow run gets a unique, short-lived token
- Trust is scoped to specific repositories and branches
Azure Configuration
Azure PROD Tenant (d477c9f8 — ciriusgroup.com)
| App Registration | Repository | Federated Credential Subject | Purpose |
|---|---|---|---|
github-actions-azure-infra | azure-infra | repo:Cirius-Group-Inc/azure-infra:ref:refs/heads/main | Terraform apply — main branch |
github-actions-azure-infra | azure-infra | repo:Cirius-Group-Inc/azure-infra:environment:production | Terraform apply — environment protection gate |
github-deploy-main | ops-automation | repo:Cirius-Group-Inc/ops-automation:ref:refs/heads/main | Cost reporting, HIPAA audit, SES send, EDL upload, backup audit |
github-actions-maester | ops-automation | repo:Cirius-Group-Inc/ops-automation:ref:refs/heads/main | Maester M365 security audit (Graph API read-only) |
cirius-findings-tracker | bedrock-hub | repo:Cirius-Group-Inc/bedrock-hub:ref:refs/heads/main | SecOps platform Container App deploys |
github-actions-azure-infra has two federated credentials — both are required. The main credential covers direct branch pushes; the environment:production credential covers environment-gated deploys that require reviewer approval before the job runs.
github-deploy-main is the multi-purpose ops-automation role. It holds policies for Cost Explorer read, SES send, cross-account backup audit assume-role, pentest target discovery, and EDL S3 upload. This is an intentional consolidation — ops-automation workflows run from a single trusted identity.
Azure DDE Tenant (ff1c5d68 — ciriusdde.com)
| App Registration | Repository | Federated Credential Subject | Purpose |
|---|---|---|---|
github-deploy-app | azure-dde-infra | repo:Cirius-Group-Inc/azure-dde-infra:ref:refs/heads/main | Terraform apply for DDE infrastructure |
github-deploy-app | ops-automation | repo:Cirius-Group-Inc/ops-automation:ref:refs/heads/main | ops-automation workflows authenticating to DDE tenant |
kobe-audit | ops-automation | repo:Cirius-Group-Inc/ops-automation:ref:refs/heads/main | DDE Maester / security audit (Graph API read-only, DDE tenant) |
github-deploy-app has two separate federated credentials: one for azure-dde-infra (DDE infrastructure repo) and one for ops-automation (compliance pipelines). This allows ops-automation workflows to authenticate independently to the DDE tenant without sharing credentials with the azure-dde-infra pipeline.
kobe-audit is the DDE-tenant equivalent of github-actions-maester. It holds the Maester Graph API permissions (read-only) and the DDE security agent permissions for the DDE tenant.
RBAC Assignments
The app registrations above are granted the following roles at subscription level:
| App Registration | Azure Role | Scope |
|---|---|---|
github-actions-azure-infra | Contributor | PROD subscription |
github-actions-azure-infra | Storage Blob Data Contributor | Terraform state storage account |
github-deploy-app | Contributor | DDE subscription |
github-deploy-app | Storage Blob Data Contributor | DDE Terraform state storage account |
github-actions-maester | Reader | PROD subscription |
kobe-audit | Reader | DDE subscription |
cirius-findings-tracker | (Container App scoped) | rg-logging-logs |
GitHub Actions Workflow Snippet (Azure)
yaml
permissions:
id-token: write
contents: read
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}AWS Configuration
AWS uses an IAM OIDC identity provider trusting GitHub's token endpoint (token.actions.githubusercontent.com), with IAM roles scoped to specific repositories via trust policy conditions.
Management Account (206820231356)
| IAM Role | Repository | Trust Condition Subject | Purpose |
|---|---|---|---|
github-actions-aws-infra | aws-infra | repo:Cirius-Group-Inc/aws-infra:ref:refs/heads/main | Terraform apply for all AWS accounts |
github-oidc-cost-reporting | ops-automation | repo:Cirius-Group-Inc/ops-automation:ref:refs/heads/main | Cost reporting, backup audit, SES send, pentest discovery, EDL upload |
Both roles live in the management account (206820231356). Cross-account access uses role assumption from these management account roles to per-account roles — only the management account roles have GitHub OIDC trust. All member accounts are reached via sts:AssumeRole chained from here.
github-oidc-cost-reporting is the AWS counterpart to github-deploy-main in Azure: the single ops-automation identity that holds all read-only + email-send permissions for compliance workflows.
OIDC Provider Configuration
One OIDC provider exists in the management account, shared by both roles:
hcl
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}GitHub Actions Workflow Snippet (AWS)
yaml
permissions:
id-token: write
contents: read
- name: Configure AWS Credentials (infra deploy)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::206820231356:role/github-actions-aws-infra
aws-region: us-west-2
- name: Configure AWS Credentials (ops-automation)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::206820231356:role/github-oidc-cost-reporting
aws-region: us-west-2Multi-Tenant Workflow (ops-automation)
The ops-automation repository authenticates to all three environments (Azure PROD, Azure DDE, AWS) within the same workflow runs. Each environment requires a separate OIDC exchange:
| Environment | App / Role | Secret Names |
|---|---|---|
| Azure PROD | github-deploy-main | AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID |
| Azure DDE | github-deploy-app (DDE) | AZURE_DDE_CLIENT_ID, AZURE_DDE_TENANT_ID, AZURE_DDE_SUBSCRIPTION_ID |
| Azure PROD Maester | github-actions-maester | MAESTER_CLIENT_ID, AZURE_TENANT_ID |
| Azure DDE Maester | kobe-audit (DDE) | DDE_AUDIT_CLIENT_ID, AZURE_DDE_TENANT_ID |
| AWS | github-oidc-cost-reporting | AWS_ROLE_ARN, AWS_REGION |
All federated credential subjects are repo:Cirius-Group-Inc/ops-automation:ref:refs/heads/main — each pointing to a different app registration or IAM role in their respective environment. A single workflow job can only hold one AWS credential at a time; re-run configure-aws-credentials to switch roles within a job.
Common Failure Modes
AADSTS70021: No matching federated identity record found
Cause: The GitHub OIDC token subject does not exactly match any federated credential on the app registration.
The subject is constructed as:
- Branch run:
repo:<org>/<repo>:ref:refs/heads/<branch> - Tag run:
repo:<org>/<repo>:ref:refs/tags/<tag> - Environment-gated run:
repo:<org>/<repo>:environment:<environment-name> - PR:
repo:<org>/<repo>:pull_request
Common mistakes:
- Org name casing:
CiriusGrouporcirius-groupinstead ofCirius-Group-Inc - Repo name mismatch: any typo in the repo name
- Branch name:
mastervsmain, or a feature branch that isn't in the credential - Environment name mismatch:
Productionvsproduction— case-sensitive
Fix: Open the app registration in Entra ID → Certificates & secrets → Federated credentials. Compare the Subject identifier field character-by-character with the workflow's actual subject.
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
Cause: AWS IAM trust policy condition does not match the token subject.
Fix: Check the IAM role trust policy StringLike condition. The subject pattern must use the exact form repo:Cirius-Group-Inc/<repo>:ref:refs/heads/<branch>. Wildcards (*) work in StringLike but not StringEquals.
Error: Missing id-token permission
Cause: The workflow job is missing permissions: id-token: write. Without this, GitHub will not issue an OIDC token.
Fix: Add to the job or workflow level:
yaml
permissions:
id-token: write
contents: readToken expired between jobs
Cause: OIDC tokens are short-lived (15 minutes). If a job waits a long time before calling azure/login or configure-aws-credentials (e.g., waiting on a long previous job), the token may expire.
Fix: Call the login step early in the job, not after long setup steps. Each job gets a fresh token at start.
Auth succeeds but wrong subscription targeted
Cause: AZURE_SUBSCRIPTION_ID secret points to the wrong subscription, or a workflow step runs against the default subscription instead of the intended one.
Fix: Always explicitly pass subscription-id in the azure/login step. Confirm the secret value targets the intended subscription.
403 Forbidden on resource operations after successful login
Cause: OIDC token authenticated successfully but the app registration lacks the required RBAC role on the target resource.
Fix: Verify Contributor (or resource-specific) role assignment on the target subscription/resource group. Note that RBAC assignments can take 1-2 minutes to propagate after creation.
How to Add a New Repository to OIDC
Azure — Adding a New Repo Trust
Identify the app registration that should trust the new repo. For infrastructure repos, use the existing tenant-specific app registration. For ops-automation type repos, create a new app registration with least-privilege RBAC.
Add a federated credential via Terraform in
azure-infra/entra/(PROD) orazure-dde-infra/entra/(DDE):
hcl
resource "azuread_application_federated_identity_credential" "new_repo_main" {
application_id = azuread_application.your_app.id
display_name = "new-repo-main"
description = "Federated identity for new-repo main branch"
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:Cirius-Group-Inc/new-repo:ref:refs/heads/main"
audiences = ["api://AzureADTokenExchange"]
}Grant RBAC — minimum required roles only. Prefer resource-group scope over subscription scope.
Add secrets to the new repo in GitHub:
AZURE_CLIENT_ID(client ID of the app registration),AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID.Update this document with the new trust entry.
AWS — Adding a New Repo Trust
Prefer reusing an existing role (
github-actions-aws-infrafor infra,github-oidc-cost-reportingfor ops-automation). Add new permissions to the existing role rather than creating a new role unless the new repo has fundamentally different trust requirements.If a new role is required, add it to
aws-infra/main/with a trust policy referencing the existing OIDC provider (viadata "aws_iam_openid_connect_provider" "github"):
hcl
resource "aws_iam_role" "github_new_workflow" {
name = "github-oidc-new-workflow"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = data.aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
StringLike = { "token.actions.githubusercontent.com:sub" = "repo:Cirius-Group-Inc/new-repo:ref:refs/heads/main" }
}
}]
})
}Add
AWS_ROLE_ARNsecret to the new repo pointing to the new role ARN.Update this document with the new trust entry.
Document History
| Date | Change | Author |
|---|---|---|
| April 2026 | Expanded Azure trust tables (split by tenant, added kobe-audit DDE, cirius-findings-tracker, second azure-infra federated credential); expanded AWS trust table (added github-oidc-cost-reporting); updated multi-tenant section with full secret mapping; replaced troubleshooting table with Common Failure Modes; added How to Add a New Repository section | Kobe |
| March 2026 | Added Maester app registration (OIDC WIF), cost reporting unified role (github-deploy-main), and explicit DDE ops-automation trust to Azure configuration table and multi-tenant workflow section | Rory |
| March 2026 | Corrected GitHub org name to Cirius-Group-Inc (was incorrectly CiriusGroup), filled in app registration names, added AWS OIDC provider config, added multi-tenant workflow section, expanded troubleshooting table | Rory |
| March 2026 | Initial draft | Rory |