diff --git a/charts/supply-chain/templates/pipeline-qtodo.yaml b/charts/supply-chain/templates/pipeline-qtodo.yaml index a2118584..6b77d973 100644 --- a/charts/supply-chain/templates/pipeline-qtodo.yaml +++ b/charts/supply-chain/templates/pipeline-qtodo.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Pipeline metadata: name: qtodo-supply-chain @@ -8,12 +8,20 @@ spec: params: - name: git-url type: string - description: The URL of the public Github qtodo repository + description: The URL of the qtodo repository (public or protected) default: {{ .Values.qtodo.repository | quote }} - name: git-revision type: string - description: The revision of the public Github qtodo repository + description: The revision of the qtodo repository default: {{ .Values.qtodo.revision }} + - name: rebuild + type: string + description: Force rebuild the image even if it already exists + default: "false" + - name: skip-checks + type: string + description: Skip pre-build checks against existing image + default: "false" - name: qtodo-build-cmd type: string description: The command to build the qtodo artifact @@ -100,9 +108,44 @@ spec: workspaces: - name: qtodo-source - name: registry-auth-config + - name: git-auth + optional: true + + results: + - name: CHAINS-GIT_URL + description: The git URL used for the build (Tekton Chains provenance) + value: $(tasks.qtodo-clone-repository.results.URL) + - name: CHAINS-GIT_COMMIT + description: The git commit SHA used for the build (Tekton Chains provenance) + value: $(tasks.qtodo-clone-repository.results.COMMIT) + - name: IMAGE_URL + description: The image URL built by the pipeline (Tekton Chains provenance) + value: $(tasks.qtodo-build-image.results.IMAGE_URL) + - name: IMAGE_DIGEST + description: The image digest built by the pipeline (Tekton Chains provenance) + value: $(tasks.qtodo-build-image.results.IMAGE_DIGEST) tasks: + - name: init + taskRef: + name: init + kind: Task + params: + - name: image-url + value: $(params.image-target) + - name: rebuild + value: $(params.rebuild) + - name: skip-checks + value: $(params.skip-checks) + - name: qtodo-clone-repository + runAfter: + - init + when: + - input: $(tasks.init.results.build) + operator: in + values: + - "true" taskRef: resolver: cluster params: @@ -120,6 +163,13 @@ spec: workspaces: - name: output workspace: qtodo-source +{{- if eq (default "https" .Values.git.credentials.authType) "ssh" }} + - name: ssh-directory + workspace: git-auth +{{- else }} + - name: basic-auth + workspace: git-auth +{{- end }} - name: qtodo-build-artifact runAfter: diff --git a/charts/supply-chain/templates/pipelinerun-qtodo.yaml b/charts/supply-chain/templates/pipelinerun-qtodo.yaml index 36b52d4b..e87cdaf4 100644 --- a/charts/supply-chain/templates/pipelinerun-qtodo.yaml +++ b/charts/supply-chain/templates/pipelinerun-qtodo.yaml @@ -92,7 +92,7 @@ spec: fi cat <<'MANIFEST' | oc create -f - - apiVersion: tekton.dev/v1beta1 + apiVersion: tekton.dev/v1 kind: PipelineRun metadata: generateName: qtodo-supply-chain- diff --git a/charts/supply-chain/templates/rbac/pipeline-sa.yaml b/charts/supply-chain/templates/rbac/pipeline-sa.yaml index 06dff21e..ba97e2d4 100644 --- a/charts/supply-chain/templates/rbac/pipeline-sa.yaml +++ b/charts/supply-chain/templates/rbac/pipeline-sa.yaml @@ -10,4 +10,7 @@ metadata: argocd.argoproj.io/compare-options: IgnoreExtraneous argocd.argoproj.io/syncOptions: ServerSideApply=true secrets: - - name: qtodo-registry-auth \ No newline at end of file + - name: qtodo-registry-auth +{{- if .Values.git.credentials.enabled }} + - name: qtodo-git-credentials +{{- end }} \ No newline at end of file diff --git a/charts/supply-chain/templates/rbac/registry-token-refresher.yaml b/charts/supply-chain/templates/rbac/registry-token-refresher.yaml index 40fd7e58..a83b4294 100644 --- a/charts/supply-chain/templates/rbac/registry-token-refresher.yaml +++ b/charts/supply-chain/templates/rbac/registry-token-refresher.yaml @@ -72,6 +72,9 @@ spec: spec: backoffLimit: 3 template: + metadata: + labels: + app.kubernetes.io/name: registry-token-refresher spec: serviceAccountName: {{ .Values.pipelineServiceAccount }} restartPolicy: Never diff --git a/charts/supply-chain/templates/secrets/qtodo-git-credentials.yaml b/charts/supply-chain/templates/secrets/qtodo-git-credentials.yaml new file mode 100644 index 00000000..ff1a0367 --- /dev/null +++ b/charts/supply-chain/templates/secrets/qtodo-git-credentials.yaml @@ -0,0 +1,58 @@ +{{- if .Values.git.credentials.enabled }} +{{- $authType := .Values.git.credentials.authType | default "https" }} +{{- $host := .Values.git.credentials.host }} +--- +apiVersion: "external-secrets.io/v1beta1" +kind: ExternalSecret +metadata: + name: qtodo-git-credentials + namespace: {{ .Release.Namespace | default .Values.global.namespace }} +spec: + refreshInterval: 15s + secretStoreRef: + name: {{ .Values.global.secretStore.name }} + kind: {{ .Values.global.secretStore.kind }} + target: + name: qtodo-git-credentials + template: +{{- if eq $authType "ssh" }} + type: kubernetes.io/ssh-auth + metadata: + annotations: + tekton.dev/git-0: {{ $host | quote }} + data: + ssh-privatekey: {{ printf "{{ index . \"%s\" }}" .Values.git.credentials.sshPrivateKeyKey | quote }} + known_hosts: {{ printf "{{ index . \"%s\" }}" .Values.git.credentials.knownHostsKey | quote }} + data: + - secretKey: {{ .Values.git.credentials.sshPrivateKeyKey }} + remoteRef: + key: {{ .Values.git.credentials.vaultPath }} + property: {{ .Values.git.credentials.sshPrivateKeyKey }} + - secretKey: {{ .Values.git.credentials.knownHostsKey }} + remoteRef: + key: {{ .Values.git.credentials.vaultPath }} + property: {{ .Values.git.credentials.knownHostsKey }} +{{- else }} +{{- $hostBare := $host | trimPrefix "https://" | trimPrefix "http://" }} +{{- $userKey := .Values.git.credentials.usernameKey }} +{{- $passKey := .Values.git.credentials.passwordKey }} + type: Opaque + metadata: + annotations: + tekton.dev/git-0: {{ $host | quote }} + data: + .gitconfig: | + [credential "{{ $host }}"] + helper = store + .git-credentials: {{ printf "https://{{ .%s }}:{{ .%s | trim }}@%s" $userKey $passKey $hostBare | quote }} + data: + - secretKey: {{ $userKey }} + remoteRef: + key: {{ .Values.git.credentials.vaultPath }} + property: {{ $userKey }} + - secretKey: {{ $passKey }} + remoteRef: + key: {{ .Values.git.credentials.vaultPath }} + property: {{ $passKey }} +{{- end }} +{{- end }} diff --git a/charts/supply-chain/templates/tasks/build-artifact.yaml b/charts/supply-chain/templates/tasks/build-artifact.yaml index 57ab161d..3d49194d 100644 --- a/charts/supply-chain/templates/tasks/build-artifact.yaml +++ b/charts/supply-chain/templates/tasks/build-artifact.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-build-artifact diff --git a/charts/supply-chain/templates/tasks/generate-sbom.yaml b/charts/supply-chain/templates/tasks/generate-sbom.yaml index aab2457f..a2c7e546 100644 --- a/charts/supply-chain/templates/tasks/generate-sbom.yaml +++ b/charts/supply-chain/templates/tasks/generate-sbom.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-generate-sbom diff --git a/charts/supply-chain/templates/tasks/init.yaml b/charts/supply-chain/templates/tasks/init.yaml new file mode 100644 index 00000000..ed5b8046 --- /dev/null +++ b/charts/supply-chain/templates/tasks/init.yaml @@ -0,0 +1,55 @@ +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + labels: + app.kubernetes.io/version: "0.1" + annotations: + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: "supply-chain" + name: init + namespace: {{ .Values.global.namespace }} +spec: + description: >- + Initialize Pipeline Task. Determines whether the image should be built + by checking if the target image already exists in the registry. + Supports rebuild and skip-checks flags. + params: + - name: image-url + description: Image URL for build by PipelineRun + - name: rebuild + description: Rebuild the image even if it exists + default: "false" + - name: skip-checks + description: Skip checks against built image + default: "false" + results: + - name: build + description: Defines if the image in param image-url should be built + steps: + - name: init + image: {{ .Values.tasks.images.skopeo }} + computeResources: + limits: + memory: 256Mi + requests: + memory: 256Mi + cpu: 100m + env: + - name: IMAGE_URL + value: $(params.image-url) + - name: REBUILD + value: $(params.rebuild) + - name: SKIP_CHECKS + value: $(params.skip-checks) + script: | + #!/bin/bash + echo "Build Initialize: $IMAGE_URL" + echo + + echo "Determine if Image Already Exists" + if [ "$REBUILD" == "true" ] || [ "$SKIP_CHECKS" == "false" ] || ! skopeo inspect --no-tags --raw "docker://$IMAGE_URL" &>/dev/null; then + echo -n "true" > $(results.build.path) + else + echo -n "false" > $(results.build.path) + fi diff --git a/charts/supply-chain/templates/tasks/restart-qtodo.yaml b/charts/supply-chain/templates/tasks/restart-qtodo.yaml index 9fe23387..8eef1981 100644 --- a/charts/supply-chain/templates/tasks/restart-qtodo.yaml +++ b/charts/supply-chain/templates/tasks/restart-qtodo.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: restart-qtodo diff --git a/charts/supply-chain/templates/tasks/sbom-attest.yaml b/charts/supply-chain/templates/tasks/sbom-attest.yaml index 850f5374..ffd81fbb 100644 --- a/charts/supply-chain/templates/tasks/sbom-attest.yaml +++ b/charts/supply-chain/templates/tasks/sbom-attest.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-sbom-attestation diff --git a/charts/supply-chain/templates/tasks/sign-artifact.yaml b/charts/supply-chain/templates/tasks/sign-artifact.yaml index 3bfbaedd..03eb52bc 100644 --- a/charts/supply-chain/templates/tasks/sign-artifact.yaml +++ b/charts/supply-chain/templates/tasks/sign-artifact.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-sign-artifact diff --git a/charts/supply-chain/templates/tasks/sign-image.yaml b/charts/supply-chain/templates/tasks/sign-image.yaml index dc820538..9e8300af 100644 --- a/charts/supply-chain/templates/tasks/sign-image.yaml +++ b/charts/supply-chain/templates/tasks/sign-image.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-sign-image diff --git a/charts/supply-chain/templates/tasks/upload-sbom.yaml b/charts/supply-chain/templates/tasks/upload-sbom.yaml index ebd82532..dfd49771 100644 --- a/charts/supply-chain/templates/tasks/upload-sbom.yaml +++ b/charts/supply-chain/templates/tasks/upload-sbom.yaml @@ -1,6 +1,6 @@ {{- if eq .Values.rhtpa.enabled true }} --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-upload-sbom diff --git a/charts/supply-chain/templates/tasks/verify-artifact.yaml b/charts/supply-chain/templates/tasks/verify-artifact.yaml index 68e7186f..b36bd66e 100644 --- a/charts/supply-chain/templates/tasks/verify-artifact.yaml +++ b/charts/supply-chain/templates/tasks/verify-artifact.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-verify-artifact diff --git a/charts/supply-chain/templates/tasks/verify-image.yaml b/charts/supply-chain/templates/tasks/verify-image.yaml index c19d22e0..baaf4ef9 100644 --- a/charts/supply-chain/templates/tasks/verify-image.yaml +++ b/charts/supply-chain/templates/tasks/verify-image.yaml @@ -1,5 +1,5 @@ --- -apiVersion: tekton.dev/v1beta1 +apiVersion: tekton.dev/v1 kind: Task metadata: name: qtodo-verify-image diff --git a/charts/supply-chain/values.yaml b/charts/supply-chain/values.yaml index 817e2793..8cfb6260 100644 --- a/charts/supply-chain/values.yaml +++ b/charts/supply-chain/values.yaml @@ -40,6 +40,18 @@ rhtpa: clientSecretVaultPath: "secret/data/hub/infra/rhtpa/rhtpa-oidc-cli" clientSecretVaultKey: "client-secret" +# git credentials for protected repositories +git: + credentials: + enabled: false + authType: "https" + host: "https://github.com" + vaultPath: "secret/data/hub/supply-chain/git-credentials" + usernameKey: "username" + passwordKey: "password" + sshPrivateKeyKey: "ssh-privatekey" + knownHostsKey: "known_hosts" + # qtodo repository configuration qtodo: repository: "https://github.com/validatedpatterns-demos/qtodo.git" @@ -145,3 +157,4 @@ tasks: mandrel: "registry.redhat.io/quarkus/mandrel-for-jdk-21-rhel8:23.1-36" syft: "registry.redhat.io/rh-syft-tech-preview/syft-rhel9:1.29.0" rhtpa: "registry.access.redhat.com/ubi9/ubi:9.7-1764794285" + skopeo: "registry.access.redhat.com/ubi9/skopeo:9.5-1745865345@sha256:d91eb0dac7308ddfb12193368a42009509925edba80da9ffd3b82d03427dc3ed" diff --git a/docs/supply-chain.md b/docs/supply-chain.md index bb86dc18..4e653893 100644 --- a/docs/supply-chain.md +++ b/docs/supply-chain.md @@ -46,6 +46,12 @@ For the Secure Supply Chain use case, the command would be: python3 scripts/gen-feature-variants.py --base values-hub.yaml --features supply-chain --registry-option ``` +If the source repository is **private** (protected), add the `protected-repos` feature: + +```shell +python3 scripts/gen-feature-variants.py --base values-hub.yaml --features supply-chain,protected-repos --registry-option +``` + Where `` is one of the options available in _Bring Your Own (BYO) Container Registry_: 1. Embedded Quay Registry @@ -69,13 +75,20 @@ By default, ZTVP deploys a built-in Red Hat Quay registry. However, you can use Uncomment the `registry-user` secret and replace the placeholder with your registry token or password: + Store your registry token in a local file: + + ```shell + mkdir -p ~/.config/validated-patterns + echo -n "your-registry-token" > ~/.config/validated-patterns/registry-token + ``` + ```yaml - name: registry-user vaultPrefixes: - hub/infra/registry fields: - name: registry-password - value: "REPLACE_WITH_REGISTRY_TOKEN" + path: ~/.config/validated-patterns/registry-token onMissingValue: error ``` @@ -251,6 +264,9 @@ Once the supply-chain application has synced in ArgoCD, start the pipeline using At the bottom we have the **workspaces**. These must be configured manually. * For **qtodo-source**, select `PersistentVolumeClaim` and the PVC name is `qtodo-workspace-source`. * For **registry-auth-config**, select `Secret` and the name of the secret is `qtodo-registry-auth`. + * For **git-auth**, the binding depends on the authentication mode (see [How it works](#how-it-works) for details): + * **HTTPS mode**: select `Secret` and the name of the secret is `qtodo-git-credentials`. The `git-clone` ClusterTask's `basic-auth` workspace requires the secret to be provided explicitly; ServiceAccount-level credential injection alone is not sufficient for HTTPS. + * **SSH mode**: leave **git-auth** unbound (empty). SSH credentials are injected automatically via the `pipeline` ServiceAccount. Binding the workspace directly causes the `git-clone` ClusterTask's `prepare.sh` to run a recursive `chmod` on the copied secret volume, which fails on the read-only Kubernetes projected volume symlinks. 5. Press **Start** to finish and run the pipeline. @@ -258,6 +274,35 @@ Once the supply-chain application has synced in ArgoCD, start the pipeline using We can also start a pipeline execution using a CLI and the Kubernetes API. We start creating a new `PipelineRun` resource referencing the `qtodo-supply-chain` pipeline. Let's create a new file called `qtodo-pipeline.yaml` and copy this content. +**HTTPS mode** (bind `git-auth` to the `qtodo-git-credentials` secret): + +```yaml +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + generateName: qtodo-manual-run- + namespace: layered-zero-trust-hub +spec: + pipelineRef: + name: qtodo-supply-chain + taskRunTemplate: + serviceAccountName: pipeline + timeouts: + pipeline: 1h0m0s + workspaces: + - name: qtodo-source + persistentVolumeClaim: + claimName: qtodo-workspace-source + - name: registry-auth-config + secret: + secretName: qtodo-registry-auth + - name: git-auth + secret: + secretName: qtodo-git-credentials +``` + +**SSH mode** (leave `git-auth` unbound): + ```yaml apiVersion: tekton.dev/v1 kind: PipelineRun @@ -282,6 +327,8 @@ spec: As was described previously, verify the values associated with the PVC storage and registry configuration. +> **Note**: The `git-auth` workspace binding differs between authentication modes. In **HTTPS mode**, the `qtodo-git-credentials` secret must be bound explicitly -- ServiceAccount-level credential injection alone is not sufficient for the `git-clone` ClusterTask's `basic-auth` workspace. In **SSH mode**, the workspace must be left **unbound**; SSH credentials are injected automatically through the `pipeline` ServiceAccount. Binding the `git-auth` workspace in SSH mode causes the `git-clone` ClusterTask's `prepare.sh` to run a recursive `chmod` on the copied secret volume, which fails on the read-only Kubernetes projected volume symlinks. + Using the previously created definition, start a new execution of the pipeline using `oc` CLI: ```shell @@ -307,10 +354,139 @@ oc get taskruns -n layered-zero-trust-hub -l tekton.dev/pipelineRun=,tekton.dev/pipelineTask= ``` +### Protected Repositories + +By default the pipeline clones the qtodo source from a **public** GitHub repository. If your source code lives in a private (protected) repository, enable the Git credentials feature so the `git-clone` task can authenticate. + +Two authentication modes are supported: + +| Mode | URL format | Vault fields | Secret type | +| ----- | ------------------------------------ | ----------------------------------- | ---------------------- | +| HTTPS | `https://github.com/org/repo.git` | `username` + `password` (PAT) | Opaque (basic-auth) | +| SSH | `git@github.com:org/repo.git` | `ssh-privatekey` + `known_hosts` | kubernetes.io/ssh-auth | + +When using the `gen-feature-variants.py` script with `--git-repo`, the auth mode is auto-detected from the URL scheme. + +#### 1. Store Git credentials in Vault + +Uncomment the `git-credentials` secret in your local `~/values-secret.yaml` (copied from `values-secret.yaml.template`). Choose **one** of the two options: + +**Option A -- HTTPS basic auth** (username + Personal Access Token): + +Store your credentials in local files to avoid plaintext in YAML: + +```shell +mkdir -p ~/.config/validated-patterns +echo -n "your-git-username" > ~/.config/validated-patterns/git-username +echo -n "your-personal-access-token" > ~/.config/validated-patterns/git-token +``` + +```yaml +- name: git-credentials + vaultPrefixes: + - hub/supply-chain + fields: + - name: username + path: ~/.config/validated-patterns/git-username + onMissingValue: error + - name: password + path: ~/.config/validated-patterns/git-token + onMissingValue: error +``` + +**Option B -- SSH key auth**: + +```yaml +- name: git-credentials + vaultPrefixes: + - hub/supply-chain + fields: + - name: ssh-privatekey + path: ~/.ssh/id_ed25519_ztvp # or id_rsa, id_ecdsa, etc. + - name: known_hosts + path: ~/.ssh/known_hosts_github +``` + +Generate a passwordless SSH key pair (if you don't already have one): + +```shell +ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_ztvp -N "" +``` + +The key **must not** be password-protected -- Tekton cannot prompt for a passphrase at runtime. + +Generate the `known_hosts` file for your Git host: + +```shell +ssh-keyscan github.com > ~/.ssh/known_hosts_github +``` + +Then load the secret into Vault: `./pattern.sh make load-secrets`. + +#### 2. Enable Git credentials in the supply-chain overrides + +**Preferred: use the generator.** Add `protected-repos` to the features list and provide your private repository URL with `--git-repo`. The generator auto-detects the auth mode and sets all overrides (host, authType, repository) automatically: + +```shell +# HTTPS +python3 scripts/gen-feature-variants.py \ + --features supply-chain,protected-repos \ + --registry-option \ + --git-repo https://github.com/your-org/qtodo.git + +# SSH +python3 scripts/gen-feature-variants.py \ + --features supply-chain,protected-repos \ + --registry-option \ + --git-repo git@github.com:your-org/qtodo.git +``` + +**Manual configuration.** Add the following overrides to the `supply-chain` application in `values-hub.yaml`: + +```yaml +- name: git.credentials.enabled + value: "true" +- name: git.credentials.authType + value: "https" # or "ssh" +- name: git.credentials.host + value: "https://github.com" # SSH: "github.com" (no scheme) +- name: git.credentials.vaultPath + value: "secret/data/hub/supply-chain/git-credentials" +``` + +#### 3. Point the pipeline at your private repository + +When using the generator with `--git-repo`, the `qtodo.repository` override is set automatically in the generated `values-hub.yaml`. If you are configuring manually, add this override to the `supply-chain` application: + +```yaml +- name: qtodo.repository + value: "https://github.com/your-org/qtodo.git" # or SSH URL (git@github.com:your-org/qtodo.git) +``` + +#### How it works + +When `git.credentials.enabled` is `true`: + +* An `ExternalSecret` (`qtodo-git-credentials`) pulls the credentials from Vault and creates a secret annotated with `tekton.dev/git-0` pointing to the configured host. + * **HTTPS mode**: creates an `Opaque` secret with `.git-credentials` and `.gitconfig` files. + * **SSH mode**: creates a `kubernetes.io/ssh-auth` secret with `ssh-privatekey` and `known_hosts` entries. +* The `pipeline` ServiceAccount lists the secret (see `pipeline-sa.yaml`). Tekton's credential initialization automatically injects the credentials into task containers -- `.gitconfig` and `.git-credentials` for HTTPS, or `~/.ssh/config`, `~/.ssh/id_*`, and `~/.ssh/known_hosts` for SSH. +* The `git-auth` workspace is declared in the pipeline as `optional: true`. How it should be bound depends on the authentication mode: + * **HTTPS mode**: the `git-auth` workspace **must** be bound to the `qtodo-git-credentials` secret. ServiceAccount-level credential injection alone is not sufficient -- without an explicit workspace binding, the `git-clone` ClusterTask cannot access the protected repository. + * **SSH mode**: the `git-auth` workspace must be left **unbound**. SSH credentials are injected automatically via the ServiceAccount. Binding the workspace triggers the `git-clone` ClusterTask's `prepare.sh`, which runs a recursive `chmod` on the copied secret volume; this fails on the read-only Kubernetes projected volume symlinks and aborts the step. +* The Vault policy `hub-supply-chain-jwt-secret` grants read access to `secret/data/hub/supply-chain/*` for the pipeline's SPIFFE identity. + +### Init task (pre-flight image check) + +The pipeline includes an `init` task that runs before `git-clone`. It uses `skopeo inspect` to check whether the target image already exists in the registry. If the image exists (and `rebuild` is not set to `"true"`), the pipeline skips the build. This avoids unnecessary rebuilds and is modeled after the [RHTAP sample pipelines](https://github.com/konflux-ci/build-definitions). + +The pipeline also emits Tekton Chains provenance results (`CHAINS-GIT_URL`, `CHAINS-GIT_COMMIT`, `IMAGE_URL`, `IMAGE_DIGEST`) so that Tekton Chains can automatically generate and sign provenance attestations. + ### Pipeline tasks The pipeline we have prepared has the following steps: +* **init**. Checks whether the target image already exists in the registry. Gates the build with a `when` condition. * **qtodo-clone-repository**. Clones the `qtodo` repository. * **qtodo-build-artifact**. Builds an _uber-jar_ of `qtodo` application. * **qtodo-sign-artifact**. Signs the JAR file generated during the build process. diff --git a/overrides/values-vault-network-policy.yaml b/overrides/values-vault-network-policy.yaml index ddd9aeba..23abfc46 100644 --- a/overrides/values-vault-network-policy.yaml +++ b/overrides/values-vault-network-policy.yaml @@ -23,6 +23,28 @@ vault: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: qtodo + # registry-token-refresher seed job — fetches Vault token for Quay registry + - ports: + - protocol: TCP + port: 8200 + from: + - podSelector: + matchLabels: + job-name: registry-token-refresher-seed + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: layered-zero-trust-hub + # registry-token-refresher CronJob — periodic token refresh to Vault + - ports: + - protocol: TCP + port: 8200 + from: + - podSelector: + matchLabels: + app.kubernetes.io/name: registry-token-refresher + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: layered-zero-trust-hub # Vault cluster replication port — HA readiness - ports: - protocol: TCP diff --git a/scripts/features/features.yaml b/scripts/features/features.yaml index 49ca349a..434d53a7 100644 --- a/scripts/features/features.yaml +++ b/scripts/features/features.yaml @@ -33,6 +33,11 @@ features: org: ztvp image_name: qtodo + protected-repos: + description: "Protected (private) Git repository support for supply-chain" + depends_on: [supply-chain] + git_repo_required: true + entra-id: description: "Azure Entra ID integration" depends_on: [supply-chain] diff --git a/scripts/features/protected-repos.yaml b/scripts/features/protected-repos.yaml new file mode 100644 index 00000000..8674e382 --- /dev/null +++ b/scripts/features/protected-repos.yaml @@ -0,0 +1,24 @@ +# Protected repository support for the supply-chain pipeline. +# Enables git-clone to authenticate against private Git repositories +# using credentials stored in Vault (ExternalSecret -> secret). +# +# Supports two authentication modes (auto-detected from --git-repo URL): +# https - basic-auth via .git-credentials (username + PAT) +# ssh - SSH key pair (ssh-privatekey + known_hosts) +# +# Requires the git-credentials secret in values-secret.yaml.template +# to be uncommented and populated with the appropriate credentials. +clusterGroup: + merge_into_applications: + supply-chain: + overrides: + - name: git.credentials.enabled + value: "true" + - name: git.credentials.authType + value: "REPLACE_WITH_GIT_AUTH_TYPE" + - name: git.credentials.host + value: "REPLACE_WITH_GIT_HOST" + - name: git.credentials.vaultPath + value: "secret/data/hub/supply-chain/git-credentials" + - name: qtodo.repository + value: "REPLACE_WITH_GIT_REPO_URL" diff --git a/scripts/features/supply-chain.yaml b/scripts/features/supply-chain.yaml index f40d9bec..fdc7dbe8 100644 --- a/scripts/features/supply-chain.yaml +++ b/scripts/features/supply-chain.yaml @@ -1,6 +1,9 @@ # Secure Supply Chain application + vault role # Depends on: pipelines, rhtas, rhtpa, storage (all resolved automatically) # Requires --registry-option to select the registry backend. +# +# Optional: add --features protected-repos to enable private Git repository +# support (see scripts/features/protected-repos.yaml). clusterGroup: applications: supply-chain: @@ -13,6 +16,14 @@ clusterGroup: - kind: ServiceAccount jqPathExpressions: - ".imagePullSecrets[]|select(.name | contains(\"-dockercfg-\"))" + - group: tekton.dev + kind: Task + jsonPointers: + - /spec/steps + - group: tekton.dev + kind: Pipeline + jsonPointers: + - /spec/tasks merge_into_applications: vault: diff --git a/scripts/gen-feature-variants.md b/scripts/gen-feature-variants.md index fe9235e9..ecab1805 100644 --- a/scripts/gen-feature-variants.md +++ b/scripts/gen-feature-variants.md @@ -57,6 +57,12 @@ python3 scripts/gen-feature-variants.py --features supply-chain --registry-optio # Generate all three supply-chain registry variants at once python3 scripts/gen-feature-variants.py --features supply-chain --registry-option all +# Supply chain with protected (private) Git repository support +python3 scripts/gen-feature-variants.py \ + --features supply-chain,protected-repos \ + --registry-option 2 \ + --git-repo https://github.com/your-org/qtodo.git + # Custom base file and output directory python3 scripts/gen-feature-variants.py \ --features rhtpa --base values-hub.yaml --outdir /tmp @@ -81,6 +87,49 @@ The output directory is created automatically if it does not exist. > without these fields, replace the placeholders manually before applying > the generated file. +## Protected Repositories (`--git-repo`) + +When the `protected-repos` feature is enabled, the `--git-repo` argument is +**required**. It specifies the private Git repository URL that the Tekton +pipeline will clone. The generator auto-detects the authentication mode +(HTTPS or SSH) from the URL scheme and sets `git.credentials.authType` and +`git.credentials.host` accordingly: + +```bash +# HTTPS (basic-auth with username + PAT) +python3 scripts/gen-feature-variants.py \ + --features supply-chain,protected-repos \ + --registry-option 1 \ + --git-repo https://github.com/your-org/qtodo.git + +# SSH (key-based auth) +python3 scripts/gen-feature-variants.py \ + --features supply-chain,protected-repos \ + --registry-option 1 \ + --git-repo git@github.com:your-org/qtodo.git +``` + +For an **HTTPS** URL the generated `values-hub.yaml` will include: + +```yaml +- name: git.credentials.authType + value: "https" +- name: git.credentials.host + value: "https://github.com" +``` + +For an **SSH** URL: + +```yaml +- name: git.credentials.authType + value: "ssh" +- name: git.credentials.host + value: "github.com" +``` + +See [Protected Repositories](../docs/supply-chain.md#protected-repositories) +for the full setup (Vault credentials, ExternalSecret, workspace selection). + ## How It Works 1. The script reads the base `values-hub.yaml`. diff --git a/scripts/gen-feature-variants.py b/scripts/gen-feature-variants.py index eb7867cb..6565e548 100755 --- a/scripts/gen-feature-variants.py +++ b/scripts/gen-feature-variants.py @@ -35,8 +35,10 @@ import argparse import copy import os +import re import sys from collections import OrderedDict +from urllib.parse import urlparse from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap, CommentedSeq @@ -304,6 +306,43 @@ def _substitute_repository_placeholders(base, org=None, image_name=None): base["global"]["registry"]["repository"] = repo +GIT_REPO_PLACEHOLDER = "REPLACE_WITH_GIT_REPO_URL" +GIT_HOST_PLACEHOLDER = "REPLACE_WITH_GIT_HOST" +GIT_AUTH_TYPE_PLACEHOLDER = "REPLACE_WITH_GIT_AUTH_TYPE" + +SSH_URL_RE = re.compile(r"^[\w.-]+@([\w.-]+):") + + +def _parse_git_repo_url(git_repo_url): + """Derive (host, auth_type) from a Git repository URL. + + HTTPS URLs -> host = "https://github.com", auth_type = "https" + SSH URLs -> host = "github.com", auth_type = "ssh" + """ + m = SSH_URL_RE.match(git_repo_url) + if m: + return m.group(1), "ssh" + parsed = urlparse(git_repo_url) + scheme = parsed.scheme or "https" + hostname = parsed.hostname or "" + return f"{scheme}://{hostname}", "https" + + +def _substitute_git_overrides(base, git_repo_url, git_host, git_auth_type): + """Replace git-related placeholders in supply-chain overrides.""" + apps = base.get("clusterGroup", {}).get("applications", {}) + sc = apps.get("supply-chain", {}) + placeholder_map = { + "qtodo.repository": (GIT_REPO_PLACEHOLDER, git_repo_url), + "git.credentials.host": (GIT_HOST_PLACEHOLDER, git_host), + "git.credentials.authType": (GIT_AUTH_TYPE_PLACEHOLDER, git_auth_type), + } + for override in sc.get("overrides", []): + entry = placeholder_map.get(override.get("name")) + if entry and str(override.get("value")) == entry[0]: + override["value"] = entry[1] + + def generate_variant( base_path, features_dir, @@ -312,6 +351,7 @@ def generate_variant( output_path, org=None, image_name=None, + git_repo_url=None, ): """Load base, merge all feature fragments + registry option, write output.""" yaml = YAML() @@ -343,6 +383,10 @@ def generate_variant( if org or image_name: _substitute_repository_placeholders(base, org=org, image_name=image_name) + if git_repo_url: + git_host, git_auth_type = _parse_git_repo_url(git_repo_url) + _substitute_git_overrides(base, git_repo_url, git_host, git_auth_type) + validate_output(base) cg = base.get("clusterGroup") if cg: @@ -360,7 +404,8 @@ def build_output_name(features, registry_option=None): """Construct the output filename from features and optional registry option.""" if "supply-chain" in features: label = REGISTRY_LABELS.get(registry_option, f"option-{registry_option}") - return f"values-hub-supply-chain-{label}.yaml" + suffix = "-protected-repos" if "protected-repos" in features else "" + return f"values-hub-supply-chain-{label}{suffix}.yaml" return f"values-hub-{'-'.join(features)}.yaml" @@ -395,6 +440,12 @@ def main(): default=None, help="Output directory (default: /tmp)", ) + parser.add_argument( + "--git-repo", + default=None, + help="Private Git repository URL for protected-repos feature " + "(e.g. https://github.com/your-org/qtodo.git)", + ) parser.add_argument( "--list-features", action="store_true", @@ -455,11 +506,24 @@ def main(): ) sys.exit(1) + needs_git_repo = any( + feature_defs.get(f, {}).get("git_repo_required") for f in resolved + ) + if needs_git_repo and not args.git_repo: + print( + "ERROR: --git-repo is required when protected-repos feature is enabled " + "(e.g. --git-repo https://github.com/your-org/qtodo.git)", + file=sys.stderr, + ) + sys.exit(1) + print(f"Base: {base}") print(f"Output: {outdir}") print(f"Features: {' -> '.join(resolved)}") if args.registry_option: print(f"Registry: option {args.registry_option}") + if args.git_repo: + print(f"Git repo: {args.git_repo}") if args.registry_option == "all": for opt_num in [1, 2, 3]: @@ -475,7 +539,14 @@ def main(): out_name = build_output_name(requested, opt_num) out_path = os.path.join(outdir, out_name) generate_variant( - base, FEATURES_DIR, resolved, reg_path, out_path, org, image_name + base, + FEATURES_DIR, + resolved, + reg_path, + out_path, + org, + image_name, + git_repo_url=args.git_repo, ) else: reg_path = None @@ -496,7 +567,14 @@ def main(): ) out_path = os.path.join(outdir, out_name) generate_variant( - base, FEATURES_DIR, resolved, reg_path, out_path, org, image_name + base, + FEATURES_DIR, + resolved, + reg_path, + out_path, + org, + image_name, + git_repo_url=args.git_repo, ) if args.registry_option and org and image_name: diff --git a/values-hub.yaml b/values-hub.yaml index cae32f1e..edc41ab7 100644 --- a/values-hub.yaml +++ b/values-hub.yaml @@ -361,6 +361,9 @@ clusterGroup: path "secret/data/hub/infra/rhtpa/rhtpa-oidc-cli" { capabilities = ["read"] } + path "secret/data/hub/supply-chain/*" { + capabilities = ["read"] + } jwt: enabled: true oidcDiscoveryUrl: https://spire-spiffe-oidc-discovery-provider.zero-trust-workload-identity-manager.svc.cluster.local @@ -577,6 +580,14 @@ clusterGroup: # - kind: ServiceAccount # jqPathExpressions: # - .imagePullSecrets[]|select(.name | contains("-dockercfg-")) + # - group: tekton.dev + # kind: Task + # jsonPointers: + # - /spec/steps + # - group: tekton.dev + # kind: Pipeline + # jsonPointers: + # - /spec/tasks # overrides: # # Registry credentials are inherited from global.registry. # # Only set app-specific overrides below. @@ -601,6 +612,15 @@ clusterGroup: # # Uncomment to auto-trigger a pipeline run on every ArgoCD sync # # - name: pipelinerun.enabled # # value: "true" + # # Protected repository credentials (Vault + ExternalSecret) + # # - name: git.credentials.enabled + # # value: "true" + # # - name: git.credentials.host + # # value: "https://github.com" + # # - name: git.credentials.vaultPath + # # value: "secret/data/hub/supply-chain/git-credentials" + # # - name: qtodo.repository + # # value: "https://github.com/your-org/qtodo.git" # # ACS Central Services acs-central: diff --git a/values-secret.yaml.template b/values-secret.yaml.template index a5b715ce..55cc3d19 100644 --- a/values-secret.yaml.template +++ b/values-secret.yaml.template @@ -219,17 +219,54 @@ secrets: # Used by: supply-chain pipeline (push), qtodo (pull) when registry enabled # Policy: hub-supply-chain-jwt-secret (read access to hub/infra/registry/*) # - # Uncomment and replace REPLACE_WITH_REGISTRY_TOKEN with your registry - # token/password in your local ~/values-secret-layered-zero-trust.yaml. + # Store your registry token in a local file to avoid plaintext in YAML: + # echo -n "your-registry-token" > ~/.config/validated-patterns/registry-token # =========================================================================== #- name: registry-user # vaultPrefixes: # - hub/infra/registry # fields: # - name: registry-password - # value: "REPLACE_WITH_REGISTRY_TOKEN" + # path: ~/.config/validated-patterns/registry-token # onMissingValue: error + # =========================================================================== + # SUPPLY CHAIN GIT CREDENTIALS (hub/supply-chain/) + # Credentials for cloning protected Git repositories in Tekton pipelines + # Policy: hub-supply-chain-jwt-secret (read access to hub/supply-chain/*) + # + # Choose ONE of the two options below depending on your auth type: + # Option A: HTTPS basic auth (username + PAT) + # Option B: SSH key auth (ssh-privatekey + known_hosts) + # =========================================================================== + # Option A: HTTPS basic auth (uncomment for HTTPS protected repositories) + # Store your Git username and PAT in local files to avoid plaintext in YAML: + # echo -n "your-git-username" > ~/.config/validated-patterns/git-username + # echo -n "your-personal-access-token" > ~/.config/validated-patterns/git-token + #- name: git-credentials + # vaultPrefixes: + # - hub/supply-chain + # fields: + # - name: username + # path: ~/.config/validated-patterns/git-username + # onMissingValue: error + # - name: password + # path: ~/.config/validated-patterns/git-token + # onMissingValue: error + + # Option B: SSH key auth (uncomment for SSH protected repositories) + # The private key must NOT be password-protected (passphrase-less). + # Generate a passwordless key: ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_ztvp -N "" + # Generate known_hosts: ssh-keyscan github.com > ~/.ssh/known_hosts_github + #- name: git-credentials + # vaultPrefixes: + # - hub/supply-chain + # fields: + # - name: ssh-privatekey + # path: ~/.ssh/id_ed25519_ztvp # Replace with your SSH private key path + # - name: known_hosts + # path: ~/.ssh/known_hosts_github # Replace with your known_hosts file path + # =========================================================================== # COCO (CONFIDENTIAL CONTAINERS) SECRETS # Uncomment the secrets below when deploying with CoCo support.