From 464bcd9e1e5676531dab613fcd86cce727bd3efb Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 10:19:17 +0200 Subject: [PATCH 01/14] fix(github-config): pin a team's GitHub identity for its whole lifecycle 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 --- k8s/bases/apps/github-config/role.yaml | 3 +- ...estrict-github-team-external-identity.yaml | 115 ++++++++++++++++++ .../restrict-github-team-management.yaml | 11 +- .../cluster-policies/kustomization.yaml | 1 + tests/policy-failure-actions.json | 1 + .../crossplane-identity/kyverno-test.yaml | 19 +++ .../crossplane-identity/resources.yaml | 12 ++ .../crossplane-identity/user-info.yaml | 11 ++ .../crossplane-identity/values.yaml | 21 ++++ .../foreign/kyverno-test.yaml | 21 ++++ .../foreign/resources.yaml | 36 ++++++ .../foreign/user-info.yaml | 11 ++ .../foreign/values.yaml | 7 ++ .../kyverno-test.yaml | 20 +++ .../lookalike-identity/kyverno-test.yaml | 19 +++ .../lookalike-identity/resources.yaml | 12 ++ .../lookalike-identity/user-info.yaml | 11 ++ .../lookalike-identity/values.yaml | 21 ++++ .../other-identity/kyverno-test.yaml | 19 +++ .../other-identity/resources.yaml | 12 ++ .../other-identity/user-info.yaml | 11 ++ .../other-identity/values.yaml | 21 ++++ .../provider/kyverno-test.yaml | 19 +++ .../provider/resources.yaml | 12 ++ .../provider/user-info.yaml | 11 ++ .../provider/values.yaml | 23 ++++ .../resources.yaml | 24 ++++ .../swapped/kyverno-test.yaml | 20 +++ .../swapped/resources.yaml | 24 ++++ .../swapped/user-info.yaml | 11 ++ .../swapped/values.yaml | 7 ++ .../unset/kyverno-test.yaml | 20 +++ .../unset/resources.yaml | 22 ++++ .../unset/user-info.yaml | 11 ++ .../unset/values.yaml | 7 ++ .../update-changed/kyverno-test.yaml | 26 ++++ .../update-changed/resources.yaml | 36 ++++++ .../update-changed/user-info.yaml | 11 ++ .../update-changed/values.yaml | 39 ++++++ .../update-cleared/kyverno-test.yaml | 19 +++ .../update-cleared/resources.yaml | 12 ++ .../update-cleared/user-info.yaml | 11 ++ .../update-cleared/values.yaml | 21 ++++ .../update-from-unapproved/kyverno-test.yaml | 19 +++ .../update-from-unapproved/resources.yaml | 14 +++ .../update-from-unapproved/user-info.yaml | 11 ++ .../update-from-unapproved/values.yaml | 21 ++++ .../update-unchanged/kyverno-test.yaml | 20 +++ .../update-unchanged/resources.yaml | 24 ++++ .../update-unchanged/user-info.yaml | 11 ++ .../update-unchanged/values.yaml | 7 ++ .../user-info.yaml | 11 ++ .../values.yaml | 7 ++ 53 files changed, 939 insertions(+), 7 deletions(-) create mode 100644 k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml create mode 100644 tests/restrict-github-team-external-identity/crossplane-identity/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/crossplane-identity/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/crossplane-identity/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/crossplane-identity/values.yaml create mode 100644 tests/restrict-github-team-external-identity/foreign/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/foreign/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/foreign/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/foreign/values.yaml create mode 100644 tests/restrict-github-team-external-identity/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/lookalike-identity/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/lookalike-identity/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/lookalike-identity/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/lookalike-identity/values.yaml create mode 100644 tests/restrict-github-team-external-identity/other-identity/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/other-identity/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/other-identity/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/other-identity/values.yaml create mode 100644 tests/restrict-github-team-external-identity/provider/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/provider/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/provider/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/provider/values.yaml create mode 100644 tests/restrict-github-team-external-identity/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/swapped/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/swapped/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/swapped/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/swapped/values.yaml create mode 100644 tests/restrict-github-team-external-identity/unset/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/unset/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/unset/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/unset/values.yaml create mode 100644 tests/restrict-github-team-external-identity/update-changed/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/update-changed/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/update-changed/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/update-changed/values.yaml create mode 100644 tests/restrict-github-team-external-identity/update-cleared/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/update-cleared/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/update-cleared/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/update-cleared/values.yaml create mode 100644 tests/restrict-github-team-external-identity/update-from-unapproved/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/update-from-unapproved/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/update-from-unapproved/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/update-from-unapproved/values.yaml create mode 100644 tests/restrict-github-team-external-identity/update-unchanged/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/update-unchanged/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/update-unchanged/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/update-unchanged/values.yaml create mode 100644 tests/restrict-github-team-external-identity/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/values.yaml diff --git a/k8s/bases/apps/github-config/role.yaml b/k8s/bases/apps/github-config/role.yaml index aa7af3c139..2ce5118573 100644 --- a/k8s/bases/apps/github-config/role.yaml +++ b/k8s/bases/apps/github-config/role.yaml @@ -53,7 +53,8 @@ rules: - delete # Team resources are high-impact because they change real GitHub org # authorization. RBAC alone cannot express which teams this SA may manage, so - # the restrict-github-team-management Kyverno admission policy constrains the + # the restrict-github-team-management and restrict-github-team-external-identity + # Kyverno admission policies constrain the # allowed team identities, reference style, and repository permissions on top # of these verbs. - apiGroups: diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml new file mode 100644 index 0000000000..6224a73473 --- /dev/null +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml @@ -0,0 +1,115 @@ +# Pins which GitHub team a github-config Team object reconciles, for the whole +# life of the object (#3144). +# +# restrict-github-team-management binds spec.forProvider.name to the object +# name, but the provider finds the remote team by the numeric ID in the +# crossplane.io/external-name annotation, not by name. Whoever can write that +# annotation can point the allow-listed object `admins` at any team in the +# organization, and every TeamMembership and TeamRepository that references +# `admins` by name then acts on that team through the org-admin provider. +# +# Measured on prod (2026-10-05), the annotation has two legitimate writers: +# +# - the provider itself, which records the ID of a team it has just created +# (`admins`: the annotation's field owner is the provider); +# - the github-config artifact, which adopts a team that already exists by +# stating its ID (`maintainers`: Observe-only, field owner is the applier). +# The same path recovers both teams after a rebuild. +# +# So neither "never set it" nor "only the provider sets it" holds. The rule +# below accepts, from anyone but the provider, exactly three cases: no +# annotation, the value the object already carries, or the approved identity of +# that object name. Everything else is a foreign team and is refused on create +# and on update alike. +# +# The approved identities live HERE and are not read from the github-config +# artifact, for the reason the member allow-list gives: that artifact is the +# input this policy constrains. A team that is deleted and created again gets a +# new ID; the provider's own write of it is admitted, the object keeps working +# because an unchanged value is always accepted, and the entry below is then +# updated in a reviewed change so the new ID can be used for recovery. +# +# The provider is exempt because the ID of a team it has just created cannot be +# known in advance. Its ServiceAccount is named after the provider package +# revision (provider-upjet-github-, read from the running pod), so +# the name changes with every provider upgrade and is matched by prefix. +# Creating a ServiceAccount in crossplane-system is a platform-level +# permission, so the prefix cannot be claimed from a tenant namespace. +# +# Admission-only (background: false), because the exemption is on the writing +# identity, which a background scan does not have. +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: restrict-github-team-external-identity + annotations: + policies.kyverno.io/title: Restrict GitHub Team External Identity + policies.kyverno.io/category: Security, GitHub + policies.kyverno.io/severity: high + policies.kyverno.io/subject: Team + policies.kyverno.io/minversion: 1.13.0 + policies.kyverno.io/description: >- + Refuses a github-config Team whose crossplane.io/external-name is set or + changed to anything other than the approved numeric identity of that team, + so an allow-listed Team object cannot be pointed at a different GitHub + team after it was admitted. The provider's own record of a team it + created is exempt. +spec: + background: false + rules: + - name: teams-external-name-is-an-approved-identity + match: + any: + - resources: + kinds: + - team.github.m.upbound.io/*/Team + operations: + - CREATE + - UPDATE + namespaces: + - github-config + exclude: + any: + - subjects: + - kind: ServiceAccount + name: provider-upjet-github-* + namespace: crossplane-system + context: + - name: externalName + variable: + jmesPath: request.object.metadata.annotations."crossplane.io/external-name" + default: "" + - name: previousExternalName + variable: + jmesPath: request.oldObject.metadata.annotations."crossplane.io/external-name" + default: "" + validate: + failureAction: Enforce + # Kyverno admits an update by default when the object ALREADY failed the + # rule before it. Here that would be a way round the rule: a team the + # provider created anew carries an identity that is not approved yet, so + # its object "already fails" when judged alone, and every later change + # to its identity would be waved through. An unchanged value is accepted + # by the conditions below instead, which is the only tolerance needed. + allowExistingViolations: false + message: >- + The crossplane.io/external-name of a github-config Team may only be + left unset, left unchanged, or set to the approved numeric identity of + that team. Adopting a team under a new identity is a reviewed change + to restrict-github-team-external-identity.yaml. + deny: + conditions: + all: + - key: "{{ externalName }}" + operator: NotEquals + value: "" + - key: "{{ externalName }}" + operator: NotEquals + value: "{{ previousExternalName }}" + # Name and identity are checked as a pair, so the approved + # identity of one team cannot be used on the other object. + - key: "{{ request.object.metadata.name }}/{{ externalName }}" + operator: AnyNotIn + value: + - admins/18573307 + - maintainers/17046606 diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml index c35becf3cb..6f72fb62ed 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml @@ -86,12 +86,11 @@ spec: # 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. + # itself writes into this annotation. It is constrained in + # restrict-github-team-external-identity instead, which approves each + # team's numeric identity and exempts the provider's own write. That rule + # depends on the writing identity, so it cannot live in this + # background-scanned policy. - name: teammemberships-reference-allow-listed-teams match: any: diff --git a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml index 3a667ca2a3..3b74201d38 100644 --- a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml +++ b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml @@ -13,6 +13,7 @@ resources: - best-practices/disable-default-sa-automount.yaml - best-practices/disallow-latest-tag.yaml - best-practices/propagate-reloader-to-flagger-primary.yaml + - best-practices/restrict-github-team-external-identity.yaml - best-practices/restrict-github-team-management.yaml - best-practices/restrict-homepage-service-groups.yaml - best-practices/restrict-tenant-issuer-refs.yaml diff --git a/tests/policy-failure-actions.json b/tests/policy-failure-actions.json index 8abe84f4f7..0c25c94884 100644 --- a/tests/policy-failure-actions.json +++ b/tests/policy-failure-actions.json @@ -4,6 +4,7 @@ "audit-privileged-rbac/privileged-rules": "Enforce", "disallow-latest-tag/require-image-tag": "Audit", "disallow-latest-tag/validate-image-tag": "Audit", + "restrict-github-team-external-identity/teams-external-name-is-an-approved-identity": "Enforce", "restrict-github-team-management/teams-allow-listed": "Enforce", "restrict-github-team-management/teams-bind-provider-identity-to-object-name": "Enforce", "restrict-github-team-management/teammemberships-reference-allow-listed-teams": "Enforce", diff --git a/tests/restrict-github-team-external-identity/crossplane-identity/kyverno-test.yaml b/tests/restrict-github-team-external-identity/crossplane-identity/kyverno-test.yaml new file mode 100644 index 0000000000..fe1aaee465 --- /dev/null +++ b/tests/restrict-github-team-external-identity/crossplane-identity/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-crossplane-identity +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/crossplane-identity/resources.yaml b/tests/restrict-github-team-external-identity/crossplane-identity/resources.yaml new file mode 100644 index 0000000000..0acc3c0c15 --- /dev/null +++ b/tests/restrict-github-team-external-identity/crossplane-identity/resources.yaml @@ -0,0 +1,12 @@ +--- +# Only the GitHub provider is exempt, not every ServiceAccount beside it: Crossplane itself and the other providers are held to the rule. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/crossplane-identity/user-info.yaml b/tests/restrict-github-team-external-identity/crossplane-identity/user-info.yaml new file mode 100644 index 0000000000..3d7f96e2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/crossplane-identity/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: crossplane-core +userInfo: + username: system:serviceaccount:crossplane-system:crossplane + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/crossplane-identity/values.yaml b/tests/restrict-github-team-external-identity/crossplane-identity/values.yaml new file mode 100644 index 0000000000..85c8f509b6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/crossplane-identity/values.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: admins + values: + request.oldObject: + metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "18573307" diff --git a/tests/restrict-github-team-external-identity/foreign/kyverno-test.yaml b/tests/restrict-github-team-external-identity/foreign/kyverno-test.yaml new file mode 100644 index 0000000000..27b99e9601 --- /dev/null +++ b/tests/restrict-github-team-external-identity/foreign/kyverno-test.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-foreign +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + - github-config/maintainers + - github-config/other + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/foreign/resources.yaml b/tests/restrict-github-team-external-identity/foreign/resources.yaml new file mode 100644 index 0000000000..8ba9152b5f --- /dev/null +++ b/tests/restrict-github-team-external-identity/foreign/resources.yaml @@ -0,0 +1,36 @@ +--- +# The escalation: an allow-listed name created against a foreign team. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" +spec: + forProvider: + privacy: closed +--- +# A slug is not an identity the provider accepts, and is not approved. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "admins" +spec: + forProvider: + privacy: closed +--- +# An approved identity under a name it does not belong to (the name itself is refused by restrict-github-team-management). +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: other + namespace: github-config + annotations: + crossplane.io/external-name: "17046606" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/foreign/user-info.yaml b/tests/restrict-github-team-external-identity/foreign/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/foreign/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/foreign/values.yaml b/tests/restrict-github-team-external-identity/foreign/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/foreign/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/kyverno-test.yaml b/tests/restrict-github-team-external-identity/kyverno-test.yaml new file mode 100644 index 0000000000..581de26bc3 --- /dev/null +++ b/tests/restrict-github-team-external-identity/kyverno-test.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity +policies: + - >- + ../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + - github-config/maintainers + kind: Team + result: pass diff --git a/tests/restrict-github-team-external-identity/lookalike-identity/kyverno-test.yaml b/tests/restrict-github-team-external-identity/lookalike-identity/kyverno-test.yaml new file mode 100644 index 0000000000..b2b84a5e9f --- /dev/null +++ b/tests/restrict-github-team-external-identity/lookalike-identity/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-lookalike-identity +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/lookalike-identity/resources.yaml b/tests/restrict-github-team-external-identity/lookalike-identity/resources.yaml new file mode 100644 index 0000000000..a37d11b540 --- /dev/null +++ b/tests/restrict-github-team-external-identity/lookalike-identity/resources.yaml @@ -0,0 +1,12 @@ +--- +# A ServiceAccount with the provider's name in a tenant namespace is not the provider. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/lookalike-identity/user-info.yaml b/tests/restrict-github-team-external-identity/lookalike-identity/user-info.yaml new file mode 100644 index 0000000000..54c1dbf15c --- /dev/null +++ b/tests/restrict-github-team-external-identity/lookalike-identity/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: lookalike +userInfo: + username: system:serviceaccount:github-config:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/lookalike-identity/values.yaml b/tests/restrict-github-team-external-identity/lookalike-identity/values.yaml new file mode 100644 index 0000000000..85c8f509b6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/lookalike-identity/values.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: admins + values: + request.oldObject: + metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "18573307" diff --git a/tests/restrict-github-team-external-identity/other-identity/kyverno-test.yaml b/tests/restrict-github-team-external-identity/other-identity/kyverno-test.yaml new file mode 100644 index 0000000000..b1ed7912a6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/other-identity/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-other-identity +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/other-identity/resources.yaml b/tests/restrict-github-team-external-identity/other-identity/resources.yaml new file mode 100644 index 0000000000..22510e1cce --- /dev/null +++ b/tests/restrict-github-team-external-identity/other-identity/resources.yaml @@ -0,0 +1,12 @@ +--- +# The rule is not limited to the github-config ServiceAccount: any writer but the provider is held to it. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/other-identity/user-info.yaml b/tests/restrict-github-team-external-identity/other-identity/user-info.yaml new file mode 100644 index 0000000000..4753562950 --- /dev/null +++ b/tests/restrict-github-team-external-identity/other-identity/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: other-identity +userInfo: + username: system:serviceaccount:flux-system:kustomize-controller + groups: + - system:serviceaccounts + - system:serviceaccounts:flux-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/other-identity/values.yaml b/tests/restrict-github-team-external-identity/other-identity/values.yaml new file mode 100644 index 0000000000..85c8f509b6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/other-identity/values.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: admins + values: + request.oldObject: + metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "18573307" diff --git a/tests/restrict-github-team-external-identity/provider/kyverno-test.yaml b/tests/restrict-github-team-external-identity/provider/kyverno-test.yaml new file mode 100644 index 0000000000..2f5f2dffd8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/provider/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-provider +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + kind: Team + result: skip diff --git a/tests/restrict-github-team-external-identity/provider/resources.yaml b/tests/restrict-github-team-external-identity/provider/resources.yaml new file mode 100644 index 0000000000..b83755599a --- /dev/null +++ b/tests/restrict-github-team-external-identity/provider/resources.yaml @@ -0,0 +1,12 @@ +--- +# The provider records the identity of a team it has just created. That value cannot be approved in advance, so the provider is exempt. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "88888888" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/provider/user-info.yaml b/tests/restrict-github-team-external-identity/provider/user-info.yaml new file mode 100644 index 0000000000..5d2f307c4d --- /dev/null +++ b/tests/restrict-github-team-external-identity/provider/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: provider +userInfo: + username: system:serviceaccount:crossplane-system:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/provider/values.yaml b/tests/restrict-github-team-external-identity/provider/values.yaml new file mode 100644 index 0000000000..a3da9ae6f6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/provider/values.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: admins + values: + request.oldObject: + metadata: + name: admins + namespace: github-config + annotations: + # The values are merged into the previous object, so "no identity" + # has to be stated as an empty one. + crossplane.io/external-name: "" diff --git a/tests/restrict-github-team-external-identity/resources.yaml b/tests/restrict-github-team-external-identity/resources.yaml new file mode 100644 index 0000000000..c5cb5c6622 --- /dev/null +++ b/tests/restrict-github-team-external-identity/resources.yaml @@ -0,0 +1,24 @@ +--- +# Recovery after a rebuild: the artifact states the approved identity of admins. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "18573307" +spec: + forProvider: + privacy: closed +--- +# The live shape: maintainers is adopted by its approved identity. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "17046606" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/swapped/kyverno-test.yaml b/tests/restrict-github-team-external-identity/swapped/kyverno-test.yaml new file mode 100644 index 0000000000..1ca1e3f00c --- /dev/null +++ b/tests/restrict-github-team-external-identity/swapped/kyverno-test.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-swapped +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + - github-config/maintainers + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/swapped/resources.yaml b/tests/restrict-github-team-external-identity/swapped/resources.yaml new file mode 100644 index 0000000000..14116f1baf --- /dev/null +++ b/tests/restrict-github-team-external-identity/swapped/resources.yaml @@ -0,0 +1,24 @@ +--- +# Each identity is approved for one name only: admins may not front the maintainers team. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "17046606" +spec: + forProvider: + privacy: closed +--- +# And maintainers may not front the admins team, which holds repository admin. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "18573307" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/swapped/user-info.yaml b/tests/restrict-github-team-external-identity/swapped/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/swapped/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/swapped/values.yaml b/tests/restrict-github-team-external-identity/swapped/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/swapped/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/unset/kyverno-test.yaml b/tests/restrict-github-team-external-identity/unset/kyverno-test.yaml new file mode 100644 index 0000000000..2f8614e071 --- /dev/null +++ b/tests/restrict-github-team-external-identity/unset/kyverno-test.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-unset +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + - github-config/maintainers + kind: Team + result: pass diff --git a/tests/restrict-github-team-external-identity/unset/resources.yaml b/tests/restrict-github-team-external-identity/unset/resources.yaml new file mode 100644 index 0000000000..7ed0ef733f --- /dev/null +++ b/tests/restrict-github-team-external-identity/unset/resources.yaml @@ -0,0 +1,22 @@ +--- +# The live shape of admins as authored: no identity, the provider creates the team. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config +spec: + forProvider: + privacy: closed +--- +# An empty value adopts nothing and is the same as no annotation. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/unset/user-info.yaml b/tests/restrict-github-team-external-identity/unset/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/unset/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/unset/values.yaml b/tests/restrict-github-team-external-identity/unset/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/unset/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/update-changed/kyverno-test.yaml b/tests/restrict-github-team-external-identity/update-changed/kyverno-test.yaml new file mode 100644 index 0000000000..524901d700 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-changed/kyverno-test.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-update-changed +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + - github-config/other + kind: Team + result: fail + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/maintainers + kind: Team + result: pass diff --git a/tests/restrict-github-team-external-identity/update-changed/resources.yaml b/tests/restrict-github-team-external-identity/update-changed/resources.yaml new file mode 100644 index 0000000000..2cf4f92491 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-changed/resources.yaml @@ -0,0 +1,36 @@ +--- +# #3144: created compliant, then re-pointed at a foreign team by an update. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" +spec: + forProvider: + privacy: closed +--- +# The same re-pointing from no identity at all. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: other + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" +spec: + forProvider: + privacy: closed +--- +# Moving from another value to the approved identity is the repair path and is accepted. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "17046606" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/update-changed/user-info.yaml b/tests/restrict-github-team-external-identity/update-changed/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-changed/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/update-changed/values.yaml b/tests/restrict-github-team-external-identity/update-changed/values.yaml new file mode 100644 index 0000000000..4f6cc788a2 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-changed/values.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: admins + values: + request.oldObject: + metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "18573307" + - name: other + values: + request.oldObject: + metadata: + name: other + namespace: github-config + annotations: + # The values are merged into the previous object, so "no identity" + # has to be stated as an empty one. + crossplane.io/external-name: "" + - name: maintainers + values: + request.oldObject: + metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" diff --git a/tests/restrict-github-team-external-identity/update-cleared/kyverno-test.yaml b/tests/restrict-github-team-external-identity/update-cleared/kyverno-test.yaml new file mode 100644 index 0000000000..797186ad4a --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-cleared/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-update-cleared +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/maintainers + kind: Team + result: pass diff --git a/tests/restrict-github-team-external-identity/update-cleared/resources.yaml b/tests/restrict-github-team-external-identity/update-cleared/resources.yaml new file mode 100644 index 0000000000..44fa96d15f --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-cleared/resources.yaml @@ -0,0 +1,12 @@ +--- +# Removing the identity points the object at no team at all, which the name +# binding in restrict-github-team-management already covers. It is accepted so +# an artifact that stops stating an identity is not wedged. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/update-cleared/user-info.yaml b/tests/restrict-github-team-external-identity/update-cleared/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-cleared/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/update-cleared/values.yaml b/tests/restrict-github-team-external-identity/update-cleared/values.yaml new file mode 100644 index 0000000000..a2d931a180 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-cleared/values.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: maintainers + values: + request.oldObject: + metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "17046606" diff --git a/tests/restrict-github-team-external-identity/update-from-unapproved/kyverno-test.yaml b/tests/restrict-github-team-external-identity/update-from-unapproved/kyverno-test.yaml new file mode 100644 index 0000000000..63a9fe1015 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-unapproved/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-update-from-unapproved +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/update-from-unapproved/resources.yaml b/tests/restrict-github-team-external-identity/update-from-unapproved/resources.yaml new file mode 100644 index 0000000000..44c4610cc3 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-unapproved/resources.yaml @@ -0,0 +1,14 @@ +--- +# A team the provider created anew carries an identity that is not approved +# yet. Re-pointing it must still be refused: the previous value being +# unapproved is not a licence to change it. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "99999999" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/update-from-unapproved/user-info.yaml b/tests/restrict-github-team-external-identity/update-from-unapproved/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-unapproved/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/update-from-unapproved/values.yaml b/tests/restrict-github-team-external-identity/update-from-unapproved/values.yaml new file mode 100644 index 0000000000..3492fff94a --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-unapproved/values.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: admins + values: + request.oldObject: + metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "88888888" diff --git a/tests/restrict-github-team-external-identity/update-unchanged/kyverno-test.yaml b/tests/restrict-github-team-external-identity/update-unchanged/kyverno-test.yaml new file mode 100644 index 0000000000..fddcc6f83f --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-unchanged/kyverno-test.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-update-unchanged +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + - github-config/maintainers + kind: Team + result: pass diff --git a/tests/restrict-github-team-external-identity/update-unchanged/resources.yaml b/tests/restrict-github-team-external-identity/update-unchanged/resources.yaml new file mode 100644 index 0000000000..e10c51bda5 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-unchanged/resources.yaml @@ -0,0 +1,24 @@ +--- +# A team the provider created anew carries an identity that is not approved yet. The artifact's next apply leaves it as it is and must not be refused, or the object could never be updated again. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "88888888" +spec: + forProvider: + privacy: closed +--- +# The steady state: every reconcile re-applies the approved identity unchanged. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "17046606" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/update-unchanged/user-info.yaml b/tests/restrict-github-team-external-identity/update-unchanged/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-unchanged/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/update-unchanged/values.yaml b/tests/restrict-github-team-external-identity/update-unchanged/values.yaml new file mode 100644 index 0000000000..dab25f2928 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-unchanged/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE diff --git a/tests/restrict-github-team-external-identity/user-info.yaml b/tests/restrict-github-team-external-identity/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/values.yaml b/tests/restrict-github-team-external-identity/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE From 8e1d8c400392089a03cb161c52ac20f676e3809b Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 10:27:29 +0200 Subject: [PATCH 02/14] fix(github-config): compare the team identity literally 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 --- ...estrict-github-team-external-identity.yaml | 32 +++++++++++-------- .../foreign/resources.yaml | 2 +- .../update-from-wildcard/kyverno-test.yaml | 19 +++++++++++ .../update-from-wildcard/resources.yaml | 12 +++++++ .../update-from-wildcard/user-info.yaml | 11 +++++++ .../update-from-wildcard/values.yaml | 21 ++++++++++++ .../wildcard/kyverno-test.yaml | 20 ++++++++++++ .../wildcard/resources.yaml | 24 ++++++++++++++ .../wildcard/user-info.yaml | 11 +++++++ .../wildcard/values.yaml | 7 ++++ 10 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 tests/restrict-github-team-external-identity/update-from-wildcard/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/update-from-wildcard/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/update-from-wildcard/values.yaml create mode 100644 tests/restrict-github-team-external-identity/wildcard/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/wildcard/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/wildcard/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/wildcard/values.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml index 6224a73473..050dd294ac 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml @@ -34,7 +34,9 @@ # revision (provider-upjet-github-, read from the running pod), so # the name changes with every provider upgrade and is matched by prefix. # Creating a ServiceAccount in crossplane-system is a platform-level -# permission, so the prefix cannot be claimed from a tenant namespace. +# permission, so the prefix cannot be claimed from a tenant namespace. It would +# also cover another package whose name starts the same way, should one ever +# be installed there. # # Admission-only (background: false), because the exemption is on the writing # identity, which a background scan does not have. @@ -100,16 +102,18 @@ spec: deny: conditions: all: - - key: "{{ externalName }}" - operator: NotEquals - value: "" - - key: "{{ externalName }}" - operator: NotEquals - value: "{{ previousExternalName }}" - # Name and identity are checked as a pair, so the approved - # identity of one team cannot be used on the other object. - - key: "{{ request.object.metadata.name }}/{{ externalName }}" - operator: AnyNotIn - value: - - admins/18573307 - - maintainers/17046606 + # ONE exact comparison, evaluated by JMESPath, and deliberately + # not three Kyverno operators. Kyverno's Equals and AnyNotIn + # treat * and ? as wildcards on either side and compare numbers + # as quantities, and the annotation is written by the party this + # rule constrains: an identity of "*" would match the approved + # one, and any later value would then count as "unchanged" from + # "*". JMESPath == and contains() compare strings literally. + # + # Accepted: no identity, the identity the object already has, or + # the approved identity of this object name. Name and identity + # are checked as a pair, so the approved identity of one team + # cannot be used on the other object. + - key: "{{ externalName == '' || externalName == previousExternalName || contains(['admins/18573307', 'maintainers/17046606'], join('/', [request.object.metadata.name, externalName])) }}" + operator: Equals + value: false diff --git a/tests/restrict-github-team-external-identity/foreign/resources.yaml b/tests/restrict-github-team-external-identity/foreign/resources.yaml index 8ba9152b5f..379e55a55b 100644 --- a/tests/restrict-github-team-external-identity/foreign/resources.yaml +++ b/tests/restrict-github-team-external-identity/foreign/resources.yaml @@ -11,7 +11,7 @@ spec: forProvider: privacy: closed --- -# A slug is not an identity the provider accepts, and is not approved. +# A slug is not an approved identity either, whatever name it spells. apiVersion: team.github.m.upbound.io/v1beta1 kind: Team metadata: diff --git a/tests/restrict-github-team-external-identity/update-from-wildcard/kyverno-test.yaml b/tests/restrict-github-team-external-identity/update-from-wildcard/kyverno-test.yaml new file mode 100644 index 0000000000..60275f792c --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-wildcard/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-update-from-wildcard +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml b/tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml new file mode 100644 index 0000000000..3c748bd066 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml @@ -0,0 +1,12 @@ +--- +# A previous identity of * must not make every later value count as unchanged. The dependent TeamMembership and TeamRepository resources accept a team slug, so a slug here is a foreign team like any other. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "security-admins" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/update-from-wildcard/user-info.yaml b/tests/restrict-github-team-external-identity/update-from-wildcard/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-wildcard/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/update-from-wildcard/values.yaml b/tests/restrict-github-team-external-identity/update-from-wildcard/values.yaml new file mode 100644 index 0000000000..ddd6e4b096 --- /dev/null +++ b/tests/restrict-github-team-external-identity/update-from-wildcard/values.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so the row tests a +# change. +policies: + - name: restrict-github-team-external-identity + resources: + - name: admins + values: + request.oldObject: + metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "*" diff --git a/tests/restrict-github-team-external-identity/wildcard/kyverno-test.yaml b/tests/restrict-github-team-external-identity/wildcard/kyverno-test.yaml new file mode 100644 index 0000000000..fd026d0710 --- /dev/null +++ b/tests/restrict-github-team-external-identity/wildcard/kyverno-test.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-wildcard +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + - github-config/maintainers + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/wildcard/resources.yaml b/tests/restrict-github-team-external-identity/wildcard/resources.yaml new file mode 100644 index 0000000000..33166b2a17 --- /dev/null +++ b/tests/restrict-github-team-external-identity/wildcard/resources.yaml @@ -0,0 +1,24 @@ +--- +# Kyverno's own comparison operators read * as "anything", which would match the approved identity. The rule compares literally, so this is refused. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "*" +spec: + forProvider: + privacy: closed +--- +# The same for a single-character wildcard. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config + annotations: + crossplane.io/external-name: "1704660?" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/wildcard/user-info.yaml b/tests/restrict-github-team-external-identity/wildcard/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/wildcard/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/wildcard/values.yaml b/tests/restrict-github-team-external-identity/wildcard/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/wildcard/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE From 3db86e1c075397775eaa3906c4e545f24aae2056 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 10:35:10 +0200 Subject: [PATCH 03/14] docs(github-config): correct the operator note and pin the no-expansion case Part of #3144 Co-Authored-By: Claude Opus 5.5 --- k8s/bases/apps/github-config/role.yaml | 8 ++++---- ...estrict-github-team-external-identity.yaml | 10 +++++----- .../templated/kyverno-test.yaml | 19 +++++++++++++++++++ .../templated/resources.yaml | 13 +++++++++++++ .../templated/user-info.yaml | 11 +++++++++++ .../templated/values.yaml | 7 +++++++ .../unset/resources.yaml | 2 +- .../update-from-wildcard/resources.yaml | 2 +- 8 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 tests/restrict-github-team-external-identity/templated/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/templated/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/templated/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/templated/values.yaml diff --git a/k8s/bases/apps/github-config/role.yaml b/k8s/bases/apps/github-config/role.yaml index 2ce5118573..5d1165f997 100644 --- a/k8s/bases/apps/github-config/role.yaml +++ b/k8s/bases/apps/github-config/role.yaml @@ -53,10 +53,10 @@ rules: - delete # Team resources are high-impact because they change real GitHub org # authorization. RBAC alone cannot express which teams this SA may manage, so - # the restrict-github-team-management and restrict-github-team-external-identity - # Kyverno admission policies constrain the - # allowed team identities, reference style, and repository permissions on top - # of these verbs. + # the restrict-github-team-management and + # restrict-github-team-external-identity Kyverno admission policies constrain + # the allowed team identities, reference style, and repository permissions on + # top of these verbs. - apiGroups: - team.github.m.upbound.io resources: diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml index 050dd294ac..20c406ed5c 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml @@ -92,7 +92,7 @@ spec: # provider created anew carries an identity that is not approved yet, so # its object "already fails" when judged alone, and every later change # to its identity would be waved through. An unchanged value is accepted - # by the conditions below instead, which is the only tolerance needed. + # by the condition below instead, which is the only tolerance needed. allowExistingViolations: false message: >- The crossplane.io/external-name of a github-config Team may only be @@ -103,10 +103,10 @@ spec: conditions: all: # ONE exact comparison, evaluated by JMESPath, and deliberately - # not three Kyverno operators. Kyverno's Equals and AnyNotIn - # treat * and ? as wildcards on either side and compare numbers - # as quantities, and the annotation is written by the party this - # rule constrains: an identity of "*" would match the approved + # not three Kyverno operators. AnyNotIn reads * and ? as wildcards + # in either operand, Equals reads them in its value and compares + # numbers as quantities, and the annotation is written by the party + # this rule constrains: an identity of "*" would match the approved # one, and any later value would then count as "unchanged" from # "*". JMESPath == and contains() compare strings literally. # diff --git a/tests/restrict-github-team-external-identity/templated/kyverno-test.yaml b/tests/restrict-github-team-external-identity/templated/kyverno-test.yaml new file mode 100644 index 0000000000..fe07d154fc --- /dev/null +++ b/tests/restrict-github-team-external-identity/templated/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-templated +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teams-external-name-is-an-approved-identity + resources: + - github-config/admins + kind: Team + result: fail diff --git a/tests/restrict-github-team-external-identity/templated/resources.yaml b/tests/restrict-github-team-external-identity/templated/resources.yaml new file mode 100644 index 0000000000..7f110a57e5 --- /dev/null +++ b/tests/restrict-github-team-external-identity/templated/resources.yaml @@ -0,0 +1,13 @@ +--- +# The identity is data and is never expanded as a policy variable: a value +# written in variable syntax is a foreign value like any other. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config + annotations: + crossplane.io/external-name: "{{ request.object.metadata.name }}" +spec: + forProvider: + privacy: closed diff --git a/tests/restrict-github-team-external-identity/templated/user-info.yaml b/tests/restrict-github-team-external-identity/templated/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/templated/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/templated/values.yaml b/tests/restrict-github-team-external-identity/templated/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/templated/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/unset/resources.yaml b/tests/restrict-github-team-external-identity/unset/resources.yaml index 7ed0ef733f..1324ca0aff 100644 --- a/tests/restrict-github-team-external-identity/unset/resources.yaml +++ b/tests/restrict-github-team-external-identity/unset/resources.yaml @@ -1,5 +1,5 @@ --- -# The live shape of admins as authored: no identity, the provider creates the team. +# The live shape of admins as authored: no identity, the provider creates the team. (On create this is also "unchanged"; update-cleared is the case that isolates the empty value.) apiVersion: team.github.m.upbound.io/v1beta1 kind: Team metadata: diff --git a/tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml b/tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml index 3c748bd066..b78d6e0e92 100644 --- a/tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml +++ b/tests/restrict-github-team-external-identity/update-from-wildcard/resources.yaml @@ -1,5 +1,5 @@ --- -# A previous identity of * must not make every later value count as unchanged. The dependent TeamMembership and TeamRepository resources accept a team slug, so a slug here is a foreign team like any other. +# A previous identity of * must not make every later value count as unchanged. A name is refused like any other value that is not the approved identity. apiVersion: team.github.m.upbound.io/v1beta1 kind: Team metadata: From 090ce5ae2c06c9d27854abdd1c3a0c99fd4d3772 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 11:20:21 +0200 Subject: [PATCH 04/14] fix(github-config): refuse a managed team that names a parent team or directory group A nested team inherits its parent's repository access on GitHub, outside the grant rules that cap what the github-config release may hand out, and a directory link moves membership outside the member allow-list. The Team rules checked neither. Refuse all four fields the provider exposes, in forProvider and initProvider, with one literal JMESPath comparison. Fixes #4511 Co-Authored-By: Claude Opus 5.5 --- .../restrict-github-team-management.yaml | 52 ++++++- tests/policy-failure-actions.json | 1 + .../kyverno-test.yaml | 11 ++ .../kyverno-test.yaml | 33 +++++ .../nested-or-directory-linked/resources.yaml | 129 ++++++++++++++++++ 5 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml create mode 100644 tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml index c35becf3cb..5cfae421ba 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml @@ -21,7 +21,8 @@ metadata: Restricts provider-upjet-github team resources in the github-config namespace to the github-config 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 + blocks repository admin grants. A Team may not be nested under a parent + team or linked to a directory group. This prevents a compromised or unintended github-config artifact from adding arbitrary users to arbitrary GitHub teams or granting privileged repository access. spec: @@ -92,6 +93,55 @@ spec: # 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. + # + # #4511: a nested team inherits every repository grant of its parent, and + # that inheritance happens on GitHub — it never passes through the + # TeamRepository rules below, which are what cap this delegation's grants. A + # Team that named a parent would hand its members the parent's access with no + # grant for those rules to judge. The directory link is refused for the same + # reason from the other side: it lets a group outside this cluster decide who + # the members are, past the member allow-list. + # + # All four fields the provider exposes are covered, in both blocks. + # parentTeamReadId and parentTeamReadSlug are documented as read-back values, + # but the schema accepts them as input, so they are refused rather than + # trusted to stay inert. An empty string is what the provider reports for + # "none" and is accepted. + # + # Measured live: neither team sets any of them, and the provider reports all + # four empty for both. The provider is NOT exempt. If a team were nested on + # GitHub by hand, the provider would try to record the parent on the admins + # object, be refused, and report the object out of sync — which is the + # signal wanted for a nesting nobody reviewed. + - name: teams-not-nested-or-directory-linked + match: + any: + - resources: + kinds: + - team.github.m.upbound.io/*/Team + namespaces: + - github-config + validate: + failureAction: Enforce + # Kyverno admits an update by default when the object already failed the + # rule before it. No object fails it today, and one that somehow did + # must not become freely editable for that reason. + allowExistingViolations: false + message: >- + A github-config Team must not set parentTeamId, parentTeamReadId, + parentTeamReadSlug or ldapDn, in forProvider or initProvider. Nesting a + team or linking it to a directory group is a reviewed change to + restrict-github-team-management.yaml. + deny: + conditions: + all: + # ONE comparison evaluated by JMESPath rather than eight Kyverno + # operators: AnyNotIn and Equals read * and ? in an operand as + # wildcards, and these fields are written by the party this rule + # constrains. JMESPath == compares strings literally. + - key: "{{ (request.object.spec.forProvider.parentTeamId || '') == '' && (request.object.spec.forProvider.parentTeamReadId || '') == '' && (request.object.spec.forProvider.parentTeamReadSlug || '') == '' && (request.object.spec.forProvider.ldapDn || '') == '' && (request.object.spec.initProvider.parentTeamId || '') == '' && (request.object.spec.initProvider.parentTeamReadId || '') == '' && (request.object.spec.initProvider.parentTeamReadSlug || '') == '' && (request.object.spec.initProvider.ldapDn || '') == '' }}" + operator: Equals + value: false - name: teammemberships-reference-allow-listed-teams match: any: diff --git a/tests/policy-failure-actions.json b/tests/policy-failure-actions.json index 8abe84f4f7..f8a28dca25 100644 --- a/tests/policy-failure-actions.json +++ b/tests/policy-failure-actions.json @@ -6,6 +6,7 @@ "disallow-latest-tag/validate-image-tag": "Audit", "restrict-github-team-management/teams-allow-listed": "Enforce", "restrict-github-team-management/teams-bind-provider-identity-to-object-name": "Enforce", + "restrict-github-team-management/teams-not-nested-or-directory-linked": "Enforce", "restrict-github-team-management/teammemberships-reference-allow-listed-teams": "Enforce", "restrict-github-team-management/teammemberships-allow-listed-members": "Enforce", "restrict-github-team-management/teammemberships-known-roles": "Enforce", diff --git a/tests/restrict-github-team-management/kyverno-test.yaml b/tests/restrict-github-team-management/kyverno-test.yaml index 04bd3b4732..baabb5c27f 100644 --- a/tests/restrict-github-team-management/kyverno-test.yaml +++ b/tests/restrict-github-team-management/kyverno-test.yaml @@ -35,6 +35,17 @@ results: - github-config/attacker-team kind: Team result: pass + # None of the paved-road teams names a parent team or a directory group. + # attacker-team is here for the same reason as above: the nesting rule judges + # only those fields, whatever the allow-list says about the object. + - policy: restrict-github-team-management + rule: teams-not-nested-or-directory-linked + resources: + - github-config/admins + - 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 diff --git a/tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml b/tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml new file mode 100644 index 0000000000..a20aa07301 --- /dev/null +++ b/tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-management-nested-or-directory-linked +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +resources: + - resources.yaml +results: + - policy: restrict-github-team-management + rule: teams-not-nested-or-directory-linked + resources: + - github-config/forprovider-parentteamid + - github-config/forprovider-parentteamreadid + - github-config/forprovider-parentteamreadslug + - github-config/forprovider-ldapdn + - github-config/initprovider-parentteamid + - github-config/initprovider-parentteamreadid + - github-config/initprovider-parentteamreadslug + - github-config/initprovider-ldapdn + - github-config/wildcard-star + - github-config/wildcard-question + - github-config/admins + kind: Team + result: fail + - policy: restrict-github-team-management + rule: teams-not-nested-or-directory-linked + resources: + - github-config/maintainers + kind: Team + result: pass diff --git a/tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml b/tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml new file mode 100644 index 0000000000..90deb6325a --- /dev/null +++ b/tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml @@ -0,0 +1,129 @@ +--- +# A parent team, a read-back parent field, or a directory link — each one alone, +# in each block the provider reads. Every one of these must be refused. The +# object names are not on the team allow-list on purpose: this fixture asserts +# only the nesting rule, which has to refuse them whatever the name. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: forprovider-parentteamid + namespace: github-config +spec: + forProvider: + parentTeamId: "1234567" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: forprovider-parentteamreadid + namespace: github-config +spec: + forProvider: + parentTeamReadId: "1234567" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: forprovider-parentteamreadslug + namespace: github-config +spec: + forProvider: + parentTeamReadSlug: other-team +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: forprovider-ldapdn + namespace: github-config +spec: + forProvider: + ldapDn: cn=owners,ou=groups,dc=example,dc=com +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: initprovider-parentteamid + namespace: github-config +spec: + initProvider: + parentTeamId: "1234567" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: initprovider-parentteamreadid + namespace: github-config +spec: + initProvider: + parentTeamReadId: "1234567" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: initprovider-parentteamreadslug + namespace: github-config +spec: + initProvider: + parentTeamReadSlug: other-team +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: initprovider-ldapdn + namespace: github-config +spec: + initProvider: + ldapDn: cn=owners,ou=groups,dc=example,dc=com +--- +# Kyverno's own operators read * and ? as wildcards, so a parent written as a +# wildcard is the value most likely to slip past a comparison. It is text like +# any other and must be refused. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: wildcard-star + namespace: github-config +spec: + forProvider: + parentTeamId: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: wildcard-question + namespace: github-config +spec: + initProvider: + parentTeamReadSlug: "?" +--- +# An approved forProvider block must not front a parent supplied through +# initProvider, which upjet merges into any field forProvider leaves unset. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: admins + namespace: github-config +spec: + forProvider: + name: Admins + privacy: secret + initProvider: + parentTeamId: "1234567" +--- +# The provider reports "no parent" and "no directory link" as empty strings. +# Writing them out changes nothing on GitHub and must stay admitted. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: maintainers + namespace: github-config +spec: + forProvider: + name: maintainers + parentTeamId: "" + parentTeamReadId: "" + parentTeamReadSlug: "" + ldapDn: "" + initProvider: + parentTeamId: "" + ldapDn: "" From 29976126292f2cbc5ae89cafa1dce5924c0d7399 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 11:24:59 +0200 Subject: [PATCH 05/14] fix(github-config): refuse non-text parent and directory values in the rule itself Review finding: `x || ''` also reads false, [] and {} as empty, so only the provider's schema stood between those values and admission. not_null() replaces a missing value only. Adds fixtures for a boolean, an empty list, a blank string and a Team with no spec, and words the message to match what is accepted. Co-Authored-By: Claude Opus 5.5 --- .../restrict-github-team-management.yaml | 18 ++++++--- .../kyverno-test.yaml | 4 ++ .../nested-or-directory-linked/resources.yaml | 38 +++++++++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml index 5cfae421ba..3ce08e99de 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml @@ -125,12 +125,15 @@ spec: failureAction: Enforce # Kyverno admits an update by default when the object already failed the # rule before it. No object fails it today, and one that somehow did - # must not become freely editable for that reason. + # must not become freely editable for that reason. The cost: such an + # object could not be changed at all, its finalizer included, until + # the field is cleared. allowExistingViolations: false message: >- - A github-config Team must not set parentTeamId, parentTeamReadId, - parentTeamReadSlug or ldapDn, in forProvider or initProvider. Nesting a - team or linking it to a directory group is a reviewed change to + A github-config Team must leave parentTeamId, parentTeamReadId, + parentTeamReadSlug and ldapDn unset or empty, in forProvider and + initProvider. Nesting a team or linking it to a directory group is a + reviewed change to restrict-github-team-management.yaml. deny: conditions: @@ -138,8 +141,11 @@ spec: # ONE comparison evaluated by JMESPath rather than eight Kyverno # operators: AnyNotIn and Equals read * and ? in an operand as # wildcards, and these fields are written by the party this rule - # constrains. JMESPath == compares strings literally. - - key: "{{ (request.object.spec.forProvider.parentTeamId || '') == '' && (request.object.spec.forProvider.parentTeamReadId || '') == '' && (request.object.spec.forProvider.parentTeamReadSlug || '') == '' && (request.object.spec.forProvider.ldapDn || '') == '' && (request.object.spec.initProvider.parentTeamId || '') == '' && (request.object.spec.initProvider.parentTeamReadId || '') == '' && (request.object.spec.initProvider.parentTeamReadSlug || '') == '' && (request.object.spec.initProvider.ldapDn || '') == '' }}" + # constrains. JMESPath == compares strings literally. not_null() + # replaces only a missing value with the empty string, so false, + # an empty list or an empty object is refused here and not left + # to the provider's schema to reject. + - key: "{{ not_null(request.object.spec.forProvider.parentTeamId, '') == '' && not_null(request.object.spec.forProvider.parentTeamReadId, '') == '' && not_null(request.object.spec.forProvider.parentTeamReadSlug, '') == '' && not_null(request.object.spec.forProvider.ldapDn, '') == '' && not_null(request.object.spec.initProvider.parentTeamId, '') == '' && not_null(request.object.spec.initProvider.parentTeamReadId, '') == '' && not_null(request.object.spec.initProvider.parentTeamReadSlug, '') == '' && not_null(request.object.spec.initProvider.ldapDn, '') == '' }}" operator: Equals value: false - name: teammemberships-reference-allow-listed-teams diff --git a/tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml b/tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml index a20aa07301..1321272df6 100644 --- a/tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml +++ b/tests/restrict-github-team-management/nested-or-directory-linked/kyverno-test.yaml @@ -23,11 +23,15 @@ results: - github-config/wildcard-star - github-config/wildcard-question - github-config/admins + - github-config/typed-false + - github-config/typed-empty-list + - github-config/whitespace kind: Team result: fail - policy: restrict-github-team-management rule: teams-not-nested-or-directory-linked resources: - github-config/maintainers + - github-config/no-spec kind: Team result: pass diff --git a/tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml b/tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml index 90deb6325a..93e1296cb1 100644 --- a/tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml +++ b/tests/restrict-github-team-management/nested-or-directory-linked/resources.yaml @@ -127,3 +127,41 @@ spec: initProvider: parentTeamId: "" ldapDn: "" +--- +# Not text at all. The provider's schema declares these fields as strings and +# would reject each of these first, but the rule must not depend on that: a +# value that is merely "empty-looking" is still a value somebody set. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: typed-false + namespace: github-config +spec: + forProvider: + parentTeamId: false +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: typed-empty-list + namespace: github-config +spec: + initProvider: + ldapDn: [] +--- +# Blank to a reader, not empty to GitHub. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: whitespace + namespace: github-config +spec: + forProvider: + parentTeamReadSlug: " " +--- +# Nothing under spec at all: every field is missing, none is set. +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: no-spec + namespace: github-config From 424028082ee2f93039e25b9ca419d4679fdcfda1 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 12:17:36 +0200 Subject: [PATCH 06/14] fix(github-config): bind a membership's or grant's stored identity to what it declares A TeamRepository is read, updated and removed by its stored identity alone, and a TeamMembership is removed by it, so an object that satisfies every rule in restrict-github-team-management could act on a different team, repository or user than the one it names (#4518). Established from the released sources of the provider version prod runs. Anyone but the provider may now only leave that identity unset, leave it unchanged together with the team reference and the repository or user, or set it to the approved identity of the referenced team followed by the declared repository or user. Co-Authored-By: Claude Opus 5.5 --- ...estrict-github-team-external-identity.yaml | 160 ++++++++++++++- tests/policy-failure-actions.json | 2 + .../grants-create/kyverno-test.yaml | 35 ++++ .../grants-create/resources.yaml | 189 ++++++++++++++++++ .../grants-create/user-info.yaml | 11 + .../grants-create/values.yaml | 7 + .../grants-provider/kyverno-test.yaml | 19 ++ .../grants-provider/resources.yaml | 16 ++ .../grants-provider/user-info.yaml | 11 + .../grants-provider/values.yaml | 7 + .../grants-update/kyverno-test.yaml | 30 +++ .../grants-update/resources.yaml | 110 ++++++++++ .../grants-update/user-info.yaml | 11 + .../grants-update/values.yaml | 64 ++++++ .../members-create/kyverno-test.yaml | 35 ++++ .../members-create/resources.yaml | 189 ++++++++++++++++++ .../members-create/user-info.yaml | 11 + .../members-create/values.yaml | 7 + .../members-provider/kyverno-test.yaml | 19 ++ .../members-provider/resources.yaml | 16 ++ .../members-provider/user-info.yaml | 11 + .../members-provider/values.yaml | 7 + .../members-update/kyverno-test.yaml | 30 +++ .../members-update/resources.yaml | 110 ++++++++++ .../members-update/user-info.yaml | 11 + .../members-update/values.yaml | 64 ++++++ 26 files changed, 1179 insertions(+), 3 deletions(-) create mode 100644 tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-create/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-create/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-create/values.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider/values.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-update/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-update/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-update/values.yaml create mode 100644 tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/members-create/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/members-create/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/members-create/values.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider/values.yaml create mode 100644 tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/members-update/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/members-update/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/members-update/values.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml index 20c406ed5c..a4199beefd 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml @@ -48,14 +48,17 @@ metadata: policies.kyverno.io/title: Restrict GitHub Team External Identity policies.kyverno.io/category: Security, GitHub policies.kyverno.io/severity: high - policies.kyverno.io/subject: Team + policies.kyverno.io/subject: Team, TeamMembership, TeamRepository policies.kyverno.io/minversion: 1.13.0 policies.kyverno.io/description: >- Refuses a github-config Team whose crossplane.io/external-name is set or changed to anything other than the approved numeric identity of that team, so an allow-listed Team object cannot be pointed at a different GitHub - team after it was admitted. The provider's own record of a team it - created is exempt. + team after it was admitted, and a TeamMembership or TeamRepository whose + crossplane.io/external-name disagrees with the team and the user or + repository the object declares, so a compliant-looking membership or + grant cannot stand for another one. The provider's own records are + exempt. spec: background: false rules: @@ -117,3 +120,154 @@ spec: - key: "{{ externalName == '' || externalName == previousExternalName || contains(['admins/18573307', 'maintainers/17046606'], join('/', [request.object.metadata.name, externalName])) }}" operator: Equals value: false + # A membership or repository grant has a stored identity of its own: + # ":" or ":" (#4518). Read from the + # released sources of the provider version prod runs (provider-upjet-github + # v0.20.0, terraform-provider-github v6.13.0, upjet v2.2.0): + # + # - a repository grant is read, updated and removed by that stored identity + # alone, so a grant object that satisfies every rule in + # restrict-github-team-management could change or remove the access of a + # different team than the one it references; + # - a membership refuses an update when its stored identity names another + # team or user, but its removal acts on what was read through that + # identity. + # + # So the stored identity has to agree with what the object itself declares. + # From anyone but the provider it may be absent, exactly what the object + # already carries while it keeps referencing the same team and the same + # repository or user, or the approved identity of the referenced team + # followed by the declared repository or user. The two team identities are + # the ones the Team rule approves, repeated here for the reason given at the + # top of this file: a change of one is a change of all three rules. + # + # "Unchanged" covers the team reference and the repository or user as well + # as the identity, so an object cannot keep an identity while being + # re-declared to stand for something else. The provider records identities + # in exactly the approved shape (measured on prod 2026-10-05: all 49 + # memberships and grants), so the artifact's routine re-apply passes either + # way. + - name: teamrepositories-external-name-matches-declared-grant + match: + any: + - resources: + kinds: + - team.github.m.upbound.io/*/TeamRepository + operations: + - CREATE + - UPDATE + namespaces: + - github-config + exclude: + any: + - subjects: + - kind: ServiceAccount + name: provider-upjet-github-* + namespace: crossplane-system + context: + - name: externalName + variable: + jmesPath: request.object.metadata.annotations."crossplane.io/external-name" + default: "" + - name: teamRef + variable: + jmesPath: request.object.spec.forProvider.teamIdRef.name + default: "" + - name: subject + variable: + jmesPath: request.object.spec.forProvider.repository + default: "" + - name: previousExternalName + variable: + jmesPath: request.oldObject.metadata.annotations."crossplane.io/external-name" + default: "" + - name: previousTeamRef + variable: + jmesPath: request.oldObject.spec.forProvider.teamIdRef.name + default: "" + - name: previousSubject + variable: + jmesPath: request.oldObject.spec.forProvider.repository + default: "" + validate: + failureAction: Enforce + # As on the Team rule: an object that already fails must not be waved + # through on update. + allowExistingViolations: false + message: >- + The crossplane.io/external-name of a github-config TeamRepository may + only be left unset, left unchanged together with its team reference + and repository, or set to the approved numeric identity of the referenced + team followed by a colon and spec.forProvider.repository. + deny: + conditions: + all: + # One literal JMESPath expression, for the reason given on the + # Team rule. Every part is compared on its own rather than as one + # joined string, so no value can borrow a separator to look like + # another pair. An empty repository is never an approved identity. + - key: "{{ externalName == '' || (externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))) }}" + operator: Equals + value: false + - name: teammemberships-external-name-matches-declared-member + match: + any: + - resources: + kinds: + - team.github.m.upbound.io/*/TeamMembership + operations: + - CREATE + - UPDATE + namespaces: + - github-config + exclude: + any: + - subjects: + - kind: ServiceAccount + name: provider-upjet-github-* + namespace: crossplane-system + context: + - name: externalName + variable: + jmesPath: request.object.metadata.annotations."crossplane.io/external-name" + default: "" + - name: teamRef + variable: + jmesPath: request.object.spec.forProvider.teamIdRef.name + default: "" + - name: subject + variable: + jmesPath: request.object.spec.forProvider.username + default: "" + - name: previousExternalName + variable: + jmesPath: request.oldObject.metadata.annotations."crossplane.io/external-name" + default: "" + - name: previousTeamRef + variable: + jmesPath: request.oldObject.spec.forProvider.teamIdRef.name + default: "" + - name: previousSubject + variable: + jmesPath: request.oldObject.spec.forProvider.username + default: "" + validate: + failureAction: Enforce + # As on the Team rule: an object that already fails must not be waved + # through on update. + allowExistingViolations: false + message: >- + The crossplane.io/external-name of a github-config TeamMembership may + only be left unset, left unchanged together with its team reference + and username, or set to the approved numeric identity of the referenced + team followed by a colon and spec.forProvider.username. + deny: + conditions: + all: + # One literal JMESPath expression, for the reason given on the + # Team rule. Every part is compared on its own rather than as one + # joined string, so no value can borrow a separator to look like + # another pair. An empty username is never an approved identity. + - key: "{{ externalName == '' || (externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))) }}" + operator: Equals + value: false diff --git a/tests/policy-failure-actions.json b/tests/policy-failure-actions.json index 0c25c94884..95f478d8e4 100644 --- a/tests/policy-failure-actions.json +++ b/tests/policy-failure-actions.json @@ -4,6 +4,8 @@ "audit-privileged-rbac/privileged-rules": "Enforce", "disallow-latest-tag/require-image-tag": "Audit", "disallow-latest-tag/validate-image-tag": "Audit", + "restrict-github-team-external-identity/teammemberships-external-name-matches-declared-member": "Enforce", + "restrict-github-team-external-identity/teamrepositories-external-name-matches-declared-grant": "Enforce", "restrict-github-team-external-identity/teams-external-name-is-an-approved-identity": "Enforce", "restrict-github-team-management/teams-allow-listed": "Enforce", "restrict-github-team-management/teams-bind-provider-identity-to-object-name": "Enforce", diff --git a/tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml new file mode 100644 index 0000000000..28fae5e52f --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-grants-create +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/admins-approved + - github-config/maintainers-approved + - github-config/unset + kind: TeamRepository + result: pass + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/swapped-team + - github-config/swapped-team-reverse + - github-config/other-subject + - github-config/foreign-team + - github-config/slug-identity + - github-config/wildcard + - github-config/wildcard-subject + - github-config/longer-subject + - github-config/undeclared-subject + kind: TeamRepository + result: fail diff --git a/tests/restrict-github-team-external-identity/grants-create/resources.yaml b/tests/restrict-github-team-external-identity/grants-create/resources.yaml new file mode 100644 index 0000000000..d9c373127c --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-create/resources.yaml @@ -0,0 +1,189 @@ +--- +# Recovery after a rebuild: the artifact states the identity the provider would record. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: admins-approved + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The same for the other approved team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: maintainers-approved + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# The usual shape: no identity, the provider creates and records it. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: unset + namespace: github-config +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# References admins but stands for the maintainers team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: swapped-team + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Names one repository and stands for another. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: other-subject + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:platform" +spec: + forProvider: + repository: ksail + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# A team that is not approved at all. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: foreign-team + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The provider also accepts a team slug; only the approved numeric identity is. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: slug-identity + namespace: github-config + annotations: + crossplane.io/external-name: "admins:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# A wildcard is not an identity. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: wildcard + namespace: github-config + annotations: + crossplane.io/external-name: "*" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Nor is a wildcard in place of the repository. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: wildcard-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:*" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The declared repository is only a prefix of the stored one. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: longer-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail-other" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Nothing declared under forProvider, so nothing can be approved. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: undeclared-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:" +spec: + forProvider: + teamIdRef: + name: admins + policy: + resolve: Always +--- +# References maintainers but stands for the admins team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: swapped-team-reverse + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: maintainers + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/grants-create/user-info.yaml b/tests/restrict-github-team-external-identity/grants-create/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-create/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/grants-create/values.yaml b/tests/restrict-github-team-external-identity/grants-create/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-create/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/grants-provider/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-provider/kyverno-test.yaml new file mode 100644 index 0000000000..503e8d738d --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-grants-provider +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/provider-recorded + kind: TeamRepository + result: skip diff --git a/tests/restrict-github-team-external-identity/grants-provider/resources.yaml b/tests/restrict-github-team-external-identity/grants-provider/resources.yaml new file mode 100644 index 0000000000..69a55f967c --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider/resources.yaml @@ -0,0 +1,16 @@ +--- +# The provider records whatever GitHub returned; it is exempt. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: provider-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/grants-provider/user-info.yaml b/tests/restrict-github-team-external-identity/grants-provider/user-info.yaml new file mode 100644 index 0000000000..5d2f307c4d --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: provider +userInfo: + username: system:serviceaccount:crossplane-system:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/grants-provider/values.yaml b/tests/restrict-github-team-external-identity/grants-provider/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml new file mode 100644 index 0000000000..1d54d73910 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-grants-update +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/unchanged-unusual + - github-config/cleared + - github-config/set-approved + kind: TeamRepository + result: pass + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/changed + - github-config/retargeted-team + - github-config/retargeted-subject + - github-config/from-wildcard + kind: TeamRepository + result: fail diff --git a/tests/restrict-github-team-external-identity/grants-update/resources.yaml b/tests/restrict-github-team-external-identity/grants-update/resources.yaml new file mode 100644 index 0000000000..d841d37ce6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-update/resources.yaml @@ -0,0 +1,110 @@ +--- +# An identity the provider recorded in a shape this rule would not approve is left as it is by the next apply, which must not be refused. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: unchanged-unusual + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail-as-recorded" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Removing the identity makes the provider create the declared one again. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: cleared + namespace: github-config +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Stating the approved identity on an object that had none. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: set-approved + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# The identity moves to another team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: changed + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# The identity stays, the team reference moves to admins. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: retargeted-team + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The identity stays, the declared repository moves. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: retargeted-subject + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:ksail" +spec: + forProvider: + repository: platform + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# A wildcard as the previous identity does not make every new one unchanged. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: from-wildcard + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/grants-update/user-info.yaml b/tests/restrict-github-team-external-identity/grants-update/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-update/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/grants-update/values.yaml b/tests/restrict-github-team-external-identity/grants-update/values.yaml new file mode 100644 index 0000000000..7414f4cbbf --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-update/values.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so each row tests +# a change; "no identity" has to be stated as an empty one. +policies: + - name: restrict-github-team-external-identity + resources: + - name: cleared + values: + request.oldObject: + metadata: + name: cleared + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail" + - name: set-approved + values: + request.oldObject: + metadata: + name: set-approved + namespace: github-config + annotations: + crossplane.io/external-name: "" + - name: changed + values: + request.oldObject: + metadata: + name: changed + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:ksail" + - name: retargeted-team + values: + request.oldObject: + metadata: + name: retargeted-team + namespace: github-config + spec: + forProvider: + teamIdRef: + name: maintainers + - name: retargeted-subject + values: + request.oldObject: + metadata: + name: retargeted-subject + namespace: github-config + spec: + forProvider: + repository: ksail + - name: from-wildcard + values: + request.oldObject: + metadata: + name: from-wildcard + namespace: github-config + annotations: + crossplane.io/external-name: "*" diff --git a/tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml new file mode 100644 index 0000000000..ef7526418e --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-members-create +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/admins-approved + - github-config/maintainers-approved + - github-config/unset + kind: TeamMembership + result: pass + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/swapped-team + - github-config/swapped-team-reverse + - github-config/other-subject + - github-config/foreign-team + - github-config/slug-identity + - github-config/wildcard + - github-config/wildcard-subject + - github-config/longer-subject + - github-config/undeclared-subject + kind: TeamMembership + result: fail diff --git a/tests/restrict-github-team-external-identity/members-create/resources.yaml b/tests/restrict-github-team-external-identity/members-create/resources.yaml new file mode 100644 index 0000000000..5d188e4384 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-create/resources.yaml @@ -0,0 +1,189 @@ +--- +# Recovery after a rebuild: the artifact states the identity the provider would record. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: admins-approved + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The same for the other approved team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: maintainers-approved + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# The usual shape: no identity, the provider creates and records it. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: unset + namespace: github-config +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# References admins but stands for the maintainers team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: swapped-team + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Names one username and stands for another. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: other-subject + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:someone-else" +spec: + forProvider: + username: devantler + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# A team that is not approved at all. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: foreign-team + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The provider also accepts a team slug; only the approved numeric identity is. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: slug-identity + namespace: github-config + annotations: + crossplane.io/external-name: "admins:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# A wildcard is not an identity. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: wildcard + namespace: github-config + annotations: + crossplane.io/external-name: "*" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Nor is a wildcard in place of the username. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: wildcard-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:*" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The declared username is only a prefix of the stored one. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: longer-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler-other" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Nothing declared under forProvider, so nothing can be approved. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: undeclared-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:" +spec: + forProvider: + teamIdRef: + name: admins + policy: + resolve: Always +--- +# References maintainers but stands for the admins team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: swapped-team-reverse + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: maintainers + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/members-create/user-info.yaml b/tests/restrict-github-team-external-identity/members-create/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-create/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/members-create/values.yaml b/tests/restrict-github-team-external-identity/members-create/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-create/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/members-provider/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-provider/kyverno-test.yaml new file mode 100644 index 0000000000..9c076ac760 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-members-provider +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/provider-recorded + kind: TeamMembership + result: skip diff --git a/tests/restrict-github-team-external-identity/members-provider/resources.yaml b/tests/restrict-github-team-external-identity/members-provider/resources.yaml new file mode 100644 index 0000000000..74257f0ae6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider/resources.yaml @@ -0,0 +1,16 @@ +--- +# The provider records whatever GitHub returned; it is exempt. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: provider-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/members-provider/user-info.yaml b/tests/restrict-github-team-external-identity/members-provider/user-info.yaml new file mode 100644 index 0000000000..5d2f307c4d --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: provider +userInfo: + username: system:serviceaccount:crossplane-system:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/members-provider/values.yaml b/tests/restrict-github-team-external-identity/members-provider/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml new file mode 100644 index 0000000000..2d7d78ecbc --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-members-update +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/unchanged-unusual + - github-config/cleared + - github-config/set-approved + kind: TeamMembership + result: pass + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/changed + - github-config/retargeted-team + - github-config/retargeted-subject + - github-config/from-wildcard + kind: TeamMembership + result: fail diff --git a/tests/restrict-github-team-external-identity/members-update/resources.yaml b/tests/restrict-github-team-external-identity/members-update/resources.yaml new file mode 100644 index 0000000000..7a3f250454 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-update/resources.yaml @@ -0,0 +1,110 @@ +--- +# An identity the provider recorded in a shape this rule would not approve is left as it is by the next apply, which must not be refused. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: unchanged-unusual + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler-as-recorded" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Removing the identity makes the provider create the declared one again. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: cleared + namespace: github-config +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Stating the approved identity on an object that had none. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: set-approved + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# The identity moves to another team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: changed + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# The identity stays, the team reference moves to admins. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: retargeted-team + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# The identity stays, the declared username moves. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: retargeted-subject + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:devantler" +spec: + forProvider: + username: someone-else + teamIdRef: + name: maintainers + policy: + resolve: Always +--- +# A wildcard as the previous identity does not make every new one unchanged. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: from-wildcard + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/members-update/user-info.yaml b/tests/restrict-github-team-external-identity/members-update/user-info.yaml new file mode 100644 index 0000000000..00cdd9d2b8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-update/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: github-config-source +userInfo: + username: system:serviceaccount:github-config:github-config + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/members-update/values.yaml b/tests/restrict-github-team-external-identity/members-update/values.yaml new file mode 100644 index 0000000000..086e98d8d2 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-update/values.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE +# kyverno test presents an update with the previous object equal to the new +# one. The values below are merged into the previous object, so each row tests +# a change; "no identity" has to be stated as an empty one. +policies: + - name: restrict-github-team-external-identity + resources: + - name: cleared + values: + request.oldObject: + metadata: + name: cleared + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler" + - name: set-approved + values: + request.oldObject: + metadata: + name: set-approved + namespace: github-config + annotations: + crossplane.io/external-name: "" + - name: changed + values: + request.oldObject: + metadata: + name: changed + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:devantler" + - name: retargeted-team + values: + request.oldObject: + metadata: + name: retargeted-team + namespace: github-config + spec: + forProvider: + teamIdRef: + name: maintainers + - name: retargeted-subject + values: + request.oldObject: + metadata: + name: retargeted-subject + namespace: github-config + spec: + forProvider: + username: devantler + - name: from-wildcard + values: + request.oldObject: + metadata: + name: from-wildcard + namespace: github-config + annotations: + crossplane.io/external-name: "*" From cb3c901f8504b49800e5e7565913abccea33c72f Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 12:23:46 +0200 Subject: [PATCH 07/14] fix(github-config): refuse a stored identity next to an initProvider repository or user Independent review: the rule read only forProvider, so a repository stated under initProvider could be moved while the identity counted as unchanged. No object on prod states one. Also names the way to re-point an object in the refusal, and covers a lookalike service account and provider updates. Co-Authored-By: Claude Opus 5.5 --- ...estrict-github-team-external-identity.yaml | 38 ++++++++++++++----- .../grants-create/kyverno-test.yaml | 2 + .../grants-create/resources.yaml | 35 +++++++++++++++++ .../grants-lookalike/kyverno-test.yaml | 19 ++++++++++ .../grants-lookalike/resources.yaml | 16 ++++++++ .../grants-lookalike/user-info.yaml | 11 ++++++ .../grants-lookalike/values.yaml | 7 ++++ .../grants-provider-update/kyverno-test.yaml | 19 ++++++++++ .../grants-provider-update/resources.yaml | 16 ++++++++ .../grants-provider-update/user-info.yaml | 11 ++++++ .../grants-provider-update/values.yaml | 7 ++++ .../grants-update/kyverno-test.yaml | 1 + .../grants-update/resources.yaml | 17 +++++++++ .../members-create/kyverno-test.yaml | 2 + .../members-create/resources.yaml | 35 +++++++++++++++++ .../members-lookalike/kyverno-test.yaml | 19 ++++++++++ .../members-lookalike/resources.yaml | 16 ++++++++ .../members-lookalike/user-info.yaml | 11 ++++++ .../members-lookalike/values.yaml | 7 ++++ .../members-provider-update/kyverno-test.yaml | 19 ++++++++++ .../members-provider-update/resources.yaml | 16 ++++++++ .../members-provider-update/user-info.yaml | 11 ++++++ .../members-provider-update/values.yaml | 7 ++++ .../members-update/kyverno-test.yaml | 1 + .../members-update/resources.yaml | 17 +++++++++ 25 files changed, 350 insertions(+), 10 deletions(-) create mode 100644 tests/restrict-github-team-external-identity/grants-lookalike/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-lookalike/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-lookalike/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-lookalike/values.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider-update/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider-update/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider-update/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-provider-update/values.yaml create mode 100644 tests/restrict-github-team-external-identity/members-lookalike/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/members-lookalike/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/members-lookalike/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/members-lookalike/values.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider-update/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider-update/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider-update/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/members-provider-update/values.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml index a4199beefd..7c7c815f82 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml @@ -177,6 +177,10 @@ spec: variable: jmesPath: request.object.spec.forProvider.repository default: "" + - name: initSubject + variable: + jmesPath: request.object.spec.initProvider.repository + default: "" - name: previousExternalName variable: jmesPath: request.oldObject.metadata.annotations."crossplane.io/external-name" @@ -198,15 +202,20 @@ spec: The crossplane.io/external-name of a github-config TeamRepository may only be left unset, left unchanged together with its team reference and repository, or set to the approved numeric identity of the referenced - team followed by a colon and spec.forProvider.repository. + team followed by a colon and spec.forProvider.repository. A stored + identity cannot be combined with spec.initProvider.repository. To point an + object at another team or repository, delete it and create it again. deny: conditions: all: # One literal JMESPath expression, for the reason given on the - # Team rule. Every part is compared on its own rather than as one - # joined string, so no value can borrow a separator to look like - # another pair. An empty repository is never an approved identity. - - key: "{{ externalName == '' || (externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))) }}" + # Team rule. The team reference is compared on its own and the + # team identity is a literal prefix, so no value can borrow a + # separator to look like another pair. An empty repository is never an + # approved identity, and one stated under initProvider is not + # read here, so it may not accompany a stored identity at all + # (no object on prod states one: measured 2026-10-05). + - key: "{{ externalName == '' || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" operator: Equals value: false - name: teammemberships-external-name-matches-declared-member @@ -239,6 +248,10 @@ spec: variable: jmesPath: request.object.spec.forProvider.username default: "" + - name: initSubject + variable: + jmesPath: request.object.spec.initProvider.username + default: "" - name: previousExternalName variable: jmesPath: request.oldObject.metadata.annotations."crossplane.io/external-name" @@ -260,14 +273,19 @@ spec: The crossplane.io/external-name of a github-config TeamMembership may only be left unset, left unchanged together with its team reference and username, or set to the approved numeric identity of the referenced - team followed by a colon and spec.forProvider.username. + team followed by a colon and spec.forProvider.username. A stored + identity cannot be combined with spec.initProvider.username. To point an + object at another team or username, delete it and create it again. deny: conditions: all: # One literal JMESPath expression, for the reason given on the - # Team rule. Every part is compared on its own rather than as one - # joined string, so no value can borrow a separator to look like - # another pair. An empty username is never an approved identity. - - key: "{{ externalName == '' || (externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))) }}" + # Team rule. The team reference is compared on its own and the + # team identity is a literal prefix, so no value can borrow a + # separator to look like another pair. An empty username is never an + # approved identity, and one stated under initProvider is not + # read here, so it may not accompany a stored identity at all + # (no object on prod states one: measured 2026-10-05). + - key: "{{ externalName == '' || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" operator: Equals value: false diff --git a/tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml index 28fae5e52f..15f10f0014 100644 --- a/tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml +++ b/tests/restrict-github-team-external-identity/grants-create/kyverno-test.yaml @@ -31,5 +31,7 @@ results: - github-config/wildcard-subject - github-config/longer-subject - github-config/undeclared-subject + - github-config/init-provider-subject + - github-config/init-provider-second-subject kind: TeamRepository result: fail diff --git a/tests/restrict-github-team-external-identity/grants-create/resources.yaml b/tests/restrict-github-team-external-identity/grants-create/resources.yaml index d9c373127c..94ac3c8125 100644 --- a/tests/restrict-github-team-external-identity/grants-create/resources.yaml +++ b/tests/restrict-github-team-external-identity/grants-create/resources.yaml @@ -187,3 +187,38 @@ spec: name: maintainers policy: resolve: Always +--- +# The repository under initProvider is not what this rule reads, so it may not accompany a stored identity. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: init-provider-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail" +spec: + forProvider: + teamIdRef: + name: admins + policy: + resolve: Always + initProvider: + repository: ksail +--- +# Nor next to a declared one. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: init-provider-second-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always + initProvider: + repository: platform diff --git a/tests/restrict-github-team-external-identity/grants-lookalike/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-lookalike/kyverno-test.yaml new file mode 100644 index 0000000000..a153c2a041 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-lookalike/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-grants-lookalike +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/lookalike-recorded + kind: TeamRepository + result: fail diff --git a/tests/restrict-github-team-external-identity/grants-lookalike/resources.yaml b/tests/restrict-github-team-external-identity/grants-lookalike/resources.yaml new file mode 100644 index 0000000000..3ac684961d --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-lookalike/resources.yaml @@ -0,0 +1,16 @@ +--- +# The same service account name in a tenant namespace is not the provider. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: lookalike-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/grants-lookalike/user-info.yaml b/tests/restrict-github-team-external-identity/grants-lookalike/user-info.yaml new file mode 100644 index 0000000000..54c1dbf15c --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-lookalike/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: lookalike +userInfo: + username: system:serviceaccount:github-config:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/grants-lookalike/values.yaml b/tests/restrict-github-team-external-identity/grants-lookalike/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-lookalike/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/grants-provider-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-provider-update/kyverno-test.yaml new file mode 100644 index 0000000000..985dc74f07 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider-update/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-grants-provider-update +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/provider-recorded + kind: TeamRepository + result: skip diff --git a/tests/restrict-github-team-external-identity/grants-provider-update/resources.yaml b/tests/restrict-github-team-external-identity/grants-provider-update/resources.yaml new file mode 100644 index 0000000000..69a55f967c --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider-update/resources.yaml @@ -0,0 +1,16 @@ +--- +# The provider records whatever GitHub returned; it is exempt. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: provider-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/grants-provider-update/user-info.yaml b/tests/restrict-github-team-external-identity/grants-provider-update/user-info.yaml new file mode 100644 index 0000000000..5d2f307c4d --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider-update/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: provider +userInfo: + username: system:serviceaccount:crossplane-system:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/grants-provider-update/values.yaml b/tests/restrict-github-team-external-identity/grants-provider-update/values.yaml new file mode 100644 index 0000000000..dab25f2928 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-provider-update/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE diff --git a/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml index 1d54d73910..34a6fa591a 100644 --- a/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml +++ b/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml @@ -26,5 +26,6 @@ results: - github-config/retargeted-team - github-config/retargeted-subject - github-config/from-wildcard + - github-config/unchanged-init-provider-subject kind: TeamRepository result: fail diff --git a/tests/restrict-github-team-external-identity/grants-update/resources.yaml b/tests/restrict-github-team-external-identity/grants-update/resources.yaml index d841d37ce6..5c0f3a841b 100644 --- a/tests/restrict-github-team-external-identity/grants-update/resources.yaml +++ b/tests/restrict-github-team-external-identity/grants-update/resources.yaml @@ -108,3 +108,20 @@ spec: name: admins policy: resolve: Always +--- +# Unchanged identity, but the repository is stated under initProvider, where it could be moved unseen. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: unchanged-init-provider-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:ksail" +spec: + forProvider: + teamIdRef: + name: admins + policy: + resolve: Always + initProvider: + repository: platform diff --git a/tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml index ef7526418e..24166ab4a1 100644 --- a/tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml +++ b/tests/restrict-github-team-external-identity/members-create/kyverno-test.yaml @@ -31,5 +31,7 @@ results: - github-config/wildcard-subject - github-config/longer-subject - github-config/undeclared-subject + - github-config/init-provider-subject + - github-config/init-provider-second-subject kind: TeamMembership result: fail diff --git a/tests/restrict-github-team-external-identity/members-create/resources.yaml b/tests/restrict-github-team-external-identity/members-create/resources.yaml index 5d188e4384..d85366e708 100644 --- a/tests/restrict-github-team-external-identity/members-create/resources.yaml +++ b/tests/restrict-github-team-external-identity/members-create/resources.yaml @@ -187,3 +187,38 @@ spec: name: maintainers policy: resolve: Always +--- +# The username under initProvider is not what this rule reads, so it may not accompany a stored identity. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: init-provider-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler" +spec: + forProvider: + teamIdRef: + name: admins + policy: + resolve: Always + initProvider: + username: devantler +--- +# Nor next to a declared one. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: init-provider-second-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always + initProvider: + username: someone-else diff --git a/tests/restrict-github-team-external-identity/members-lookalike/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-lookalike/kyverno-test.yaml new file mode 100644 index 0000000000..3c6108e90f --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-lookalike/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-members-lookalike +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/lookalike-recorded + kind: TeamMembership + result: fail diff --git a/tests/restrict-github-team-external-identity/members-lookalike/resources.yaml b/tests/restrict-github-team-external-identity/members-lookalike/resources.yaml new file mode 100644 index 0000000000..c14b122379 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-lookalike/resources.yaml @@ -0,0 +1,16 @@ +--- +# The same service account name in a tenant namespace is not the provider. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: lookalike-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/members-lookalike/user-info.yaml b/tests/restrict-github-team-external-identity/members-lookalike/user-info.yaml new file mode 100644 index 0000000000..54c1dbf15c --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-lookalike/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: lookalike +userInfo: + username: system:serviceaccount:github-config:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:github-config + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/members-lookalike/values.yaml b/tests/restrict-github-team-external-identity/members-lookalike/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-lookalike/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/members-provider-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-provider-update/kyverno-test.yaml new file mode 100644 index 0000000000..14091f38b6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider-update/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-members-provider-update +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/provider-recorded + kind: TeamMembership + result: skip diff --git a/tests/restrict-github-team-external-identity/members-provider-update/resources.yaml b/tests/restrict-github-team-external-identity/members-provider-update/resources.yaml new file mode 100644 index 0000000000..74257f0ae6 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider-update/resources.yaml @@ -0,0 +1,16 @@ +--- +# The provider records whatever GitHub returned; it is exempt. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: provider-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/members-provider-update/user-info.yaml b/tests/restrict-github-team-external-identity/members-provider-update/user-info.yaml new file mode 100644 index 0000000000..5d2f307c4d --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider-update/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: provider +userInfo: + username: system:serviceaccount:crossplane-system:provider-upjet-github-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/members-provider-update/values.yaml b/tests/restrict-github-team-external-identity/members-provider-update/values.yaml new file mode 100644 index 0000000000..dab25f2928 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-provider-update/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: UPDATE diff --git a/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml index 2d7d78ecbc..7989bdfba5 100644 --- a/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml +++ b/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml @@ -26,5 +26,6 @@ results: - github-config/retargeted-team - github-config/retargeted-subject - github-config/from-wildcard + - github-config/unchanged-init-provider-subject kind: TeamMembership result: fail diff --git a/tests/restrict-github-team-external-identity/members-update/resources.yaml b/tests/restrict-github-team-external-identity/members-update/resources.yaml index 7a3f250454..809a3451db 100644 --- a/tests/restrict-github-team-external-identity/members-update/resources.yaml +++ b/tests/restrict-github-team-external-identity/members-update/resources.yaml @@ -108,3 +108,20 @@ spec: name: admins policy: resolve: Always +--- +# Unchanged identity, but the username is stated under initProvider, where it could be moved unseen. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: unchanged-init-provider-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:devantler" +spec: + forProvider: + teamIdRef: + name: admins + policy: + resolve: Always + initProvider: + username: someone-else From c0f4d0236dc9ba3b22f24571cbdf0293a6c8db86 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 12:31:25 +0200 Subject: [PATCH 08/14] docs(github-config): say the initProvider value is not compared Co-Authored-By: Claude Opus 5.5 --- .../restrict-github-team-external-identity.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml index 7c7c815f82..68a64f27a1 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml @@ -213,7 +213,7 @@ spec: # team identity is a literal prefix, so no value can borrow a # separator to look like another pair. An empty repository is never an # approved identity, and one stated under initProvider is not - # read here, so it may not accompany a stored identity at all + # compared here, so it may not accompany a stored identity at all # (no object on prod states one: measured 2026-10-05). - key: "{{ externalName == '' || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" operator: Equals @@ -284,7 +284,7 @@ spec: # team identity is a literal prefix, so no value can borrow a # separator to look like another pair. An empty username is never an # approved identity, and one stated under initProvider is not - # read here, so it may not accompany a stored identity at all + # compared here, so it may not accompany a stored identity at all # (no object on prod states one: measured 2026-10-05). - key: "{{ externalName == '' || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" operator: Equals From 77707f8145fc1db2b4a64d762d7aa370e4cb277e Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 13:20:55 +0200 Subject: [PATCH 09/14] fix(github-config): compare tenant-written text literally in the team rules Kyverno's list operators read * and ? in either operand as wildcards, so a value of * matched every entry of an allow-list. Measured on v1.19.1, 25 such values were admitted by restrict-github-team-management: a team display name, a referenced team, a member login, a role and a repository permission. Every condition is now one JMESPath boolean compared with `Equals false`, so each comparison is exact. A membership's or grant's team reference may also only name the github-config namespace, because the provider resolves the reference in the namespace it names (#4525). Fixes #4517 Fixes #4525 Co-Authored-By: Claude Opus 5.5 --- .../restrict-github-team-management.yaml | 218 +++--- .../literal-comparison/kyverno-test.yaml | 115 +++ .../literal-comparison/resources.yaml | 667 ++++++++++++++++++ 3 files changed, 882 insertions(+), 118 deletions(-) create mode 100644 tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml create mode 100644 tests/restrict-github-team-management/literal-comparison/resources.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml index 3ce08e99de..3a014a17b6 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml @@ -27,6 +27,15 @@ metadata: teams or granting privileged repository access. spec: background: true + # #4517: every value these rules judge is written by the party they + # constrain, so every comparison is LITERAL. Kyverno's list operators (AnyIn, + # AnyNotIn, ...) read * and ? in either operand as wildcards, which lets a + # value of * match any entry of an allow-list: measured on v1.19.1, 25 such + # values were admitted by the rules below. Each condition is therefore one + # JMESPath boolean compared with `Equals false` — JMESPath == and contains() + # compare text exactly. Write a new condition the same way, and never with a + # list operator. not_null() replaces only a missing value with the empty + # string, and to_string() keeps a non-text value from being read as empty. rules: - name: teams-allow-listed match: @@ -42,11 +51,9 @@ spec: deny: conditions: all: - - key: "{{ request.object.metadata.name }}" - operator: AnyNotIn - value: - - admins - - maintainers + - key: "{{ contains(['admins', 'maintainers'], request.object.metadata.name) }}" + operator: Equals + value: false # 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 "admins" @@ -71,16 +78,12 @@ spec: deny: conditions: any: - - key: "{{ to_lower(request.object.spec.forProvider.name || '') }}" - operator: AnyNotIn - value: - - "" - - "{{ to_lower(request.object.metadata.name) }}" - - key: "{{ to_lower(request.object.spec.initProvider.name || '') }}" - operator: AnyNotIn - value: - - "" - - "{{ to_lower(request.object.metadata.name) }}" + - key: "{{ not_null(request.object.spec.forProvider.name, '') == '' || to_lower(to_string(request.object.spec.forProvider.name)) == to_lower(request.object.metadata.name) }}" + operator: Equals + value: false + - key: "{{ not_null(request.object.spec.initProvider.name, '') == '' || to_lower(to_string(request.object.spec.initProvider.name)) == to_lower(request.object.metadata.name) }}" + operator: Equals + value: false # 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 @@ -168,33 +171,38 @@ spec: # writes the resolved ID into that field, so a deny on it fires on the # controller's write and deadlocks the resource. Measured live: all 39 # TeamMembership/TeamRepository resources carry a populated teamId. - - key: "{{ request.object.spec.forProvider.teamIdRef.policy.resolve || '' }}" - operator: NotEquals - value: Always - - key: "{{ request.object.spec.forProvider.teamIdSelector || `{}` }}" - operator: NotEquals - value: {} - - key: "{{ request.object.spec.forProvider.teamIdRef.name || '' }}" - operator: AnyNotIn - value: - - admins - - maintainers + - key: "{{ not_null(request.object.spec.forProvider.teamIdRef.policy.resolve, '') == 'Always' }}" + operator: Equals + value: false + - key: "{{ not_null(request.object.spec.forProvider.teamIdSelector, `{}`) == `{}` }}" + operator: Equals + value: false + - key: "{{ contains(['admins', 'maintainers'], not_null(request.object.spec.forProvider.teamIdRef.name, '')) }}" + operator: Equals + value: false + # #4525: the provider reads the referenced Team from the namespace the + # reference names and falls back to this object's own only when that is + # empty, so a foreign namespace would resolve to a Team object the team + # rules above never judged. + - key: "{{ contains(['', 'github-config'], not_null(request.object.spec.forProvider.teamIdRef.namespace, '')) }}" + operator: Equals + value: false # 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: - - "" - - admins - - maintainers + - key: "{{ not_null(request.object.spec.initProvider.teamId, '') == '' }}" + operator: Equals + value: false + - key: "{{ not_null(request.object.spec.initProvider.teamIdSelector, `{}`) == `{}` }}" + operator: Equals + value: false + - key: "{{ contains(['', 'admins', 'maintainers'], not_null(request.object.spec.initProvider.teamIdRef.name, '')) }}" + operator: Equals + value: false + - key: "{{ contains(['', 'github-config'], not_null(request.object.spec.initProvider.teamIdRef.namespace, '')) }}" + operator: Equals + value: false # #3146: the reference rule above constrains WHICH team is written to and # nothing constrains WHO is written into it. A TeamMembership naming `admins` # with policy.resolve Always, no teamId and no selector satisfies every @@ -235,19 +243,16 @@ spec: # GitHub logins are case-insensitive, so normalise before matching: # without it an allow-listed account spelled with different case is # refused, which is a false denial rather than a bypass but a real one. - - key: "{{ to_lower(request.object.spec.forProvider.username || request.object.spec.initProvider.username || '') }}" - operator: AnyNotIn - value: - - devantler + - key: "{{ contains(['devantler'], to_lower(to_string(request.object.spec.forProvider.username || request.object.spec.initProvider.username || ''))) }}" + operator: Equals + value: false # The initProvider value is constrained on its own as well, so an # approved forProvider username cannot front a foreign account if # the merge semantics ever widen. Same belt-and-braces the # reference conditions above apply to initProvider.teamIdRef. - - key: "{{ to_lower(request.object.spec.initProvider.username || '') }}" - operator: AnyNotIn - value: - - "" - - devantler + - key: "{{ contains(['', 'devantler'], to_lower(to_string(not_null(request.object.spec.initProvider.username, '')))) }}" + operator: Equals + value: false # An allow-list, not a deny-list on `maintainer`: the team role is an # enumeration the provider passes straight through to GitHub, so a value # outside the two documented roles must be refused rather than assumed @@ -284,18 +289,12 @@ spec: # initProvider condition below is what constrains the value the # merge would supply. Folding the fallback in would add a conjunct # no fixture can isolate. - - key: "{{ to_lower(request.object.spec.forProvider.role || '') }}" - operator: AnyNotIn - value: - - "" - - member - - maintainer - - key: "{{ to_lower(request.object.spec.initProvider.role || '') }}" - operator: AnyNotIn - value: - - "" - - member - - maintainer + - key: "{{ contains(['', 'member', 'maintainer'], to_lower(to_string(not_null(request.object.spec.forProvider.role, '')))) }}" + operator: Equals + value: false + - key: "{{ contains(['', 'member', 'maintainer'], to_lower(to_string(not_null(request.object.spec.initProvider.role, '')))) }}" + operator: Equals + value: false - name: teamrepositories-reference-allow-listed-teams match: any: @@ -316,55 +315,48 @@ spec: # writes the resolved ID into that field, so a deny on it fires on the # controller's write and deadlocks the resource. Measured live: all 39 # TeamMembership/TeamRepository resources carry a populated teamId. - - key: "{{ request.object.spec.forProvider.teamIdRef.policy.resolve || '' }}" - operator: NotEquals - value: Always - - key: "{{ request.object.spec.forProvider.teamIdSelector || `{}` }}" - operator: NotEquals - value: {} - - key: "{{ request.object.spec.forProvider.teamIdRef.name || '' }}" - operator: AnyNotIn - value: - - admins - - maintainers + - key: "{{ not_null(request.object.spec.forProvider.teamIdRef.policy.resolve, '') == 'Always' }}" + operator: Equals + value: false + - key: "{{ not_null(request.object.spec.forProvider.teamIdSelector, `{}`) == `{}` }}" + operator: Equals + value: false + - key: "{{ contains(['admins', 'maintainers'], not_null(request.object.spec.forProvider.teamIdRef.name, '')) }}" + operator: Equals + value: false + # #4525: the provider reads the referenced Team from the namespace the + # reference names and falls back to this object's own only when that is + # empty, so a foreign namespace would resolve to a Team object the team + # rules above never judged. + - key: "{{ contains(['', 'github-config'], not_null(request.object.spec.forProvider.teamIdRef.namespace, '')) }}" + operator: Equals + value: false # 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 || '' }}" - operator: AnyNotIn - value: - - "" - - pull - - triage - - push - - maintain - - admin + - key: "{{ contains(['', 'pull', 'triage', 'push', 'maintain', 'admin'], not_null(request.object.spec.forProvider.permission, '')) }}" + operator: Equals + value: false # 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: - - "" - - admins - - maintainers - - key: "{{ request.object.spec.initProvider.permission || '' }}" - operator: AnyNotIn - value: - - "" - - pull - - triage - - push - - maintain - - admin + - key: "{{ not_null(request.object.spec.initProvider.teamId, '') == '' }}" + operator: Equals + value: false + - key: "{{ not_null(request.object.spec.initProvider.teamIdSelector, `{}`) == `{}` }}" + operator: Equals + value: false + - key: "{{ contains(['', 'admins', 'maintainers'], not_null(request.object.spec.initProvider.teamIdRef.name, '')) }}" + operator: Equals + value: false + - key: "{{ contains(['', 'github-config'], not_null(request.object.spec.initProvider.teamIdRef.namespace, '')) }}" + operator: Equals + value: false + - key: "{{ contains(['', 'pull', 'triage', 'push', 'maintain', 'admin'], not_null(request.object.spec.initProvider.permission, '')) }}" + operator: Equals + value: false # #3208: the privilege ceiling is per-team, not flat. A single cap cannot express the # intent — capping everything at `push` denies all 37 live grants, while widening it to # `admin` lets the maintainers team be granted `admin` and makes the rule vacuous. @@ -390,28 +382,18 @@ spec: # actually reconciled is forProvider's when set and initProvider's otherwise. # Keying this precondition on forProvider alone let an initProvider-only # maintainers reference skip the rule entirely and take admin. - - key: "{{ request.object.spec.forProvider.teamIdRef.name || request.object.spec.initProvider.teamIdRef.name || '' }}" + - key: "{{ (request.object.spec.forProvider.teamIdRef.name || request.object.spec.initProvider.teamIdRef.name || '') == 'maintainers' }}" operator: Equals - value: maintainers + value: true validate: failureAction: Enforce message: GitHub TeamRepository resources referencing the maintainers team may grant at most maintain; admin is reserved to the admins team. deny: conditions: any: - - key: "{{ request.object.spec.forProvider.permission || '' }}" - operator: AnyNotIn - value: - - "" - - pull - - triage - - push - - maintain - - key: "{{ request.object.spec.initProvider.permission || '' }}" - operator: AnyNotIn - value: - - "" - - pull - - triage - - push - - maintain + - key: "{{ contains(['', 'pull', 'triage', 'push', 'maintain'], not_null(request.object.spec.forProvider.permission, '')) }}" + operator: Equals + value: false + - key: "{{ contains(['', 'pull', 'triage', 'push', 'maintain'], not_null(request.object.spec.initProvider.permission, '')) }}" + operator: Equals + value: false diff --git a/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml new file mode 100644 index 0000000000..9db0b03e49 --- /dev/null +++ b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml @@ -0,0 +1,115 @@ +--- +# #4517, #4525: every value these rules compare is written by the party they +# constrain, so each comparison has to be literal. Kyverno's list and equality +# operators read * and ? in either operand as wildcards; a value of * then +# matches every entry of an allow-list. Each resource here is on the paved road +# except for the one field its name says, so a row can only fail on the +# condition that judges that field. +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-management-literal-comparison +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +resources: + - resources.yaml +results: + - policy: restrict-github-team-management + rule: teams-bind-provider-identity-to-object-name + resources: + - github-config/display-name-star + - github-config/display-name-question + - github-config/init-display-name-star + - github-config/init-display-name-question + kind: Team + result: fail + - policy: restrict-github-team-management + rule: teammemberships-reference-allow-listed-teams + resources: + - github-config/ref-name-star + - github-config/ref-name-question + - github-config/resolve-star + - github-config/resolve-question + - github-config/init-team-id-star + - github-config/init-team-id-question + - github-config/init-ref-name-star + - github-config/init-ref-name-question + # #4525: the provider resolves a reference in the namespace the reference + # names, so a Team object outside github-config is one these rules never + # judged. + - github-config/ref-namespace-foreign + - github-config/ref-namespace-star + - github-config/ref-namespace-question + - github-config/init-ref-namespace-foreign + # A selector resolves to whichever Team carries the labels, so it is + # refused even beside an allow-listed reference. + - github-config/selector-beside-reference + - github-config/init-selector-beside-reference + kind: TeamMembership + result: fail + # Naming github-config itself is the same reference as leaving it out. + - policy: restrict-github-team-management + rule: teammemberships-reference-allow-listed-teams + resources: + - github-config/ref-namespace-own + kind: TeamMembership + result: pass + - policy: restrict-github-team-management + rule: teammemberships-allow-listed-members + resources: + - github-config/username-star + - github-config/username-question + - github-config/init-username-star + - github-config/init-only-username-star + kind: TeamMembership + result: fail + - policy: restrict-github-team-management + rule: teammemberships-known-roles + resources: + - github-config/role-star + - github-config/role-question + - github-config/init-role-star + kind: TeamMembership + result: fail + - policy: restrict-github-team-management + rule: teamrepositories-reference-allow-listed-teams + resources: + - github-config/repo-ref-name-star + - github-config/repo-ref-name-question + - github-config/repo-resolve-star + - github-config/repo-init-team-id-star + - github-config/repo-init-ref-name-star + - github-config/repo-ref-namespace-foreign + - github-config/repo-ref-namespace-star + - github-config/repo-init-ref-namespace-foreign + - github-config/permission-star + - github-config/permission-question + - github-config/init-permission-star + - github-config/capped-wildcard-team-admin + - github-config/repo-selector-beside-reference + - github-config/repo-init-selector-beside-reference + kind: TeamRepository + result: fail + - policy: restrict-github-team-management + rule: teamrepositories-reference-allow-listed-teams + resources: + - github-config/repo-ref-namespace-own + kind: TeamRepository + result: pass + - policy: restrict-github-team-management + rule: teamrepositories-maintainers-capped-below-admin + resources: + - github-config/capped-permission-star + - github-config/capped-permission-question + - github-config/capped-init-permission-star + kind: TeamRepository + result: fail + # A reference that is not literally `maintainers` is not the maintainers + # team: the ceiling leaves it alone and the reference rule above refuses it. + - policy: restrict-github-team-management + rule: teamrepositories-maintainers-capped-below-admin + resources: + - github-config/capped-wildcard-team-admin + kind: TeamRepository + result: skip diff --git a/tests/restrict-github-team-management/literal-comparison/resources.yaml b/tests/restrict-github-team-management/literal-comparison/resources.yaml new file mode 100644 index 0000000000..de28cb5b8d --- /dev/null +++ b/tests/restrict-github-team-management/literal-comparison/resources.yaml @@ -0,0 +1,667 @@ +# Every object is on the paved road (allow-listed reference that re-resolves, +# allow-listed member, known role, permitted grant) except for the single field +# its name says. The * and ? values are the ones a list operator would read as +# wildcards; the namespace rows are #4525. +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: display-name-star + namespace: github-config +spec: + forProvider: + privacy: closed + name: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: display-name-question + namespace: github-config +spec: + forProvider: + privacy: closed + name: "display-name-questio?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: init-display-name-star + namespace: github-config +spec: + forProvider: + privacy: closed + initProvider: + name: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: Team +metadata: + name: init-display-name-question + namespace: github-config +spec: + forProvider: + privacy: closed + initProvider: + name: "init-display-name-questio?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: ref-name-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "*" + policy: + resolve: "Always" + username: devantler + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: ref-name-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admin?" + policy: + resolve: "Always" + username: devantler + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: resolve-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "*" + username: devantler + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: resolve-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Alway?" + username: devantler + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-team-id-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + teamId: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-team-id-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + teamId: "?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-ref-name-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + teamIdRef: + name: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-ref-name-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + teamIdRef: + name: "admin?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: ref-namespace-foreign + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "other-namespace" + username: devantler + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: ref-namespace-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "*" + username: devantler + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: ref-namespace-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "github-confi?" + username: devantler + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-ref-namespace-foreign + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + teamIdRef: + name: admins + namespace: other-namespace +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: ref-namespace-own + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "github-config" + username: devantler + role: member + initProvider: + teamIdRef: + name: admins + namespace: github-config +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: username-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: "*" + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: username-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: "d?vantler" + role: member +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-username-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + username: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-only-username-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + role: member + initProvider: + username: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: role-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: role-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: "membe?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-role-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + role: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: selector-beside-reference + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + teamIdSelector: + matchLabels: + team: admins +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: init-selector-beside-reference + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + teamIdSelector: + matchLabels: + team: admins +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-ref-name-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "*" + policy: + resolve: "Always" + repository: platform + permission: push +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-ref-name-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "maintainer?" + policy: + resolve: "Always" + repository: platform + permission: push +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-resolve-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "*" + repository: platform + permission: push +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-init-team-id-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: push + initProvider: + teamId: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-init-ref-name-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: push + initProvider: + teamIdRef: + name: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-ref-namespace-foreign + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "other-namespace" + repository: platform + permission: push +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-ref-namespace-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "*" + repository: platform + permission: push +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-init-ref-namespace-foreign + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: push + initProvider: + teamIdRef: + name: admins + namespace: other-namespace +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-ref-namespace-own + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "github-config" + repository: platform + permission: push + initProvider: + teamIdRef: + name: admins + namespace: github-config +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: permission-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: permission-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: "pus?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: init-permission-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: push + initProvider: + permission: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: capped-permission-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "maintainers" + policy: + resolve: "Always" + repository: platform + permission: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: capped-permission-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "maintainers" + policy: + resolve: "Always" + repository: platform + permission: "maintai?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: capped-init-permission-star + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "maintainers" + policy: + resolve: "Always" + repository: platform + permission: push + initProvider: + permission: "*" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: capped-wildcard-team-admin + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "maintainer?" + policy: + resolve: "Always" + repository: platform + permission: admin +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-selector-beside-reference + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: push + teamIdSelector: + matchLabels: + team: admins +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-init-selector-beside-reference + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + repository: platform + permission: push + initProvider: + teamIdSelector: + matchLabels: + team: admins From a901a6c9b65f67a57eeb55e19aa31e2d18ebe776 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 13:31:18 +0200 Subject: [PATCH 10/14] fix(github-config): say the namespace limit in the deny messages and cover an empty one Co-Authored-By: Claude Opus 5.5 --- .../restrict-github-team-management.yaml | 7 ++++--- .../literal-comparison/kyverno-test.yaml | 13 ++++++++----- .../literal-comparison/resources.yaml | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml index 3a014a17b6..2825d96278 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml @@ -20,7 +20,8 @@ metadata: policies.kyverno.io/description: >- Restricts provider-upjet-github team resources in the github-config namespace to the github-config CODEOWNERS teams, requires TeamMembership and - TeamRepository resources to reference those Team objects by name, and + TeamRepository resources to reference those Team objects by name inside + the github-config namespace, and blocks repository admin grants. A Team may not be nested under a parent team or linked to a directory group. This prevents a compromised or unintended github-config artifact from adding arbitrary users to arbitrary GitHub @@ -161,7 +162,7 @@ spec: - github-config validate: failureAction: Enforce - message: GitHub TeamMembership resources must set spec.forProvider.teamIdRef.name to admins or maintainers with policy.resolve Always, so the reference re-resolves on every reconcile; selectors are not allowed, and initProvider must not carry a teamId, selector or non-allow-listed reference. + message: GitHub TeamMembership resources must set spec.forProvider.teamIdRef.name to admins or maintainers with policy.resolve Always, so the reference re-resolves on every reconcile; selectors are not allowed, a reference may name no namespace other than github-config, and initProvider must not carry a teamId, selector or non-allow-listed reference. deny: conditions: any: @@ -305,7 +306,7 @@ spec: - github-config validate: failureAction: Enforce - message: GitHub TeamRepository resources must reference admins or maintainers by spec.forProvider.teamIdRef.name with policy.resolve Always, and may grant only pull, triage or push permission, in forProvider and initProvider. + message: GitHub TeamRepository resources must reference admins or maintainers by spec.forProvider.teamIdRef.name with policy.resolve Always, may name no namespace other than github-config in that reference, and may grant only pull, triage or push permission, in forProvider and initProvider. deny: conditions: any: diff --git a/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml index 9db0b03e49..90b2230858 100644 --- a/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml +++ b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml @@ -1,10 +1,12 @@ --- # #4517, #4525: every value these rules compare is written by the party they -# constrain, so each comparison has to be literal. Kyverno's list and equality -# operators read * and ? in either operand as wildcards; a value of * then -# matches every entry of an allow-list. Each resource here is on the paved road -# except for the one field its name says, so a row can only fail on the -# condition that judges that field. +# constrain, so each comparison has to be literal. Kyverno's list operators +# read * and ? in either operand as wildcards; a value of * then matches every +# entry of an allow-list. Its equality operators read only the policy's own +# side that way, so the resolve and teamId rows were refused before as well and +# are here to keep it so. Each resource is on the paved road except for the one +# field its name says, so a row fails on what judges that field: one condition, +# or for init-only-username-star the two that both read the merged username. apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: @@ -53,6 +55,7 @@ results: rule: teammemberships-reference-allow-listed-teams resources: - github-config/ref-namespace-own + - github-config/ref-namespace-empty kind: TeamMembership result: pass - policy: restrict-github-team-management diff --git a/tests/restrict-github-team-management/literal-comparison/resources.yaml b/tests/restrict-github-team-management/literal-comparison/resources.yaml index de28cb5b8d..9525c0a2ee 100644 --- a/tests/restrict-github-team-management/literal-comparison/resources.yaml +++ b/tests/restrict-github-team-management/literal-comparison/resources.yaml @@ -665,3 +665,22 @@ spec: teamIdSelector: matchLabels: team: admins +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership +metadata: + name: ref-namespace-empty + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "" + username: devantler + role: member + initProvider: + teamIdRef: + name: admins + namespace: "" From 55a2f03a9c5e1a87becd7eab3a155a2ff1d39b17 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 13:42:18 +0200 Subject: [PATCH 11/14] fix(github-config): name the permissions the grant rule really admits Co-Authored-By: Claude Opus 5.5 --- .../restrict-github-team-management.yaml | 2 +- .../literal-comparison/kyverno-test.yaml | 1 + .../literal-comparison/resources.yaml | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml index 2825d96278..f88b677b21 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-management.yaml @@ -306,7 +306,7 @@ spec: - github-config validate: failureAction: Enforce - message: GitHub TeamRepository resources must reference admins or maintainers by spec.forProvider.teamIdRef.name with policy.resolve Always, may name no namespace other than github-config in that reference, and may grant only pull, triage or push permission, in forProvider and initProvider. + message: GitHub TeamRepository resources must reference admins or maintainers by spec.forProvider.teamIdRef.name with policy.resolve Always, may name no namespace other than github-config in that reference, and may grant only the pull, triage, push, maintain or admin permission, in forProvider and initProvider; the maintainers team is capped below admin by a separate rule. deny: conditions: any: diff --git a/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml index 90b2230858..4c355ceaac 100644 --- a/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml +++ b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml @@ -98,6 +98,7 @@ results: rule: teamrepositories-reference-allow-listed-teams resources: - github-config/repo-ref-namespace-own + - github-config/repo-ref-namespace-empty kind: TeamRepository result: pass - policy: restrict-github-team-management diff --git a/tests/restrict-github-team-management/literal-comparison/resources.yaml b/tests/restrict-github-team-management/literal-comparison/resources.yaml index 9525c0a2ee..108970ee08 100644 --- a/tests/restrict-github-team-management/literal-comparison/resources.yaml +++ b/tests/restrict-github-team-management/literal-comparison/resources.yaml @@ -684,3 +684,22 @@ spec: teamIdRef: name: admins namespace: "" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamRepository +metadata: + name: repo-ref-namespace-empty + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + namespace: "" + repository: platform + permission: push + initProvider: + teamIdRef: + name: admins + namespace: "" From c27f534443fc37e59a899849505c220156bff6bb Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 16:22:00 +0200 Subject: [PATCH 12/14] test(github-config): cover initProvider role question wildcard --- .../literal-comparison/kyverno-test.yaml | 1 + .../literal-comparison/resources.yaml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml index 4c355ceaac..94d8685250 100644 --- a/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml +++ b/tests/restrict-github-team-management/literal-comparison/kyverno-test.yaml @@ -73,6 +73,7 @@ results: - github-config/role-star - github-config/role-question - github-config/init-role-star + - github-config/init-role-question kind: TeamMembership result: fail - policy: restrict-github-team-management diff --git a/tests/restrict-github-team-management/literal-comparison/resources.yaml b/tests/restrict-github-team-management/literal-comparison/resources.yaml index 108970ee08..6cd38ac0a4 100644 --- a/tests/restrict-github-team-management/literal-comparison/resources.yaml +++ b/tests/restrict-github-team-management/literal-comparison/resources.yaml @@ -354,6 +354,22 @@ spec: --- apiVersion: team.github.m.upbound.io/v1beta1 kind: TeamMembership +metadata: + name: init-role-question + namespace: github-config +spec: + forProvider: + teamIdRef: + name: "admins" + policy: + resolve: "Always" + username: devantler + role: member + initProvider: + role: "membe?" +--- +apiVersion: team.github.m.upbound.io/v1beta1 +kind: TeamMembership metadata: name: selector-beside-reference namespace: github-config From a88c4f670ff019bb04d19774295db9d92e13096b Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 6 Oct 2026 00:17:53 +0200 Subject: [PATCH 13/14] ci: re-run code scanning after the cancelled analysis The GitHub Actions incident on 5 October cancelled this head's code scanning runs, and those runs cannot be retried. No file changes. Co-Authored-By: Claude Opus 5.5 From 413b8b2038d87e30587245f1e137306d87bbdc53 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 6 Oct 2026 12:21:31 +0200 Subject: [PATCH 14/14] fix(github-config): refuse removing a stored identity while retargeting the object Removing the stored identity was accepted unconditionally, so one write could clear it and point the membership or grant at another team or subject, after which the provider may record the old identity again on an object that now declares something else. Removal is now accepted only while the team reference and subject stay as they were. Adds fixtures that fail without the rule, and one for another service account in the provider's namespace so widening the exemption is caught. Co-Authored-By: Claude Opus 5.5 --- ...estrict-github-team-external-identity.yaml | 12 +++---- .../grants-other-account/kyverno-test.yaml | 19 ++++++++++++ .../grants-other-account/resources.yaml | 16 ++++++++++ .../grants-other-account/user-info.yaml | 11 +++++++ .../grants-other-account/values.yaml | 7 +++++ .../grants-update/kyverno-test.yaml | 2 ++ .../grants-update/resources.yaml | 30 ++++++++++++++++++ .../grants-update/values.yaml | 31 +++++++++++++++++++ .../members-other-account/kyverno-test.yaml | 19 ++++++++++++ .../members-other-account/resources.yaml | 16 ++++++++++ .../members-other-account/user-info.yaml | 11 +++++++ .../members-other-account/values.yaml | 7 +++++ .../members-update/kyverno-test.yaml | 2 ++ .../members-update/resources.yaml | 30 ++++++++++++++++++ .../members-update/values.yaml | 31 +++++++++++++++++++ 15 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 tests/restrict-github-team-external-identity/grants-other-account/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-other-account/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-other-account/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/grants-other-account/values.yaml create mode 100644 tests/restrict-github-team-external-identity/members-other-account/kyverno-test.yaml create mode 100644 tests/restrict-github-team-external-identity/members-other-account/resources.yaml create mode 100644 tests/restrict-github-team-external-identity/members-other-account/user-info.yaml create mode 100644 tests/restrict-github-team-external-identity/members-other-account/values.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml index 68a64f27a1..f1073265ac 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml @@ -200,8 +200,8 @@ spec: allowExistingViolations: false message: >- The crossplane.io/external-name of a github-config TeamRepository may - only be left unset, left unchanged together with its team reference - and repository, or set to the approved numeric identity of the referenced + only be left unset, removed or left unchanged while its team reference + and repository stay as they were, or set to the approved numeric identity of the referenced team followed by a colon and spec.forProvider.repository. A stored identity cannot be combined with spec.initProvider.repository. To point an object at another team or repository, delete it and create it again. @@ -215,7 +215,7 @@ spec: # approved identity, and one stated under initProvider is not # compared here, so it may not accompany a stored identity at all # (no object on prod states one: measured 2026-10-05). - - key: "{{ externalName == '' || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" + - key: "{{ (externalName == '' && (previousExternalName == '' || (teamRef == previousTeamRef && subject == previousSubject))) || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" operator: Equals value: false - name: teammemberships-external-name-matches-declared-member @@ -271,8 +271,8 @@ spec: allowExistingViolations: false message: >- The crossplane.io/external-name of a github-config TeamMembership may - only be left unset, left unchanged together with its team reference - and username, or set to the approved numeric identity of the referenced + only be left unset, removed or left unchanged while its team reference + and username stay as they were, or set to the approved numeric identity of the referenced team followed by a colon and spec.forProvider.username. A stored identity cannot be combined with spec.initProvider.username. To point an object at another team or username, delete it and create it again. @@ -286,6 +286,6 @@ spec: # approved identity, and one stated under initProvider is not # compared here, so it may not accompany a stored identity at all # (no object on prod states one: measured 2026-10-05). - - key: "{{ externalName == '' || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" + - key: "{{ (externalName == '' && (previousExternalName == '' || (teamRef == previousTeamRef && subject == previousSubject))) || (initSubject == '' && ((externalName == previousExternalName && teamRef == previousTeamRef && subject == previousSubject) || (subject != '' && ((teamRef == 'admins' && externalName == join('', ['18573307:', subject])) || (teamRef == 'maintainers' && externalName == join('', ['17046606:', subject])))))) }}" operator: Equals value: false diff --git a/tests/restrict-github-team-external-identity/grants-other-account/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-other-account/kyverno-test.yaml new file mode 100644 index 0000000000..f19b44ae8c --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-other-account/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-grants-other-account +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teamrepositories-external-name-matches-declared-grant + resources: + - github-config/lookalike-recorded + kind: TeamRepository + result: fail diff --git a/tests/restrict-github-team-external-identity/grants-other-account/resources.yaml b/tests/restrict-github-team-external-identity/grants-other-account/resources.yaml new file mode 100644 index 0000000000..3ac684961d --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-other-account/resources.yaml @@ -0,0 +1,16 @@ +--- +# The same service account name in a tenant namespace is not the provider. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: lookalike-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:ksail" +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/grants-other-account/user-info.yaml b/tests/restrict-github-team-external-identity/grants-other-account/user-info.yaml new file mode 100644 index 0000000000..c3400502e8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-other-account/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: other-account +userInfo: + username: system:serviceaccount:crossplane-system:provider-upjet-aws-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/grants-other-account/values.yaml b/tests/restrict-github-team-external-identity/grants-other-account/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/grants-other-account/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml index 34a6fa591a..da51acbfab 100644 --- a/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml +++ b/tests/restrict-github-team-external-identity/grants-update/kyverno-test.yaml @@ -27,5 +27,7 @@ results: - github-config/retargeted-subject - github-config/from-wildcard - github-config/unchanged-init-provider-subject + - github-config/cleared-retargeted-team + - github-config/cleared-retargeted-subject kind: TeamRepository result: fail diff --git a/tests/restrict-github-team-external-identity/grants-update/resources.yaml b/tests/restrict-github-team-external-identity/grants-update/resources.yaml index 5c0f3a841b..fbe4dc08dc 100644 --- a/tests/restrict-github-team-external-identity/grants-update/resources.yaml +++ b/tests/restrict-github-team-external-identity/grants-update/resources.yaml @@ -125,3 +125,33 @@ spec: resolve: Always initProvider: repository: platform +--- +# Removing the stored identity while pointing the object somewhere else: the provider +# may write the old identity back onto an object that now declares another team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: cleared-retargeted-team + namespace: github-config +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Removing the stored identity while pointing the object somewhere else: the provider +# may write the old identity back onto an object that now declares another subject. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamRepository +metadata: + name: cleared-retargeted-subject + namespace: github-config +spec: + forProvider: + repository: ksail + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/grants-update/values.yaml b/tests/restrict-github-team-external-identity/grants-update/values.yaml index 7414f4cbbf..c4382c866e 100644 --- a/tests/restrict-github-team-external-identity/grants-update/values.yaml +++ b/tests/restrict-github-team-external-identity/grants-update/values.yaml @@ -19,6 +19,11 @@ policies: namespace: github-config annotations: crossplane.io/external-name: "18573307:ksail" + spec: + forProvider: + repository: ksail + teamIdRef: + name: admins - name: set-approved values: request.oldObject: @@ -62,3 +67,29 @@ policies: namespace: github-config annotations: crossplane.io/external-name: "*" + - name: cleared-retargeted-team + values: + request.oldObject: + metadata: + name: cleared-retargeted-team + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:ksail" + spec: + forProvider: + repository: ksail + teamIdRef: + name: maintainers + - name: cleared-retargeted-subject + values: + request.oldObject: + metadata: + name: cleared-retargeted-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:platform" + spec: + forProvider: + repository: platform + teamIdRef: + name: admins diff --git a/tests/restrict-github-team-external-identity/members-other-account/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-other-account/kyverno-test.yaml new file mode 100644 index 0000000000..9a436b5f26 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-other-account/kyverno-test.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-github-team-external-identity-members-other-account +policies: + - >- + ../../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-github-team-external-identity.yaml +resources: + - resources.yaml +variables: values.yaml +userinfo: user-info.yaml +results: + - policy: restrict-github-team-external-identity + rule: teammemberships-external-name-matches-declared-member + resources: + - github-config/lookalike-recorded + kind: TeamMembership + result: fail diff --git a/tests/restrict-github-team-external-identity/members-other-account/resources.yaml b/tests/restrict-github-team-external-identity/members-other-account/resources.yaml new file mode 100644 index 0000000000..c14b122379 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-other-account/resources.yaml @@ -0,0 +1,16 @@ +--- +# The same service account name in a tenant namespace is not the provider. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: lookalike-recorded + namespace: github-config + annotations: + crossplane.io/external-name: "99999999:devantler" +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/members-other-account/user-info.yaml b/tests/restrict-github-team-external-identity/members-other-account/user-info.yaml new file mode 100644 index 0000000000..c3400502e8 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-other-account/user-info.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: UserInfo +metadata: + name: other-account +userInfo: + username: system:serviceaccount:crossplane-system:provider-upjet-aws-f5d83a3e04ea + groups: + - system:serviceaccounts + - system:serviceaccounts:crossplane-system + - system:authenticated diff --git a/tests/restrict-github-team-external-identity/members-other-account/values.yaml b/tests/restrict-github-team-external-identity/members-other-account/values.yaml new file mode 100644 index 0000000000..82b86be385 --- /dev/null +++ b/tests/restrict-github-team-external-identity/members-other-account/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +metadata: + name: values +globalValues: + request.operation: CREATE diff --git a/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml b/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml index 7989bdfba5..74dcb855b3 100644 --- a/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml +++ b/tests/restrict-github-team-external-identity/members-update/kyverno-test.yaml @@ -27,5 +27,7 @@ results: - github-config/retargeted-subject - github-config/from-wildcard - github-config/unchanged-init-provider-subject + - github-config/cleared-retargeted-team + - github-config/cleared-retargeted-subject kind: TeamMembership result: fail diff --git a/tests/restrict-github-team-external-identity/members-update/resources.yaml b/tests/restrict-github-team-external-identity/members-update/resources.yaml index 809a3451db..8ba0b67379 100644 --- a/tests/restrict-github-team-external-identity/members-update/resources.yaml +++ b/tests/restrict-github-team-external-identity/members-update/resources.yaml @@ -125,3 +125,33 @@ spec: resolve: Always initProvider: username: someone-else +--- +# Removing the stored identity while pointing the object somewhere else: the provider +# may write the old identity back onto an object that now declares another team. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: cleared-retargeted-team + namespace: github-config +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always +--- +# Removing the stored identity while pointing the object somewhere else: the provider +# may write the old identity back onto an object that now declares another subject. +apiVersion: team.github.m.upbound.io/v1alpha1 +kind: TeamMembership +metadata: + name: cleared-retargeted-subject + namespace: github-config +spec: + forProvider: + username: devantler + teamIdRef: + name: admins + policy: + resolve: Always diff --git a/tests/restrict-github-team-external-identity/members-update/values.yaml b/tests/restrict-github-team-external-identity/members-update/values.yaml index 086e98d8d2..f752c84f3b 100644 --- a/tests/restrict-github-team-external-identity/members-update/values.yaml +++ b/tests/restrict-github-team-external-identity/members-update/values.yaml @@ -19,6 +19,11 @@ policies: namespace: github-config annotations: crossplane.io/external-name: "18573307:devantler" + spec: + forProvider: + username: devantler + teamIdRef: + name: admins - name: set-approved values: request.oldObject: @@ -62,3 +67,29 @@ policies: namespace: github-config annotations: crossplane.io/external-name: "*" + - name: cleared-retargeted-team + values: + request.oldObject: + metadata: + name: cleared-retargeted-team + namespace: github-config + annotations: + crossplane.io/external-name: "17046606:devantler" + spec: + forProvider: + username: devantler + teamIdRef: + name: maintainers + - name: cleared-retargeted-subject + values: + request.oldObject: + metadata: + name: cleared-retargeted-subject + namespace: github-config + annotations: + crossplane.io/external-name: "18573307:someone-else" + spec: + forProvider: + username: someone-else + teamIdRef: + name: admins