From 927482536d7177576fda4acb88c2777bf8285427 Mon Sep 17 00:00:00 2001 From: Min Zhang Date: Fri, 6 Feb 2026 16:28:55 -0500 Subject: [PATCH 1/2] feat: BYO container registry support Restructure registry configuration to support three deployment states: - Fresh install: No registry configured (both disabled by default) - Built-in Quay: quay.enabled=true uses hub/infra/quay/ vault path - External/BYO: externalRegistry.enabled=true uses hub/infra/registry/ path Changes: - Add externalRegistry.enabled flag to supply-chain and qtodo charts - Separate vault paths for built-in Quay vs external registry - Templates conditionally select vault path based on enabled flags - Update supply-chain.md with BYO registry setup instructions - Add helm template method and oc monitoring commands to supply-chain.md - Follow VP best practice: external registry secrets in local ~/values-secret.yaml To enable supply-chain: 1. Uncomment openshift-pipelines namespace and subscription 2. Uncomment supply-chain vault role (JWT auth) 3. Configure registry (BYO or built-in Quay) in application overrides - For BYO registry: - Set externalRegistry.enabled=true and configure registry settings - Add registry credentials to ~/values-secret.yaml - For built-in Quay: - Enable openshift-storage namespace - Enable ODF, NooBaa MCG - Enable Quay operator subscription, quay-registry application 4. RHTAS (signing): Enable rhtas-operator subscription and trusted-artifact-signer namespace 5. RHTPA (SBOM): Enable rhtpa-operator subscription, ODF, NooBaa, and trusted-profile-analyzer Signed-off-by: Min Zhang --- .../templates/registry-external-secret.yaml | 11 +- charts/qtodo/values.yaml | 23 ++- .../templates/pipeline-qtodo.yaml | 11 +- .../templates/pipelinerun-qtodo.yaml | 21 +++ .../rbac/registry-image-namespace.yaml | 28 ++++ ...egistry-pass.yaml => qtodo-quay-pass.yaml} | 11 +- .../secrets/qtodo-registry-auth.yaml | 34 ++++- charts/supply-chain/values.yaml | 47 ++++-- docs/supply-chain.md | 136 ++++++++++++++++++ values-hub.yaml | 81 ++++++++--- values-secret.yaml.template | 45 +++--- 11 files changed, 383 insertions(+), 65 deletions(-) create mode 100644 charts/supply-chain/templates/pipelinerun-qtodo.yaml create mode 100644 charts/supply-chain/templates/rbac/registry-image-namespace.yaml rename charts/supply-chain/templates/secrets/{qtodo-registry-pass.yaml => qtodo-quay-pass.yaml} (55%) diff --git a/charts/qtodo/templates/registry-external-secret.yaml b/charts/qtodo/templates/registry-external-secret.yaml index 8646909d..6b6979c9 100644 --- a/charts/qtodo/templates/registry-external-secret.yaml +++ b/charts/qtodo/templates/registry-external-secret.yaml @@ -18,7 +18,7 @@ spec: .dockerconfigjson: | { "auths": { - "{{ .Values.app.images.main.registry.domain | default (printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain) }}": { + "{{ required "app.images.main.registry.domain is required when registry.auth is enabled" .Values.app.images.main.registry.domain }}": { "auth": "{{ `{{ printf "%s:%s" "` }}{{ .Values.app.images.main.registry.user }}{{ `" .password | b64enc }}` }}" } } @@ -26,6 +26,11 @@ spec: data: - secretKey: password remoteRef: - key: {{ .Values.app.images.main.registry.vaultPath }} - property: {{ .Values.app.images.main.registry.passwordVaultKey }} + {{- if .Values.app.images.main.registry.builtinQuay.enabled }} + key: {{ .Values.app.images.main.registry.builtinQuay.vaultPath }} + property: {{ .Values.app.images.main.registry.builtinQuay.passwordVaultKey }} + {{- else if .Values.app.images.main.registry.externalRegistry.enabled }} + key: {{ .Values.app.images.main.registry.externalRegistry.vaultPath }} + property: {{ .Values.app.images.main.registry.externalRegistry.passwordVaultKey }} + {{- end }} {{- end }} \ No newline at end of file diff --git a/charts/qtodo/values.yaml b/charts/qtodo/values.yaml index d5fd70c8..52b13d33 100644 --- a/charts/qtodo/values.yaml +++ b/charts/qtodo/values.yaml @@ -15,13 +15,26 @@ app: # Modified to Always to force a pull so we can test changes to the container image without requiring manual deletion of images or restarts of argo pullPolicy: Always registry: + # auth: controls whether to create registry auth secret + # Set to true when using private registry (built-in Quay or external) auth: false secretName: qtodo-registry-auth - user: quay-user - # domain: quay-registry-quay-quay-enterprise.apps.example.com - # Registry credentials - stored in quay path - vaultPath: secret/data/hub/infra/quay/quay-users - passwordVaultKey: quay-user-password + user: registry-user + # domain: registry.example.com # REQUIRED when auth is enabled + + # Built-in Quay registry (optional) + # When enabled, uses auto-generated credentials from Vault + builtinQuay: + enabled: false + vaultPath: secret/data/hub/infra/quay/quay-users + passwordVaultKey: quay-user-password + + # External/BYO registry (optional) + # When enabled, uses user-provided credentials from Vault + externalRegistry: + enabled: false + vaultPath: secret/data/hub/infra/registry/registry-user + passwordVaultKey: registry-password spiffeHelper: name: registry.redhat.io/zero-trust-workload-identity-manager/spiffe-helper-rhel9 version: v0.10.0 diff --git a/charts/supply-chain/templates/pipeline-qtodo.yaml b/charts/supply-chain/templates/pipeline-qtodo.yaml index 13ae2c8c..39fe8494 100644 --- a/charts/supply-chain/templates/pipeline-qtodo.yaml +++ b/charts/supply-chain/templates/pipeline-qtodo.yaml @@ -1,3 +1,12 @@ +{{- /* Determine registry domain: auto-construct for built-in Quay, require for external */ -}} +{{- $registryDomain := "" -}} +{{- if .Values.registry.domain -}} + {{- $registryDomain = .Values.registry.domain -}} +{{- else if .Values.quay.enabled -}} + {{- $registryDomain = printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain -}} +{{- else -}} + {{- fail "registry.domain is required for external registry" -}} +{{- end -}} --- apiVersion: tekton.dev/v1beta1 kind: Pipeline @@ -25,7 +34,7 @@ spec: - name: image-target type: string description: qtodo image push destination (e.g. quay.io/ztvp/qtodo:latest) - default: {{ .Values.registry.domain | default (printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain) }}/{{ .Values.registry.org }}/{{ .Values.registry.repo }}:{{ .Values.qtodo.tag }} + default: {{ $registryDomain }}/{{ .Values.registry.org }}/{{ .Values.registry.repo }}:{{ .Values.qtodo.tag }} - name: image-tls-verify type: string description: Whether to verify TLS when pushing to the OCI registry diff --git a/charts/supply-chain/templates/pipelinerun-qtodo.yaml b/charts/supply-chain/templates/pipelinerun-qtodo.yaml new file mode 100644 index 00000000..820c8da4 --- /dev/null +++ b/charts/supply-chain/templates/pipelinerun-qtodo.yaml @@ -0,0 +1,21 @@ +{{- if .Values.pipelinerun.enabled }} +--- +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + generateName: qtodo-supply-chain- + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + pipelineRef: + name: qtodo-supply-chain + workspaces: + - name: qtodo-source + persistentVolumeClaim: + claimName: qtodo-workspace-source + - name: registry-auth-config + secret: + secretName: {{ .Values.registry.authSecretName }} +{{- end }} diff --git a/charts/supply-chain/templates/rbac/registry-image-namespace.yaml b/charts/supply-chain/templates/rbac/registry-image-namespace.yaml new file mode 100644 index 00000000..35f3ab76 --- /dev/null +++ b/charts/supply-chain/templates/rbac/registry-image-namespace.yaml @@ -0,0 +1,28 @@ +{{- if and (index .Values.registry "embeddedOCP") (index .Values.registry.embeddedOCP "ensureImageNamespaceRBAC") }} +# When using the embedded OCP image registry, the pipeline pushes to a namespace +# that matches registry.org (e.g. ztvp). This ensures that namespace exists and +# the pipeline SA has system:image-builder so the push succeeds (transparent to the user). +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Values.registry.org }} + annotations: + argocd.argoproj.io/sync-wave: "0" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: pipeline-image-builder + namespace: {{ .Values.registry.org }} + annotations: + argocd.argoproj.io/sync-wave: "0" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:image-builder +subjects: + - kind: ServiceAccount + name: pipeline + namespace: {{ .Values.global.namespace }} +{{- end }} diff --git a/charts/supply-chain/templates/secrets/qtodo-registry-pass.yaml b/charts/supply-chain/templates/secrets/qtodo-quay-pass.yaml similarity index 55% rename from charts/supply-chain/templates/secrets/qtodo-registry-pass.yaml rename to charts/supply-chain/templates/secrets/qtodo-quay-pass.yaml index 65406f8d..d66f6507 100644 --- a/charts/supply-chain/templates/secrets/qtodo-registry-pass.yaml +++ b/charts/supply-chain/templates/secrets/qtodo-quay-pass.yaml @@ -1,3 +1,10 @@ +{{/* + Quay User Provisioner Secret + Purpose: Provides password for the Quay user provisioner job to create/update users in built-in Quay + Used by: quay-user-job.yaml (CronJob that provisions Quay users) + Only created when: quay.enabled=true (built-in Quay registry) + Not used for: BYO/external registry (use qtodo-registry-auth.yaml instead) +*/}} {{- if eq .Values.quay.enabled true }} --- apiVersion: "external-secrets.io/v1beta1" @@ -19,6 +26,6 @@ spec: data: - secretKey: password remoteRef: - key: {{ .Values.registry.vaultPath }} - property: {{ .Values.registry.passwordVaultKey }} + key: {{ .Values.quay.vaultPath }} + property: {{ .Values.quay.passwordVaultKey }} {{- end }} \ No newline at end of file diff --git a/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml b/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml index 416e8020..eb889c9f 100644 --- a/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml +++ b/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml @@ -1,3 +1,25 @@ +{{/* + Pipeline Registry Auth Secret + Purpose: Provides dockerconfigjson for pipeline to push/pull images + Used by: Tekton pipeline tasks (build-image, sign-image, verify-image) + Created when: quay.enabled=true OR externalRegistry.enabled=true + Vault path: Automatically selects based on which registry is enabled + - Built-in Quay: quay.vaultPath (auto-generated credentials) + - BYO Registry: externalRegistry.vaultPath (user-provided credentials) + Registry domain: + - Built-in Quay: auto-constructed as quay-registry-quay-quay-enterprise. + - BYO Registry: must be explicitly set via registry.domain +*/}} +{{- if or .Values.quay.enabled .Values.externalRegistry.enabled }} +{{- /* Determine registry domain: auto-construct for built-in Quay, require for external */ -}} +{{- $registryDomain := "" -}} +{{- if .Values.registry.domain -}} + {{- $registryDomain = .Values.registry.domain -}} +{{- else if .Values.quay.enabled -}} + {{- $registryDomain = printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain -}} +{{- else -}} + {{- fail "registry.domain is required for external registry" -}} +{{- end -}} --- apiVersion: "external-secrets.io/v1beta1" kind: ExternalSecret @@ -17,7 +39,7 @@ spec: .dockerconfigjson: | { "auths": { - "{{ .Values.registry.domain | default (printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain) }}": { + "{{ $registryDomain }}": { "auth": "{{ `{{ printf "%s:%s" "` }}{{ .Values.registry.user }}{{ `" .password | b64enc }}` }}" } } @@ -25,5 +47,11 @@ spec: data: - secretKey: password remoteRef: - key: {{ .Values.registry.vaultPath }} - property: {{ .Values.registry.passwordVaultKey }} \ No newline at end of file + {{- if .Values.quay.enabled }} + key: {{ .Values.quay.vaultPath }} + property: {{ .Values.quay.passwordVaultKey }} + {{- else if .Values.externalRegistry.enabled }} + key: {{ .Values.externalRegistry.vaultPath }} + property: {{ .Values.externalRegistry.passwordVaultKey }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/supply-chain/values.yaml b/charts/supply-chain/values.yaml index 4a54d048..aed322b4 100644 --- a/charts/supply-chain/values.yaml +++ b/charts/supply-chain/values.yaml @@ -26,26 +26,55 @@ qtodo: buildCmd: "./mvnw -s settings.xml package -DskipTests -Dquarkus.package.jar.type=uber-jar" containerfile: "./Containerfile" -# quay registry configuration -# used to create a new user in quay. Generic registry configuration is below. +# =========================================================================== +# BUILT-IN QUAY REGISTRY (optional) +# When enabled, deploys internal Quay registry with auto-generated credentials +# =========================================================================== quay: enabled: true email: "quay-user@example.com" + # Vault path for auto-generated Quay credentials + vaultPath: "secret/data/hub/infra/quay/quay-users" + passwordVaultKey: "quay-user-password" + # User provisioner job settings job: image: registry.access.redhat.com/ubi9/ubi:9.7-1764794285 schedule: "*/5 * * * *" -# container registry configuration +# =========================================================================== +# EXTERNAL/BYO REGISTRY (optional) +# User-provided credentials for external registry (quay.io, ghcr.io, etc.) +# Enable this when using an external registry instead of built-in Quay +# =========================================================================== +externalRegistry: + enabled: false + # Vault path for user-provided credentials + vaultPath: "secret/data/hub/infra/registry/registry-user" + passwordVaultKey: "registry-password" + +# =========================================================================== +# COMMON REGISTRY SETTINGS (shared by both built-in Quay and external registry) +# =========================================================================== registry: - # Commented to generate it dynamically - # domain: "quay-registry-quay-quay-enterprise.hub.example.com" + # For built-in Quay: domain is auto-constructed from hubClusterDomain + # For external registry: REQUIRED - set explicitly (e.g., quay.io, ghcr.io) + # domain: "registry.example.com" org: "ztvp" repo: "qtodo" tlsVerify: "true" - user: "quay-user" - passwordVaultKey: "quay-user-password" - # Infrastructure secrets - stored in quay path - vaultPath: "secret/data/hub/infra/quay/quay-users" + user: "registry-user" + # Secret name for registry auth (dockerconfigjson) + authSecretName: "qtodo-registry-auth" + # Embedded OCP registry only: create image namespace (registry.org) and grant + # pipeline SA system:image-builder so the pipeline can push. Set to true only when + # using the in-cluster OpenShift image registry; leave false for quay.io or other external registries. + embeddedOCP: + ensureImageNamespaceRBAC: false + +# pipeline run configuration +pipelinerun: + # Set to true to automatically trigger a pipeline run on ArgoCD sync + enabled: false # spire configuration spire: diff --git a/docs/supply-chain.md b/docs/supply-chain.md index d2459fad..55cb4d9e 100644 --- a/docs/supply-chain.md +++ b/docs/supply-chain.md @@ -20,6 +20,99 @@ In our demo, we will use a number of additional ZTVP components. These component * [Multicloud Object Gateway](https://docs.redhat.com/en/documentation/red_hat_openshift_container_storage/4.8/html/managing_hybrid_and_multicloud_resources/index) is a data service for OpenShift that provides an S3-compatible object storage. In our case, this component is necessary to provide a storage system to Quay. * [Red Hat OpenShift Pipelines](https://docs.redhat.com/en/documentation/red_hat_openshift_pipelines/1.20) is a cloud-native CI/CD solution built on the Tekton framework. We will use this product to automate our secure supply chain process, but you could use your own CI/CD solution if one exists. +## Bring Your Own (BYO) Container Registry + +By default, ZTVP deploys a built-in Red Hat Quay registry. However, you can use your own container registry (e.g., quay.io, Docker Hub, GitHub Container Registry, or a private registry) instead. + +### Configuration Steps + +1. **Disable built-in Quay registry** (optional - if not using Quay): Comment out the Quay-related applications in `values-hub.yaml`: `quay-enterprise` namespace, `quay-operator` subscription, and `quay-registry` application. + +2. **Configure registry credentials in Vault**: Per VP rule, add your registry credentials to `~/values-secrets.yaml` (or `~/values-secret.yaml` / `~/values-secret-layered-zero-trust.yaml` per VP lookup order): + + ```bash + # Copy template to local file if not already done + cp values-secret.yaml.template ~/values-secrets.yaml + ``` + + Add the registry-user secret (same format for **BYO external registry** and **embedded OCP registry**): + + ```yaml + - name: registry-user + vaultPrefixes: + - hub/infra/registry + fields: + - name: registry-password + value: "REPLACE_WITH_REGISTRY_TOKEN" + onMissingValue: error + ``` + + Replace `REPLACE_WITH_REGISTRY_TOKEN` with: + * **Embedded OCP registry:** output of `oc whoami -t` (after `oc login`). + * **External registry (BYO):** your registry token or password (e.g. quay.io, ghcr.io). + + > **Note**: Never commit `~/values-secrets.yaml` (or your local values-secret file) to git. This file contains sensitive credentials and should remain local. + +3. **Set registry configuration in values-hub.yaml**: For the supply-chain application, add these overrides: + + ```yaml + overrides: + # Disable built-in Quay + - name: quay.enabled + value: "false" + # Enable external registry + - name: externalRegistry.enabled + value: "true" + # External registry settings + - name: registry.domain + value: "your-registry.example.com" + - name: registry.user + value: "your-username" + - name: registry.org + value: "your-org" + ``` + +4. **Configure qtodo for custom registry** (if pulling from custom registry): + + ```yaml + overrides: + - name: app.images.main.registry.auth + value: true + - name: app.images.main.registry.domain + value: "your-registry.example.com" + - name: app.images.main.registry.user + value: "your-username" + ``` + +### Required Configuration + +| Parameter | Description | Example | +| --------- | ----------- | ------- | +| `registry.domain` | Registry hostname (required for BYO only) | `quay.io`, `ghcr.io`, `registry.example.com` | +| `registry.org` | Organization/namespace | `my-org` | +| `registry.repo` | Repository name | `qtodo` | +| `registry.user` | Registry username | `my-robot-account` | +| `quay.enabled` | Set to `false` for BYO registry | `false` | + +> **Note**: For built-in Quay registry, `registry.domain` is automatically constructed as `quay-registry-quay-quay-enterprise.` and does not need to be specified. For BYO/external registries, `registry.domain` is **required**. + +### Vault Paths + +Registry credentials are stored at different paths based on registry type: + +| Registry Type | Vault Path | Password Key | +| --------------- | ---------------------------------------------- | -------------------- | +| Built-in Quay | `secret/data/hub/infra/quay/quay-users` | `quay-user-password` | +| BYO Registry | `secret/data/hub/infra/registry/registry-user` | `registry-password` | + +The chart automatically selects the correct vault path based on the enabled flags: + +* `quay.enabled=true`: Uses built-in Quay vault path +* `externalRegistry.enabled=true`: Uses external registry vault path +* Both disabled (default): No registry auth secret created (fresh install state) + +The Vault policy `hub-supply-chain-jwt-secret` grants read access to both paths for the pipeline service account. + ## Automatic approach To automate the application building and certifying process, we will use _Red Hat OpenShift Pipelines_. @@ -78,12 +171,55 @@ Using the previously created definition, start a new execution of the pipeline u oc create -f qtodo-pipeline.yaml ``` +#### Using Helm Template + +You can also trigger a pipeline run using the Helm template included in the chart. + +**For Built-in Quay Registry:** + +```shell +helm template supply-chain charts/supply-chain \ + --set pipelinerun.enabled=true \ + --set quay.enabled=true \ + --set global.namespace=layered-zero-trust-hub \ + --set global.hubClusterDomain=apps.example.com \ + --show-only templates/pipelinerun-qtodo.yaml | oc create -f - +``` + +> **Note**: For built-in Quay, `registry.domain` is auto-constructed from `global.hubClusterDomain`. + +**For BYO/External Registry:** + +```shell +helm template supply-chain charts/supply-chain \ + --set pipelinerun.enabled=true \ + --set externalRegistry.enabled=true \ + --set global.namespace=layered-zero-trust-hub \ + --set registry.domain=quay.io \ + --show-only templates/pipelinerun-qtodo.yaml | oc create -f - +``` + +This renders the PipelineRun template with the correct PVC and secret workspace bindings, then creates it in the cluster. + You can review the current pipeline logs using the [Tekton CLI](https://tekton.dev/docs/cli/). ```shell tkn pipeline logs -n layered-zero-trust-hub -L -f ``` +Or use `oc` commands to monitor progress: + +```shell +# List pipeline runs +oc get pipelinerun -n layered-zero-trust-hub + +# Check task status for a specific run +oc get taskruns -n layered-zero-trust-hub -l tekton.dev/pipelineRun= + +# View logs for a specific task +oc logs -n layered-zero-trust-hub -l tekton.dev/pipelineRun=,tekton.dev/pipelineTask= +``` + ### Pipeline tasks The pipeline we have prepared has the following steps: diff --git a/values-hub.yaml b/values-hub.yaml index b8da45d9..c5b506d4 100644 --- a/values-hub.yaml +++ b/values-hub.yaml @@ -286,6 +286,9 @@ clusterGroup: path "secret/data/hub/infra/quay/*" { capabilities = ["read"] } + path "secret/data/hub/infra/registry/*" { + capabilities = ["read"] + } path "secret/data/hub/infra/rhtpa/rhtpa-oidc-cli" { capabilities = ["read"] } @@ -434,8 +437,7 @@ clusterGroup: project: hub path: charts/qtodo ignoreDifferences: - - group: "" - kind: ServiceAccount + - kind: ServiceAccount jqPathExpressions: - .imagePullSecrets[]|select(.name | contains("-dockercfg-")) overrides: @@ -463,26 +465,61 @@ clusterGroup: # value: quay-user-password # Secure Supply Chain - Uncomment to enable # supply-chain: - # name: supply-chain - # project: hub - # path: charts/supply-chain - # ignoreDifferences: - # - group: "" - # kind: ServiceAccount - # jqPathExpressions: - # - .imagePullSecrets[]|select(.name | contains("-dockercfg-")) - # overrides: - # # Don't forget to uncomment the RHTAS and RHTPA components in this same file - # - name: rhtas.enabled - # value: true - # - name: rhtpa.enabled - # value: true - # - name: registry.tlsVerify - # value: "false" - # - name: registry.user - # value: quay-admin - # - name: registry.passwordVaultKey - # value: quay-admin-password + # name: supply-chain + # project: hub + # path: charts/supply-chain + # ignoreDifferences: + # - kind: ServiceAccount + # jqPathExpressions: + # - .imagePullSecrets[]|select(.name | contains("-dockercfg-")) + # overrides: + # # ============================================================ + # # OPTION 1: Built-in Quay Registry + # # Requires: quay-enterprise namespace, quay-operator, quay-registry app + # # Note: registry.domain is auto-constructed from hubClusterDomain + # # ============================================================ + # # - name: quay.enabled + # # value: "true" + # # - name: externalRegistry.enabled + # # value: "false" + # # - name: registry.tlsVerify + # # value: "false" + # # - name: registry.user + # # value: quay-user + # # ============================================================ + # # OPTION 2: BYO/External Registry + # # Requires: registry credentials in ~/values-secret.yaml + # # Note: registry.domain is REQUIRED for external registry + # # ============================================================ + # # - name: quay.enabled + # # value: "false" + # # - name: externalRegistry.enabled + # # value: "true" + # # - name: registry.domain + # # value: quay.io + # # - name: registry.org + # # value: your-org + # # - name: registry.user + # # value: your-username + # # ============================================================ + # # OPTION 3: Embedded OCP Registry (comment out Option 1, 2; uncomment below) + # # ============================================================ + # # - name: registry.domain + # # value: default-route-openshift-image-registry.apps. + # # - name: registry.org + # # value: ztvp + # # - name: registry.user + # # value: admin + # # Embedded OCP registry only: create image namespace and grant pipeline push (transparent) + # # - name: registry.embeddedOCP.ensureImageNamespaceRBAC + # # value: "true" + # # ============================================================ + # # Enable RHTAS signing + # # - name: rhtas.enabled + # # value: "true" + # # Enable RHTPA SBOM upload + # # - name: rhtpa.enabled + # # value: "true" argoCD: resourceHealthChecks: - check: | diff --git a/values-secret.yaml.template b/values-secret.yaml.template index e1f48ac9..16cbc205 100644 --- a/values-secret.yaml.template +++ b/values-secret.yaml.template @@ -16,7 +16,8 @@ version: "2.0" # Infrastructure Secrets (hub/infra/*): # hub/infra/keycloak/ - Keycloak infrastructure secrets # hub/infra/rhtpa/ - RHTPA infrastructure secrets -# hub/infra/quay/ - Quay registry credentials +# hub/infra/quay/ - Built-in Quay registry credentials (auto-generated) +# hub/infra/registry/ - BYO container registry credentials (user-provided) # hub/infra/users/ - User credentials managed by IdP # # Framework Secrets: @@ -150,34 +151,38 @@ secrets: vaultPolicy: alphaNumericPolicy # =========================================================================== - # QUAY INFRASTRUCTURE SECRETS (hub/infra/quay/) - # Registry credentials for Quay - # Policy: hub-infra-quay-secret (read access to hub/infra/quay/*) + # BUILT-IN QUAY REGISTRY SECRETS (hub/infra/quay/) + # Auto-generated credentials for built-in Quay registry + # Used by: Quay user provisioner job, supply-chain pipeline (when quay.enabled=true) + # Policy: hub-supply-chain-jwt-secret (read access to hub/infra/quay/*) # =========================================================================== - name: quay-users vaultPrefixes: - hub/infra/quay fields: - - name: quay-admin-password - onMissingValue: generate - vaultPolicy: validatedPatternDefaultPolicy - name: quay-user-password onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy - # External Registry Credentials (e.g., Quay.io, Docker Hub, GHCR) - # Reserved for future use with container signing workflows - # Uncomment and provide your credentials when needed - #- name: external-registry - # vaultPrefixes: - # - hub/infra - # fields: - # - name: username - # value: "your-registry-username" # Replace with your username - # onMissingValue: error - # - name: password - # value: "your-registry-token" # Replace with your token/password - # onMissingValue: error + # =========================================================================== + # BYO / EMBEDDED OCP REGISTRY SECRETS (hub/infra/registry/) + # User-provided credentials for external or embedded OCP registry. + # Used by: supply-chain pipeline (push), qtodo (pull) when externalRegistry.enabled=true + # Policy: hub-supply-chain-jwt-secret (read access to hub/infra/registry/*) + # + # VP rule: add this (with your token) to ~/values-secrets.yaml (or + # ~/values-secret.yaml / ~/values-secret-layered-zero-trust.yaml per VP lookup). + # Replace REPLACE_WITH_REGISTRY_TOKEN in your local file: + # - Embedded OCP registry: use output of oc whoami -t + # - External registry (BYO): use your registry token/password + # =========================================================================== + - name: registry-user + vaultPrefixes: + - hub/infra/registry + fields: + - name: registry-password + value: "REPLACE_WITH_REGISTRY_TOKEN" + onMissingValue: error # =========================================================================== # HUB-SPECIFIC SECRETS (hub/) From b1203c18a3986b9b4b6f353739bc777a18157a0a Mon Sep 17 00:00:00 2001 From: Min Zhang Date: Wed, 18 Feb 2026 10:08:11 -0500 Subject: [PATCH 2/2] feat: unified registry configuration with multi-registry support Refactor supply-chain and qtodo charts to use a single, option-agnostic registry configuration instead of separate per-registry blocks. Registry options (configure one in values-hub.yaml): - Option 1: Built-in Quay Registry - Option 2: BYO/External Registry (quay.io, ghcr.io, etc.) - Option 3: Embedded OCP Image Registry Key changes: Supply-chain chart: * Unified registry.* parameters (domain, org, user, vaultPath, passwordVaultKey) * Use tpl function to resolve template expressions in registry.domain values passed as --set parameters from the validated patterns framework * Embedded OCP registry automation (registry.embeddedOCP.ensureImageNamespaceRBAC): - Auto-create image namespace matching registry.org - Grant pipeline SA system:image-builder via RoleBinding - Enable default route on OCP image registry via Kubernetes API (curl-based Job using ServiceAccount token, no oc CLI dependency) * ArgoCD hook annotations on the route-enabler Job (Sync + HookSucceeded) * Rename qtodo-registry-pass to qtodo-quay-pass for clarity Qtodo chart: * Unified app.images.main.registry.* parameters * Use tpl function in registry-external-secret.yaml for domain resolution ztvp-certificates chart: * Node-level image pull trust for kubelet (imagePullTrust.*) * Create ConfigMap with ingress CA per registry hostname in openshift-config * Patch image.config.openshift.io/cluster additionalTrustedCA * RBAC for patching image.config.openshift.io resources Documentation: * Comprehensive supply-chain.md with configuration steps for all three registry options, vault paths, and example overrides * Updated values-secret.yaml.template with registry credential examples Signed-off-by: Min Zhang --- .gitignore | 1 + .../templates/registry-external-secret.yaml | 13 +-- charts/qtodo/values.yaml | 20 +--- .../templates/pipeline-qtodo.yaml | 11 +- .../templates/quay/quay-user-job.yaml | 2 +- .../rbac/registry-image-namespace.yaml | 91 +++++++++++++++ .../templates/secrets/qtodo-quay-pass.yaml | 8 +- .../secrets/qtodo-registry-auth.yaml | 38 ++----- charts/supply-chain/values.yaml | 58 ++++++---- .../files/extract-certificates.sh.tpl | 74 +++++++++++- charts/ztvp-certificates/templates/rbac.yaml | 33 ++++++ charts/ztvp-certificates/values.yaml | 19 ++++ docs/supply-chain.md | 106 ++++++++++++++---- values-hub.yaml | 98 +++++++++++++--- 14 files changed, 439 insertions(+), 133 deletions(-) diff --git a/.gitignore b/.gitignore index 0d29468e..9e951fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,6 @@ super-linter-output github_conf # Editor and IDE specific files +.cursor/ .cursorrules .vscode/ diff --git a/charts/qtodo/templates/registry-external-secret.yaml b/charts/qtodo/templates/registry-external-secret.yaml index 6b6979c9..0fe1e1ad 100644 --- a/charts/qtodo/templates/registry-external-secret.yaml +++ b/charts/qtodo/templates/registry-external-secret.yaml @@ -18,7 +18,7 @@ spec: .dockerconfigjson: | { "auths": { - "{{ required "app.images.main.registry.domain is required when registry.auth is enabled" .Values.app.images.main.registry.domain }}": { + "{{ tpl (required "app.images.main.registry.domain is required when registry.auth is enabled" .Values.app.images.main.registry.domain) $ }}": { "auth": "{{ `{{ printf "%s:%s" "` }}{{ .Values.app.images.main.registry.user }}{{ `" .password | b64enc }}` }}" } } @@ -26,11 +26,6 @@ spec: data: - secretKey: password remoteRef: - {{- if .Values.app.images.main.registry.builtinQuay.enabled }} - key: {{ .Values.app.images.main.registry.builtinQuay.vaultPath }} - property: {{ .Values.app.images.main.registry.builtinQuay.passwordVaultKey }} - {{- else if .Values.app.images.main.registry.externalRegistry.enabled }} - key: {{ .Values.app.images.main.registry.externalRegistry.vaultPath }} - property: {{ .Values.app.images.main.registry.externalRegistry.passwordVaultKey }} - {{- end }} -{{- end }} \ No newline at end of file + key: {{ required "app.images.main.registry.vaultPath is required when registry.auth is enabled" .Values.app.images.main.registry.vaultPath }} + property: {{ required "app.images.main.registry.passwordVaultKey is required when registry.auth is enabled" .Values.app.images.main.registry.passwordVaultKey }} +{{- end }} diff --git a/charts/qtodo/values.yaml b/charts/qtodo/values.yaml index 52b13d33..0270e896 100644 --- a/charts/qtodo/values.yaml +++ b/charts/qtodo/values.yaml @@ -15,26 +15,14 @@ app: # Modified to Always to force a pull so we can test changes to the container image without requiring manual deletion of images or restarts of argo pullPolicy: Always registry: - # auth: controls whether to create registry auth secret - # Set to true when using private registry (built-in Quay or external) + # Set to true to create registry auth secret for image pulls auth: false secretName: qtodo-registry-auth user: registry-user # domain: registry.example.com # REQUIRED when auth is enabled - - # Built-in Quay registry (optional) - # When enabled, uses auto-generated credentials from Vault - builtinQuay: - enabled: false - vaultPath: secret/data/hub/infra/quay/quay-users - passwordVaultKey: quay-user-password - - # External/BYO registry (optional) - # When enabled, uses user-provided credentials from Vault - externalRegistry: - enabled: false - vaultPath: secret/data/hub/infra/registry/registry-user - passwordVaultKey: registry-password + # Vault path and key for registry password (set for your scenario) + vaultPath: "" + passwordVaultKey: "" spiffeHelper: name: registry.redhat.io/zero-trust-workload-identity-manager/spiffe-helper-rhel9 version: v0.10.0 diff --git a/charts/supply-chain/templates/pipeline-qtodo.yaml b/charts/supply-chain/templates/pipeline-qtodo.yaml index 39fe8494..add87310 100644 --- a/charts/supply-chain/templates/pipeline-qtodo.yaml +++ b/charts/supply-chain/templates/pipeline-qtodo.yaml @@ -1,12 +1,3 @@ -{{- /* Determine registry domain: auto-construct for built-in Quay, require for external */ -}} -{{- $registryDomain := "" -}} -{{- if .Values.registry.domain -}} - {{- $registryDomain = .Values.registry.domain -}} -{{- else if .Values.quay.enabled -}} - {{- $registryDomain = printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain -}} -{{- else -}} - {{- fail "registry.domain is required for external registry" -}} -{{- end -}} --- apiVersion: tekton.dev/v1beta1 kind: Pipeline @@ -34,7 +25,7 @@ spec: - name: image-target type: string description: qtodo image push destination (e.g. quay.io/ztvp/qtodo:latest) - default: {{ $registryDomain }}/{{ .Values.registry.org }}/{{ .Values.registry.repo }}:{{ .Values.qtodo.tag }} + default: {{ tpl (required "registry.domain is required" .Values.registry.domain) $ }}/{{ .Values.registry.org }}/{{ .Values.registry.repo }}:{{ .Values.qtodo.tag }} - name: image-tls-verify type: string description: Whether to verify TLS when pushing to the OCI registry diff --git a/charts/supply-chain/templates/quay/quay-user-job.yaml b/charts/supply-chain/templates/quay/quay-user-job.yaml index 417afcc7..4fa0f7c4 100644 --- a/charts/supply-chain/templates/quay/quay-user-job.yaml +++ b/charts/supply-chain/templates/quay/quay-user-job.yaml @@ -19,7 +19,7 @@ spec: command: ["python3", "/app/create_user.py"] env: - name: QUAY_HOST - value: {{ .Values.registry.domain | default (printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain) }} + value: {{ tpl (.Values.registry.domain | default (printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain)) $ }} - name: QUAY_ADMIN_USER value: {{ .Values.registry.user }} - name: QUAY_ADMIN_PASSWORD diff --git a/charts/supply-chain/templates/rbac/registry-image-namespace.yaml b/charts/supply-chain/templates/rbac/registry-image-namespace.yaml index 35f3ab76..ebd5bf45 100644 --- a/charts/supply-chain/templates/rbac/registry-image-namespace.yaml +++ b/charts/supply-chain/templates/rbac/registry-image-namespace.yaml @@ -25,4 +25,95 @@ subjects: - kind: ServiceAccount name: pipeline namespace: {{ .Values.global.namespace }} +--- +# Enable the default route on the embedded OCP image registry so that +# the pipeline can push and external clients can pull images via the route. +# Uses a Job because the imageregistry config is a cluster-singleton managed +# by the image-registry operator; declarative ownership would conflict. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: registry-route-enabler + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "0" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Values.global.namespace }}-registry-route-enabler + annotations: + argocd.argoproj.io/sync-wave: "0" +rules: +- apiGroups: ["imageregistry.operator.openshift.io"] + resources: ["configs"] + verbs: ["get", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.global.namespace }}-registry-route-enabler + annotations: + argocd.argoproj.io/sync-wave: "0" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Values.global.namespace }}-registry-route-enabler +subjects: +- kind: ServiceAccount + name: registry-route-enabler + namespace: {{ .Values.global.namespace }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: enable-registry-default-route + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "1" + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: HookSucceeded +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: registry-route-enabler + restartPolicy: Never + containers: + - name: enable-route + image: registry.access.redhat.com/ubi9/ubi:9.7-1764794285 + command: + - /bin/sh + - -ce + - | + APISERVER="https://kubernetes.default.svc" + TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" + CACERT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + RESOURCE_URL="${APISERVER}/apis/imageregistry.operator.openshift.io/v1/configs/cluster" + AUTH_HEADER="Authorization: Bearer ${TOKEN}" + + echo "Checking current defaultRoute status..." + BODY=$(curl -sS --cacert "${CACERT}" -H "${AUTH_HEADER}" "${RESOURCE_URL}") + rc=$?; if [ $rc -ne 0 ]; then echo "ERROR: GET failed (curl rc=${rc})"; exit 1; fi + + # Parse defaultRoute from JSON without jq/grep dependency + case "${BODY}" in + *'"defaultRoute":true'*) echo "Default route already enabled, nothing to do."; exit 0 ;; + esac + + echo "Enabling default route on embedded OCP image registry..." + RESP=$(curl -sS -w "\n%{http_code}" --cacert "${CACERT}" \ + -H "${AUTH_HEADER}" \ + -H "Content-Type: application/merge-patch+json" \ + -X PATCH -d '{"spec":{"defaultRoute":true}}' \ + "${RESOURCE_URL}") + HTTP_CODE=$(echo "${RESP}" | tail -1) + + if [ "${HTTP_CODE}" -ge 200 ] 2>/dev/null && [ "${HTTP_CODE}" -lt 300 ] 2>/dev/null; then + echo "Default route enabled successfully (HTTP ${HTTP_CODE})." + else + echo "ERROR: PATCH failed (HTTP ${HTTP_CODE})." + echo "${RESP}" | head -5 + exit 1 + fi {{- end }} diff --git a/charts/supply-chain/templates/secrets/qtodo-quay-pass.yaml b/charts/supply-chain/templates/secrets/qtodo-quay-pass.yaml index d66f6507..2f42cedd 100644 --- a/charts/supply-chain/templates/secrets/qtodo-quay-pass.yaml +++ b/charts/supply-chain/templates/secrets/qtodo-quay-pass.yaml @@ -3,7 +3,7 @@ Purpose: Provides password for the Quay user provisioner job to create/update users in built-in Quay Used by: quay-user-job.yaml (CronJob that provisions Quay users) Only created when: quay.enabled=true (built-in Quay registry) - Not used for: BYO/external registry (use qtodo-registry-auth.yaml instead) + Uses unified registry.vaultPath and registry.passwordVaultKey */}} {{- if eq .Values.quay.enabled true }} --- @@ -26,6 +26,6 @@ spec: data: - secretKey: password remoteRef: - key: {{ .Values.quay.vaultPath }} - property: {{ .Values.quay.passwordVaultKey }} -{{- end }} \ No newline at end of file + key: {{ .Values.registry.vaultPath }} + property: {{ .Values.registry.passwordVaultKey }} +{{- end }} diff --git a/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml b/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml index eb889c9f..ea3e1e2b 100644 --- a/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml +++ b/charts/supply-chain/templates/secrets/qtodo-registry-auth.yaml @@ -2,29 +2,16 @@ Pipeline Registry Auth Secret Purpose: Provides dockerconfigjson for pipeline to push/pull images Used by: Tekton pipeline tasks (build-image, sign-image, verify-image) - Created when: quay.enabled=true OR externalRegistry.enabled=true - Vault path: Automatically selects based on which registry is enabled - - Built-in Quay: quay.vaultPath (auto-generated credentials) - - BYO Registry: externalRegistry.vaultPath (user-provided credentials) - Registry domain: - - Built-in Quay: auto-constructed as quay-registry-quay-quay-enterprise. - - BYO Registry: must be explicitly set via registry.domain + Created when: registry.enabled=true + Registry-agnostic: works for built-in Quay, BYO (quay.io, ghcr.io), or embedded OCP. + Set registry.domain, registry.vaultPath, and registry.passwordVaultKey for your scenario. */}} -{{- if or .Values.quay.enabled .Values.externalRegistry.enabled }} -{{- /* Determine registry domain: auto-construct for built-in Quay, require for external */ -}} -{{- $registryDomain := "" -}} -{{- if .Values.registry.domain -}} - {{- $registryDomain = .Values.registry.domain -}} -{{- else if .Values.quay.enabled -}} - {{- $registryDomain = printf "quay-registry-quay-quay-enterprise.%s" .Values.global.hubClusterDomain -}} -{{- else -}} - {{- fail "registry.domain is required for external registry" -}} -{{- end -}} +{{- if .Values.registry.enabled }} --- apiVersion: "external-secrets.io/v1beta1" kind: ExternalSecret metadata: - name: qtodo-registry-auth + name: {{ .Values.registry.authSecretName }} namespace: {{ .Release.Namespace | default .Values.global.namespace }} spec: refreshInterval: 15s @@ -32,14 +19,14 @@ spec: name: {{ .Values.global.secretStore.name }} kind: {{ .Values.global.secretStore.kind }} target: - name: qtodo-registry-auth + name: {{ .Values.registry.authSecretName }} template: type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: | { "auths": { - "{{ $registryDomain }}": { + "{{ tpl (required "registry.domain is required when registry.enabled=true" .Values.registry.domain) $ }}": { "auth": "{{ `{{ printf "%s:%s" "` }}{{ .Values.registry.user }}{{ `" .password | b64enc }}` }}" } } @@ -47,11 +34,6 @@ spec: data: - secretKey: password remoteRef: - {{- if .Values.quay.enabled }} - key: {{ .Values.quay.vaultPath }} - property: {{ .Values.quay.passwordVaultKey }} - {{- else if .Values.externalRegistry.enabled }} - key: {{ .Values.externalRegistry.vaultPath }} - property: {{ .Values.externalRegistry.passwordVaultKey }} - {{- end }} -{{- end }} \ No newline at end of file + key: {{ required "registry.vaultPath is required when registry.enabled=true" .Values.registry.vaultPath }} + property: {{ required "registry.passwordVaultKey is required when registry.enabled=true" .Values.registry.passwordVaultKey }} +{{- end }} diff --git a/charts/supply-chain/values.yaml b/charts/supply-chain/values.yaml index aed322b4..0b6be272 100644 --- a/charts/supply-chain/values.yaml +++ b/charts/supply-chain/values.yaml @@ -27,47 +27,59 @@ qtodo: containerfile: "./Containerfile" # =========================================================================== -# BUILT-IN QUAY REGISTRY (optional) -# When enabled, deploys internal Quay registry with auto-generated credentials +# QUAY USER PROVISIONER (only for built-in Quay registry) +# When enabled, runs a CronJob that provisions users in the built-in Quay instance. +# This is Quay-specific and not needed for BYO or embedded OCP registries. # =========================================================================== quay: - enabled: true + enabled: false email: "quay-user@example.com" - # Vault path for auto-generated Quay credentials - vaultPath: "secret/data/hub/infra/quay/quay-users" - passwordVaultKey: "quay-user-password" - # User provisioner job settings job: image: registry.access.redhat.com/ubi9/ubi:9.7-1764794285 schedule: "*/5 * * * *" # =========================================================================== -# EXTERNAL/BYO REGISTRY (optional) -# User-provided credentials for external registry (quay.io, ghcr.io, etc.) -# Enable this when using an external registry instead of built-in Quay -# =========================================================================== -externalRegistry: - enabled: false - # Vault path for user-provided credentials - vaultPath: "secret/data/hub/infra/registry/registry-user" - passwordVaultKey: "registry-password" - -# =========================================================================== -# COMMON REGISTRY SETTINGS (shared by both built-in Quay and external registry) +# REGISTRY CONFIGURATION (option-agnostic) +# Works for all registry types: built-in Quay, BYO (quay.io, ghcr.io, etc.), +# or embedded OCP image registry. Set the values for your scenario. +# +# Scenario-specific values (set in values-hub.yaml overrides): +# Built-in Quay: +# domain: quay-registry-quay-quay-enterprise.apps. +# vaultPath: secret/data/hub/infra/quay/quay-users +# passwordVaultKey: quay-user-password +# BYO (quay.io, ghcr.io, etc.): +# domain: quay.io (or your registry hostname) +# vaultPath: secret/data/hub/infra/registry/registry-user +# passwordVaultKey: registry-password +# Embedded OCP: +# domain: default-route-openshift-image-registry.apps. +# vaultPath: secret/data/hub/infra/registry/registry-user +# passwordVaultKey: registry-password +# embeddedOCP.ensureImageNamespaceRBAC: true # =========================================================================== registry: - # For built-in Quay: domain is auto-constructed from hubClusterDomain - # For external registry: REQUIRED - set explicitly (e.g., quay.io, ghcr.io) - # domain: "registry.example.com" + # Set to true to create the registry auth secret (dockerconfigjson) + enabled: false + # Registry hostname (REQUIRED when enabled) + domain: "" + # Organization/namespace within the registry org: "ztvp" + # Repository name repo: "qtodo" + # Whether to verify TLS when pushing to the registry tlsVerify: "true" + # Registry username user: "registry-user" + # Vault path to the secret containing the registry password + vaultPath: "" + # Key within the Vault secret that holds the password + passwordVaultKey: "" # Secret name for registry auth (dockerconfigjson) authSecretName: "qtodo-registry-auth" # Embedded OCP registry only: create image namespace (registry.org) and grant # pipeline SA system:image-builder so the pipeline can push. Set to true only when - # using the in-cluster OpenShift image registry; leave false for quay.io or other external registries. + # using the in-cluster OpenShift image registry; leave false for other registries. embeddedOCP: ensureImageNamespaceRBAC: false diff --git a/charts/ztvp-certificates/files/extract-certificates.sh.tpl b/charts/ztvp-certificates/files/extract-certificates.sh.tpl index 5db24275..6c97e6f8 100644 --- a/charts/ztvp-certificates/files/extract-certificates.sh.tpl +++ b/charts/ztvp-certificates/files/extract-certificates.sh.tpl @@ -336,7 +336,79 @@ else fi # =================================================================== -# PHASE 9: Automatic Rollout (if enabled) +# PHASE 9: Configure Node-Level Image Pull Trust (if enabled) +# Creates a ConfigMap with registry-hostname keys containing the ingress CA, +# then patches image.config.openshift.io/cluster to reference it. +# This allows kubelet to pull images from registries behind the cluster ingress +# (e.g. built-in Quay) without "x509: certificate signed by unknown authority". +# =================================================================== + +{{- if .Values.imagePullTrust.enabled }} +{{- if .Values.imagePullTrust.registries }} +log "Configuring node-level image pull trust" + +if [[ "$INGRESS_CA_FOUND" != "true" ]]; then + error "imagePullTrust is enabled but no ingress CA was extracted. Cannot configure image pull trust." + error "Ensure autoDetect is true or provide a custom ingress CA source." + exit 1 +fi + +# Build the ConfigMap data with registry hostnames as keys +# Each key is a registry hostname, value is the ingress CA PEM +REGISTRY_CM_DATA="" +{{- range .Values.imagePullTrust.registries }} +log "Adding registry trust: {{ tpl . $ }}" +{{- end }} + +log "Creating ConfigMap: {{ .Values.global.namespace }}/{{ .Values.imagePullTrust.configMapName }}" + +# Combine all ingress CA files into one PEM for registry trust +COMBINED_INGRESS_CA="${TEMP_DIR}/combined-ingress-ca.pem" +> "${COMBINED_INGRESS_CA}" +for f in "${TEMP_DIR}"/ingress-ca-*.crt; do + [[ -f "$f" ]] || continue + cat "$f" >> "${COMBINED_INGRESS_CA}" + echo "" >> "${COMBINED_INGRESS_CA}" +done + +# Create the ConfigMap with registry hostnames as keys +cat <<'CMEOF' > "${TEMP_DIR}/registry-cas-cm.yaml" +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.imagePullTrust.configMapName }} + namespace: {{ .Values.global.namespace }} + labels: + app.kubernetes.io/name: ztvp-certificates + app.kubernetes.io/component: image-pull-trust + app.kubernetes.io/managed-by: ztvp-certificate-manager +data: {} +CMEOF + +oc apply -f "${TEMP_DIR}/registry-cas-cm.yaml" + +# Patch each registry hostname as a key with the ingress CA PEM +{{- range .Values.imagePullTrust.registries }} +log "Patching ConfigMap key: {{ tpl . $ }}" +oc create configmap {{ $.Values.imagePullTrust.configMapName }} \ + -n {{ $.Values.global.namespace }} \ + --from-file="{{ tpl . $ }}=${COMBINED_INGRESS_CA}" \ + --dry-run=client -o yaml | oc apply -f - +{{- end }} + +# Patch image.config.openshift.io/cluster to reference the ConfigMap +log "Patching image.config.openshift.io/cluster additionalTrustedCA" +oc patch image.config.openshift.io/cluster --type merge \ + -p "{\"spec\":{\"additionalTrustedCA\":{\"name\":\"{{ .Values.imagePullTrust.configMapName }}\"}}}" + +log "Node-level image pull trust configured successfully" +log "Note: MCO will roll this out to nodes (may take a few minutes)" + +{{- end }} +{{- end }} + +# =================================================================== +# PHASE 10: Automatic Rollout (if enabled) # =================================================================== {{- if .Values.rollout.enabled }} diff --git a/charts/ztvp-certificates/templates/rbac.yaml b/charts/ztvp-certificates/templates/rbac.yaml index 94949dc7..9d6b9ccd 100644 --- a/charts/ztvp-certificates/templates/rbac.yaml +++ b/charts/ztvp-certificates/templates/rbac.yaml @@ -80,6 +80,39 @@ subjects: - kind: ServiceAccount name: {{ include "ztvp-certificates.serviceAccountName" . }} namespace: {{ .Values.global.namespace }} +{{- if .Values.imagePullTrust.enabled }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-image-config + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +rules: +# Patch image.config.openshift.io/cluster to set additionalTrustedCA +- apiGroups: ["config.openshift.io"] + resources: ["images"] + verbs: ["get", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-image-config + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "ztvp-certificates.fullname" . }}-image-config +subjects: +- kind: ServiceAccount + name: {{ include "ztvp-certificates.serviceAccountName" . }} + namespace: {{ .Values.global.namespace }} +{{- end }} {{- if .Values.rollout.enabled }} --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/charts/ztvp-certificates/values.yaml b/charts/ztvp-certificates/values.yaml index 354ee4c7..53d374f7 100644 --- a/charts/ztvp-certificates/values.yaml +++ b/charts/ztvp-certificates/values.yaml @@ -177,6 +177,25 @@ distribution: # Requires: ManagedClusterSetBinding in the namespace method: "acm-policy" +# Node-level image pull trust for kubelet +# Configures image.config.openshift.io/cluster additionalTrustedCA so that +# kubelet can pull images from registries behind the cluster's ingress (e.g. +# built-in Quay). Without this, kubelet image pulls fail with +# "x509: certificate signed by unknown authority" when the ingress uses a +# self-signed or cluster-internal CA. +imagePullTrust: + # Set to true to create the registry-CA ConfigMap and patch image.config + enabled: false + # ConfigMap name created in openshift-config for image.config additionalTrustedCA + configMapName: ztvp-registry-cas + # Registry hostnames that need the ingress CA for image pulls. + # Each becomes a key in the ConfigMap with the ingress CA as the value. + # Use {{ .Values.global.hubClusterDomain }} in values-hub.yaml overrides. + registries: [] + # Example (built-in Quay): + # registries: + # - quay-registry-quay-quay-enterprise.apps.example.com + # Debugging options debug: # Enable verbose logging in extraction job diff --git a/docs/supply-chain.md b/docs/supply-chain.md index 55cb4d9e..5e92aa73 100644 --- a/docs/supply-chain.md +++ b/docs/supply-chain.md @@ -57,19 +57,18 @@ By default, ZTVP deploys a built-in Red Hat Quay registry. However, you can use ```yaml overrides: - # Disable built-in Quay - - name: quay.enabled - value: "false" - # Enable external registry - - name: externalRegistry.enabled + - name: registry.enabled value: "true" - # External registry settings - name: registry.domain value: "your-registry.example.com" - name: registry.user value: "your-username" - name: registry.org value: "your-org" + - name: registry.vaultPath + value: "secret/data/hub/infra/registry/registry-user" + - name: registry.passwordVaultKey + value: "registry-password" ``` 4. **Configure qtodo for custom registry** (if pulling from custom registry): @@ -88,31 +87,88 @@ By default, ZTVP deploys a built-in Red Hat Quay registry. However, you can use | Parameter | Description | Example | | --------- | ----------- | ------- | -| `registry.domain` | Registry hostname (required for BYO only) | `quay.io`, `ghcr.io`, `registry.example.com` | +| `registry.enabled` | Enable registry auth secret creation | `true` | +| `registry.domain` | Registry hostname (REQUIRED) | `quay.io`, `ghcr.io`, `registry.example.com` | | `registry.org` | Organization/namespace | `my-org` | | `registry.repo` | Repository name | `qtodo` | | `registry.user` | Registry username | `my-robot-account` | -| `quay.enabled` | Set to `false` for BYO registry | `false` | +| `registry.vaultPath` | Vault path for registry password | `secret/data/hub/infra/registry/registry-user` | +| `registry.passwordVaultKey` | Key within the Vault secret | `registry-password` | -> **Note**: For built-in Quay registry, `registry.domain` is automatically constructed as `quay-registry-quay-quay-enterprise.` and does not need to be specified. For BYO/external registries, `registry.domain` is **required**. +> **Note**: All registry types (built-in Quay, BYO, embedded OCP) use the same parameters. Set `registry.domain`, `registry.vaultPath`, and `registry.passwordVaultKey` to the appropriate values for your scenario. See the Vault Paths table below for scenario-specific values. ### Vault Paths Registry credentials are stored at different paths based on registry type: -| Registry Type | Vault Path | Password Key | -| --------------- | ---------------------------------------------- | -------------------- | -| Built-in Quay | `secret/data/hub/infra/quay/quay-users` | `quay-user-password` | -| BYO Registry | `secret/data/hub/infra/registry/registry-user` | `registry-password` | +| Registry Type | Vault Path | Password Key | +| ------------------ | ---------------------------------------------- | -------------------- | +| Built-in Quay | `secret/data/hub/infra/quay/quay-users` | `quay-user-password` | +| BYO Registry | `secret/data/hub/infra/registry/registry-user` | `registry-password` | +| Embedded OCP | `secret/data/hub/infra/registry/registry-user` | `registry-password` | -The chart automatically selects the correct vault path based on the enabled flags: - -* `quay.enabled=true`: Uses built-in Quay vault path -* `externalRegistry.enabled=true`: Uses external registry vault path -* Both disabled (default): No registry auth secret created (fresh install state) +Set `registry.vaultPath` and `registry.passwordVaultKey` in your `values-hub.yaml` overrides to match your scenario. When `registry.enabled=false` (default), no registry auth secret is created (fresh install state). The Vault policy `hub-supply-chain-jwt-secret` grants read access to both paths for the pipeline service account. +### Embedded OCP Registry + +To use the in-cluster OpenShift image registry instead of an external registry: + +1. **Enable `registry.embeddedOCP.ensureImageNamespaceRBAC`** in the supply-chain overrides. The chart will automatically: + * Create the image namespace matching `registry.org` (e.g. `ztvp`) + * Grant the pipeline ServiceAccount `system:image-builder` in that namespace + * Enable the default route on the image registry (via a one-time Job) + +2. **Set the registry domain** to `default-route-openshift-image-registry.apps.`. + +3. **Set the registry user** to `admin` (or a user with push permissions). + +4. **Store the token in Vault**: Use `oc whoami -t` output as the `registry-password` value in `~/values-secrets.yaml`. + +Example supply-chain overrides: + +```yaml +overrides: + - name: registry.enabled + value: "true" + - name: registry.domain + value: default-route-openshift-image-registry.apps. + - name: registry.org + value: ztvp + - name: registry.user + value: admin + - name: registry.vaultPath + value: "secret/data/hub/infra/registry/registry-user" + - name: registry.passwordVaultKey + value: "registry-password" + - name: registry.embeddedOCP.ensureImageNamespaceRBAC + value: "true" +``` + +### Node-Level Image Pull Trust + +When using a registry behind the cluster ingress (Option 1: Built-in Quay or Option 3: Embedded OCP Registry), kubelet cannot pull images by default because the ingress certificate is self-signed and not trusted at the node level. + +The `ztvp-certificates` application handles this by patching `image.config.openshift.io/cluster` with the ingress CA certificate for the configured registry hostnames. Enable it by uncommenting the `imagePullTrust` overrides in `values-hub.yaml`: + +```yaml +# ztvp-certificates overrides +- name: imagePullTrust.enabled + value: "true" +- name: imagePullTrust.registries[0] + value: +``` + +Set `` to match your registry option: + +| Option | Registry Hostname | +| ------ | ----------------- | +| Option 1: Built-in Quay | `quay-registry-quay-quay-enterprise.apps.` | +| Option 3: Embedded OCP | `default-route-openshift-image-registry.apps.` | + +> **Note**: Option 2 (BYO/External Registry) does not require `imagePullTrust` because external registries like quay.io and ghcr.io use publicly trusted certificates. + ## Automatic approach To automate the application building and certifying process, we will use _Red Hat OpenShift Pipelines_. @@ -180,22 +236,24 @@ You can also trigger a pipeline run using the Helm template included in the char ```shell helm template supply-chain charts/supply-chain \ --set pipelinerun.enabled=true \ - --set quay.enabled=true \ + --set registry.enabled=true \ + --set registry.domain=quay-registry-quay-quay-enterprise.apps.example.com \ + --set registry.vaultPath=secret/data/hub/infra/quay/quay-users \ + --set registry.passwordVaultKey=quay-user-password \ --set global.namespace=layered-zero-trust-hub \ - --set global.hubClusterDomain=apps.example.com \ --show-only templates/pipelinerun-qtodo.yaml | oc create -f - ``` -> **Note**: For built-in Quay, `registry.domain` is auto-constructed from `global.hubClusterDomain`. - **For BYO/External Registry:** ```shell helm template supply-chain charts/supply-chain \ --set pipelinerun.enabled=true \ - --set externalRegistry.enabled=true \ - --set global.namespace=layered-zero-trust-hub \ + --set registry.enabled=true \ --set registry.domain=quay.io \ + --set registry.vaultPath=secret/data/hub/infra/registry/registry-user \ + --set registry.passwordVaultKey=registry-password \ + --set global.namespace=layered-zero-trust-hub \ --show-only templates/pipelinerun-qtodo.yaml | oc create -f - ``` diff --git a/values-hub.yaml b/values-hub.yaml index c5b506d4..048fbadc 100644 --- a/values-hub.yaml +++ b/values-hub.yaml @@ -209,6 +209,15 @@ clusterGroup: - name: rollout.strategy value: labeled + # Node-level image pull trust for kubelet + # Required when pulling images from registries behind the cluster ingress + # (e.g. built-in Quay, embedded OCP registry). Patches image.config.openshift.io/cluster. + # Uncomment and set the registry hostname when enabling a registry option. + # - name: imagePullTrust.enabled + # value: "true" + # - name: imagePullTrust.registries[0] + # value: + # Note: additionalCertificates (complex nested array) temporarily disabled # Need to find proper way to pass complex structures in Validated Patterns acm: @@ -451,18 +460,61 @@ clusterGroup: value: qtodo - name: app.vault.secretPath value: secret/data/apps/qtodo/qtodo-db - # For Secure Supply Chain, we changed the qtodo image to use the one built in the secure supply chain + # ============================================================ + # Secure Supply Chain: pull pipeline-built image from registry + # Uncomment the option matching your supply-chain registry choice. + # ============================================================ + # OPTION 1: Built-in Quay Registry # - name: app.images.main.name # value: quay-registry-quay-quay-enterprise.apps.{{ $.Values.global.clusterDomain }}/ztvp/qtodo # - name: app.images.main.version # value: latest - # Uncomment to enable registry authentication # - name: app.images.main.registry.auth - # value: true + # value: "true" + # - name: app.images.main.registry.domain + # value: quay-registry-quay-quay-enterprise.apps.{{ $.Values.global.clusterDomain }} # - name: app.images.main.registry.user # value: quay-user + # - name: app.images.main.registry.vaultPath + # value: "secret/data/hub/infra/quay/quay-users" # - name: app.images.main.registry.passwordVaultKey - # value: quay-user-password + # value: "quay-user-password" + # ============================================================ + # OPTION 2: BYO/External Registry + # - name: app.images.main.name + # value: quay.io/minzhang/qtodo + # - name: app.images.main.version + # value: latest + # - name: app.images.main.registry.auth + # value: "true" + # - name: app.images.main.registry.domain + # value: quay.io + # - name: app.images.main.registry.user + # value: minzhang + # - name: app.images.main.registry.vaultPath + # value: "secret/data/hub/infra/registry/registry-user" + # - name: app.images.main.registry.passwordVaultKey + # value: "registry-password" + # ============================================================ + # OPTION 3: Embedded OCP Registry + # - name: app.images.main.name + # value: default-route-openshift-image-registry.apps.{{ $.Values.global.clusterDomain }}/ztvp/qtodo + # - name: app.images.main.version + # value: latest + # - name: app.images.main.registry.auth + # value: "true" + # - name: app.images.main.registry.domain + # value: default-route-openshift-image-registry.apps.{{ $.Values.global.clusterDomain }} + # - name: app.images.main.registry.user + # value: admin + # - name: app.images.main.registry.vaultPath + # value: "secret/data/hub/infra/registry/registry-user" + # - name: app.images.main.registry.passwordVaultKey + # value: "registry-password" + # ============================================================ + # DEFAULT: No pipeline image (comment out all options above) + # qtodo uses the upstream image from the chart's default values.yaml + # ============================================================ # Secure Supply Chain - Uncomment to enable # supply-chain: # name: supply-chain @@ -474,26 +526,29 @@ clusterGroup: # - .imagePullSecrets[]|select(.name | contains("-dockercfg-")) # overrides: # # ============================================================ - # # OPTION 1: Built-in Quay Registry + # # OPTION 1: Built-in Quay Registry (comment out Option 2, uncomment below) # # Requires: quay-enterprise namespace, quay-operator, quay-registry app - # # Note: registry.domain is auto-constructed from hubClusterDomain # # ============================================================ # # - name: quay.enabled # # value: "true" - # # - name: externalRegistry.enabled - # # value: "false" + # # - name: registry.enabled + # # value: "true" + # # - name: registry.domain + # # value: quay-registry-quay-quay-enterprise.apps.{{ $.Values.global.clusterDomain }} # # - name: registry.tlsVerify # # value: "false" # # - name: registry.user # # value: quay-user + # # - name: registry.vaultPath + # # value: "secret/data/hub/infra/quay/quay-users" + # # - name: registry.passwordVaultKey + # # value: "quay-user-password" # # ============================================================ - # # OPTION 2: BYO/External Registry - # # Requires: registry credentials in ~/values-secret.yaml - # # Note: registry.domain is REQUIRED for external registry + # # OPTION 2: BYO/External Registry (comment out Option 3, uncomment below) + # # Store token in Vault (hub/infra/registry/registry-user, field registry-password) + # # and in ~/values-secrets.yaml. # # ============================================================ - # # - name: quay.enabled - # # value: "false" - # # - name: externalRegistry.enabled + # # - name: registry.enabled # # value: "true" # # - name: registry.domain # # value: quay.io @@ -501,16 +556,25 @@ clusterGroup: # # value: your-org # # - name: registry.user # # value: your-username + # # - name: registry.vaultPath + # # value: "secret/data/hub/infra/registry/registry-user" + # # - name: registry.passwordVaultKey + # # value: "registry-password" # # ============================================================ - # # OPTION 3: Embedded OCP Registry (comment out Option 1, 2; uncomment below) + # # OPTION 3: Embedded OCP Registry # # ============================================================ + # # - name: registry.enabled + # # value: "true" # # - name: registry.domain - # # value: default-route-openshift-image-registry.apps. + # # value: default-route-openshift-image-registry.apps.{{ $.Values.global.clusterDomain }} # # - name: registry.org # # value: ztvp # # - name: registry.user # # value: admin - # # Embedded OCP registry only: create image namespace and grant pipeline push (transparent) + # # - name: registry.vaultPath + # # value: "secret/data/hub/infra/registry/registry-user" + # # - name: registry.passwordVaultKey + # # value: "registry-password" # # - name: registry.embeddedOCP.ensureImageNamespaceRBAC # # value: "true" # # ============================================================