-
Notifications
You must be signed in to change notification settings - Fork 4
fix(github-config): constrain team management #2718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c8a6040
6b5eee9
73032ca
2b57d41
e1bd002
5186097
426766d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| # Constrains the high-impact GitHub team managed resources delegated to the | ||
| # github-config tenant. The tenant's Flux Kustomization applies a separately | ||
| # published OCI artifact as the github-config ServiceAccount, and those | ||
| # Crossplane resources reconcile with an org-admin GitHub App. Namespace RBAC can | ||
| # limit the API group, but it cannot restrict resource spec fields such as the | ||
| # team, member, repository, role, or permission. This admission guard keeps that | ||
| # delegation scoped to the intended CODEOWNERS teams and blocks direct numeric | ||
| # team IDs/selectors that would bypass the allow-list enforced through | ||
| # Crossplane references. | ||
| apiVersion: kyverno.io/v1 | ||
| kind: ClusterPolicy | ||
| metadata: | ||
| name: restrict-github-team-management | ||
| annotations: | ||
| policies.kyverno.io/title: Restrict GitHub Team Management | ||
| policies.kyverno.io/category: Security, GitHub | ||
| policies.kyverno.io/severity: high | ||
| policies.kyverno.io/subject: Team, TeamMembership, TeamRepository | ||
| policies.kyverno.io/minversion: 1.6.0 | ||
| policies.kyverno.io/description: >- | ||
| Restricts provider-upjet-github team resources in the github-config | ||
| namespace to the platform CODEOWNERS teams, requires TeamMembership and | ||
| TeamRepository resources to reference those Team objects by name, and | ||
| blocks repository admin grants. This prevents a compromised or unintended | ||
| github-config artifact from adding arbitrary users to arbitrary GitHub | ||
| teams or granting privileged repository access. | ||
| spec: | ||
| validationFailureAction: Enforce | ||
| background: true | ||
| rules: | ||
| - name: teams-allow-listed | ||
| match: | ||
| any: | ||
| - resources: | ||
| kinds: | ||
| - team.github.m.upbound.io/*/Team | ||
| namespaces: | ||
| - github-config | ||
| validate: | ||
| message: GitHub Team resources managed by github-config must be one of platform or maintainers. | ||
| deny: | ||
| conditions: | ||
| all: | ||
| - key: "{{ request.object.metadata.name }}" | ||
|
devantler marked this conversation as resolved.
devantler marked this conversation as resolved.
|
||
| operator: AnyNotIn | ||
| value: | ||
| - platform | ||
| - maintainers | ||
| # The allow-list above constrains the Kubernetes object name, but the | ||
| # provider takes the real GitHub team from spec.forProvider.name (and | ||
| # spec.initProvider.name). Without this rule a Team object named "platform" | ||
| # may carry forProvider.name: some-other-team, so the reference rules below | ||
| # accept teamIdRef.name: platform while the org-admin provider reconciles a | ||
| # different remote team. Binding the provider identity to the object name is | ||
| # what makes the object name a faithful proxy for the remote team, which is | ||
| # the assumption the two reference rules rest on. Both fields are authored | ||
| # by the tenant and never rewritten by the provider, so pinning them carries | ||
| # no reconciliation risk. | ||
| - name: teams-bind-provider-identity-to-object-name | ||
| match: | ||
| any: | ||
| - resources: | ||
| kinds: | ||
| - team.github.m.upbound.io/*/Team | ||
| namespaces: | ||
| - github-config | ||
| validate: | ||
| message: GitHub Team spec.forProvider.name and spec.initProvider.name must be omitted or equal to metadata.name, so the allow-listed object name is the team actually reconciled. | ||
| deny: | ||
| conditions: | ||
| any: | ||
| - key: "{{ request.object.spec.forProvider.name || '' }}" | ||
| operator: AnyNotIn | ||
| value: | ||
| - "" | ||
| - "{{ request.object.metadata.name }}" | ||
| - key: "{{ request.object.spec.initProvider.name || '' }}" | ||
| operator: AnyNotIn | ||
| value: | ||
| - "" | ||
| - "{{ request.object.metadata.name }}" | ||
| # crossplane.io/external-name is the other way the local name can be | ||
| # detached from the remote team, and it is NOT constrained here. A rule | ||
| # accepting only an empty annotation or metadata.name looks like the | ||
| # obvious counterpart to the binding above, but it breaks legitimate | ||
| # re-adoption: after a rebuild, or whenever a Team CR is recreated, the | ||
| # existing GitHub team must be adopted by the numeric team ID the provider | ||
| # itself writes into this annotation — and refusing that makes Crossplane | ||
| # try to create a team that already exists, so github-config cannot recover | ||
| # its own teams. Constraining it safely needs a way to tell our teams' | ||
| # numeric identities from a foreign one, plus the provider ServiceAccount | ||
| # excluded so its post-create write still succeeds. Tracked in #3144 rather | ||
| # than half-closed here. | ||
| - name: teammemberships-reference-allow-listed-teams | ||
| match: | ||
| any: | ||
| - resources: | ||
| kinds: | ||
| - team.github.m.upbound.io/*/TeamMembership | ||
| namespaces: | ||
| - github-config | ||
| validate: | ||
| message: GitHub TeamMembership resources must use spec.forProvider.teamIdRef.name set to platform or maintainers; direct teamId values and selectors are not allowed, in forProvider or initProvider. | ||
| deny: | ||
| conditions: | ||
| any: | ||
| - key: "{{ request.object.spec.forProvider.teamId || '' }}" | ||
| operator: NotEquals | ||
| value: "" | ||
|
Comment on lines
+107
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an allowed Useful? React with 👍 / 👎. |
||
| - key: "{{ request.object.spec.forProvider.teamIdSelector || `{}` }}" | ||
| operator: NotEquals | ||
| value: {} | ||
| - key: "{{ request.object.spec.forProvider.teamIdRef.name || '' }}" | ||
| operator: AnyNotIn | ||
| value: | ||
| - platform | ||
| - maintainers | ||
|
devantler marked this conversation as resolved.
|
||
| # upjet merges initProvider into any unset forProvider field, so | ||
| # every constraint above has to hold there too — otherwise an | ||
| # approved forProvider reference fronts a foreign team supplied | ||
| # under initProvider. | ||
| - key: "{{ request.object.spec.initProvider.teamId || '' }}" | ||
| operator: NotEquals | ||
| value: "" | ||
| - key: "{{ request.object.spec.initProvider.teamIdSelector || `{}` }}" | ||
| operator: NotEquals | ||
| value: {} | ||
| - key: "{{ request.object.spec.initProvider.teamIdRef.name || '' }}" | ||
| operator: AnyNotIn | ||
| value: | ||
| - "" | ||
| - platform | ||
| - maintainers | ||
| - name: teamrepositories-reference-allow-listed-teams | ||
| match: | ||
| any: | ||
| - resources: | ||
| kinds: | ||
| - team.github.m.upbound.io/*/TeamRepository | ||
| namespaces: | ||
| - github-config | ||
| validate: | ||
| message: GitHub TeamRepository resources must reference platform or maintainers by spec.forProvider.teamIdRef.name and may grant only pull, triage or push permission, in forProvider and initProvider. | ||
| deny: | ||
| conditions: | ||
| any: | ||
| - key: "{{ request.object.spec.forProvider.teamId || '' }}" | ||
| operator: NotEquals | ||
| value: "" | ||
| - key: "{{ request.object.spec.forProvider.teamIdSelector || `{}` }}" | ||
| operator: NotEquals | ||
| value: {} | ||
| - key: "{{ request.object.spec.forProvider.teamIdRef.name || '' }}" | ||
| operator: AnyNotIn | ||
| value: | ||
| - platform | ||
| - maintainers | ||
| # An allow-list, not a deny-list on `admin`: GitHub also offers | ||
| # `maintain` and organization-defined custom repository roles, so | ||
| # denying only `admin` fails open the moment the tenant picks | ||
| # another privileged role. An empty value is the provider default | ||
| # (`pull`). | ||
| - key: "{{ request.object.spec.forProvider.permission || '' }}" | ||
|
devantler marked this conversation as resolved.
|
||
| operator: AnyNotIn | ||
| value: | ||
| - "" | ||
| - pull | ||
| - triage | ||
| - push | ||
| # Same initProvider merge as above: an approved forProvider | ||
| # reference must not be able to front a foreign team or an admin | ||
| # grant supplied under initProvider. | ||
| - key: "{{ request.object.spec.initProvider.teamId || '' }}" | ||
| operator: NotEquals | ||
| value: "" | ||
| - key: "{{ request.object.spec.initProvider.teamIdSelector || `{}` }}" | ||
| operator: NotEquals | ||
| value: {} | ||
| - key: "{{ request.object.spec.initProvider.teamIdRef.name || '' }}" | ||
| operator: AnyNotIn | ||
| value: | ||
| - "" | ||
| - platform | ||
| - maintainers | ||
| - key: "{{ request.object.spec.initProvider.permission || '' }}" | ||
| operator: AnyNotIn | ||
| value: | ||
| - "" | ||
| - pull | ||
| - triage | ||
| - push | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| apiVersion: cli.kyverno.io/v1alpha1 | ||
| kind: Test | ||
| metadata: | ||
| name: restrict-github-team-management-forprovider-mismatch | ||
| policies: | ||
| - >- | ||
| ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml | ||
| resources: | ||
| - resources.yaml | ||
| results: | ||
| # The object name is allow-listed, so that rule cannot catch this. | ||
| - policy: restrict-github-team-management | ||
| rule: teams-allow-listed | ||
| resources: | ||
| - github-config/platform | ||
| kind: Team | ||
| result: pass | ||
| # Only the identity binding catches it. | ||
| - policy: restrict-github-team-management | ||
| rule: teams-bind-provider-identity-to-object-name | ||
| resources: | ||
| - github-config/platform | ||
| kind: Team | ||
| result: fail |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| # THE ESCALATION THIS RULE EXISTS TO STOP. metadata.name passes the allow-list, | ||
| # so a TeamMembership with teamIdRef.name: platform resolves to this object — | ||
| # while the org-admin provider reconciles the unrelated team named here. | ||
| # Its own fixture directory because it must reuse the allow-listed object name | ||
| # that the paved-road fixture already occupies. | ||
| apiVersion: team.github.m.upbound.io/v1beta1 | ||
| kind: Team | ||
| metadata: | ||
| name: platform | ||
| namespace: github-config | ||
| spec: | ||
| forProvider: | ||
| name: some-other-team | ||
| privacy: closed |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| --- | ||
| apiVersion: cli.kyverno.io/v1alpha1 | ||
| kind: Test | ||
| metadata: | ||
| name: restrict-github-team-management-initprovider-mismatch | ||
| policies: | ||
| - >- | ||
| ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml | ||
| resources: | ||
| - resources.yaml | ||
| results: | ||
| - policy: restrict-github-team-management | ||
| rule: teams-allow-listed | ||
| resources: | ||
| - github-config/platform | ||
| kind: Team | ||
| result: pass | ||
| - policy: restrict-github-team-management | ||
| rule: teams-bind-provider-identity-to-object-name | ||
| resources: | ||
| - github-config/platform | ||
| kind: Team | ||
| result: fail |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| # The same detachment expressed through initProvider, which upjet merges into | ||
| # forProvider when the field is unset there. | ||
| apiVersion: team.github.m.upbound.io/v1beta1 | ||
| kind: Team | ||
| metadata: | ||
| name: platform | ||
| namespace: github-config | ||
| spec: | ||
| initProvider: | ||
| name: some-other-team |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| --- | ||
| apiVersion: cli.kyverno.io/v1alpha1 | ||
| kind: Test | ||
| metadata: | ||
| name: restrict-github-team-management | ||
| policies: | ||
| - >- | ||
| ../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml | ||
| resources: | ||
| - resources.yaml | ||
| results: | ||
| # Only the CODEOWNERS teams may be managed through this delegation. | ||
| - policy: restrict-github-team-management | ||
| rule: teams-allow-listed | ||
| resources: | ||
| - github-config/attacker-team | ||
| kind: Team | ||
| result: fail | ||
| - policy: restrict-github-team-management | ||
| rule: teams-allow-listed | ||
| resources: | ||
| - github-config/platform | ||
| - github-config/maintainers | ||
| kind: Team | ||
| result: pass | ||
| # A self-consistent provider identity passes, including when it is omitted and | ||
| # Crossplane derives the remote team from the object name. attacker-team is | ||
| # here on purpose: it fails the allow-list above yet must pass this rule, so a | ||
| # regression that collapsed the two rules into one would be caught. | ||
| - policy: restrict-github-team-management | ||
| rule: teams-bind-provider-identity-to-object-name | ||
| resources: | ||
| - github-config/platform | ||
| - github-config/maintainers | ||
| - github-config/attacker-team | ||
| kind: Team | ||
| result: pass | ||
| # References must go through the allow-listed Team objects, never a raw ID. | ||
| - policy: restrict-github-team-management | ||
| rule: teammemberships-reference-allow-listed-teams | ||
| resources: | ||
| - github-config/direct-team-id | ||
| - github-config/initprovider-foreign-team | ||
| - github-config/selector-reference | ||
| - github-config/non-allow-listed-reference | ||
| kind: TeamMembership | ||
| result: fail | ||
| - policy: restrict-github-team-management | ||
| rule: teammemberships-reference-allow-listed-teams | ||
| resources: | ||
| - github-config/platform-member | ||
| kind: TeamMembership | ||
| result: pass | ||
| # Repository grants are capped below admin. | ||
| - policy: restrict-github-team-management | ||
| rule: teamrepositories-reference-allow-listed-teams | ||
| resources: | ||
| - github-config/platform-repo-admin | ||
| - github-config/initprovider-admin-grant | ||
| - github-config/maintain-grant | ||
| kind: TeamRepository | ||
| result: fail | ||
| - policy: restrict-github-team-management | ||
| rule: teamrepositories-reference-allow-listed-teams | ||
| resources: | ||
| - github-config/platform-repo | ||
| kind: TeamRepository | ||
| result: pass |
Uh oh!
There was an error while loading. Please reload this page.