From bad2c4667df8eda837f7ca7167c9c8390cab9955 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Tue, 9 Jun 2026 18:37:10 -0500 Subject: [PATCH 1/6] feat: add opt-in Istio JWT auth --- .github/workflows/on-pr.yaml | 3 +- .github/workflows/on-push-main.yaml | 3 +- Makefile | 3 +- README.md | 29 +- apis/gitkbs/definition.yaml | 61 +++- examples/gitkbs/with-istio-jwt.yaml | 26 ++ functions/render/000-state-init.yaml.gotmpl | 42 ++- functions/render/010-state-status.yaml.gotmpl | 59 +++- functions/render/100-namespace.yaml.gotmpl | 2 +- .../render/200-helm-release-gitkb.yaml.gotmpl | 3 + .../render/450-auth-istio-jwt.yaml.gotmpl | 231 +++++++++++++++ functions/render/999-status.yaml.gotmpl | 8 + tests/test-render/main.k | 280 +++++++++++++++++- 13 files changed, 737 insertions(+), 13 deletions(-) create mode 100644 examples/gitkbs/with-istio-jwt.yaml create mode 100644 functions/render/450-auth-istio-jwt.yaml.gotmpl diff --git a/.github/workflows/on-pr.yaml b/.github/workflows/on-pr.yaml index 928b848..248167b 100644 --- a/.github/workflows/on-pr.yaml +++ b/.github/workflows/on-pr.yaml @@ -31,7 +31,8 @@ jobs: examples: | [ { "example": "examples/gitkbs/minimal.yaml" }, - { "example": "examples/gitkbs/standard.yaml" } + { "example": "examples/gitkbs/standard.yaml" }, + { "example": "examples/gitkbs/with-istio-jwt.yaml" } ] api_path: apis/gitkbs error_on_missing_schemas: true diff --git a/.github/workflows/on-push-main.yaml b/.github/workflows/on-push-main.yaml index 4a78f9a..1f5af97 100644 --- a/.github/workflows/on-push-main.yaml +++ b/.github/workflows/on-push-main.yaml @@ -27,7 +27,8 @@ jobs: examples: | [ { "example": "examples/gitkbs/minimal.yaml" }, - { "example": "examples/gitkbs/standard.yaml" } + { "example": "examples/gitkbs/standard.yaml" }, + { "example": "examples/gitkbs/with-istio-jwt.yaml" } ] api_path: apis/gitkbs error_on_missing_schemas: true diff --git a/Makefile b/Makefile index 4c31c26..8d6a8b8 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,8 @@ generate-configuration: EXAMPLES := \ examples/gitkbs/minimal.yaml:: \ - examples/gitkbs/standard.yaml:: + examples/gitkbs/standard.yaml:: \ + examples/gitkbs/with-istio-jwt.yaml:: render\:all: @tmpdir=$$(mktemp -d); \ diff --git a/README.md b/README.md index 35015ee..e04c971 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,30 @@ spec: kind: ClusterIssuer ``` -Authentication is intentionally not part of this pass. Public installs should add Gateway/OIDC policy before exposing writable GitKB sync traffic beyond trusted networks. +For Istio ambient clusters, enable service-level JWT enforcement with `auth.istioJwt`. +This renders: + +- ambient enrollment on the GitKB namespace +- waypoint labels on the chart-owned GitKB Service +- an Istio waypoint `Gateway` +- service-targeted `RequestAuthentication` +- service-targeted `AuthorizationPolicy` requiring a valid JWT + +```yaml +spec: + auth: + istioJwt: + enabled: true + issuer: https://auth.ops.com.ai + jwksUri: https://auth.ops.com.ai/oauth/v2/keys + audiences: + - "373410628280264299" +``` + +This protects the workload for requests that carry a valid bearer token. It does +not configure GitKB CLI token acquisition or map a sync host to a separate OIDC +issuer; keep that client-side requirement in mind before enabling auth on an +existing sync remote. ## Import Existing @@ -129,6 +152,7 @@ The XR publishes the operational fields needed by downstream automation: - `status.exposure.url` - full public URL for the GitKB remote. - `status.exposure.routeReady` - composed HTTPRoute readiness. - `status.exposure.certificateReady` - composed Certificate readiness when enabled. +- `status.auth.istioJwt` - whether Istio JWT auth rendered and whether the waypoint and policies are ready. ## Composed Resources @@ -136,6 +160,9 @@ The XR publishes the operational fields needed by downstream automation: - `kubernetes.m.crossplane.io/Object` Namespace - creates the target namespace. - `kubernetes.m.crossplane.io/Object` HTTPRoute - optional Gateway API route when `exposure.enabled` is true. - `kubernetes.m.crossplane.io/Object` Certificate - optional cert-manager Certificate when `exposure.certificate.enabled` is true. +- `kubernetes.m.crossplane.io/Object` Gateway - optional Istio waypoint when `auth.istioJwt.enabled` and `issuer` are set. +- `kubernetes.m.crossplane.io/Object` RequestAuthentication - optional JWT validation policy when `auth.istioJwt.enabled` and `issuer` are set. +- `kubernetes.m.crossplane.io/Object` AuthorizationPolicy - optional valid-JWT requirement when `auth.istioJwt.enabled` and `issuer` are set. - `protection.crossplane.io/Usage` - protects dependency deletion order once resources are ready. ## Development diff --git a/apis/gitkbs/definition.yaml b/apis/gitkbs/definition.yaml index 6126c8f..92557c3 100644 --- a/apis/gitkbs/definition.yaml +++ b/apis/gitkbs/definition.yaml @@ -68,7 +68,7 @@ spec: description: Helm release name for the gitkb-server chart. Defaults to metadata.name. type: string chartVersion: - description: Version of the gitkb-server chart. Defaults to "0.1.0". + description: Version of the gitkb-server chart. Defaults to "0.2.0". type: string gitkb: description: GitKB runtime settings passed to the chart. @@ -165,7 +165,7 @@ spec: type: boolean default: true exposure: - description: Optional unauthenticated Gateway API exposure. Public exposure is intentionally unauthenticated in this pass; add Gateway/OIDC policy in a later pass. + description: Optional Gateway API exposure. Pair with auth.istioJwt for Istio ambient JWT enforcement before broad public use. type: object properties: enabled: @@ -235,6 +235,45 @@ spec: - Issuer - ClusterIssuer default: ClusterIssuer + auth: + description: Optional authentication and mesh-policy resources. GitKB CLI token acquisition is not configured by these fields. + type: object + properties: + istioJwt: + description: Istio ambient waypoint JWT enforcement for the GitKB Service. Requires an Istio ambient mesh and a valid issuer/JWKS configuration. + type: object + properties: + enabled: + description: Render ambient namespace labels, service waypoint labels, a waypoint Gateway, RequestAuthentication, and AuthorizationPolicy. + type: boolean + default: false + issuer: + description: Expected JWT issuer, for example https://auth.ops.com.ai. + type: string + jwksUri: + description: JWKS URI for the issuer. If omitted, Istio may use OIDC discovery for issuers that support it. + type: string + audiences: + description: Optional JWT audiences accepted by Istio, for example a Zitadel project ID. + type: array + items: + type: string + requestPrincipals: + description: Request principals allowed by the AuthorizationPolicy. Defaults to ["*"] to require any valid JWT. + type: array + items: + type: string + waypoint: + description: Istio waypoint settings. + type: object + properties: + name: + description: Waypoint Gateway name. Defaults to -waypoint. + type: string + gatewayClassName: + description: GatewayClass used for the waypoint. Defaults to istio-waypoint. + type: string + default: istio-waypoint values: description: Helm values merged over stack defaults for the gitkb-server chart. type: object @@ -288,5 +327,23 @@ spec: type: boolean certificateReady: type: boolean + auth: + type: object + properties: + istioJwt: + type: object + properties: + enabled: + type: boolean + rendered: + type: boolean + issuer: + type: string + waypointReady: + type: boolean + requestAuthenticationReady: + type: boolean + authorizationPolicyReady: + type: boolean required: - spec diff --git a/examples/gitkbs/with-istio-jwt.yaml b/examples/gitkbs/with-istio-jwt.yaml new file mode 100644 index 0000000..0e22943 --- /dev/null +++ b/examples/gitkbs/with-istio-jwt.yaml @@ -0,0 +1,26 @@ +apiVersion: hops.ops.com.ai/v1alpha1 +kind: GitKB +metadata: + name: platform-kb-auth + namespace: default +spec: + clusterName: pat-local + namespace: gitkb + gitkb: + org: hops-ops + repo: hops + name: Hops Knowledge Base + exposure: + enabled: true + domain: kb.ops.com.ai + gatewayRef: + name: platform + namespace: istio-ingress + sectionName: https + auth: + istioJwt: + enabled: true + issuer: https://auth.ops.com.ai + jwksUri: https://auth.ops.com.ai/oauth/v2/keys + audiences: + - "373410628280264299" diff --git a/functions/render/000-state-init.yaml.gotmpl b/functions/render/000-state-init.yaml.gotmpl index ce4fb5c..4d996dc 100644 --- a/functions/render/000-state-init.yaml.gotmpl +++ b/functions/render/000-state-init.yaml.gotmpl @@ -2,8 +2,7 @@ # # Initialize $state with spec defaults. # -# This pass intentionally exposes GitKB without auth when exposure.enabled is -# true. Gateway/OIDC policy is deferred to a later pass. +# Gateway exposure remains authless unless auth.istioJwt is explicitly enabled. {{- $xr := getCompositeResource . }} {{- $spec := $xr.spec | default dict }} @@ -13,7 +12,7 @@ {{- $clusterName := $spec.clusterName | default $name }} {{- $namespace := $spec.namespace | default "gitkb" }} {{- $releaseName := $spec.releaseName | default $name }} -{{- $chartVersion := $spec.chartVersion | default "0.1.0" }} +{{- $chartVersion := $spec.chartVersion | default "0.2.0" }} {{- $managementPolicies := $spec.managementPolicies | default (list "*") }} {{- $defaultLabels := dict @@ -82,6 +81,22 @@ {{- $certSecretNamespace := $cert.secretNamespace | default ($gatewayRef.namespace | default $namespace) }} {{- $certIssuerRef := $cert.issuerRef | default dict }} +{{- $auth := $spec.auth | default dict }} +{{- $istioJwt := $auth.istioJwt | default dict }} +{{- $istioJwtEnabled := false }} +{{- if hasKey $istioJwt "enabled" }}{{- $istioJwtEnabled = $istioJwt.enabled }}{{- end }} +{{- $waypoint := $istioJwt.waypoint | default dict }} +{{- $waypointName := $waypoint.name | default (printf "%s-waypoint" $releaseName) }} +{{- $istioJwtIssuer := $istioJwt.issuer | default "" }} +{{- $istioJwtRendered := false }} +{{- if and $istioJwtEnabled $istioJwtIssuer }} + {{- $istioJwtRendered = true }} +{{- end }} +{{- $namespaceLabels := $labels }} +{{- if $istioJwtRendered }} + {{- $namespaceLabels = mergeOverwrite (dict) $labels (dict "istio.io/dataplane-mode" "ambient") }} +{{- end }} + {{- $state := dict "name" $name "kind" $xr.kind @@ -93,6 +108,7 @@ "chartVersion" $chartVersion "managementPolicies" $managementPolicies "labels" $labels + "namespaceLabels" $namespaceLabels "helmProviderConfigRef" $helmProviderConfigRef "kubernetesProviderConfigRef" $kubernetesProviderConfigRef "serviceName" $serviceName @@ -156,6 +172,26 @@ ) ) ) + "auth" (dict + "istioJwt" (dict + "enabled" $istioJwtEnabled + "rendered" $istioJwtRendered + "issuer" $istioJwtIssuer + "jwksUri" ($istioJwt.jwksUri | default "") + "audiences" ($istioJwt.audiences | default (list)) + "requestPrincipals" ($istioJwt.requestPrincipals | default (list "*")) + "waypoint" (dict + "name" $waypointName + "gatewayClassName" ($waypoint.gatewayClassName | default "istio-waypoint") + ) + "requestAuthenticationName" (printf "%s-jwt" $releaseName) + "authorizationPolicyName" (printf "%s-require-jwt" $releaseName) + "serviceLabels" (dict + "istio.io/use-waypoint" $waypointName + "istio.io/ingress-use-waypoint" "true" + ) + ) + ) "values" ($spec.values | default dict) "overrideAllValues" ($spec.overrideAllValues | default dict) "observed" (dict) diff --git a/functions/render/010-state-status.yaml.gotmpl b/functions/render/010-state-status.yaml.gotmpl index a3ea422..c48ca7b 100644 --- a/functions/render/010-state-status.yaml.gotmpl +++ b/functions/render/010-state-status.yaml.gotmpl @@ -5,7 +5,7 @@ {{- $observed := $.observed.resources | default dict }} {{- $checkReady := dict }} -{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" }} +{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "auth-waypoint" "auth-requestauthentication" "auth-authorizationpolicy" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" "usage-auth-waypoint-namespace" "usage-auth-jwt-namespace" "usage-auth-policy-namespace" "usage-auth-jwt-gitkb" "usage-auth-policy-gitkb" }} {{- $entry := get $observed $key | default dict }} {{- $resource := $entry.resource | default dict }} {{- $status := $resource.status | default dict }} @@ -22,6 +22,7 @@ {{- $routeRendered := and $exp.enabled $exp.domain $exp.gatewayRef.name }} {{- $certificateRendered := and $exp.enabled $exp.certificate.enabled $exp.domain }} {{- $certificateUsesGitkbNamespace := and $certificateRendered (eq $exp.certificate.secretNamespace $state.namespace) }} +{{- $auth := $state.auth.istioJwt }} {{- $namespaceReady := get $checkReady "namespace" }} {{- $releaseReady := get $checkReady "helm-release-gitkb" }} @@ -55,7 +56,41 @@ {{- $certificateNamespaceUsageReady = get $checkReady "usage-certificate-namespace" }} {{- end }} -{{- $ready := and $namespaceReady $releaseReady $routeReadyForOverall $certificateReadyForOverall $gitkbNamespaceUsageReady $httprouteGitkbUsageReady $httprouteNamespaceUsageReady $certificateNamespaceUsageReady }} +{{- $authWaypointReady := false }} +{{- $authRequestAuthenticationReady := false }} +{{- $authAuthorizationPolicyReady := false }} +{{- $authReadyForOverall := true }} +{{- $authWaypointNamespaceUsageReady := true }} +{{- $authJwtNamespaceUsageReady := true }} +{{- $authPolicyNamespaceUsageReady := true }} +{{- $authJwtGitkbUsageReady := true }} +{{- $authPolicyGitkbUsageReady := true }} +{{- if $auth.enabled }} + {{- $authReadyForOverall = false }} + {{- if $auth.rendered }} + {{- $authWaypointReady = get $checkReady "auth-waypoint" }} + {{- $authRequestAuthenticationReady = get $checkReady "auth-requestauthentication" }} + {{- $authAuthorizationPolicyReady = get $checkReady "auth-authorizationpolicy" }} + {{- if and $namespaceReady $authWaypointReady }} + {{- $authWaypointNamespaceUsageReady = get $checkReady "usage-auth-waypoint-namespace" }} + {{- end }} + {{- if and $namespaceReady $authRequestAuthenticationReady }} + {{- $authJwtNamespaceUsageReady = get $checkReady "usage-auth-jwt-namespace" }} + {{- end }} + {{- if and $namespaceReady $authAuthorizationPolicyReady }} + {{- $authPolicyNamespaceUsageReady = get $checkReady "usage-auth-policy-namespace" }} + {{- end }} + {{- if and $releaseReady $authRequestAuthenticationReady }} + {{- $authJwtGitkbUsageReady = get $checkReady "usage-auth-jwt-gitkb" }} + {{- end }} + {{- if and $releaseReady $authAuthorizationPolicyReady }} + {{- $authPolicyGitkbUsageReady = get $checkReady "usage-auth-policy-gitkb" }} + {{- end }} + {{- $authReadyForOverall = and $authWaypointReady $authRequestAuthenticationReady $authAuthorizationPolicyReady $authWaypointNamespaceUsageReady $authJwtNamespaceUsageReady $authPolicyNamespaceUsageReady $authJwtGitkbUsageReady $authPolicyGitkbUsageReady }} + {{- end }} +{{- end }} + +{{- $ready := and $namespaceReady $releaseReady $routeReadyForOverall $certificateReadyForOverall $gitkbNamespaceUsageReady $httprouteGitkbUsageReady $httprouteNamespaceUsageReady $certificateNamespaceUsageReady $authReadyForOverall }} {{- $exposureURL := "" }} {{- if and $exp.enabled $exp.domain }} @@ -75,6 +110,16 @@ "certificateRendered" $certificateRendered "certificateUsesGitkbNamespace" $certificateUsesGitkbNamespace ) + "auth" (dict + "waypointReady" $authWaypointReady + "requestAuthenticationReady" $authRequestAuthenticationReady + "authorizationPolicyReady" $authAuthorizationPolicyReady + "waypointNamespaceUsageReady" $authWaypointNamespaceUsageReady + "jwtNamespaceUsageReady" $authJwtNamespaceUsageReady + "policyNamespaceUsageReady" $authPolicyNamespaceUsageReady + "jwtGitkbUsageReady" $authJwtGitkbUsageReady + "policyGitkbUsageReady" $authPolicyGitkbUsageReady + ) ) }} {{- $state = set $state "status" (dict @@ -99,4 +144,14 @@ "routeReady" $routeReady "certificateReady" $certificateReady ) + "auth" (dict + "istioJwt" (dict + "enabled" $auth.enabled + "rendered" $auth.rendered + "issuer" $auth.issuer + "waypointReady" $authWaypointReady + "requestAuthenticationReady" $authRequestAuthenticationReady + "authorizationPolicyReady" $authAuthorizationPolicyReady + ) + ) ) }} diff --git a/functions/render/100-namespace.yaml.gotmpl b/functions/render/100-namespace.yaml.gotmpl index a49f36b..d6eb9ca 100644 --- a/functions/render/100-namespace.yaml.gotmpl +++ b/functions/render/100-namespace.yaml.gotmpl @@ -18,7 +18,7 @@ spec: kind: Namespace metadata: name: {{ $state.namespace }} - labels: {{ $state.labels | toJson }} + labels: {{ $state.namespaceLabels | toJson }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/render/200-helm-release-gitkb.yaml.gotmpl b/functions/render/200-helm-release-gitkb.yaml.gotmpl index f8eb7f1..3c7c09e 100644 --- a/functions/render/200-helm-release-gitkb.yaml.gotmpl +++ b/functions/render/200-helm-release-gitkb.yaml.gotmpl @@ -14,6 +14,9 @@ "seed" $state.seed "persistence" $state.persistence }} +{{- if $state.auth.istioJwt.rendered }} + {{- $chartDefaults = mergeOverwrite $chartDefaults (dict "service" (dict "labels" $state.auth.istioJwt.serviceLabels)) }} +{{- end }} --- apiVersion: helm.m.crossplane.io/v1beta1 diff --git a/functions/render/450-auth-istio-jwt.yaml.gotmpl b/functions/render/450-auth-istio-jwt.yaml.gotmpl new file mode 100644 index 0000000..d706825 --- /dev/null +++ b/functions/render/450-auth-istio-jwt.yaml.gotmpl @@ -0,0 +1,231 @@ +# code: language=yaml +# +# Optional Istio ambient waypoint JWT enforcement for GitKB sync traffic. +# This proves and packages the mesh-side auth gate only; GitKB CLI token +# acquisition/injection remains a separate client-side concern. + +{{- $auth := $state.auth.istioJwt }} +{{- $observedAuth := $state.observed.auth | default dict }} +{{- $waypointLabels := mergeOverwrite (dict) $state.labels (dict "istio.io/waypoint-for" "service") }} + +{{- if $auth.rendered }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: {{ $state.name }}-auth-waypoint + annotations: + {{ setResourceNameAnnotation "auth-waypoint" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: {{ $auth.waypoint.name }} + namespace: {{ $state.namespace }} + labels: + {{- toYaml $waypointLabels | nindent 10 }} + spec: + gatewayClassName: {{ $auth.waypoint.gatewayClassName }} + listeners: + - name: mesh + port: 15008 + protocol: HBONE + allowedRoutes: + namespaces: + from: Same + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} + +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: {{ $state.name }}-auth-requestauthentication + annotations: + {{ setResourceNameAnnotation "auth-requestauthentication" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: security.istio.io/v1 + kind: RequestAuthentication + metadata: + name: {{ $auth.requestAuthenticationName }} + namespace: {{ $state.namespace }} + labels: {{ $state.labels | toJson }} + spec: + targetRefs: + - group: "" + kind: Service + name: {{ $state.serviceName }} + jwtRules: + - issuer: {{ $auth.issuer | quote }} + {{- if $auth.jwksUri }} + jwksUri: {{ $auth.jwksUri | quote }} + {{- end }} + {{- if $auth.audiences }} + audiences: + {{- range $audience := $auth.audiences }} + - {{ $audience | quote }} + {{- end }} + {{- end }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} + +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: {{ $state.name }}-auth-authorizationpolicy + annotations: + {{ setResourceNameAnnotation "auth-authorizationpolicy" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: security.istio.io/v1 + kind: AuthorizationPolicy + metadata: + name: {{ $auth.authorizationPolicyName }} + namespace: {{ $state.namespace }} + labels: {{ $state.labels | toJson }} + spec: + targetRefs: + - group: "" + kind: Service + name: {{ $state.serviceName }} + action: ALLOW + rules: + - from: + - source: + requestPrincipals: + {{- range $principal := $auth.requestPrincipals }} + - {{ $principal | quote }} + {{- end }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- end }} + +{{- if and $auth.rendered ($state.observed.namespace.ready | default false) ($observedAuth.waypointReady | default false) }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-waypoint-before-namespace + annotations: + {{ setResourceNameAnnotation "usage-auth-waypoint-namespace" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-namespace + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-auth-waypoint +{{- end }} + +{{- if and $auth.rendered ($state.observed.namespace.ready | default false) ($observedAuth.requestAuthenticationReady | default false) }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-jwt-before-namespace + annotations: + {{ setResourceNameAnnotation "usage-auth-jwt-namespace" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-namespace + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-auth-requestauthentication +{{- end }} + +{{- if and $auth.rendered ($state.observed.namespace.ready | default false) ($observedAuth.authorizationPolicyReady | default false) }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-policy-before-namespace + annotations: + {{ setResourceNameAnnotation "usage-auth-policy-namespace" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-namespace + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-auth-authorizationpolicy +{{- end }} + +{{- if and $auth.rendered ($state.observed.release.ready | default false) ($observedAuth.requestAuthenticationReady | default false) }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-jwt-before-gitkb + annotations: + {{ setResourceNameAnnotation "usage-auth-jwt-gitkb" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: helm.m.crossplane.io/v1beta1 + kind: Release + resourceRef: + name: {{ $state.releaseName }} + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-auth-requestauthentication +{{- end }} + +{{- if and $auth.rendered ($state.observed.release.ready | default false) ($observedAuth.authorizationPolicyReady | default false) }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-policy-before-gitkb + annotations: + {{ setResourceNameAnnotation "usage-auth-policy-gitkb" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: helm.m.crossplane.io/v1beta1 + kind: Release + resourceRef: + name: {{ $state.releaseName }} + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $state.name }}-auth-authorizationpolicy +{{- end }} diff --git a/functions/render/999-status.yaml.gotmpl b/functions/render/999-status.yaml.gotmpl index 10a0cca..717657e 100644 --- a/functions/render/999-status.yaml.gotmpl +++ b/functions/render/999-status.yaml.gotmpl @@ -26,3 +26,11 @@ status: certificateEnabled: {{ $s.exposure.certificateEnabled }} routeReady: {{ $s.exposure.routeReady }} certificateReady: {{ $s.exposure.certificateReady }} + auth: + istioJwt: + enabled: {{ $s.auth.istioJwt.enabled }} + rendered: {{ $s.auth.istioJwt.rendered }} + issuer: {{ $s.auth.istioJwt.issuer | quote }} + waypointReady: {{ $s.auth.istioJwt.waypointReady }} + requestAuthenticationReady: {{ $s.auth.istioJwt.requestAuthenticationReady }} + authorizationPolicyReady: {{ $s.auth.istioJwt.authorizationPolicyReady }} diff --git a/tests/test-render/main.k b/tests/test-render/main.k index b5efc51..c230a5d 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -45,7 +45,7 @@ items = [ chart = { name = "gitkb-server" repository = "https://hops-ops.github.io/gitkb-server-chart" - version = "0.1.0" + version = "0.2.0" } namespace = "gitkb" values = { @@ -172,6 +172,284 @@ items = [ } } + metav1alpha1.CompositionTest { + metadata.name = "istio-jwt-auth-renders-waypoint-and-policies" + spec = { + compositionPath = "apis/gitkbs/composition.yaml" + xrdPath = "apis/gitkbs/definition.yaml" + timeoutSeconds = 60 + validate = False + xr = _base_xr | { + spec.auth = { + istioJwt = { + enabled = True + issuer = "https://auth.ops.com.ai" + jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" + audiences = ["373410628280264299"] + } + } + } + assertResources = [ + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "platform-kb-namespace" + spec.forProvider.manifest.metadata.labels = { + "istio.io/dataplane-mode" = "ambient" + } + } + { + apiVersion = "helm.m.crossplane.io/v1beta1" + kind = "Release" + metadata.name = "platform-kb" + spec.forProvider.values.service.labels = { + "istio.io/use-waypoint" = "platform-kb-waypoint" + "istio.io/ingress-use-waypoint" = "true" + } + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "platform-kb-auth-waypoint" + spec.forProvider.manifest = { + apiVersion = "gateway.networking.k8s.io/v1" + kind = "Gateway" + metadata = { + name = "platform-kb-waypoint" + namespace = "gitkb" + labels = { + "istio.io/waypoint-for" = "service" + } + } + spec = { + gatewayClassName = "istio-waypoint" + listeners = [{ + name = "mesh" + port = 15008 + protocol = "HBONE" + allowedRoutes.namespaces.from = "Same" + }] + } + } + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "platform-kb-auth-requestauthentication" + spec.forProvider.manifest = { + apiVersion = "security.istio.io/v1" + kind = "RequestAuthentication" + metadata = { + name = "platform-kb-jwt" + namespace = "gitkb" + } + spec = { + targetRefs = [{ + group = "" + kind = "Service" + name = "platform-kb-gitkb-server" + }] + jwtRules = [{ + issuer = "https://auth.ops.com.ai" + jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" + audiences = ["373410628280264299"] + }] + } + } + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "platform-kb-auth-authorizationpolicy" + spec.forProvider.manifest = { + apiVersion = "security.istio.io/v1" + kind = "AuthorizationPolicy" + metadata = { + name = "platform-kb-require-jwt" + namespace = "gitkb" + } + spec = { + targetRefs = [{ + group = "" + kind = "Service" + name = "platform-kb-gitkb-server" + }] + action = "ALLOW" + rules = [{ + from = [{ + source.requestPrincipals = ["*"] + }] + }] + } + } + } + ] + } + } + + metav1alpha1.CompositionTest { + metadata.name = "observed-auth-ready-renders-usages-and-ready-status" + spec = { + compositionPath = "apis/gitkbs/composition.yaml" + xrdPath = "apis/gitkbs/definition.yaml" + timeoutSeconds = 60 + validate = False + xr = _base_xr | { + spec.auth = { + istioJwt = { + enabled = True + issuer = "https://auth.ops.com.ai" + jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" + } + } + } + observedResources = [ + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata = { + name = "platform-kb-namespace" + annotations = {"crossplane.io/composition-resource-name" = "namespace"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "helm.m.crossplane.io/v1beta1" + kind = "Release" + metadata = { + name = "platform-kb" + annotations = {"crossplane.io/composition-resource-name" = "helm-release-gitkb"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata = { + name = "platform-kb-auth-waypoint" + annotations = {"crossplane.io/composition-resource-name" = "auth-waypoint"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata = { + name = "platform-kb-auth-requestauthentication" + annotations = {"crossplane.io/composition-resource-name" = "auth-requestauthentication"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata = { + name = "platform-kb-auth-authorizationpolicy" + annotations = {"crossplane.io/composition-resource-name" = "auth-authorizationpolicy"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata = { + name = "platform-kb-delete-gitkb-before-namespace" + annotations = {"crossplane.io/composition-resource-name" = "usage-gitkb-namespace"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata = { + name = "platform-kb-delete-auth-waypoint-before-namespace" + annotations = {"crossplane.io/composition-resource-name" = "usage-auth-waypoint-namespace"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata = { + name = "platform-kb-delete-auth-jwt-before-namespace" + annotations = {"crossplane.io/composition-resource-name" = "usage-auth-jwt-namespace"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata = { + name = "platform-kb-delete-auth-policy-before-namespace" + annotations = {"crossplane.io/composition-resource-name" = "usage-auth-policy-namespace"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata = { + name = "platform-kb-delete-auth-jwt-before-gitkb" + annotations = {"crossplane.io/composition-resource-name" = "usage-auth-jwt-gitkb"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata = { + name = "platform-kb-delete-auth-policy-before-gitkb" + annotations = {"crossplane.io/composition-resource-name" = "usage-auth-policy-gitkb"} + } + status.conditions = _ready_conditions + } + ] + assertResources = [ + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-waypoint-before-namespace" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-jwt-before-namespace" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-policy-before-namespace" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-jwt-before-gitkb" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-policy-before-gitkb" + } + { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "GitKB" + metadata.name = "platform-kb" + status = { + ready = True + auth.istioJwt = { + enabled = True + rendered = True + issuer = "https://auth.ops.com.ai" + waypointReady = True + requestAuthenticationReady = True + authorizationPolicyReady = True + } + } + } + ] + } + } + metav1alpha1.CompositionTest { metadata.name = "observed-ready-renders-usages-and-ready-status" spec = { From 0ea51db3aa9c0da4c28228df5d7514defd250580 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Tue, 9 Jun 2026 18:52:06 -0500 Subject: [PATCH 2/6] fix: use upgrade-safe gitkb chart --- apis/gitkbs/definition.yaml | 2 +- functions/render/000-state-init.yaml.gotmpl | 2 +- tests/test-render/main.k | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apis/gitkbs/definition.yaml b/apis/gitkbs/definition.yaml index 92557c3..954df6c 100644 --- a/apis/gitkbs/definition.yaml +++ b/apis/gitkbs/definition.yaml @@ -68,7 +68,7 @@ spec: description: Helm release name for the gitkb-server chart. Defaults to metadata.name. type: string chartVersion: - description: Version of the gitkb-server chart. Defaults to "0.2.0". + description: Version of the gitkb-server chart. Defaults to "0.2.1". type: string gitkb: description: GitKB runtime settings passed to the chart. diff --git a/functions/render/000-state-init.yaml.gotmpl b/functions/render/000-state-init.yaml.gotmpl index 4d996dc..a65996e 100644 --- a/functions/render/000-state-init.yaml.gotmpl +++ b/functions/render/000-state-init.yaml.gotmpl @@ -12,7 +12,7 @@ {{- $clusterName := $spec.clusterName | default $name }} {{- $namespace := $spec.namespace | default "gitkb" }} {{- $releaseName := $spec.releaseName | default $name }} -{{- $chartVersion := $spec.chartVersion | default "0.2.0" }} +{{- $chartVersion := $spec.chartVersion | default "0.2.1" }} {{- $managementPolicies := $spec.managementPolicies | default (list "*") }} {{- $defaultLabels := dict diff --git a/tests/test-render/main.k b/tests/test-render/main.k index c230a5d..205c197 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -45,7 +45,7 @@ items = [ chart = { name = "gitkb-server" repository = "https://hops-ops.github.io/gitkb-server-chart" - version = "0.2.0" + version = "0.2.1" } namespace = "gitkb" values = { From 56ef372ce968216c22f6cfa52dfff532758bc038 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Wed, 10 Jun 2026 07:18:22 -0500 Subject: [PATCH 3/6] feat(auth): create GitKB OIDC sync client --- README.md | 42 +++- apis/gitkbs/definition.yaml | 91 +++++++- examples/gitkbs/with-istio-jwt.yaml | 12 +- functions/render/000-state-init.yaml.gotmpl | 39 +++- functions/render/010-state-status.yaml.gotmpl | 80 ++++++- .../400-auth-zitadel-client.yaml.gotmpl | 99 +++++++++ .../render/450-auth-istio-jwt.yaml.gotmpl | 8 +- functions/render/999-status.yaml.gotmpl | 17 ++ tests/test-render/main.k | 201 +++++++++++++++++- upbound.yaml | 18 +- 10 files changed, 577 insertions(+), 30 deletions(-) create mode 100644 functions/render/400-auth-zitadel-client.yaml.gotmpl diff --git a/README.md b/README.md index e04c971..a562667 100644 --- a/README.md +++ b/README.md @@ -89,30 +89,37 @@ spec: kind: ClusterIssuer ``` -For Istio ambient clusters, enable service-level JWT enforcement with `auth.istioJwt`. -This renders: +For Istio ambient clusters, enable a dedicated GitKB Zitadel client with +`auth.oidcClient`, then enable service-level JWT enforcement with +`auth.istioJwt`. This renders: +- a GitKB-owned Zitadel Project, Role, MachineUser, and Grant - ambient enrollment on the GitKB namespace - waypoint labels on the chart-owned GitKB Service - an Istio waypoint `Gateway` -- service-targeted `RequestAuthentication` +- service-targeted `RequestAuthentication` using the GitKB Project ID as an audience - service-targeted `AuthorizationPolicy` requiring a valid JWT ```yaml spec: auth: - istioJwt: + oidcClient: enabled: true issuer: https://auth.ops.com.ai jwksUri: https://auth.ops.com.ai/oauth/v2/keys - audiences: - - "373410628280264299" + zitadelProviderConfigRef: + name: zitadel-tenant-stack + kind: ProviderConfig + zitadelOrgId: "373268222482392664" + machineUserName: gitkb-sync + istioJwt: + enabled: true ``` -This protects the workload for requests that carry a valid bearer token. It does -not configure GitKB CLI token acquisition or map a sync host to a separate OIDC -issuer; keep that client-side requirement in mind before enabling auth on an -existing sync remote. +This protects the workload for requests that carry a valid bearer token issued +for the GitKB Project audience. Configure the GitKB CLI remote with +`status.auth.oidcClient.clientId`, the Project audience scope, and the secret +referenced by `status.auth.oidcClient.clientSecretRef`. ## Import Existing @@ -136,6 +143,16 @@ Configure the GitKB CLI remote to the status URL or to the same domain and repo ```toml [sync.remotes.origin] url = "https://kb.ops.com.ai/hops-ops/hops" + +[sync.remotes.origin.auth] +issuer = "https://auth.ops.com.ai" +client_id = "gitkb-sync" +client_secret_env = "GITKB_OIDC_CLIENT_SECRET" +scopes = [ + "openid", + "profile", + "urn:zitadel:iam:org:project:id::aud", +] ``` The server route strips `/hops-ops/hops` before forwarding traffic to `git-kb serve`, so the CLI talks to the normal GitKB endpoints under that public path. @@ -152,6 +169,7 @@ The XR publishes the operational fields needed by downstream automation: - `status.exposure.url` - full public URL for the GitKB remote. - `status.exposure.routeReady` - composed HTTPRoute readiness. - `status.exposure.certificateReady` - composed Certificate readiness when enabled. +- `status.auth.oidcClient` - GitKB-owned Zitadel client ID, Project audience, credential Secret reference, and readiness. - `status.auth.istioJwt` - whether Istio JWT auth rendered and whether the waypoint and policies are ready. ## Composed Resources @@ -160,6 +178,10 @@ The XR publishes the operational fields needed by downstream automation: - `kubernetes.m.crossplane.io/Object` Namespace - creates the target namespace. - `kubernetes.m.crossplane.io/Object` HTTPRoute - optional Gateway API route when `exposure.enabled` is true. - `kubernetes.m.crossplane.io/Object` Certificate - optional cert-manager Certificate when `exposure.certificate.enabled` is true. +- `project.zitadel.m.crossplane.io/Project` - optional GitKB Project when `auth.oidcClient.enabled` is true. +- `project.zitadel.m.crossplane.io/Role` - optional GitKB sync Role when `auth.oidcClient.enabled` is true. +- `user.zitadel.m.crossplane.io/MachineUser` - optional GitKB OAuth2 client when `auth.oidcClient.enabled` is true. +- `user.zitadel.m.crossplane.io/Grant` - optional Role grant to the GitKB MachineUser when `auth.oidcClient.enabled` is true. - `kubernetes.m.crossplane.io/Object` Gateway - optional Istio waypoint when `auth.istioJwt.enabled` and `issuer` are set. - `kubernetes.m.crossplane.io/Object` RequestAuthentication - optional JWT validation policy when `auth.istioJwt.enabled` and `issuer` are set. - `kubernetes.m.crossplane.io/Object` AuthorizationPolicy - optional valid-JWT requirement when `auth.istioJwt.enabled` and `issuer` are set. diff --git a/apis/gitkbs/definition.yaml b/apis/gitkbs/definition.yaml index 954df6c..a82fec0 100644 --- a/apis/gitkbs/definition.yaml +++ b/apis/gitkbs/definition.yaml @@ -236,9 +236,55 @@ spec: - ClusterIssuer default: ClusterIssuer auth: - description: Optional authentication and mesh-policy resources. GitKB CLI token acquisition is not configured by these fields. + description: Optional authentication and mesh-policy resources for GitKB sync traffic. type: object properties: + oidcClient: + description: | + Dedicated Zitadel client-credentials identity for GitKB + CLI sync. When enabled, the composition creates its own + Zitadel Project, Role, MachineUser, and Grant. The + MachineUser connection Secret contains + `attribute.client_id` and `attribute.client_secret`; + use those values for GitKB CLI OAuth2 + client_credentials authentication. + type: object + properties: + enabled: + description: Render the GitKB-owned Zitadel client identity. + type: boolean + default: false + issuer: + description: Expected OAuth/OIDC issuer, for example https://auth.ops.com.ai. + type: string + jwksUri: + description: JWKS URI for the issuer. If omitted, Istio may use OIDC discovery. + type: string + zitadelProviderConfigRef: + description: Zitadel ProviderConfig used to provision the Project, Role, MachineUser, and Grant. + type: object + properties: + name: + type: string + kind: + type: string + default: ProviderConfig + zitadelOrgId: + description: Zitadel Org ID that owns the GitKB Project and MachineUser. + type: string + projectName: + description: Zitadel Project name. Defaults to -gitkb. + type: string + roleKey: + description: Zitadel Role key granted to the GitKB MachineUser. + type: string + default: gitkb:sync + machineUserName: + description: MachineUser username. This becomes the OAuth2 client_id. Defaults to -sync. + type: string + clientSecretName: + description: Connection Secret name for client credentials. Defaults to -secret. + type: string istioJwt: description: Istio ambient waypoint JWT enforcement for the GitKB Service. Requires an Istio ambient mesh and a valid issuer/JWKS configuration. type: object @@ -258,6 +304,10 @@ spec: type: array items: type: string + useOidcClientAudience: + description: Add the GitKB-owned Zitadel Project ID to accepted audiences once observed. Has no effect unless auth.oidcClient.enabled is true. Defaults to true. + type: boolean + default: true requestPrincipals: description: Request principals allowed by the AuthorizationPolicy. Defaults to ["*"] to require any valid JWT. type: array @@ -330,6 +380,41 @@ spec: auth: type: object properties: + oidcClient: + type: object + properties: + enabled: + type: boolean + rendered: + type: boolean + issuer: + type: string + projectId: + description: Observed Zitadel Project ID. Use as the audience scope with urn:zitadel:iam:org:project:id::aud. + type: string + clientId: + description: Observed OAuth2 client_id for GitKB CLI client_credentials authentication. + type: string + clientSecretRef: + description: Secret containing the GitKB OAuth2 client credentials. + type: object + properties: + name: + type: string + namespace: + type: string + clientIdKey: + type: string + clientSecretKey: + type: string + projectReady: + type: boolean + roleReady: + type: boolean + machineUserReady: + type: boolean + grantReady: + type: boolean istioJwt: type: object properties: @@ -339,6 +424,10 @@ spec: type: boolean issuer: type: string + audiences: + type: array + items: + type: string waypointReady: type: boolean requestAuthenticationReady: diff --git a/examples/gitkbs/with-istio-jwt.yaml b/examples/gitkbs/with-istio-jwt.yaml index 0e22943..68df580 100644 --- a/examples/gitkbs/with-istio-jwt.yaml +++ b/examples/gitkbs/with-istio-jwt.yaml @@ -18,9 +18,15 @@ spec: namespace: istio-ingress sectionName: https auth: - istioJwt: + oidcClient: enabled: true issuer: https://auth.ops.com.ai jwksUri: https://auth.ops.com.ai/oauth/v2/keys - audiences: - - "373410628280264299" + zitadelProviderConfigRef: + name: zitadel-tenant-stack + kind: ProviderConfig + zitadelOrgId: "373268222482392664" + projectName: gitkb-sync + machineUserName: gitkb-sync + istioJwt: + enabled: true diff --git a/functions/render/000-state-init.yaml.gotmpl b/functions/render/000-state-init.yaml.gotmpl index a65996e..28ee9b9 100644 --- a/functions/render/000-state-init.yaml.gotmpl +++ b/functions/render/000-state-init.yaml.gotmpl @@ -82,12 +82,31 @@ {{- $certIssuerRef := $cert.issuerRef | default dict }} {{- $auth := $spec.auth | default dict }} +{{- $oidcClient := $auth.oidcClient | default dict }} +{{- $oidcClientEnabled := false }} +{{- if hasKey $oidcClient "enabled" }}{{- $oidcClientEnabled = $oidcClient.enabled }}{{- end }} +{{- $oidcProviderConfigRef := $oidcClient.zitadelProviderConfigRef | default dict }} +{{- $oidcProviderConfigRef = dict + "name" ($oidcProviderConfigRef.name | default "") + "kind" ($oidcProviderConfigRef.kind | default "ProviderConfig") +}} +{{- $oidcOrgId := $oidcClient.zitadelOrgId | default "" }} +{{- $oidcMachineUserName := $oidcClient.machineUserName | default (printf "%s-sync" $releaseName) }} +{{- $oidcClientSecretName := $oidcClient.clientSecretName | default (printf "%s-secret" $oidcMachineUserName) }} +{{- $oidcClientRendered := false }} +{{- if and $oidcClientEnabled $oidcProviderConfigRef.name $oidcOrgId }} + {{- $oidcClientRendered = true }} +{{- end }} + {{- $istioJwt := $auth.istioJwt | default dict }} {{- $istioJwtEnabled := false }} {{- if hasKey $istioJwt "enabled" }}{{- $istioJwtEnabled = $istioJwt.enabled }}{{- end }} {{- $waypoint := $istioJwt.waypoint | default dict }} {{- $waypointName := $waypoint.name | default (printf "%s-waypoint" $releaseName) }} -{{- $istioJwtIssuer := $istioJwt.issuer | default "" }} +{{- $istioJwtIssuer := $istioJwt.issuer | default ($oidcClient.issuer | default "") }} +{{- $istioJwtJwksUri := $istioJwt.jwksUri | default ($oidcClient.jwksUri | default "") }} +{{- $istioJwtUseOidcClientAudience := true }} +{{- if hasKey $istioJwt "useOidcClientAudience" }}{{- $istioJwtUseOidcClientAudience = $istioJwt.useOidcClientAudience }}{{- end }} {{- $istioJwtRendered := false }} {{- if and $istioJwtEnabled $istioJwtIssuer }} {{- $istioJwtRendered = true }} @@ -173,12 +192,28 @@ ) ) "auth" (dict + "oidcClient" (dict + "enabled" $oidcClientEnabled + "rendered" $oidcClientRendered + "issuer" ($oidcClient.issuer | default "") + "jwksUri" ($oidcClient.jwksUri | default "") + "zitadelProviderConfigRef" $oidcProviderConfigRef + "zitadelOrgId" $oidcOrgId + "projectName" ($oidcClient.projectName | default (printf "%s-gitkb" $releaseName)) + "roleKey" ($oidcClient.roleKey | default "gitkb:sync") + "machineUserName" $oidcMachineUserName + "clientSecretName" $oidcClientSecretName + "clientIdKey" "attribute.client_id" + "clientSecretKey" "attribute.client_secret" + ) "istioJwt" (dict "enabled" $istioJwtEnabled "rendered" $istioJwtRendered "issuer" $istioJwtIssuer - "jwksUri" ($istioJwt.jwksUri | default "") + "jwksUri" $istioJwtJwksUri "audiences" ($istioJwt.audiences | default (list)) + "effectiveAudiences" ($istioJwt.audiences | default (list)) + "useOidcClientAudience" $istioJwtUseOidcClientAudience "requestPrincipals" ($istioJwt.requestPrincipals | default (list "*")) "waypoint" (dict "name" $waypointName diff --git a/functions/render/010-state-status.yaml.gotmpl b/functions/render/010-state-status.yaml.gotmpl index c48ca7b..a57215c 100644 --- a/functions/render/010-state-status.yaml.gotmpl +++ b/functions/render/010-state-status.yaml.gotmpl @@ -5,7 +5,7 @@ {{- $observed := $.observed.resources | default dict }} {{- $checkReady := dict }} -{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "auth-waypoint" "auth-requestauthentication" "auth-authorizationpolicy" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" "usage-auth-waypoint-namespace" "usage-auth-jwt-namespace" "usage-auth-policy-namespace" "usage-auth-jwt-gitkb" "usage-auth-policy-gitkb" }} +{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "auth-zitadel-project" "auth-zitadel-role" "auth-zitadel-machineuser" "auth-zitadel-grant" "auth-waypoint" "auth-requestauthentication" "auth-authorizationpolicy" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" "usage-auth-waypoint-namespace" "usage-auth-jwt-namespace" "usage-auth-policy-namespace" "usage-auth-jwt-gitkb" "usage-auth-policy-gitkb" }} {{- $entry := get $observed $key | default dict }} {{- $resource := $entry.resource | default dict }} {{- $status := $resource.status | default dict }} @@ -22,8 +22,22 @@ {{- $routeRendered := and $exp.enabled $exp.domain $exp.gatewayRef.name }} {{- $certificateRendered := and $exp.enabled $exp.certificate.enabled $exp.domain }} {{- $certificateUsesGitkbNamespace := and $certificateRendered (eq $exp.certificate.secretNamespace $state.namespace) }} +{{- $oidcClient := $state.auth.oidcClient }} {{- $auth := $state.auth.istioJwt }} +{{- $oidcProjectEntry := get $observed "auth-zitadel-project" | default dict }} +{{- $oidcProjectResource := $oidcProjectEntry.resource | default dict }} +{{- $oidcProjectStatus := $oidcProjectResource.status | default dict }} +{{- $oidcProjectAtProvider := $oidcProjectStatus.atProvider | default dict }} +{{- $oidcProjectId := $oidcProjectAtProvider.id | default "" }} + +{{- $oidcMachineUserEntry := get $observed "auth-zitadel-machineuser" | default dict }} +{{- $oidcMachineUserResource := $oidcMachineUserEntry.resource | default dict }} +{{- $oidcMachineUserStatus := $oidcMachineUserResource.status | default dict }} +{{- $oidcMachineUserAtProvider := $oidcMachineUserStatus.atProvider | default dict }} +{{- $oidcMachineUserId := $oidcMachineUserAtProvider.id | default ($oidcMachineUserAtProvider.userId | default "") }} +{{- $oidcClientId := $oidcMachineUserAtProvider.userName | default "" }} + {{- $namespaceReady := get $checkReady "namespace" }} {{- $releaseReady := get $checkReady "helm-release-gitkb" }} {{- $routeReady := false }} @@ -39,6 +53,41 @@ {{- $certificateReadyForOverall = $certificateReady }} {{- end }} +{{- $oidcProjectReady := false }} +{{- $oidcRoleReady := false }} +{{- $oidcMachineUserReady := false }} +{{- $oidcGrantReady := false }} +{{- $oidcReadyForOverall := true }} +{{- if $oidcClient.enabled }} + {{- $oidcReadyForOverall = false }} + {{- if $oidcClient.rendered }} + {{- $oidcProjectReady = get $checkReady "auth-zitadel-project" }} + {{- $oidcRoleReady = get $checkReady "auth-zitadel-role" }} + {{- $oidcMachineUserReady = get $checkReady "auth-zitadel-machineuser" }} + {{- $oidcGrantReady = get $checkReady "auth-zitadel-grant" }} + {{- $oidcReadyForOverall = and $oidcProjectReady $oidcRoleReady $oidcMachineUserReady $oidcGrantReady }} + {{- end }} +{{- end }} + +{{- $effectiveAudiences := $auth.audiences | default (list) }} +{{- if and $auth.useOidcClientAudience $oidcClient.rendered $oidcProjectId }} + {{- $effectiveAudiences = append $effectiveAudiences $oidcProjectId }} +{{- end }} +{{- $authRendered := $auth.rendered }} +{{- if and $auth.enabled $auth.useOidcClientAudience $oidcClient.rendered (not $oidcProjectId) }} + {{- $authRendered = false }} +{{- end }} +{{- $namespaceLabels := $state.labels }} +{{- if $authRendered }} + {{- $namespaceLabels = mergeOverwrite (dict) $state.labels (dict "istio.io/dataplane-mode" "ambient") }} +{{- end }} +{{- $state = set $state "namespaceLabels" $namespaceLabels }} +{{- $state = set $state "auth" (mergeOverwrite $state.auth (dict "istioJwt" (mergeOverwrite $state.auth.istioJwt (dict + "rendered" $authRendered + "effectiveAudiences" $effectiveAudiences +)))) }} +{{- $auth = $state.auth.istioJwt }} + {{- $gitkbNamespaceUsageReady := true }} {{- if and $namespaceReady $releaseReady }} {{- $gitkbNamespaceUsageReady = get $checkReady "usage-gitkb-namespace" }} @@ -90,7 +139,7 @@ {{- end }} {{- end }} -{{- $ready := and $namespaceReady $releaseReady $routeReadyForOverall $certificateReadyForOverall $gitkbNamespaceUsageReady $httprouteGitkbUsageReady $httprouteNamespaceUsageReady $certificateNamespaceUsageReady $authReadyForOverall }} +{{- $ready := and $namespaceReady $releaseReady $routeReadyForOverall $certificateReadyForOverall $gitkbNamespaceUsageReady $httprouteGitkbUsageReady $httprouteNamespaceUsageReady $certificateNamespaceUsageReady $oidcReadyForOverall $authReadyForOverall }} {{- $exposureURL := "" }} {{- if and $exp.enabled $exp.domain }} @@ -111,6 +160,15 @@ "certificateUsesGitkbNamespace" $certificateUsesGitkbNamespace ) "auth" (dict + "oidcClient" (dict + "projectReady" $oidcProjectReady + "roleReady" $oidcRoleReady + "machineUserReady" $oidcMachineUserReady + "grantReady" $oidcGrantReady + "projectId" $oidcProjectId + "machineUserId" $oidcMachineUserId + "clientId" $oidcClientId + ) "waypointReady" $authWaypointReady "requestAuthenticationReady" $authRequestAuthenticationReady "authorizationPolicyReady" $authAuthorizationPolicyReady @@ -145,10 +203,28 @@ "certificateReady" $certificateReady ) "auth" (dict + "oidcClient" (dict + "enabled" $oidcClient.enabled + "rendered" $oidcClient.rendered + "issuer" $oidcClient.issuer + "projectId" $oidcProjectId + "clientId" $oidcClientId + "clientSecretRef" (dict + "name" $oidcClient.clientSecretName + "namespace" $state.xrNamespace + "clientIdKey" $oidcClient.clientIdKey + "clientSecretKey" $oidcClient.clientSecretKey + ) + "projectReady" $oidcProjectReady + "roleReady" $oidcRoleReady + "machineUserReady" $oidcMachineUserReady + "grantReady" $oidcGrantReady + ) "istioJwt" (dict "enabled" $auth.enabled "rendered" $auth.rendered "issuer" $auth.issuer + "audiences" $auth.effectiveAudiences "waypointReady" $authWaypointReady "requestAuthenticationReady" $authRequestAuthenticationReady "authorizationPolicyReady" $authAuthorizationPolicyReady diff --git a/functions/render/400-auth-zitadel-client.yaml.gotmpl b/functions/render/400-auth-zitadel-client.yaml.gotmpl new file mode 100644 index 0000000..fa1be53 --- /dev/null +++ b/functions/render/400-auth-zitadel-client.yaml.gotmpl @@ -0,0 +1,99 @@ +# code: language=yaml +# +# Dedicated Zitadel client-credentials identity for GitKB sync. +# +# The CLI uses OAuth2 client_credentials. In this Zitadel install the +# working primitive for that flow is a JWT MachineUser with a client secret. +# The Project ID is also the JWT audience Istio can validate. + +{{- $oidc := $state.auth.oidcClient }} +{{- $observedAuth := $state.observed.auth | default dict }} +{{- $observedOidc := $observedAuth.oidcClient | default dict }} + +{{- if $oidc.rendered }} +--- +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Project +metadata: + name: {{ $state.name }}-auth-zitadel-project + annotations: + {{ setResourceNameAnnotation "auth-zitadel-project" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + orgId: {{ $oidc.zitadelOrgId | quote }} + name: {{ $oidc.projectName | quote }} + hasProjectCheck: false + projectRoleAssertion: true + projectRoleCheck: false + providerConfigRef: + name: {{ $oidc.zitadelProviderConfigRef.name }} + kind: {{ $oidc.zitadelProviderConfigRef.kind }} + +--- +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: {{ $state.name }}-auth-zitadel-role + annotations: + {{ setResourceNameAnnotation "auth-zitadel-role" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + orgId: {{ $oidc.zitadelOrgId | quote }} + projectIdRef: + name: {{ $state.name }}-auth-zitadel-project + roleKey: {{ $oidc.roleKey | quote }} + displayName: GitKB Sync + group: gitkb + providerConfigRef: + name: {{ $oidc.zitadelProviderConfigRef.name }} + kind: {{ $oidc.zitadelProviderConfigRef.kind }} + +--- +apiVersion: user.zitadel.m.crossplane.io/v1alpha1 +kind: MachineUser +metadata: + name: {{ $state.name }}-auth-zitadel-machineuser + annotations: + {{ setResourceNameAnnotation "auth-zitadel-machineuser" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + orgId: {{ $oidc.zitadelOrgId | quote }} + userName: {{ $oidc.machineUserName | quote }} + name: GitKB Sync + description: ServiceUser for GitKB CLI sync client_credentials access. + accessTokenType: ACCESS_TOKEN_TYPE_JWT + withSecret: true + writeConnectionSecretToRef: + name: {{ $oidc.clientSecretName }} + providerConfigRef: + name: {{ $oidc.zitadelProviderConfigRef.name }} + kind: {{ $oidc.zitadelProviderConfigRef.kind }} + +{{- if and $observedOidc.projectId $observedOidc.machineUserId }} +--- +apiVersion: user.zitadel.m.crossplane.io/v1alpha1 +kind: Grant +metadata: + name: {{ $state.name }}-auth-zitadel-grant + annotations: + {{ setResourceNameAnnotation "auth-zitadel-grant" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + orgId: {{ $oidc.zitadelOrgId | quote }} + projectId: {{ $observedOidc.projectId | quote }} + userId: {{ $observedOidc.machineUserId | quote }} + roleKeys: + - {{ $oidc.roleKey | quote }} + providerConfigRef: + name: {{ $oidc.zitadelProviderConfigRef.name }} + kind: {{ $oidc.zitadelProviderConfigRef.kind }} +{{- end }} +{{- end }} diff --git a/functions/render/450-auth-istio-jwt.yaml.gotmpl b/functions/render/450-auth-istio-jwt.yaml.gotmpl index d706825..4f69410 100644 --- a/functions/render/450-auth-istio-jwt.yaml.gotmpl +++ b/functions/render/450-auth-istio-jwt.yaml.gotmpl @@ -1,8 +1,8 @@ # code: language=yaml # # Optional Istio ambient waypoint JWT enforcement for GitKB sync traffic. -# This proves and packages the mesh-side auth gate only; GitKB CLI token -# acquisition/injection remains a separate client-side concern. +# When auth.oidcClient is enabled, the generated Zitadel Project ID is +# added to RequestAuthentication audiences after it is observed. {{- $auth := $state.auth.istioJwt }} {{- $observedAuth := $state.observed.auth | default dict }} @@ -69,9 +69,9 @@ spec: {{- if $auth.jwksUri }} jwksUri: {{ $auth.jwksUri | quote }} {{- end }} - {{- if $auth.audiences }} + {{- if $auth.effectiveAudiences }} audiences: - {{- range $audience := $auth.audiences }} + {{- range $audience := $auth.effectiveAudiences }} - {{ $audience | quote }} {{- end }} {{- end }} diff --git a/functions/render/999-status.yaml.gotmpl b/functions/render/999-status.yaml.gotmpl index 717657e..7bec929 100644 --- a/functions/render/999-status.yaml.gotmpl +++ b/functions/render/999-status.yaml.gotmpl @@ -27,10 +27,27 @@ status: routeReady: {{ $s.exposure.routeReady }} certificateReady: {{ $s.exposure.certificateReady }} auth: + oidcClient: + enabled: {{ $s.auth.oidcClient.enabled }} + rendered: {{ $s.auth.oidcClient.rendered }} + issuer: {{ $s.auth.oidcClient.issuer | quote }} + projectId: {{ $s.auth.oidcClient.projectId | quote }} + clientId: {{ $s.auth.oidcClient.clientId | quote }} + clientSecretRef: + name: {{ $s.auth.oidcClient.clientSecretRef.name | quote }} + namespace: {{ $s.auth.oidcClient.clientSecretRef.namespace | quote }} + clientIdKey: {{ $s.auth.oidcClient.clientSecretRef.clientIdKey | quote }} + clientSecretKey: {{ $s.auth.oidcClient.clientSecretRef.clientSecretKey | quote }} + projectReady: {{ $s.auth.oidcClient.projectReady }} + roleReady: {{ $s.auth.oidcClient.roleReady }} + machineUserReady: {{ $s.auth.oidcClient.machineUserReady }} + grantReady: {{ $s.auth.oidcClient.grantReady }} istioJwt: enabled: {{ $s.auth.istioJwt.enabled }} rendered: {{ $s.auth.istioJwt.rendered }} issuer: {{ $s.auth.istioJwt.issuer | quote }} + audiences: + {{- toYaml $s.auth.istioJwt.audiences | nindent 8 }} waypointReady: {{ $s.auth.istioJwt.waypointReady }} requestAuthenticationReady: {{ $s.auth.istioJwt.requestAuthenticationReady }} authorizationPolicyReady: {{ $s.auth.istioJwt.authorizationPolicyReady }} diff --git a/tests/test-render/main.k b/tests/test-render/main.k index 205c197..d270c17 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -185,7 +185,7 @@ items = [ enabled = True issuer = "https://auth.ops.com.ai" jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" - audiences = ["373410628280264299"] + audiences = ["gitkb-project-audience"] } } } @@ -252,7 +252,7 @@ items = [ jwtRules = [{ issuer = "https://auth.ops.com.ai" jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" - audiences = ["373410628280264299"] + audiences = ["gitkb-project-audience"] }] } } @@ -287,6 +287,203 @@ items = [ } } + metav1alpha1.CompositionTest { + metadata.name = "oidc-client-renders-zitadel-identity" + spec = { + compositionPath = "apis/gitkbs/composition.yaml" + xrdPath = "apis/gitkbs/definition.yaml" + timeoutSeconds = 60 + validate = False + xr = _base_xr | { + spec.auth = { + oidcClient = { + enabled = True + issuer = "https://auth.ops.com.ai" + jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" + zitadelProviderConfigRef = { + name = "zitadel-tenant-stack" + kind = "ProviderConfig" + } + zitadelOrgId = "org-123" + projectName = "gitkb-sync" + machineUserName = "gitkb-sync" + } + } + } + assertResources = [ + { + apiVersion = "project.zitadel.m.crossplane.io/v1alpha1" + kind = "Project" + metadata.name = "platform-kb-auth-zitadel-project" + spec.forProvider = { + orgId = "org-123" + name = "gitkb-sync" + hasProjectCheck = False + projectRoleAssertion = True + projectRoleCheck = False + } + spec.providerConfigRef = { + name = "zitadel-tenant-stack" + kind = "ProviderConfig" + } + } + { + apiVersion = "project.zitadel.m.crossplane.io/v1alpha1" + kind = "Role" + metadata.name = "platform-kb-auth-zitadel-role" + spec.forProvider = { + orgId = "org-123" + projectIdRef.name = "platform-kb-auth-zitadel-project" + roleKey = "gitkb:sync" + displayName = "GitKB Sync" + group = "gitkb" + } + } + { + apiVersion = "user.zitadel.m.crossplane.io/v1alpha1" + kind = "MachineUser" + metadata.name = "platform-kb-auth-zitadel-machineuser" + spec.forProvider = { + orgId = "org-123" + userName = "gitkb-sync" + accessTokenType = "ACCESS_TOKEN_TYPE_JWT" + withSecret = True + } + spec.writeConnectionSecretToRef.name = "gitkb-sync-secret" + } + { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "GitKB" + metadata.name = "platform-kb" + status.auth.oidcClient = { + enabled = True + rendered = True + issuer = "https://auth.ops.com.ai" + clientSecretRef = { + name = "gitkb-sync-secret" + namespace = "default" + clientIdKey = "attribute.client_id" + clientSecretKey = "attribute.client_secret" + } + } + } + ] + } + } + + metav1alpha1.CompositionTest { + metadata.name = "oidc-client-observed-project-becomes-istio-audience" + spec = { + compositionPath = "apis/gitkbs/composition.yaml" + xrdPath = "apis/gitkbs/definition.yaml" + timeoutSeconds = 60 + validate = False + xr = _base_xr | { + spec.auth = { + oidcClient = { + enabled = True + issuer = "https://auth.ops.com.ai" + jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" + zitadelProviderConfigRef = { + name = "zitadel-tenant-stack" + kind = "ProviderConfig" + } + zitadelOrgId = "org-123" + projectName = "gitkb-sync" + machineUserName = "gitkb-sync" + } + istioJwt = { + enabled = True + } + } + } + observedResources = [ + { + apiVersion = "project.zitadel.m.crossplane.io/v1alpha1" + kind = "Project" + metadata = { + name = "platform-kb-auth-zitadel-project" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-project"} + } + status = { + atProvider.id = "gitkb-project-id" + conditions = _ready_conditions + } + } + { + apiVersion = "project.zitadel.m.crossplane.io/v1alpha1" + kind = "Role" + metadata = { + name = "platform-kb-auth-zitadel-role" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-role"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "user.zitadel.m.crossplane.io/v1alpha1" + kind = "MachineUser" + metadata = { + name = "platform-kb-auth-zitadel-machineuser" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-machineuser"} + } + status = { + atProvider = { + id = "gitkb-user-id" + userName = "gitkb-sync" + } + conditions = _ready_conditions + } + } + ] + assertResources = [ + { + apiVersion = "user.zitadel.m.crossplane.io/v1alpha1" + kind = "Grant" + metadata.name = "platform-kb-auth-zitadel-grant" + spec.forProvider = { + orgId = "org-123" + projectId = "gitkb-project-id" + userId = "gitkb-user-id" + roleKeys = ["gitkb:sync"] + } + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "platform-kb-auth-requestauthentication" + spec.forProvider.manifest.spec.jwtRules = [{ + issuer = "https://auth.ops.com.ai" + jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" + audiences = ["gitkb-project-id"] + }] + } + { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "GitKB" + metadata.name = "platform-kb" + status.auth = { + oidcClient = { + enabled = True + rendered = True + projectId = "gitkb-project-id" + clientId = "gitkb-sync" + projectReady = True + roleReady = True + machineUserReady = True + grantReady = False + } + istioJwt = { + enabled = True + rendered = True + issuer = "https://auth.ops.com.ai" + audiences = ["gitkb-project-id"] + } + } + } + ] + } + } + metav1alpha1.CompositionTest { metadata.name = "observed-auth-ready-renders-usages-and-ready-status" spec = { diff --git a/upbound.yaml b/upbound.yaml index d365704..72b1e88 100644 --- a/upbound.yaml +++ b/upbound.yaml @@ -16,9 +16,13 @@ spec: kind: Provider package: xpkg.crossplane.io/crossplane-contrib/provider-kubernetes version: '>=v1' + - apiVersion: pkg.crossplane.io/v1 + kind: Provider + package: xpkg.crossplane.io/crossplane-contrib/provider-upjet-zitadel + version: '>=v0.1.1' description: GitKB serves a GitKB knowledge base from the gitkb-server chart and optionally exposes it through Gateway API with TLS certificate and ExternalDNS - wiring. + wiring, plus a dedicated Zitadel client for authenticated sync. license: Apache-2.0 maintainer: Patrick Lee Scott readme: | @@ -29,15 +33,17 @@ spec: A `GitKB` claim owns namespace creation, chart installation, optional Gateway API HTTPRoute exposure, optional cert-manager Certificate - provisioning, and ExternalDNS annotations. `spec.exposure.domain` is the - public domain, while `spec.gitkb.org` and `spec.gitkb.repo` derive the - default public path `//`, for example: + provisioning, ExternalDNS annotations, and optional Zitadel + client-credentials auth. `spec.exposure.domain` is the public domain, while + `spec.gitkb.org` and `spec.gitkb.repo` derive the default public path + `//`, for example: `https://kb.ops.com.ai/hops-ops/hops` The HTTPRoute strips that prefix before forwarding traffic to `git-kb serve`, so many GitKB repos can share one Gateway domain. This - pass is intentionally unauthenticated; add Gateway/OIDC policy before - exposing writable GitKB sync traffic beyond trusted networks. + For authenticated sync traffic, enable `auth.oidcClient` and + `auth.istioJwt` so GitKB gets its own Zitadel MachineUser client and Istio + validates that client's project audience. repository: ghcr.io/hops-ops/gitkb-stack source: github.com/hops-ops/gitkb-stack From e3d474ffc6a380105624a17b13120767f8c3be28 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Wed, 10 Jun 2026 09:57:19 -0500 Subject: [PATCH 4/6] feat: provision gitkb oidc device app --- README.md | 29 +++-- apis/gitkbs/definition.yaml | 105 +++++++++++++----- examples/gitkbs/with-istio-jwt.yaml | 7 +- functions/render/000-state-init.yaml.gotmpl | 26 ++++- functions/render/010-state-status.yaml.gotmpl | 45 ++++++-- .../400-auth-zitadel-client.yaml.gotmpl | 44 +++++++- functions/render/999-status.yaml.gotmpl | 24 ++-- tests/test-render/main.k | 85 ++++++++++++-- upbound.yaml | 7 +- 9 files changed, 290 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index a562667..acf9abb 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,9 @@ For Istio ambient clusters, enable a dedicated GitKB Zitadel client with `auth.oidcClient`, then enable service-level JWT enforcement with `auth.istioJwt`. This renders: -- a GitKB-owned Zitadel Project, Role, MachineUser, and Grant +- a GitKB-owned Zitadel Project +- a native public OIDC Application for CLI device login +- a Role, MachineUser, and Grant for automation sync - ambient enrollment on the GitKB namespace - waypoint labels on the chart-owned GitKB Service - an Istio waypoint `Gateway` @@ -111,15 +113,20 @@ spec: name: zitadel-tenant-stack kind: ProviderConfig zitadelOrgId: "373268222482392664" - machineUserName: gitkb-sync + projectName: gitkb + device: + applicationName: gitkb-cli + machine: + userName: gitkb-sync istioJwt: enabled: true ``` This protects the workload for requests that carry a valid bearer token issued -for the GitKB Project audience. Configure the GitKB CLI remote with -`status.auth.oidcClient.clientId`, the Project audience scope, and the secret -referenced by `status.auth.oidcClient.clientSecretRef`. +for the GitKB Project audience. Configure the GitKB CLI remote with the Project +audience scope, the device-flow client ID from +`status.auth.oidcClient.device.clientSecretRef`, and the machine credentials +from `status.auth.oidcClient.machine.clientSecretRef`. ## Import Existing @@ -146,13 +153,18 @@ url = "https://kb.ops.com.ai/hops-ops/hops" [sync.remotes.origin.auth] issuer = "https://auth.ops.com.ai" -client_id = "gitkb-sync" -client_secret_env = "GITKB_OIDC_CLIENT_SECRET" scopes = [ "openid", "profile", "urn:zitadel:iam:org:project:id::aud", ] + +[sync.remotes.origin.auth.device] +client_id = "" + +[sync.remotes.origin.auth.machine] +client_id = "gitkb-sync" +client_secret_env = "GITKB_OIDC_CLIENT_SECRET" ``` The server route strips `/hops-ops/hops` before forwarding traffic to `git-kb serve`, so the CLI talks to the normal GitKB endpoints under that public path. @@ -169,7 +181,7 @@ The XR publishes the operational fields needed by downstream automation: - `status.exposure.url` - full public URL for the GitKB remote. - `status.exposure.routeReady` - composed HTTPRoute readiness. - `status.exposure.certificateReady` - composed Certificate readiness when enabled. -- `status.auth.oidcClient` - GitKB-owned Zitadel client ID, Project audience, credential Secret reference, and readiness. +- `status.auth.oidcClient` - GitKB-owned Zitadel Project audience, device-flow client Secret reference, machine credential Secret reference, and readiness. - `status.auth.istioJwt` - whether Istio JWT auth rendered and whether the waypoint and policies are ready. ## Composed Resources @@ -179,6 +191,7 @@ The XR publishes the operational fields needed by downstream automation: - `kubernetes.m.crossplane.io/Object` HTTPRoute - optional Gateway API route when `exposure.enabled` is true. - `kubernetes.m.crossplane.io/Object` Certificate - optional cert-manager Certificate when `exposure.certificate.enabled` is true. - `project.zitadel.m.crossplane.io/Project` - optional GitKB Project when `auth.oidcClient.enabled` is true. +- `application.zitadel.m.crossplane.io/Oidc` - optional native public OIDC app for CLI device login when `auth.oidcClient.enabled` is true. - `project.zitadel.m.crossplane.io/Role` - optional GitKB sync Role when `auth.oidcClient.enabled` is true. - `user.zitadel.m.crossplane.io/MachineUser` - optional GitKB OAuth2 client when `auth.oidcClient.enabled` is true. - `user.zitadel.m.crossplane.io/Grant` - optional Role grant to the GitKB MachineUser when `auth.oidcClient.enabled` is true. diff --git a/apis/gitkbs/definition.yaml b/apis/gitkbs/definition.yaml index a82fec0..3871918 100644 --- a/apis/gitkbs/definition.yaml +++ b/apis/gitkbs/definition.yaml @@ -241,13 +241,12 @@ spec: properties: oidcClient: description: | - Dedicated Zitadel client-credentials identity for GitKB - CLI sync. When enabled, the composition creates its own - Zitadel Project, Role, MachineUser, and Grant. The - MachineUser connection Secret contains - `attribute.client_id` and `attribute.client_secret`; - use those values for GitKB CLI OAuth2 - client_credentials authentication. + Dedicated Zitadel identities for GitKB CLI sync. When + enabled, the composition creates its own Zitadel Project, + native OIDC device-flow Application, sync Role, + MachineUser, and Grant. Human users authenticate through + the device-flow Application; automation authenticates + through the MachineUser client_credentials identity. type: object properties: enabled: @@ -261,7 +260,7 @@ spec: description: JWKS URI for the issuer. If omitted, Istio may use OIDC discovery. type: string zitadelProviderConfigRef: - description: Zitadel ProviderConfig used to provision the Project, Role, MachineUser, and Grant. + description: Zitadel ProviderConfig used to provision the Project, device Application, Role, MachineUser, and Grant. type: object properties: name: @@ -270,7 +269,7 @@ spec: type: string default: ProviderConfig zitadelOrgId: - description: Zitadel Org ID that owns the GitKB Project and MachineUser. + description: Zitadel Org ID that owns the GitKB Project, device Application, and MachineUser. type: string projectName: description: Zitadel Project name. Defaults to -gitkb. @@ -279,12 +278,36 @@ spec: description: Zitadel Role key granted to the GitKB MachineUser. type: string default: gitkb:sync - machineUserName: - description: MachineUser username. This becomes the OAuth2 client_id. Defaults to -sync. - type: string - clientSecretName: - description: Connection Secret name for client credentials. Defaults to -secret. - type: string + device: + description: Human device-flow OIDC Application settings. + type: object + properties: + applicationName: + description: Zitadel native OIDC Application name for GitKB CLI device login. Defaults to -cli. + type: string + clientSecretName: + description: Connection Secret containing the generated device-flow client_id and client_secret. Defaults to -oidc-client. + type: string + redirectUris: + description: Redirect URIs to set on the native OIDC Application. Defaults to ["http://localhost"] because the Zitadel provider requires at least one redirect URI even when device flow is used. + type: array + items: + type: string + responseTypes: + description: Response types to set on the native OIDC Application. Defaults to ["OIDC_RESPONSE_TYPE_CODE"] because the Zitadel provider requires at least one response type even when device flow is used. + type: array + items: + type: string + machine: + description: Automation MachineUser settings for client_credentials sync. + type: object + properties: + userName: + description: MachineUser username. This becomes the OAuth2 client_id. Defaults to -sync. + type: string + clientSecretName: + description: Connection Secret name for client credentials. Defaults to -secret. + type: string istioJwt: description: Istio ambient waypoint JWT enforcement for the GitKB Service. Requires an Istio ambient mesh and a valid issuer/JWKS configuration. type: object @@ -392,27 +415,55 @@ spec: projectId: description: Observed Zitadel Project ID. Use as the audience scope with urn:zitadel:iam:org:project:id::aud. type: string - clientId: - description: Observed OAuth2 client_id for GitKB CLI client_credentials authentication. - type: string - clientSecretRef: - description: Secret containing the GitKB OAuth2 client credentials. + device: + description: Observed human device-flow OIDC Application details. type: object properties: - name: + applicationId: + description: Observed Zitadel Application ID. type: string - namespace: - type: string - clientIdKey: + clientSecretRef: + description: Secret containing the generated device-flow client_id and client_secret. + type: object + properties: + name: + type: string + namespace: + type: string + clientIdKey: + type: string + clientSecretKey: + type: string + ready: + type: boolean + machine: + description: Observed automation MachineUser client credentials. + type: object + properties: + clientId: + description: Observed OAuth2 client_id for GitKB CLI client_credentials authentication. type: string - clientSecretKey: + userId: + description: Observed Zitadel MachineUser ID. type: string + clientSecretRef: + description: Secret containing the MachineUser client credentials. + type: object + properties: + name: + type: string + namespace: + type: string + clientIdKey: + type: string + clientSecretKey: + type: string + ready: + type: boolean projectReady: type: boolean roleReady: type: boolean - machineUserReady: - type: boolean grantReady: type: boolean istioJwt: diff --git a/examples/gitkbs/with-istio-jwt.yaml b/examples/gitkbs/with-istio-jwt.yaml index 68df580..930f231 100644 --- a/examples/gitkbs/with-istio-jwt.yaml +++ b/examples/gitkbs/with-istio-jwt.yaml @@ -26,7 +26,10 @@ spec: name: zitadel-tenant-stack kind: ProviderConfig zitadelOrgId: "373268222482392664" - projectName: gitkb-sync - machineUserName: gitkb-sync + projectName: gitkb + device: + applicationName: gitkb-cli + machine: + userName: gitkb-sync istioJwt: enabled: true diff --git a/functions/render/000-state-init.yaml.gotmpl b/functions/render/000-state-init.yaml.gotmpl index 28ee9b9..ee7110e 100644 --- a/functions/render/000-state-init.yaml.gotmpl +++ b/functions/render/000-state-init.yaml.gotmpl @@ -91,8 +91,12 @@ "kind" ($oidcProviderConfigRef.kind | default "ProviderConfig") }} {{- $oidcOrgId := $oidcClient.zitadelOrgId | default "" }} -{{- $oidcMachineUserName := $oidcClient.machineUserName | default (printf "%s-sync" $releaseName) }} -{{- $oidcClientSecretName := $oidcClient.clientSecretName | default (printf "%s-secret" $oidcMachineUserName) }} +{{- $oidcDevice := $oidcClient.device | default dict }} +{{- $oidcMachine := $oidcClient.machine | default dict }} +{{- $oidcDeviceApplicationName := $oidcDevice.applicationName | default (printf "%s-cli" $releaseName) }} +{{- $oidcDeviceClientSecretName := $oidcDevice.clientSecretName | default (printf "%s-oidc-client" $oidcDeviceApplicationName) }} +{{- $oidcMachineUserName := $oidcMachine.userName | default ($oidcClient.machineUserName | default (printf "%s-sync" $releaseName)) }} +{{- $oidcMachineClientSecretName := $oidcMachine.clientSecretName | default ($oidcClient.clientSecretName | default (printf "%s-secret" $oidcMachineUserName)) }} {{- $oidcClientRendered := false }} {{- if and $oidcClientEnabled $oidcProviderConfigRef.name $oidcOrgId }} {{- $oidcClientRendered = true }} @@ -201,10 +205,20 @@ "zitadelOrgId" $oidcOrgId "projectName" ($oidcClient.projectName | default (printf "%s-gitkb" $releaseName)) "roleKey" ($oidcClient.roleKey | default "gitkb:sync") - "machineUserName" $oidcMachineUserName - "clientSecretName" $oidcClientSecretName - "clientIdKey" "attribute.client_id" - "clientSecretKey" "attribute.client_secret" + "device" (dict + "applicationName" $oidcDeviceApplicationName + "clientSecretName" $oidcDeviceClientSecretName + "redirectUris" ($oidcDevice.redirectUris | default (list "http://localhost")) + "responseTypes" ($oidcDevice.responseTypes | default (list "OIDC_RESPONSE_TYPE_CODE")) + "clientIdKey" "attribute.client_id" + "clientSecretKey" "attribute.client_secret" + ) + "machine" (dict + "userName" $oidcMachineUserName + "clientSecretName" $oidcMachineClientSecretName + "clientIdKey" "attribute.client_id" + "clientSecretKey" "attribute.client_secret" + ) ) "istioJwt" (dict "enabled" $istioJwtEnabled diff --git a/functions/render/010-state-status.yaml.gotmpl b/functions/render/010-state-status.yaml.gotmpl index a57215c..e8342ac 100644 --- a/functions/render/010-state-status.yaml.gotmpl +++ b/functions/render/010-state-status.yaml.gotmpl @@ -5,7 +5,7 @@ {{- $observed := $.observed.resources | default dict }} {{- $checkReady := dict }} -{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "auth-zitadel-project" "auth-zitadel-role" "auth-zitadel-machineuser" "auth-zitadel-grant" "auth-waypoint" "auth-requestauthentication" "auth-authorizationpolicy" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" "usage-auth-waypoint-namespace" "usage-auth-jwt-namespace" "usage-auth-policy-namespace" "usage-auth-jwt-gitkb" "usage-auth-policy-gitkb" }} +{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "auth-zitadel-project" "auth-zitadel-device-app" "auth-zitadel-role" "auth-zitadel-machineuser" "auth-zitadel-grant" "auth-waypoint" "auth-requestauthentication" "auth-authorizationpolicy" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" "usage-auth-waypoint-namespace" "usage-auth-jwt-namespace" "usage-auth-policy-namespace" "usage-auth-jwt-gitkb" "usage-auth-policy-gitkb" }} {{- $entry := get $observed $key | default dict }} {{- $resource := $entry.resource | default dict }} {{- $status := $resource.status | default dict }} @@ -31,12 +31,18 @@ {{- $oidcProjectAtProvider := $oidcProjectStatus.atProvider | default dict }} {{- $oidcProjectId := $oidcProjectAtProvider.id | default "" }} +{{- $oidcDeviceEntry := get $observed "auth-zitadel-device-app" | default dict }} +{{- $oidcDeviceResource := $oidcDeviceEntry.resource | default dict }} +{{- $oidcDeviceStatus := $oidcDeviceResource.status | default dict }} +{{- $oidcDeviceAtProvider := $oidcDeviceStatus.atProvider | default dict }} +{{- $oidcDeviceApplicationId := $oidcDeviceAtProvider.id | default "" }} + {{- $oidcMachineUserEntry := get $observed "auth-zitadel-machineuser" | default dict }} {{- $oidcMachineUserResource := $oidcMachineUserEntry.resource | default dict }} {{- $oidcMachineUserStatus := $oidcMachineUserResource.status | default dict }} {{- $oidcMachineUserAtProvider := $oidcMachineUserStatus.atProvider | default dict }} {{- $oidcMachineUserId := $oidcMachineUserAtProvider.id | default ($oidcMachineUserAtProvider.userId | default "") }} -{{- $oidcClientId := $oidcMachineUserAtProvider.userName | default "" }} +{{- $oidcMachineClientId := $oidcMachineUserAtProvider.userName | default "" }} {{- $namespaceReady := get $checkReady "namespace" }} {{- $releaseReady := get $checkReady "helm-release-gitkb" }} @@ -54,6 +60,7 @@ {{- end }} {{- $oidcProjectReady := false }} +{{- $oidcDeviceReady := false }} {{- $oidcRoleReady := false }} {{- $oidcMachineUserReady := false }} {{- $oidcGrantReady := false }} @@ -62,10 +69,11 @@ {{- $oidcReadyForOverall = false }} {{- if $oidcClient.rendered }} {{- $oidcProjectReady = get $checkReady "auth-zitadel-project" }} + {{- $oidcDeviceReady = get $checkReady "auth-zitadel-device-app" }} {{- $oidcRoleReady = get $checkReady "auth-zitadel-role" }} {{- $oidcMachineUserReady = get $checkReady "auth-zitadel-machineuser" }} {{- $oidcGrantReady = get $checkReady "auth-zitadel-grant" }} - {{- $oidcReadyForOverall = and $oidcProjectReady $oidcRoleReady $oidcMachineUserReady $oidcGrantReady }} + {{- $oidcReadyForOverall = and $oidcProjectReady $oidcDeviceReady $oidcRoleReady $oidcMachineUserReady $oidcGrantReady }} {{- end }} {{- end }} @@ -162,12 +170,14 @@ "auth" (dict "oidcClient" (dict "projectReady" $oidcProjectReady + "deviceReady" $oidcDeviceReady "roleReady" $oidcRoleReady "machineUserReady" $oidcMachineUserReady "grantReady" $oidcGrantReady "projectId" $oidcProjectId + "deviceApplicationId" $oidcDeviceApplicationId "machineUserId" $oidcMachineUserId - "clientId" $oidcClientId + "machineClientId" $oidcMachineClientId ) "waypointReady" $authWaypointReady "requestAuthenticationReady" $authRequestAuthenticationReady @@ -208,16 +218,29 @@ "rendered" $oidcClient.rendered "issuer" $oidcClient.issuer "projectId" $oidcProjectId - "clientId" $oidcClientId - "clientSecretRef" (dict - "name" $oidcClient.clientSecretName - "namespace" $state.xrNamespace - "clientIdKey" $oidcClient.clientIdKey - "clientSecretKey" $oidcClient.clientSecretKey + "device" (dict + "applicationId" $oidcDeviceApplicationId + "clientSecretRef" (dict + "name" $oidcClient.device.clientSecretName + "namespace" $state.xrNamespace + "clientIdKey" $oidcClient.device.clientIdKey + "clientSecretKey" $oidcClient.device.clientSecretKey + ) + "ready" $oidcDeviceReady + ) + "machine" (dict + "clientId" $oidcMachineClientId + "userId" $oidcMachineUserId + "clientSecretRef" (dict + "name" $oidcClient.machine.clientSecretName + "namespace" $state.xrNamespace + "clientIdKey" $oidcClient.machine.clientIdKey + "clientSecretKey" $oidcClient.machine.clientSecretKey + ) + "ready" $oidcMachineUserReady ) "projectReady" $oidcProjectReady "roleReady" $oidcRoleReady - "machineUserReady" $oidcMachineUserReady "grantReady" $oidcGrantReady ) "istioJwt" (dict diff --git a/functions/render/400-auth-zitadel-client.yaml.gotmpl b/functions/render/400-auth-zitadel-client.yaml.gotmpl index fa1be53..3462fe9 100644 --- a/functions/render/400-auth-zitadel-client.yaml.gotmpl +++ b/functions/render/400-auth-zitadel-client.yaml.gotmpl @@ -1,10 +1,11 @@ # code: language=yaml # -# Dedicated Zitadel client-credentials identity for GitKB sync. +# Dedicated Zitadel identities for GitKB sync. # -# The CLI uses OAuth2 client_credentials. In this Zitadel install the -# working primitive for that flow is a JWT MachineUser with a client secret. -# The Project ID is also the JWT audience Istio can validate. +# Human CLI login uses a public native OIDC Application with the Device Code +# grant. Automation uses a JWT MachineUser with a client secret for OAuth2 +# client_credentials. The Project ID is also the JWT audience Istio can +# validate. {{- $oidc := $state.auth.oidcClient }} {{- $observedAuth := $state.observed.auth | default dict }} @@ -31,6 +32,37 @@ spec: name: {{ $oidc.zitadelProviderConfigRef.name }} kind: {{ $oidc.zitadelProviderConfigRef.kind }} +--- +apiVersion: application.zitadel.m.crossplane.io/v1alpha1 +kind: Oidc +metadata: + name: {{ $state.name }}-auth-zitadel-device-app + annotations: + {{ setResourceNameAnnotation "auth-zitadel-device-app" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + name: {{ $oidc.device.applicationName | quote }} + projectIdRef: + name: {{ $state.name }}-auth-zitadel-project + appType: OIDC_APP_TYPE_NATIVE + authMethodType: OIDC_AUTH_METHOD_TYPE_NONE + grantTypes: + - OIDC_GRANT_TYPE_DEVICE_CODE + - OIDC_GRANT_TYPE_REFRESH_TOKEN + redirectUris: {{ $oidc.device.redirectUris | toJson }} + responseTypes: {{ $oidc.device.responseTypes | toJson }} + accessTokenType: OIDC_TOKEN_TYPE_JWT + accessTokenRoleAssertion: true + idTokenRoleAssertion: true + idTokenUserinfoAssertion: true + writeConnectionSecretToRef: + name: {{ $oidc.device.clientSecretName }} + providerConfigRef: + name: {{ $oidc.zitadelProviderConfigRef.name }} + kind: {{ $oidc.zitadelProviderConfigRef.kind }} + --- apiVersion: project.zitadel.m.crossplane.io/v1alpha1 kind: Role @@ -64,13 +96,13 @@ spec: managementPolicies: {{ $state.managementPolicies | toJson }} forProvider: orgId: {{ $oidc.zitadelOrgId | quote }} - userName: {{ $oidc.machineUserName | quote }} + userName: {{ $oidc.machine.userName | quote }} name: GitKB Sync description: ServiceUser for GitKB CLI sync client_credentials access. accessTokenType: ACCESS_TOKEN_TYPE_JWT withSecret: true writeConnectionSecretToRef: - name: {{ $oidc.clientSecretName }} + name: {{ $oidc.machine.clientSecretName }} providerConfigRef: name: {{ $oidc.zitadelProviderConfigRef.name }} kind: {{ $oidc.zitadelProviderConfigRef.kind }} diff --git a/functions/render/999-status.yaml.gotmpl b/functions/render/999-status.yaml.gotmpl index 7bec929..3e4f643 100644 --- a/functions/render/999-status.yaml.gotmpl +++ b/functions/render/999-status.yaml.gotmpl @@ -32,15 +32,25 @@ status: rendered: {{ $s.auth.oidcClient.rendered }} issuer: {{ $s.auth.oidcClient.issuer | quote }} projectId: {{ $s.auth.oidcClient.projectId | quote }} - clientId: {{ $s.auth.oidcClient.clientId | quote }} - clientSecretRef: - name: {{ $s.auth.oidcClient.clientSecretRef.name | quote }} - namespace: {{ $s.auth.oidcClient.clientSecretRef.namespace | quote }} - clientIdKey: {{ $s.auth.oidcClient.clientSecretRef.clientIdKey | quote }} - clientSecretKey: {{ $s.auth.oidcClient.clientSecretRef.clientSecretKey | quote }} + device: + applicationId: {{ $s.auth.oidcClient.device.applicationId | quote }} + clientSecretRef: + name: {{ $s.auth.oidcClient.device.clientSecretRef.name | quote }} + namespace: {{ $s.auth.oidcClient.device.clientSecretRef.namespace | quote }} + clientIdKey: {{ $s.auth.oidcClient.device.clientSecretRef.clientIdKey | quote }} + clientSecretKey: {{ $s.auth.oidcClient.device.clientSecretRef.clientSecretKey | quote }} + ready: {{ $s.auth.oidcClient.device.ready }} + machine: + clientId: {{ $s.auth.oidcClient.machine.clientId | quote }} + userId: {{ $s.auth.oidcClient.machine.userId | quote }} + clientSecretRef: + name: {{ $s.auth.oidcClient.machine.clientSecretRef.name | quote }} + namespace: {{ $s.auth.oidcClient.machine.clientSecretRef.namespace | quote }} + clientIdKey: {{ $s.auth.oidcClient.machine.clientSecretRef.clientIdKey | quote }} + clientSecretKey: {{ $s.auth.oidcClient.machine.clientSecretRef.clientSecretKey | quote }} + ready: {{ $s.auth.oidcClient.machine.ready }} projectReady: {{ $s.auth.oidcClient.projectReady }} roleReady: {{ $s.auth.oidcClient.roleReady }} - machineUserReady: {{ $s.auth.oidcClient.machineUserReady }} grantReady: {{ $s.auth.oidcClient.grantReady }} istioJwt: enabled: {{ $s.auth.istioJwt.enabled }} diff --git a/tests/test-render/main.k b/tests/test-render/main.k index d270c17..e0fa39b 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -305,8 +305,13 @@ items = [ kind = "ProviderConfig" } zitadelOrgId = "org-123" - projectName = "gitkb-sync" - machineUserName = "gitkb-sync" + projectName = "gitkb" + device = { + applicationName = "gitkb-cli" + } + machine = { + userName = "gitkb-sync" + } } } } @@ -317,7 +322,7 @@ items = [ metadata.name = "platform-kb-auth-zitadel-project" spec.forProvider = { orgId = "org-123" - name = "gitkb-sync" + name = "gitkb" hasProjectCheck = False projectRoleAssertion = True projectRoleCheck = False @@ -339,6 +344,28 @@ items = [ group = "gitkb" } } + { + apiVersion = "application.zitadel.m.crossplane.io/v1alpha1" + kind = "Oidc" + metadata.name = "platform-kb-auth-zitadel-device-app" + spec.forProvider = { + name = "gitkb-cli" + projectIdRef.name = "platform-kb-auth-zitadel-project" + appType = "OIDC_APP_TYPE_NATIVE" + authMethodType = "OIDC_AUTH_METHOD_TYPE_NONE" + grantTypes = [ + "OIDC_GRANT_TYPE_DEVICE_CODE" + "OIDC_GRANT_TYPE_REFRESH_TOKEN" + ] + redirectUris = ["http://localhost"] + responseTypes = ["OIDC_RESPONSE_TYPE_CODE"] + accessTokenType = "OIDC_TOKEN_TYPE_JWT" + accessTokenRoleAssertion = True + idTokenRoleAssertion = True + idTokenUserinfoAssertion = True + } + spec.writeConnectionSecretToRef.name = "gitkb-cli-oidc-client" + } { apiVersion = "user.zitadel.m.crossplane.io/v1alpha1" kind = "MachineUser" @@ -359,11 +386,21 @@ items = [ enabled = True rendered = True issuer = "https://auth.ops.com.ai" - clientSecretRef = { - name = "gitkb-sync-secret" - namespace = "default" - clientIdKey = "attribute.client_id" - clientSecretKey = "attribute.client_secret" + device = { + clientSecretRef = { + name = "gitkb-cli-oidc-client" + namespace = "default" + clientIdKey = "attribute.client_id" + clientSecretKey = "attribute.client_secret" + } + } + machine = { + clientSecretRef = { + name = "gitkb-sync-secret" + namespace = "default" + clientIdKey = "attribute.client_id" + clientSecretKey = "attribute.client_secret" + } } } } @@ -389,8 +426,13 @@ items = [ kind = "ProviderConfig" } zitadelOrgId = "org-123" - projectName = "gitkb-sync" - machineUserName = "gitkb-sync" + projectName = "gitkb" + device = { + applicationName = "gitkb-cli" + } + machine = { + userName = "gitkb-sync" + } } istioJwt = { enabled = True @@ -419,6 +461,18 @@ items = [ } status.conditions = _ready_conditions } + { + apiVersion = "application.zitadel.m.crossplane.io/v1alpha1" + kind = "Oidc" + metadata = { + name = "platform-kb-auth-zitadel-device-app" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-device-app"} + } + status = { + atProvider.id = "gitkb-cli-app-id" + conditions = _ready_conditions + } + } { apiVersion = "user.zitadel.m.crossplane.io/v1alpha1" kind = "MachineUser" @@ -466,10 +520,17 @@ items = [ enabled = True rendered = True projectId = "gitkb-project-id" - clientId = "gitkb-sync" + device = { + applicationId = "gitkb-cli-app-id" + ready = True + } + machine = { + clientId = "gitkb-sync" + userId = "gitkb-user-id" + ready = True + } projectReady = True roleReady = True - machineUserReady = True grantReady = False } istioJwt = { diff --git a/upbound.yaml b/upbound.yaml index 72b1e88..a9dc826 100644 --- a/upbound.yaml +++ b/upbound.yaml @@ -34,7 +34,7 @@ spec: A `GitKB` claim owns namespace creation, chart installation, optional Gateway API HTTPRoute exposure, optional cert-manager Certificate provisioning, ExternalDNS annotations, and optional Zitadel - client-credentials auth. `spec.exposure.domain` is the public domain, while + OIDC auth. `spec.exposure.domain` is the public domain, while `spec.gitkb.org` and `spec.gitkb.repo` derive the default public path `//`, for example: @@ -43,7 +43,8 @@ spec: The HTTPRoute strips that prefix before forwarding traffic to `git-kb serve`, so many GitKB repos can share one Gateway domain. This For authenticated sync traffic, enable `auth.oidcClient` and - `auth.istioJwt` so GitKB gets its own Zitadel MachineUser client and Istio - validates that client's project audience. + `auth.istioJwt` so GitKB gets its own Zitadel device-flow client for human + login, MachineUser client for automation, and Istio validates the shared + project audience. repository: ghcr.io/hops-ops/gitkb-stack source: github.com/hops-ops/gitkb-stack From 4181b53a3917ffb1c4b5e101edb26dc7784cae7e Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Wed, 10 Jun 2026 11:06:36 -0500 Subject: [PATCH 5/6] fix: protect gitkb oidc auth resources --- functions/render/010-state-status.yaml.gotmpl | 29 +++- .../400-auth-zitadel-client.yaml.gotmpl | 115 +++++++++++++++ tests/test-render/main.k | 131 ++++++++++++++++++ 3 files changed, 273 insertions(+), 2 deletions(-) diff --git a/functions/render/010-state-status.yaml.gotmpl b/functions/render/010-state-status.yaml.gotmpl index e8342ac..1490062 100644 --- a/functions/render/010-state-status.yaml.gotmpl +++ b/functions/render/010-state-status.yaml.gotmpl @@ -5,7 +5,7 @@ {{- $observed := $.observed.resources | default dict }} {{- $checkReady := dict }} -{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "auth-zitadel-project" "auth-zitadel-device-app" "auth-zitadel-role" "auth-zitadel-machineuser" "auth-zitadel-grant" "auth-waypoint" "auth-requestauthentication" "auth-authorizationpolicy" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" "usage-auth-waypoint-namespace" "usage-auth-jwt-namespace" "usage-auth-policy-namespace" "usage-auth-jwt-gitkb" "usage-auth-policy-gitkb" }} +{{- range $key := list "namespace" "helm-release-gitkb" "exposure-certificate" "exposure-httproute" "auth-zitadel-project" "auth-zitadel-device-app" "auth-zitadel-role" "auth-zitadel-machineuser" "auth-zitadel-grant" "auth-waypoint" "auth-requestauthentication" "auth-authorizationpolicy" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" "usage-auth-device-project" "usage-auth-role-project" "usage-auth-grant-project" "usage-auth-grant-machineuser" "usage-auth-grant-role" "usage-auth-waypoint-namespace" "usage-auth-jwt-namespace" "usage-auth-policy-namespace" "usage-auth-jwt-gitkb" "usage-auth-policy-gitkb" }} {{- $entry := get $observed $key | default dict }} {{- $resource := $entry.resource | default dict }} {{- $status := $resource.status | default dict }} @@ -65,6 +65,11 @@ {{- $oidcMachineUserReady := false }} {{- $oidcGrantReady := false }} {{- $oidcReadyForOverall := true }} +{{- $oidcDeviceProjectUsageReady := true }} +{{- $oidcRoleProjectUsageReady := true }} +{{- $oidcGrantProjectUsageReady := true }} +{{- $oidcGrantMachineUserUsageReady := true }} +{{- $oidcGrantRoleUsageReady := true }} {{- if $oidcClient.enabled }} {{- $oidcReadyForOverall = false }} {{- if $oidcClient.rendered }} @@ -73,7 +78,22 @@ {{- $oidcRoleReady = get $checkReady "auth-zitadel-role" }} {{- $oidcMachineUserReady = get $checkReady "auth-zitadel-machineuser" }} {{- $oidcGrantReady = get $checkReady "auth-zitadel-grant" }} - {{- $oidcReadyForOverall = and $oidcProjectReady $oidcDeviceReady $oidcRoleReady $oidcMachineUserReady $oidcGrantReady }} + {{- if and $oidcProjectReady $oidcDeviceReady }} + {{- $oidcDeviceProjectUsageReady = get $checkReady "usage-auth-device-project" }} + {{- end }} + {{- if and $oidcProjectReady $oidcRoleReady }} + {{- $oidcRoleProjectUsageReady = get $checkReady "usage-auth-role-project" }} + {{- end }} + {{- if and $oidcProjectReady $oidcGrantReady }} + {{- $oidcGrantProjectUsageReady = get $checkReady "usage-auth-grant-project" }} + {{- end }} + {{- if and $oidcMachineUserReady $oidcGrantReady }} + {{- $oidcGrantMachineUserUsageReady = get $checkReady "usage-auth-grant-machineuser" }} + {{- end }} + {{- if and $oidcRoleReady $oidcGrantReady }} + {{- $oidcGrantRoleUsageReady = get $checkReady "usage-auth-grant-role" }} + {{- end }} + {{- $oidcReadyForOverall = and $oidcProjectReady $oidcDeviceReady $oidcRoleReady $oidcMachineUserReady $oidcGrantReady $oidcDeviceProjectUsageReady $oidcRoleProjectUsageReady $oidcGrantProjectUsageReady $oidcGrantMachineUserUsageReady $oidcGrantRoleUsageReady }} {{- end }} {{- end }} @@ -178,6 +198,11 @@ "deviceApplicationId" $oidcDeviceApplicationId "machineUserId" $oidcMachineUserId "machineClientId" $oidcMachineClientId + "deviceProjectUsageReady" $oidcDeviceProjectUsageReady + "roleProjectUsageReady" $oidcRoleProjectUsageReady + "grantProjectUsageReady" $oidcGrantProjectUsageReady + "grantMachineUserUsageReady" $oidcGrantMachineUserUsageReady + "grantRoleUsageReady" $oidcGrantRoleUsageReady ) "waypointReady" $authWaypointReady "requestAuthenticationReady" $authRequestAuthenticationReady diff --git a/functions/render/400-auth-zitadel-client.yaml.gotmpl b/functions/render/400-auth-zitadel-client.yaml.gotmpl index 3462fe9..691517f 100644 --- a/functions/render/400-auth-zitadel-client.yaml.gotmpl +++ b/functions/render/400-auth-zitadel-client.yaml.gotmpl @@ -128,4 +128,119 @@ spec: name: {{ $oidc.zitadelProviderConfigRef.name }} kind: {{ $oidc.zitadelProviderConfigRef.kind }} {{- end }} + +{{- if and $observedOidc.projectReady $observedOidc.deviceReady }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-device-before-project + annotations: + {{ setResourceNameAnnotation "usage-auth-device-project" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: project.zitadel.m.crossplane.io/v1alpha1 + kind: Project + resourceRef: + name: {{ $state.name }}-auth-zitadel-project + by: + apiVersion: application.zitadel.m.crossplane.io/v1alpha1 + kind: Oidc + resourceRef: + name: {{ $state.name }}-auth-zitadel-device-app +{{- end }} + +{{- if and $observedOidc.projectReady $observedOidc.roleReady }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-role-before-project + annotations: + {{ setResourceNameAnnotation "usage-auth-role-project" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: project.zitadel.m.crossplane.io/v1alpha1 + kind: Project + resourceRef: + name: {{ $state.name }}-auth-zitadel-project + by: + apiVersion: project.zitadel.m.crossplane.io/v1alpha1 + kind: Role + resourceRef: + name: {{ $state.name }}-auth-zitadel-role +{{- end }} + +{{- if and $observedOidc.projectReady $observedOidc.grantReady }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-grant-before-project + annotations: + {{ setResourceNameAnnotation "usage-auth-grant-project" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: project.zitadel.m.crossplane.io/v1alpha1 + kind: Project + resourceRef: + name: {{ $state.name }}-auth-zitadel-project + by: + apiVersion: user.zitadel.m.crossplane.io/v1alpha1 + kind: Grant + resourceRef: + name: {{ $state.name }}-auth-zitadel-grant +{{- end }} + +{{- if and $observedOidc.machineUserReady $observedOidc.grantReady }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-grant-before-machineuser + annotations: + {{ setResourceNameAnnotation "usage-auth-grant-machineuser" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: user.zitadel.m.crossplane.io/v1alpha1 + kind: MachineUser + resourceRef: + name: {{ $state.name }}-auth-zitadel-machineuser + by: + apiVersion: user.zitadel.m.crossplane.io/v1alpha1 + kind: Grant + resourceRef: + name: {{ $state.name }}-auth-zitadel-grant +{{- end }} + +{{- if and $observedOidc.roleReady $observedOidc.grantReady }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-auth-grant-before-role + annotations: + {{ setResourceNameAnnotation "usage-auth-grant-role" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: project.zitadel.m.crossplane.io/v1alpha1 + kind: Role + resourceRef: + name: {{ $state.name }}-auth-zitadel-role + by: + apiVersion: user.zitadel.m.crossplane.io/v1alpha1 + kind: Grant + resourceRef: + name: {{ $state.name }}-auth-zitadel-grant +{{- end }} {{- end }} diff --git a/tests/test-render/main.k b/tests/test-render/main.k index e0fa39b..c2ff193 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -545,6 +545,137 @@ items = [ } } + metav1alpha1.CompositionTest { + metadata.name = "observed-oidc-client-ready-renders-deletion-usages" + spec = { + compositionPath = "apis/gitkbs/composition.yaml" + xrdPath = "apis/gitkbs/definition.yaml" + timeoutSeconds = 60 + validate = False + xr = _base_xr | { + spec.auth = { + oidcClient = { + enabled = True + issuer = "https://auth.ops.com.ai" + jwksUri = "https://auth.ops.com.ai/oauth/v2/keys" + zitadelProviderConfigRef = { + name = "zitadel-tenant-stack" + kind = "ProviderConfig" + } + zitadelOrgId = "org-123" + projectName = "gitkb" + device = { + applicationName = "gitkb-cli" + } + machine = { + userName = "gitkb-sync" + } + } + } + } + observedResources = [ + { + apiVersion = "project.zitadel.m.crossplane.io/v1alpha1" + kind = "Project" + metadata = { + name = "platform-kb-auth-zitadel-project" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-project"} + } + status = { + atProvider.id = "gitkb-project-id" + conditions = _ready_conditions + } + } + { + apiVersion = "project.zitadel.m.crossplane.io/v1alpha1" + kind = "Role" + metadata = { + name = "platform-kb-auth-zitadel-role" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-role"} + } + status.conditions = _ready_conditions + } + { + apiVersion = "application.zitadel.m.crossplane.io/v1alpha1" + kind = "Oidc" + metadata = { + name = "platform-kb-auth-zitadel-device-app" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-device-app"} + } + status = { + atProvider.id = "gitkb-cli-app-id" + conditions = _ready_conditions + } + } + { + apiVersion = "user.zitadel.m.crossplane.io/v1alpha1" + kind = "MachineUser" + metadata = { + name = "platform-kb-auth-zitadel-machineuser" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-machineuser"} + } + status = { + atProvider = { + id = "gitkb-user-id" + userName = "gitkb-sync" + } + conditions = _ready_conditions + } + } + { + apiVersion = "user.zitadel.m.crossplane.io/v1alpha1" + kind = "Grant" + metadata = { + name = "platform-kb-auth-zitadel-grant" + annotations = {"crossplane.io/composition-resource-name" = "auth-zitadel-grant"} + } + status.conditions = _ready_conditions + } + ] + assertResources = [ + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-device-before-project" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-role-before-project" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-grant-before-project" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-grant-before-machineuser" + } + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "platform-kb-delete-auth-grant-before-role" + } + { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "GitKB" + metadata.name = "platform-kb" + status.auth.oidcClient = { + enabled = True + rendered = True + projectReady = True + device.ready = True + roleReady = True + machine.ready = True + grantReady = True + } + } + ] + } + } + metav1alpha1.CompositionTest { metadata.name = "observed-auth-ready-renders-usages-and-ready-status" spec = { From cbc1c1ac01dc0a09c752ff7507f0cb4aca97041a Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Wed, 10 Jun 2026 11:44:32 -0500 Subject: [PATCH 6/6] fix: enforce gitkb istio auth guardrails --- .../render/200-helm-release-gitkb.yaml.gotmpl | 13 +-- .../render/450-auth-istio-jwt.yaml.gotmpl | 6 +- tests/test-render/main.k | 89 +++++++++++++++++++ upbound.yaml | 8 +- 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/functions/render/200-helm-release-gitkb.yaml.gotmpl b/functions/render/200-helm-release-gitkb.yaml.gotmpl index 3c7c09e..b7c3dd1 100644 --- a/functions/render/200-helm-release-gitkb.yaml.gotmpl +++ b/functions/render/200-helm-release-gitkb.yaml.gotmpl @@ -14,10 +14,6 @@ "seed" $state.seed "persistence" $state.persistence }} -{{- if $state.auth.istioJwt.rendered }} - {{- $chartDefaults = mergeOverwrite $chartDefaults (dict "service" (dict "labels" $state.auth.istioJwt.serviceLabels)) }} -{{- end }} - --- apiVersion: helm.m.crossplane.io/v1beta1 kind: Release @@ -36,10 +32,17 @@ spec: namespace: {{ $state.namespace }} wait: true {{- if $state.overrideAllValues }} + {{- $overrideValues := $state.overrideAllValues }} + {{- if $state.auth.istioJwt.rendered }} + {{- $overrideValues = mergeOverwrite $overrideValues (dict "service" (dict "labels" $state.auth.istioJwt.serviceLabels)) }} + {{- end }} values: - {{- toYaml $state.overrideAllValues | nindent 6 }} + {{- toYaml $overrideValues | nindent 6 }} {{- else }} {{- $merged := mergeOverwrite $chartDefaults $state.values }} + {{- if $state.auth.istioJwt.rendered }} + {{- $merged = mergeOverwrite $merged (dict "service" (dict "labels" $state.auth.istioJwt.serviceLabels)) }} + {{- end }} values: {{- toYaml $merged | nindent 6 }} {{- end }} diff --git a/functions/render/450-auth-istio-jwt.yaml.gotmpl b/functions/render/450-auth-istio-jwt.yaml.gotmpl index 4f69410..c0838a5 100644 --- a/functions/render/450-auth-istio-jwt.yaml.gotmpl +++ b/functions/render/450-auth-istio-jwt.yaml.gotmpl @@ -7,6 +7,10 @@ {{- $auth := $state.auth.istioJwt }} {{- $observedAuth := $state.observed.auth | default dict }} {{- $waypointLabels := mergeOverwrite (dict) $state.labels (dict "istio.io/waypoint-for" "service") }} +{{- $requestPrincipals := $auth.requestPrincipals | default (list) }} +{{- if eq (len $requestPrincipals) 0 }} + {{- $requestPrincipals = list "*" }} +{{- end }} {{- if $auth.rendered }} --- @@ -107,7 +111,7 @@ spec: - from: - source: requestPrincipals: - {{- range $principal := $auth.requestPrincipals }} + {{- range $principal := $requestPrincipals }} - {{ $principal | quote }} {{- end }} providerConfigRef: diff --git a/tests/test-render/main.k b/tests/test-render/main.k index c2ff193..db78166 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -287,6 +287,95 @@ items = [ } } + metav1alpha1.CompositionTest { + metadata.name = "istio-jwt-auth-enforces-service-labels-and-principal-default" + spec = { + compositionPath = "apis/gitkbs/composition.yaml" + xrdPath = "apis/gitkbs/definition.yaml" + timeoutSeconds = 60 + validate = False + xr = _base_xr | { + spec.values = { + service.labels = { + "app.kubernetes.io/custom" = "kept" + "istio.io/use-waypoint" = "user-overridden" + "istio.io/ingress-use-waypoint" = "false" + } + } + spec.auth = { + istioJwt = { + enabled = True + issuer = "https://auth.ops.com.ai" + requestPrincipals = [] + } + } + } + assertResources = [ + { + apiVersion = "helm.m.crossplane.io/v1beta1" + kind = "Release" + metadata.name = "platform-kb" + spec.forProvider.values.service.labels = { + "app.kubernetes.io/custom" = "kept" + "istio.io/use-waypoint" = "platform-kb-waypoint" + "istio.io/ingress-use-waypoint" = "true" + } + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "platform-kb-auth-authorizationpolicy" + spec.forProvider.manifest.spec.rules = [{ + from = [{ + source.requestPrincipals = ["*"] + }] + }] + } + ] + } + } + + metav1alpha1.CompositionTest { + metadata.name = "istio-jwt-auth-enforces-service-labels-with-override-values" + spec = { + compositionPath = "apis/gitkbs/composition.yaml" + xrdPath = "apis/gitkbs/definition.yaml" + timeoutSeconds = 60 + validate = False + xr = _base_xr | { + spec.overrideAllValues = { + service.labels = { + "app.kubernetes.io/custom" = "kept" + "istio.io/use-waypoint" = "user-overridden" + "istio.io/ingress-use-waypoint" = "false" + } + server.port = 9090 + } + spec.auth = { + istioJwt = { + enabled = True + issuer = "https://auth.ops.com.ai" + } + } + } + assertResources = [ + { + apiVersion = "helm.m.crossplane.io/v1beta1" + kind = "Release" + metadata.name = "platform-kb" + spec.forProvider.values = { + service.labels = { + "app.kubernetes.io/custom" = "kept" + "istio.io/use-waypoint" = "platform-kb-waypoint" + "istio.io/ingress-use-waypoint" = "true" + } + server.port = 9090 + } + } + ] + } + } + metav1alpha1.CompositionTest { metadata.name = "oidc-client-renders-zitadel-identity" spec = { diff --git a/upbound.yaml b/upbound.yaml index a9dc826..8d13d24 100644 --- a/upbound.yaml +++ b/upbound.yaml @@ -41,10 +41,10 @@ spec: `https://kb.ops.com.ai/hops-ops/hops` The HTTPRoute strips that prefix before forwarding traffic to - `git-kb serve`, so many GitKB repos can share one Gateway domain. This + `git-kb serve`, so many GitKB repos can share one Gateway domain. For authenticated sync traffic, enable `auth.oidcClient` and - `auth.istioJwt` so GitKB gets its own Zitadel device-flow client for human - login, MachineUser client for automation, and Istio validates the shared - project audience. + `auth.istioJwt` so GitKB provisions its own Zitadel device-flow client for + human login and MachineUser client for automation, while Istio validates + the shared project audience. repository: ghcr.io/hops-ops/gitkb-stack source: github.com/hops-ops/gitkb-stack