From 464bcd9e1e5676531dab613fcd86cce727bd3efb Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 10:19:17 +0200 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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: