Skip to content

Register only the federated credential format the repo actually uses - #17

Merged
pinodeca merged 1 commit into
mainfrom
fix/entra-oidc-subject-format
Jul 31, 2026
Merged

Register only the federated credential format the repo actually uses#17
pinodeca merged 1 commit into
mainfrom
fix/entra-oidc-subject-format

Conversation

@ChrisKrawczyk

Copy link
Copy Markdown
Contributor

Problem

The Entra Live Test fails on PRs during azure/login@v2 with:

AADSTS700213: No matching federated identity record found for presented
assertion subject 'repository_owner_id:6154722:repository_id:1157352866:pull_request'

main and scheduled runs pass; only PRs fail.

Root cause

GitHub emits exactly one subject per OIDC token, and its format depends on the repo's subject claim customization:

$ gh api repos/microsoft/duroxide-pg/actions/oidc/customization/sub
{"use_default":false,"include_claim_keys":["repository_owner_id","repository_id","context"], ...}

With use_default: false, the repo:OWNER/REPO:... form is never emitted. But this script registered both formats unconditionally:

add_federated "github-pr"   "repo:${GH_REPO}:pull_request"        # can never match
add_federated "github-main" "repo:${GH_REPO}:ref:refs/heads/main" # can never match
# ...then, only if gh happened to be present, the ID-based pair

So every app it provisioned carried two permanently-dead credentials. A tenant security sweep (SFIInitiative_July8) later removed them and took github-pr-id with them:

Date Event
2026-07-08 Sweep appended *SFIInitiative_July8 to the subjects of github-pr, github-main, and github-pr-id, silently breaking them
2026-07-16 Sweep deleted all three

github-main-id was untouched, which is why main kept working and made this look like a PR-specific misconfiguration rather than a deletion.

The credential has been restored out-of-band; this PR stops the script from recreating the problem.

Changes

  • Register only the matching pair. Detect the customization and register either the ID-based or the repo-slug credentials, never both.
  • Fail loudly instead of silently. gh is now a hard requirement. Previously, if gh was missing or the repo lookup failed, the script warned and skipped the ID-based credentials, happily provisioning an app that could never authenticate.
  • Surface leftovers. Warn about credentials in the other format so they are visible before a sweep removes them.
  • Docs for the subject formats, the AADSTS700213 failure mode, and how to audit credential changes via the Entra audit log.

Also corrected a wrong example in the docs: FIC subjects are matched exactly, so the previous refs/tags/v* wildcard example could never have worked.

A jq gotcha worth flagging

The first version of the detection used (.use_default // true) == false. jq's // treats false as absent, so that yields true for use_default: false, inverting the check. Verified against the real templates and edge cases:

Input Result
real duroxide-pg / duroxide-pg-opt (use_default:false, has repository_id) ID-based
{"use_default":true} repo-slug
{"use_default":false,"include_claim_keys":["repo","context"]} repo-slug
{} repo-slug

Testing

  • bash -n clean.
  • Branch-selection logic unit-tested against the five cases above.
  • Not re-run end-to-end: the script mutates the live shared CI app and PG server. It is idempotent, so a maintainer can re-run it to confirm it reports both credentials already exist.

Note for the sister repo

microsoft/duroxide-pg-opt has the identical script and was hit by the same sweep on the same days. Its github-pr-id has also been restored, and a matching PR follows, including a fix for its script defaulting to this repo's GH_REPO/APP_NAME.

Follow-up (not in this PR)

This was a deliberate, cadenced security sweep, not drift. Re-adding the credential does not exempt the app, so it is worth confirming with the sweep owner whether a bare ...:pull_request subject is acceptable or should be narrowed to an environment-scoped subject.

The Entra live test failed on PRs with AADSTS700213 because the app had no
federated identity credential matching the subject GitHub sent.

GitHub emits exactly one subject per OIDC token, and its format depends on
the repo's subject claim customization. The microsoft org sets it to the
ID-based form (repository_owner_id/repository_id), so the default
repo:OWNER/REPO:... form is never emitted.

This script registered BOTH formats unconditionally, which left two
permanently-dead credentials on every app it provisioned. A tenant security
sweep later removed them along with github-pr-id, and the only symptom was
azure/login@v2 failing on PRs while main kept working.

Changes:
- Detect the subject customization and register only the matching pair,
  instead of registering the repo-slug pair unconditionally.
- Make gh a hard requirement and fail loudly when the customization or the
  owner/repo IDs cannot be read. Previously it warned and skipped the
  ID-based credentials, silently provisioning an app that could never
  authenticate.
- Warn about leftover credentials in the other format so they are visible
  before a security sweep removes them.
- Document the subject formats, the AADSTS700213 failure mode, and how to
  audit credential changes via the Entra audit log.

Note: jq's // operator treats false as absent, so use_default must be
compared against true directly rather than via (.use_default // true).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0db6138a-d819-4d2f-88c0-a0e0b11ad755
@pinodeca
pinodeca merged commit 26fd4a3 into main Jul 31, 2026
4 checks passed
@pinodeca
pinodeca deleted the fix/entra-oidc-subject-format branch July 31, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants