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..acf9abb 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,44 @@ 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 a dedicated GitKB Zitadel client with +`auth.oidcClient`, then enable service-level JWT enforcement with +`auth.istioJwt`. This renders: + +- 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` +- service-targeted `RequestAuthentication` using the GitKB Project ID as an audience +- service-targeted `AuthorizationPolicy` requiring a valid JWT + +```yaml +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: "373268222482392664" + 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 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 @@ -113,6 +150,21 @@ 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" +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. @@ -129,6 +181,8 @@ 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 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 @@ -136,6 +190,14 @@ 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. +- `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. +- `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..3871918 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.1". 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,118 @@ spec: - Issuer - ClusterIssuer default: ClusterIssuer + auth: + description: Optional authentication and mesh-policy resources for GitKB sync traffic. + type: object + properties: + oidcClient: + description: | + 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: + 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, device Application, 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, device Application, 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 + 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 + 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 + 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 + 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 +400,90 @@ spec: type: boolean certificateReady: type: boolean + 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 + device: + description: Observed human device-flow OIDC Application details. + type: object + properties: + applicationId: + description: Observed Zitadel Application ID. + type: string + 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 + 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 + grantReady: + type: boolean + istioJwt: + type: object + properties: + enabled: + type: boolean + rendered: + type: boolean + issuer: + type: string + audiences: + type: array + items: + 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..930f231 --- /dev/null +++ b/examples/gitkbs/with-istio-jwt.yaml @@ -0,0 +1,35 @@ +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: + 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: "373268222482392664" + 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 ce4fb5c..ee7110e 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.1" }} {{- $managementPolicies := $spec.managementPolicies | default (list "*") }} {{- $defaultLabels := dict @@ -82,6 +81,45 @@ {{- $certSecretNamespace := $cert.secretNamespace | default ($gatewayRef.namespace | default $namespace) }} {{- $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 "" }} +{{- $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 }} +{{- 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 ($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 }} +{{- 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 +131,7 @@ "chartVersion" $chartVersion "managementPolicies" $managementPolicies "labels" $labels + "namespaceLabels" $namespaceLabels "helmProviderConfigRef" $helmProviderConfigRef "kubernetesProviderConfigRef" $kubernetesProviderConfigRef "serviceName" $serviceName @@ -156,6 +195,52 @@ ) ) ) + "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") + "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 + "rendered" $istioJwtRendered + "issuer" $istioJwtIssuer + "jwksUri" $istioJwtJwksUri + "audiences" ($istioJwt.audiences | default (list)) + "effectiveAudiences" ($istioJwt.audiences | default (list)) + "useOidcClientAudience" $istioJwtUseOidcClientAudience + "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..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" "usage-gitkb-namespace" "usage-httproute-gitkb" "usage-httproute-namespace" "usage-certificate-namespace" }} +{{- 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 }} @@ -22,6 +22,27 @@ {{- $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 "" }} + +{{- $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 "") }} +{{- $oidcMachineClientId := $oidcMachineUserAtProvider.userName | default "" }} {{- $namespaceReady := get $checkReady "namespace" }} {{- $releaseReady := get $checkReady "helm-release-gitkb" }} @@ -38,6 +59,63 @@ {{- $certificateReadyForOverall = $certificateReady }} {{- end }} +{{- $oidcProjectReady := false }} +{{- $oidcDeviceReady := false }} +{{- $oidcRoleReady := false }} +{{- $oidcMachineUserReady := false }} +{{- $oidcGrantReady := false }} +{{- $oidcReadyForOverall := true }} +{{- $oidcDeviceProjectUsageReady := true }} +{{- $oidcRoleProjectUsageReady := true }} +{{- $oidcGrantProjectUsageReady := true }} +{{- $oidcGrantMachineUserUsageReady := true }} +{{- $oidcGrantRoleUsageReady := true }} +{{- if $oidcClient.enabled }} + {{- $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" }} + {{- 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 }} + +{{- $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" }} @@ -55,7 +133,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 $oidcReadyForOverall $authReadyForOverall }} {{- $exposureURL := "" }} {{- if and $exp.enabled $exp.domain }} @@ -75,6 +187,32 @@ "certificateRendered" $certificateRendered "certificateUsesGitkbNamespace" $certificateUsesGitkbNamespace ) + "auth" (dict + "oidcClient" (dict + "projectReady" $oidcProjectReady + "deviceReady" $oidcDeviceReady + "roleReady" $oidcRoleReady + "machineUserReady" $oidcMachineUserReady + "grantReady" $oidcGrantReady + "projectId" $oidcProjectId + "deviceApplicationId" $oidcDeviceApplicationId + "machineUserId" $oidcMachineUserId + "machineClientId" $oidcMachineClientId + "deviceProjectUsageReady" $oidcDeviceProjectUsageReady + "roleProjectUsageReady" $oidcRoleProjectUsageReady + "grantProjectUsageReady" $oidcGrantProjectUsageReady + "grantMachineUserUsageReady" $oidcGrantMachineUserUsageReady + "grantRoleUsageReady" $oidcGrantRoleUsageReady + ) + "waypointReady" $authWaypointReady + "requestAuthenticationReady" $authRequestAuthenticationReady + "authorizationPolicyReady" $authAuthorizationPolicyReady + "waypointNamespaceUsageReady" $authWaypointNamespaceUsageReady + "jwtNamespaceUsageReady" $authJwtNamespaceUsageReady + "policyNamespaceUsageReady" $authPolicyNamespaceUsageReady + "jwtGitkbUsageReady" $authJwtGitkbUsageReady + "policyGitkbUsageReady" $authPolicyGitkbUsageReady + ) ) }} {{- $state = set $state "status" (dict @@ -99,4 +237,45 @@ "routeReady" $routeReady "certificateReady" $certificateReady ) + "auth" (dict + "oidcClient" (dict + "enabled" $oidcClient.enabled + "rendered" $oidcClient.rendered + "issuer" $oidcClient.issuer + "projectId" $oidcProjectId + "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 + "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/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..b7c3dd1 100644 --- a/functions/render/200-helm-release-gitkb.yaml.gotmpl +++ b/functions/render/200-helm-release-gitkb.yaml.gotmpl @@ -14,7 +14,6 @@ "seed" $state.seed "persistence" $state.persistence }} - --- apiVersion: helm.m.crossplane.io/v1beta1 kind: Release @@ -33,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/400-auth-zitadel-client.yaml.gotmpl b/functions/render/400-auth-zitadel-client.yaml.gotmpl new file mode 100644 index 0000000..691517f --- /dev/null +++ b/functions/render/400-auth-zitadel-client.yaml.gotmpl @@ -0,0 +1,246 @@ +# code: language=yaml +# +# Dedicated Zitadel identities for GitKB sync. +# +# 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 }} +{{- $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: 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 +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.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.machine.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 }} + +{{- 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/functions/render/450-auth-istio-jwt.yaml.gotmpl b/functions/render/450-auth-istio-jwt.yaml.gotmpl new file mode 100644 index 0000000..c0838a5 --- /dev/null +++ b/functions/render/450-auth-istio-jwt.yaml.gotmpl @@ -0,0 +1,235 @@ +# code: language=yaml +# +# Optional Istio ambient waypoint JWT enforcement for GitKB sync traffic. +# 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 }} +{{- $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 }} +--- +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.effectiveAudiences }} + audiences: + {{- range $audience := $auth.effectiveAudiences }} + - {{ $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 := $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..3e4f643 100644 --- a/functions/render/999-status.yaml.gotmpl +++ b/functions/render/999-status.yaml.gotmpl @@ -26,3 +26,38 @@ status: certificateEnabled: {{ $s.exposure.certificateEnabled }} 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 }} + 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 }} + 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 b5efc51..db78166 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.1" } namespace = "gitkb" values = { @@ -172,6 +172,762 @@ 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 = ["gitkb-project-audience"] + } + } + } + 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 = ["gitkb-project-audience"] + }] + } + } + } + { + 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 = "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 = { + 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" + } + } + } + } + assertResources = [ + { + apiVersion = "project.zitadel.m.crossplane.io/v1alpha1" + kind = "Project" + metadata.name = "platform-kb-auth-zitadel-project" + spec.forProvider = { + orgId = "org-123" + name = "gitkb" + 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 = "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" + 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" + 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" + } + } + } + } + ] + } + } + + 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" + device = { + applicationName = "gitkb-cli" + } + machine = { + userName = "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 = "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 + } + } + ] + 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" + device = { + applicationId = "gitkb-cli-app-id" + ready = True + } + machine = { + clientId = "gitkb-sync" + userId = "gitkb-user-id" + ready = True + } + projectReady = True + roleReady = True + grantReady = False + } + istioJwt = { + enabled = True + rendered = True + issuer = "https://auth.ops.com.ai" + audiences = ["gitkb-project-id"] + } + } + } + ] + } + } + + 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 = { + 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 = { diff --git a/upbound.yaml b/upbound.yaml index d365704..8d13d24 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,18 @@ 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 + OIDC 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. + `git-kb serve`, so many GitKB repos can share one Gateway domain. + For authenticated sync traffic, enable `auth.oidcClient` and + `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