Skip to content

fix(github-config): pin a team's GitHub identity for its whole lifecycle - #4509

Merged
devantler merged 3 commits into
mainfrom
claude/team-external-name-lifecycle-3144
Oct 5, 2026
Merged

devantler merged 3 commits into
mainfrom
claude/team-external-name-lifecycle-3144

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Why

The cluster manages two GitHub teams that control who may approve and administer the organization's repositories. The existing guardrails check a team's name, but the team that is actually managed is chosen by a numeric identity stored beside it, and nothing checked that. A faulty or compromised configuration release could therefore create a correctly named team and then re-point it at a different team in the organization.

What

The cluster now refuses any attempt to set or change that identity to anything other than the approved identity of that team, both when a team is first declared and on every later change. Leaving the identity alone still works, so do recovery after a rebuild and the component that records the identity of a team it has just created.

Fixes #3144

A github-config Team could be created compliant and then re-pointed at a
foreign GitHub team by changing crossplane.io/external-name, because the
provider finds the remote team by that numeric ID and nothing constrained it.

Add an admission policy that accepts the annotation only when it is unset,
unchanged, or the approved identity of that team, on create and update. The
provider's own record of a team it created is exempt, matched by the
revision-suffixed ServiceAccount name it runs as.

Part of #3144

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 16 minutes.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 16 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: e8fdadaa-7e30-494d-826d-cdbe59c20e37
📥 Commits

Reviewing files that changed from the base of the PR and between 6a5f434 and 464bcd9.

📒 Files selected for processing (53)
  • k8s/bases/apps/github-config/role.yaml
  • k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml
  • k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml
  • k8s/bases/infrastructure/cluster-policies/kustomization.yaml
  • tests/policy-failure-actions.json
  • tests/restrict-github-team-external-identity/crossplane-identity/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/crossplane-identity/resources.yaml
  • tests/restrict-github-team-external-identity/crossplane-identity/user-info.yaml
  • tests/restrict-github-team-external-identity/crossplane-identity/values.yaml
  • tests/restrict-github-team-external-identity/foreign/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/foreign/resources.yaml
  • tests/restrict-github-team-external-identity/foreign/user-info.yaml
  • tests/restrict-github-team-external-identity/foreign/values.yaml
  • tests/restrict-github-team-external-identity/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/lookalike-identity/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/lookalike-identity/resources.yaml
  • tests/restrict-github-team-external-identity/lookalike-identity/user-info.yaml
  • tests/restrict-github-team-external-identity/lookalike-identity/values.yaml
  • tests/restrict-github-team-external-identity/other-identity/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/other-identity/resources.yaml
  • tests/restrict-github-team-external-identity/other-identity/user-info.yaml
  • tests/restrict-github-team-external-identity/other-identity/values.yaml
  • tests/restrict-github-team-external-identity/provider/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/provider/resources.yaml
  • tests/restrict-github-team-external-identity/provider/user-info.yaml
  • tests/restrict-github-team-external-identity/provider/values.yaml
  • tests/restrict-github-team-external-identity/resources.yaml
  • tests/restrict-github-team-external-identity/swapped/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/swapped/resources.yaml
  • tests/restrict-github-team-external-identity/swapped/user-info.yaml
  • tests/restrict-github-team-external-identity/swapped/values.yaml
  • tests/restrict-github-team-external-identity/unset/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/unset/resources.yaml
  • tests/restrict-github-team-external-identity/unset/user-info.yaml
  • tests/restrict-github-team-external-identity/unset/values.yaml
  • tests/restrict-github-team-external-identity/update-changed/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/update-changed/resources.yaml
  • tests/restrict-github-team-external-identity/update-changed/user-info.yaml
  • tests/restrict-github-team-external-identity/update-changed/values.yaml
  • tests/restrict-github-team-external-identity/update-cleared/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/update-cleared/resources.yaml
  • tests/restrict-github-team-external-identity/update-cleared/user-info.yaml
  • tests/restrict-github-team-external-identity/update-cleared/values.yaml
  • tests/restrict-github-team-external-identity/update-from-unapproved/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/update-from-unapproved/resources.yaml
  • tests/restrict-github-team-external-identity/update-from-unapproved/user-info.yaml
  • tests/restrict-github-team-external-identity/update-from-unapproved/values.yaml
  • tests/restrict-github-team-external-identity/update-unchanged/kyverno-test.yaml
  • tests/restrict-github-team-external-identity/update-unchanged/resources.yaml
  • tests/restrict-github-team-external-identity/update-unchanged/user-info.yaml
  • tests/restrict-github-team-external-identity/update-unchanged/values.yaml
  • tests/restrict-github-team-external-identity/user-info.yaml
  • tests/restrict-github-team-external-identity/values.yaml
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Kyverno's Equals and AnyNotIn read * and ? as wildcards on either side, so an
identity of "*" matched the approved one and any later value then counted as
unchanged. Evaluate the three accepted cases in one JMESPath expression, which
compares strings literally, and cover both wildcard routes with fixtures.

Part of #3144

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devantler

devantler commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Tried on a real admission controller

