Skip to content

Commit d5c7a48

Browse files
feat: corporate CA trust for pipeline git-clone from internal hosts (#142)
* feat: corporate CA trust for pipeline git-clone from internal hosts Add support for the git-clone task to trust corporate/internal CA certificates when cloning from private Git servers (e.g. GitLab behind a corporate CA). Supply-chain chart: - Add conditional ssl-ca-directory workspace to pipeline and pipelinerun templates (gated by git.sslCABundle.enabled) - Add git.sslCABundle values (enabled, configMapName) defaulting to the ztvp-trusted-ca ConfigMap - Set CRT_FILENAME param so git-clone finds the CA bundle file ztvp-certificates chart: - Auto-detect internal Git hosts via customCA.remoteHosts: the extraction Job connects to the host on port 443, extracts the full CA chain from the TLS handshake, and merges it into the bundle - Distribute ztvp-trusted-ca to the pipeline namespace via the targetNamespaces list Generator (gen-feature-variants.py): - Auto-enable git.sslCABundle and customCA.remoteHosts when --git-repo points to a non-public host (not github.com/gitlab.com/bitbucket.org) - Add git.sslCABundle.enabled to the protected-repos feature fragment and to the commented-out overrides in the base values-hub.yaml values-hub.yaml: - Replace hand-edited file with gen-feature-variants output for consistent indentation and complete feature composition Documentation: - Add "Corporate CA trust for internal Git hosts" section to docs/supply-chain.md covering enablement, auto-extraction, and manual CA provisioning alternatives Signed-off-by: Min Zhang <minzhang@redhat.com> * fix: trim username in git-credentials to handle trailing newlines Signed-off-by: Manuel Lorenzo <mlorenzofr@redhat.com> * fix: address PR review feedback - Skip SSL CA bundle workspace/params when authType is SSH (not needed for SSH connections to git) - Add hostname validation in _parse_git_repo_url() to fail early on malformed URLs Signed-off-by: Min Zhang <minzhang@redhat.com> * fix: update ExternalSecret API version from v1beta1 to v1 The External Secrets Operator no longer serves v1beta1; only v1 is available on the cluster, causing supply-chain sync failures. Signed-off-by: Min Zhang <minzhang@redhat.com> * docs: mention Vault JWT roles that must be uncommented for supply chain The supply-chain doc lists values-hub.yaml sections to uncomment but did not mention overrides/values-vault-jwt.yaml. Without the rhtpa and supply-chain JWT roles, RHTPA and the pipeline SA cannot authenticate to Vault via SPIFFE. Signed-off-by: Min Zhang <minzhang@redhat.com> * docs: add repo-server restart guidance for post-install custom CA When a custom CA is added after the pattern is already deployed, the ArgoCD repo-server init container will not re-run to pick up the updated trusted-ca-bundle. Document the rollout restart workaround and broaden the x509 troubleshooting entry to cover Gitea and other self-hosted Git servers beyond GitLab. Signed-off-by: Min Zhang <minzhang@redhat.com> * docs: decouple corporate CA trust from protected repositories The corporate CA trust configuration was nested as step 4 under "Protected Repositories", implying it only applies to private repos. In reality these are orthogonal concerns -- a public repo on an internal Git server behind a corporate CA also needs CA trust without any git credentials. Promote "Corporate CA Trust for Internal Git Hosts" to its own top-level section, split the combined "How it works" block, and add bidirectional cross-references. Also fix the duplicate SSH mode PipelineRun YAML and add ssl-ca-directory guidance to the Web Console instructions. Signed-off-by: Min Zhang <minzhang@redhat.com> --------- Signed-off-by: Min Zhang <minzhang@redhat.com> Signed-off-by: Manuel Lorenzo <mlorenzofr@redhat.com> Co-authored-by: Manuel Lorenzo <mlorenzofr@redhat.com>
1 parent e457810 commit d5c7a48

13 files changed

Lines changed: 209 additions & 18 deletions

charts/supply-chain/templates/pipeline-qtodo.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ spec:
110110
- name: registry-auth-config
111111
- name: git-auth
112112
optional: true
113+
{{- if and .Values.git.sslCABundle.enabled (ne (default "https" .Values.git.credentials.authType) "ssh") }}
114+
- name: ssl-ca-directory
115+
optional: true
116+
{{- end }}
113117

114118
results:
115119
- name: CHAINS-GIT_URL
@@ -160,6 +164,10 @@ spec:
160164
value: $(params.git-url)
161165
- name: REVISION
162166
value: $(params.git-revision)
167+
{{- if and .Values.git.sslCABundle.enabled (ne (default "https" .Values.git.credentials.authType) "ssh") }}
168+
- name: CRT_FILENAME
169+
value: tls-ca-bundle.pem
170+
{{- end }}
163171
workspaces:
164172
- name: output
165173
workspace: qtodo-source
@@ -170,6 +178,10 @@ spec:
170178
- name: basic-auth
171179
workspace: git-auth
172180
{{- end }}
181+
{{- if and .Values.git.sslCABundle.enabled (ne (default "https" .Values.git.credentials.authType) "ssh") }}
182+
- name: ssl-ca-directory
183+
workspace: ssl-ca-directory
184+
{{- end }}
173185

174186
- name: qtodo-build-artifact
175187
runAfter:

charts/supply-chain/templates/pipelinerun-qtodo.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ spec:
107107
- name: registry-auth-config
108108
secret:
109109
secretName: {{ .Values.registry.authSecretName }}
110+
{{- if and .Values.git.sslCABundle.enabled (ne (default "https" .Values.git.credentials.authType) "ssh") }}
111+
- name: ssl-ca-directory
112+
configMap:
113+
name: {{ .Values.git.sslCABundle.configMapName }}
114+
{{- end }}
110115
MANIFEST
111116
echo "PipelineRun created successfully."
112117
{{- end }}

charts/supply-chain/templates/secrets/qtodo-git-credentials.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{{- $authType := .Values.git.credentials.authType | default "https" }}
33
{{- $host := .Values.git.credentials.host }}
44
---
5-
apiVersion: "external-secrets.io/v1beta1"
5+
apiVersion: "external-secrets.io/v1"
66
kind: ExternalSecret
77
metadata:
88
name: qtodo-git-credentials
@@ -44,7 +44,7 @@ spec:
4444
.gitconfig: |
4545
[credential "{{ $host }}"]
4646
helper = store
47-
.git-credentials: {{ printf "https://{{ .%s }}:{{ .%s | trim }}@%s" $userKey $passKey $hostBare | quote }}
47+
.git-credentials: {{ printf "https://{{ .%s | trim }}:{{ .%s | trim }}@%s" $userKey $passKey $hostBare | quote }}
4848
data:
4949
- secretKey: {{ $userKey }}
5050
remoteRef:

charts/supply-chain/templates/secrets/rhtas-client-secret.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{{- if and .Values.rhtas.oidc.enabled (ne .Values.rhtas.oidc.clientSecretName "") }}
22
---
3-
apiVersion: "external-secrets.io/v1beta1"
3+
apiVersion: "external-secrets.io/v1"
44
kind: ExternalSecret
55
metadata:
66
name: {{ .Values.rhtas.oidc.clientSecretName }}

charts/supply-chain/values.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ git:
5151
passwordKey: "password"
5252
sshPrivateKeyKey: "ssh-privatekey"
5353
knownHostsKey: "known_hosts"
54+
# Corporate/custom CA bundle for HTTPS git clones from internal hosts.
55+
# When enabled, the git-clone task mounts the CA ConfigMap as the
56+
# ssl-ca-directory workspace so TLS verification succeeds against
57+
# internal Git servers (e.g. GitLab behind a corporate CA).
58+
sslCABundle:
59+
enabled: false
60+
configMapName: "ztvp-trusted-ca"
5461

5562
# qtodo repository configuration
5663
qtodo:

charts/ztvp-certificates/files/extract-certificates.sh.tpl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ log "ZTVP CA Certificate Extraction"
2222
log "==========================================="
2323
log "Auto-detect: {{ .Values.autoDetect }}"
2424
log "Custom CA: {{ .Values.customCA.secretRef.enabled }}"
25+
log "Remote hosts: {{ len .Values.customCA.remoteHosts }}"
2526
log "Namespace: {{ .Values.global.namespace }}"
2627
log "ConfigMap: {{ .Values.configMapName }}"
2728

@@ -46,6 +47,30 @@ else
4647
fi
4748
{{- end }}
4849

50+
# ===================================================================
51+
# PHASE 1.5: Extract CA chains from remote hosts (if configured)
52+
# No authentication required -- CAs are part of the public TLS handshake.
53+
# ===================================================================
54+
55+
{{- if .Values.customCA.remoteHosts }}
56+
REMOTE_HOST_COUNT=0
57+
{{- range $host := .Values.customCA.remoteHosts }}
58+
log "Extracting CA chain from remote host: {{ $host }}:443"
59+
REMOTE_CERTS=$(openssl s_client -connect {{ $host }}:443 -showcerts </dev/null 2>/dev/null \
60+
| awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/')
61+
if [[ -n "$REMOTE_CERTS" ]]; then
62+
SAFE_NAME=$(echo "{{ $host }}" | tr '.:' '-')
63+
echo "$REMOTE_CERTS" > "${TEMP_DIR}/remote-${SAFE_NAME}.crt"
64+
REMOTE_HOST_COUNT=$((REMOTE_HOST_COUNT + 1))
65+
CUSTOM_CA_FOUND=true
66+
log "OK: Extracted CA chain from {{ $host }}"
67+
else
68+
error "Failed to extract CA chain from {{ $host }}:443 (is the host reachable?)"
69+
fi
70+
{{- end }}
71+
log "Extracted CA chains from $REMOTE_HOST_COUNT remote host(s)"
72+
{{- end }}
73+
4974
# ===================================================================
5075
# PHASE 2: Extract Ingress CA (if auto-detect enabled)
5176
# ===================================================================
@@ -298,6 +323,7 @@ metadata:
298323
ztvp.io/auto-detect: "{{ .Values.autoDetect }}"
299324
ztvp.io/custom-ca-enabled: "{{ .Values.customCA.secretRef.enabled }}"
300325
ztvp.io/custom-ca-found: "${CUSTOM_CA_FOUND}"
326+
ztvp.io/remote-hosts: "{{ len .Values.customCA.remoteHosts }}"
301327
ztvp.io/ingress-ca-found: "${INGRESS_CA_FOUND}"
302328
ztvp.io/service-ca-found: "${SERVICE_CA_FOUND}"
303329
ztvp.io/cluster-ca-found: "${CLUSTER_CA_FOUND}"

charts/ztvp-certificates/values.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ customCA:
3838
# oc create secret generic <name> --from-file=ca.crt=/path/to/cert.crt -n openshift-config
3939
# Configure via overrides/values-ztvp-certificates.yaml (using extraValueFiles)
4040
additionalCertificates: []
41+
42+
# Remote host CA extraction: fetch TLS CA chains directly from remote hosts.
43+
# No authentication needed -- CA certificates are part of the public TLS handshake.
44+
# The extraction Job runs openssl s_client against each host on port 443 and
45+
# saves the full certificate chain. Useful for internal Git servers, registries,
46+
# or any service behind a corporate CA.
47+
# The CronJob keeps the extracted CAs fresh automatically.
48+
remoteHosts: []
49+
# Example:
50+
# remoteHosts:
51+
# - gitlab.cee.redhat.com
52+
# - registry.internal.example.com
4153
# Example:
4254
# additionalCertificates:
4355
# - name: corporate-root-ca

docs/private-repos.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,10 @@ Expected output: `Synced` (or `OutOfSync` if you have uncommitted changes).
246246
in a container without your Git host's fingerprint in known_hosts.
247247

248248
* **HTTPS: "x509: certificate signed by unknown authority"** -- This
249-
affects internal/self-hosted GitLab instances whose TLS certificates are
250-
signed by a corporate CA. GitHub and public GitLab (`gitlab.com`) use
251-
publicly trusted CAs and do not require this step.
249+
affects internal/self-hosted Git servers (e.g. Gitea, GitLab) whose TLS
250+
certificates are signed by a custom or corporate CA. GitHub and public
251+
GitLab (`gitlab.com`) use publicly trusted CAs and do not require this
252+
step.
252253

253254
The corporate CA must be in the cluster trust store **before** install
254255
because the VP operator needs it to clone the repository. Add the internal CA
@@ -263,6 +264,18 @@ oc patch proxy/cluster --type=merge \
263264

264265
Wait a few minutes for operator pods to restart with the updated bundle.
265266

267+
If the custom CA is added **after** the pattern is already deployed, the
268+
`trusted-ca-bundle` ConfigMap will be updated by the cluster CA injector,
269+
but the ArgoCD repo-server will **not** pick it up automatically. The
270+
repo-server uses an init container (`fetch-ca`) that copies the CA bundle
271+
into an `emptyDir` volume at pod startup; this only runs once. Restart
272+
the repo-server to load the updated bundle:
273+
274+
```shell
275+
oc rollout restart deployment/vp-gitops-repo-server -n vp-gitops
276+
oc rollout status deployment/vp-gitops-repo-server -n vp-gitops
277+
```
278+
266279
> [!NOTE]
267280
> After the pattern deploys, the `ztvp-certificates` chart automatically
268281
> merges your `custom-ca` content into its managed `ztvp-proxy-ca`

docs/supply-chain.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ In this project, we used the [qtodo](https://github.com/validatedpatterns-demos/
1818
> * `applications.noobaa-mcg` — NooBaa MCG object storage (required by Quay and RHTPA)
1919
> * `subscriptions.odf` and `subscriptions.quay-operator` and their namespace entries
2020
>
21+
> Additionally, uncomment the following Vault JWT roles in `overrides/values-vault-jwt.yaml` so that RHTPA and the pipeline ServiceAccount can authenticate to Vault via SPIFFE:
22+
>
23+
> * `rhtpa` role — allows RHTPA to read its OIDC credentials from Vault
24+
> * `supply-chain` role — allows the Tekton pipeline ServiceAccount to read git credentials, registry credentials, and RHTPA OIDC secrets from Vault
25+
>
2126
> If you prefer to use an external image registry instead of Quay, skip the Quay and NooBaa sections and set the registry parameters in the `supply-chain` application overrides accordingly.
2227
2328
## Components
@@ -267,6 +272,7 @@ Once the supply-chain application has synced in ArgoCD, start the pipeline using
267272
* For **git-auth**, the binding depends on the authentication mode (see [How it works](#how-it-works) for details):
268273
* **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.
269274
* **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.
275+
* For **ssl-ca-directory** (HTTPS mode with internal Git hosts only): if `git.sslCABundle.enabled` is `true`, select `ConfigMap` and the name is `ztvp-trusted-ca`. This is only needed when cloning over HTTPS from a Git server behind a corporate or self-signed CA (see [Corporate CA Trust for Internal Git Hosts](#corporate-ca-trust-for-internal-git-hosts)).
270276

271277
5. Press **Start** to finish and run the pipeline.
272278

@@ -299,6 +305,10 @@ spec:
299305
- name: git-auth
300306
secret:
301307
secretName: qtodo-git-credentials
308+
# Add this workspace when git.sslCABundle.enabled is true (internal Git hosts):
309+
# - name: ssl-ca-directory
310+
# configMap:
311+
# name: ztvp-trusted-ca
302312
```
303313

304314
**SSH mode** (leave `git-auth` unbound):
@@ -476,6 +486,41 @@ When `git.credentials.enabled` is `true`:
476486
* **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.
477487
* The Vault policy `hub-supply-chain-jwt-secret` grants read access to `secret/data/hub/supply-chain/*` for the pipeline's SPIFFE identity.
478488

489+
> [!NOTE]
490+
> If your internal Git server also uses a corporate or self-signed CA, see [Corporate CA Trust for Internal Git Hosts](#corporate-ca-trust-for-internal-git-hosts) to configure TLS trust.
491+
492+
### Corporate CA Trust for Internal Git Hosts
493+
494+
This section applies whenever the pipeline clones from a Git server whose TLS certificate is signed by a corporate or self-signed CA, regardless of whether the repository is private. It is only relevant for HTTPS clones; SSH connections do not use TLS certificate verification.
495+
496+
> [!NOTE]
497+
> Public Git hosts (github.com, gitlab.com) use publicly trusted certificates and do not require this. If the repository is also private, combine these settings with the [Protected Repositories](#protected-repositories) configuration above.
498+
499+
When a repository is hosted on an internal Git server (e.g. GitLab behind a corporate CA), the `git-clone` task will fail with `SSL certificate problem: self-signed certificate in certificate chain` because the pod does not trust the corporate CA.
500+
501+
The `ztvp-certificates` chart already extracts and distributes the cluster's CA bundle (ingress, service, and any custom/corporate CAs). When the `supply-chain` feature is enabled, the `ztvp-trusted-ca` ConfigMap is automatically distributed to the pipeline namespace (`layered-zero-trust-hub`) via ACM policy.
502+
503+
To make the `git-clone` task use this CA bundle, enable the SSL CA bundle mount in the `supply-chain` application overrides:
504+
505+
```yaml
506+
- name: git.sslCABundle.enabled
507+
value: "true"
508+
```
509+
510+
This binds the `ztvp-trusted-ca` ConfigMap as the `ssl-ca-directory` workspace on the `git-clone` task and sets the `CRT_FILENAME` parameter to `tls-ca-bundle.pem` (matching the key in the ConfigMap). The upstream `git-clone` ClusterTask uses this file to set `GIT_SSL_CAPATH`, so TLS verification succeeds against internal Git servers.
511+
512+
The corporate CA must be included in the `ztvp-trusted-ca` bundle. The easiest way is to use **automatic remote host extraction** -- add the Git host to `customCA.remoteHosts` in the `ztvp-certificates` overrides:
513+
514+
```yaml
515+
# ztvp-certificates overrides in values-hub.yaml
516+
- name: customCA.remoteHosts[0]
517+
value: "gitlab.internal.example.com"
518+
```
519+
520+
The `ztvp-certificates` extraction Job will connect to the host on port 443, extract the full CA chain from the TLS handshake (no authentication needed), and merge it into the CA bundle. The CronJob keeps it fresh automatically.
521+
522+
Alternatively, you can provide the CA certificate manually via `customCA.secretRef` or `customCA.additionalCertificates`. See the [ztvp-certificates documentation](./ztvp-certificates.md) for details.
523+
479524
### Init task (pre-flight image check)
480525

481526
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).

scripts/features/protected-repos.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
# https - basic-auth via .git-credentials (username + PAT)
77
# ssh - SSH key pair (ssh-privatekey + known_hosts)
88
#
9+
# For internal Git hosts (corporate CA), the generator auto-enables
10+
# customCA.remoteHosts and git.sslCABundle so the pipeline trusts the
11+
# server's TLS certificate without manual CA provisioning.
12+
#
913
# Requires the git-credentials secret in values-secret.yaml.template
1014
# to be uncommented and populated with the appropriate credentials.
1115
clusterGroup:
@@ -20,5 +24,11 @@ clusterGroup:
2024
value: "REPLACE_WITH_GIT_HOST"
2125
- name: git.credentials.vaultPath
2226
value: "secret/data/hub/supply-chain/git-credentials"
27+
- name: git.sslCABundle.enabled
28+
value: "REPLACE_WITH_SSL_CA_ENABLED"
2329
- name: qtodo.repository
2430
value: "REPLACE_WITH_GIT_REPO_URL"
31+
ztvp-certificates:
32+
overrides:
33+
- name: "customCA.remoteHosts[0]"
34+
value: "REPLACE_WITH_GIT_HOSTNAME"

0 commit comments

Comments
 (0)