Tried at 8e1d8c40; the key refusals (*, a foreign identity, a value in variable syntax) and the approved repair were repeated with the policy from the current head 3db86e1c075397775eaa3906c4e545f24aae2056, which changes comments and fixtures only. The offline test tool cannot show a real update (it presents the previous object as equal to the new one), so the rule was also tried on a throwaway single-node cluster on the agent host: Kubernetes 1.34, Kyverno chart 3.9.1 (v1.19.1, the version production runs), and the Team resource definition copied from production. Production was only read. Each request was sent as the account named.

First, the hole an independent review found in the first commit was real. With the policy from 464bcd9e loaded, the tenant account created admins with identity * (admitted), then changed it to a team slug (admitted). Kyverno's own comparison operators read * as a wildcard. The second commit compares literally.

With the fixed policy:

Who Request Result
tenant create admins with identity * refused
tenant create admins against a foreign team refused
tenant create admins with no identity admitted
provider record the identity of the team it created (not on the approved list) admitted
tenant re-apply admins unchanged, server-side apply, identity owned by the provider admitted
tenant change an unrelated label admitted
tenant re-point admins at a foreign team refused
tenant set identity * refused
tenant set a team slug refused
tenant set the maintainers identity on admins refused
account with the provider's name, in the tenant namespace re-point refused
Crossplane's own account re-point refused
cluster administrator re-point refused
provider under a different package revision record a new identity admitted
tenant repair admins to its approved identity admitted
tenant approved identity with a leading zero refused
tenant adopt maintainers by its approved identity admitted
tenant set the admins identity on maintainers refused
tenant remove the identity admitted
tenant state the approved identity again admitted

All twenty matched what was intended. Two rows are worth knowing: a cluster administrator is held to the rule too, so adopting a team under a new identity by hand needs the approved list changed first; and the tenant's routine re-apply of a team whose identity is not yet approved keeps working, which is what stops a newly created team from wedging.

Also run: the 24 offline fixture rows pass and the repository's guard confirms every row was really evaluated; 21 deliberately broken versions of the policy were tried against them and 20 are caught (the one not caught there, switching the rule to report-only, is caught by the enforcement ledger check instead). The policy from the first commit fails the new wildcard fixtures.

Not exercised: the provider itself against GitHub. The trial covers what the cluster admits, not what the provider then does with it.

Follow-ups found on the way and filed: #4511 (a managed team can still be nested under another team), #4517 (the older team rules use the same wildcard-reading comparisons), #4518 (whether a membership or grant can be re-pointed by its stored identity).

…on case

Part of #3144

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Generated by the Agentic Engineer

Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)

Reviewed commit: 3db86e1c075397775eaa3906c4e545f24aae2056

  • CodeRabbit: rate limited — it refused the request on the first head ("Review rate limited", 08:20Z) and has reviewed nothing on this pull request.
  • Codex: usage limit since 2026-10-05T05:05Z.
  • Cursor Bugbot: usage limit since 2026-10-05T05:09Z; no review from it on record.

An independent reviewer read the change in two rounds, checking Kyverno's behaviour against its 1.19.1 source rather than from memory.

Round 1, at 464bcd9e, found one P0: the rule compared the identity with Kyverno operators that read * and ? as wildcards, so an identity of * was admitted and any later non-numeric value then counted as unchanged. Reproduced on a real admission controller, then fixed in 8e1d8c40 by evaluating the three accepted cases in one literal comparison, with fixtures for both wildcard routes. The first commit's policy fails those fixtures.

Round 2, at 8e1d8c40, found no P0 or P1 in the new comparison. It tried about thirty hostile values on create and update (variable syntax, quotes, spacing, leading zeros, other number spellings, a 300 kB value); all were refused. A value that makes the comparison itself fail is refused too, because the policy and the chart both fail closed. It found no legitimate flow that is now refused.

Round 2 also raised, and this head or a follow-up answers:

  • P2, the older team rules use the same wildcard-reading operators on tenant-written text: outside this change, filed as #4517.
  • P2, unverified, the stored identity of memberships and grants is not checked: filed as #4518 with verification as its first step.
  • P3 comment and fixture notes (operator wording, a stale plural, an unsourced provider claim, a ragged comment, a case pinning that variable syntax is not expanded): all applied in 3db86e1c, which changes comments and fixtures only.

Round 1 also noted that a managed team can still be nested under another team: filed as #4511.

Known and accepted: the provider exemption matches by name prefix in the Crossplane namespace, so it would also cover another package whose name starts the same way; creating an account there is a platform-level permission. A cluster administrator is held to the rule as well.

Not exercised: the provider against GitHub. The trial on a real admission controller is recorded in a comment on this pull request and was repeated for the key cases with the policy from this head.

Verdict: no P0/P1 findings

@devantler
devantler marked this pull request as ready for review October 5, 2026 09:59
@devantler
devantler added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 66f5503 Oct 5, 2026
33 checks passed
@devantler
devantler deleted the claude/team-external-name-lifecycle-3144 branch October 5, 2026 16:49
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Team external-name is only pinned at creation, so a tenant UPDATE can still adopt a foreign GitHub team

1 participant