Skip to content

Commit 2f1c9b8

Browse files
committed
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>
1 parent 3d68f0b commit 2f1c9b8

10 files changed

Lines changed: 207 additions & 12 deletions

File tree

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 .Values.git.sslCABundle.enabled }}
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 .Values.git.sslCABundle.enabled }}
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 .Values.git.sslCABundle.enabled }}
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 .Values.git.sslCABundle.enabled }}
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/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/supply-chain.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,41 @@ spec:
323323
- name: registry-auth-config
324324
secret:
325325
secretName: qtodo-registry-auth
326+
- name: git-auth
327+
secret:
328+
secretName: qtodo-git-credentials
329+
# Add this workspace when git.sslCABundle.enabled is true (internal Git hosts):
330+
# - name: ssl-ca-directory
331+
# configMap:
332+
# name: ztvp-trusted-ca
333+
```
334+
335+
**SSH mode** (leave `git-auth` unbound):
336+
337+
```yaml
338+
apiVersion: tekton.dev/v1
339+
kind: PipelineRun
340+
metadata:
341+
generateName: qtodo-manual-run-
342+
namespace: layered-zero-trust-hub
343+
spec:
344+
pipelineRef:
345+
name: qtodo-supply-chain
346+
taskRunTemplate:
347+
serviceAccountName: pipeline
348+
timeouts:
349+
pipeline: 1h0m0s
350+
workspaces:
351+
- name: qtodo-source
352+
persistentVolumeClaim:
353+
claimName: qtodo-workspace-source
354+
- name: registry-auth-config
355+
secret:
356+
secretName: qtodo-registry-auth
357+
# Add this workspace when git.sslCABundle.enabled is true (internal Git hosts):
358+
# - name: ssl-ca-directory
359+
# configMap:
360+
# name: ztvp-trusted-ca
326361
```
327362

328363
As was described previously, verify the values associated with the PVC storage and registry configuration.
@@ -463,6 +498,36 @@ When using the generator with `--git-repo`, the `qtodo.repository` override is s
463498
value: "https://github.com/your-org/qtodo.git" # or SSH URL (git@github.com:your-org/qtodo.git)
464499
```
465500

501+
#### 4. Corporate CA trust for internal Git hosts (HTTPS only)
502+
503+
When the private 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.
504+
505+
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.
506+
507+
To make the `git-clone` task use this CA bundle, enable the SSL CA bundle mount:
508+
509+
```yaml
510+
- name: git.sslCABundle.enabled
511+
value: "true"
512+
```
513+
514+
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.
515+
516+
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:
517+
518+
```yaml
519+
# ztvp-certificates overrides in values-hub.yaml
520+
- name: customCA.remoteHosts[0]
521+
value: "gitlab.internal.example.com"
522+
```
523+
524+
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.
525+
526+
Alternatively, you can provide the CA certificate manually via `customCA.secretRef` or `customCA.additionalCertificates`. See the [ztvp-certificates documentation](./ztvp-certificates.md) for details.
527+
528+
> [!NOTE]
529+
> This setting is only needed for HTTPS clones from internal Git hosts whose TLS certificates are signed by a corporate or self-signed CA. Public Git hosts (github.com, gitlab.com) use publicly trusted certificates and do not require this.
530+
466531
#### How it works
467532

468533
When `git.credentials.enabled` is `true`:
@@ -475,6 +540,7 @@ When `git.credentials.enabled` is `true`:
475540
* **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.
476541
* **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.
477542
* The Vault policy `hub-supply-chain-jwt-secret` grants read access to `secret/data/hub/supply-chain/*` for the pipeline's SPIFFE identity.
543+
* When `git.sslCABundle.enabled` is `true`, the pipeline mounts the `ztvp-trusted-ca` ConfigMap as the `ssl-ca-directory` workspace and sets `CRT_FILENAME` to `tls-ca-bundle.pem`. The `git-clone` ClusterTask uses this file to configure `GIT_SSL_CAPATH` so HTTPS clones trust the corporate CA. The CA bundle is populated automatically when `customCA.remoteHosts` is configured in the `ztvp-certificates` overrides -- the extraction Job fetches the CA chain from each host via TLS handshake (no authentication needed).
478544

479545
### Init task (pre-flight image check)
480546

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"

scripts/features/supply-chain.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ clusterGroup:
2626
- /spec/tasks
2727

2828
merge_into_applications:
29+
ztvp-certificates:
30+
overrides:
31+
- name: "distribution.targetNamespaces[0]"
32+
value: "qtodo"
33+
- name: "distribution.targetNamespaces[1]"
34+
value: "{{ $.Values.global.pattern }}-hub"
2935
vault:
3036
jwt:
3137
roles:

scripts/gen-feature-variants.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -309,39 +309,83 @@ def _substitute_repository_placeholders(base, org=None, image_name=None):
309309
GIT_REPO_PLACEHOLDER = "REPLACE_WITH_GIT_REPO_URL"
310310
GIT_HOST_PLACEHOLDER = "REPLACE_WITH_GIT_HOST"
311311
GIT_AUTH_TYPE_PLACEHOLDER = "REPLACE_WITH_GIT_AUTH_TYPE"
312+
SSL_CA_ENABLED_PLACEHOLDER = "REPLACE_WITH_SSL_CA_ENABLED"
313+
GIT_HOSTNAME_PLACEHOLDER = "REPLACE_WITH_GIT_HOSTNAME"
314+
315+
PUBLIC_GIT_HOSTS = {"github.com", "gitlab.com", "bitbucket.org"}
312316

313317
SSH_URL_RE = re.compile(r"^[\w.-]+@([\w.-]+):")
314318

315319

316320
def _parse_git_repo_url(git_repo_url):
317-
"""Derive (host, auth_type) from a Git repository URL.
321+
"""Derive (host, auth_type, hostname) from a Git repository URL.
318322
319-
HTTPS URLs -> host = "https://github.com", auth_type = "https"
320-
SSH URLs -> host = "github.com", auth_type = "ssh"
323+
HTTPS URLs -> host = "https://github.com", auth_type = "https", hostname = "github.com"
324+
SSH URLs -> host = "github.com", auth_type = "ssh", hostname = "github.com"
321325
"""
322326
m = SSH_URL_RE.match(git_repo_url)
323327
if m:
324-
return m.group(1), "ssh"
328+
hostname = m.group(1)
329+
return hostname, "ssh", hostname
325330
parsed = urlparse(git_repo_url)
326331
scheme = parsed.scheme or "https"
327332
hostname = parsed.hostname or ""
328-
return f"{scheme}://{hostname}", "https"
333+
return f"{scheme}://{hostname}", "https", hostname
329334

330335

331-
def _substitute_git_overrides(base, git_repo_url, git_host, git_auth_type):
332-
"""Replace git-related placeholders in supply-chain overrides."""
336+
def _substitute_git_overrides(
337+
base, git_repo_url, git_host, git_auth_type, git_hostname
338+
):
339+
"""Replace git-related placeholders in supply-chain and ztvp-certificates overrides."""
333340
apps = base.get("clusterGroup", {}).get("applications", {})
341+
is_internal = git_hostname not in PUBLIC_GIT_HOSTS
342+
334343
sc = apps.get("supply-chain", {})
335-
placeholder_map = {
344+
sc_placeholder_map = {
336345
"qtodo.repository": (GIT_REPO_PLACEHOLDER, git_repo_url),
337346
"git.credentials.host": (GIT_HOST_PLACEHOLDER, git_host),
338347
"git.credentials.authType": (GIT_AUTH_TYPE_PLACEHOLDER, git_auth_type),
348+
"git.sslCABundle.enabled": (
349+
SSL_CA_ENABLED_PLACEHOLDER,
350+
"true" if is_internal else "false",
351+
),
339352
}
340-
for override in sc.get("overrides", []):
341-
entry = placeholder_map.get(override.get("name"))
353+
sc_overrides = sc.get("overrides", [])
354+
for override in sc_overrides:
355+
entry = sc_placeholder_map.get(override.get("name"))
342356
if entry and str(override.get("value")) == entry[0]:
343357
override["value"] = entry[1]
344358

359+
# Remove git.sslCABundle.enabled override when false (public hosts)
360+
if not is_internal:
361+
sc_overrides[:] = [
362+
o
363+
for o in sc_overrides
364+
if not (
365+
o.get("name") == "git.sslCABundle.enabled" and o.get("value") == "false"
366+
)
367+
]
368+
369+
certs = apps.get("ztvp-certificates", {})
370+
certs_overrides = certs.get("overrides", [])
371+
if is_internal:
372+
for override in certs_overrides:
373+
if (
374+
override.get("name") == "customCA.remoteHosts[0]"
375+
and str(override.get("value")) == GIT_HOSTNAME_PLACEHOLDER
376+
):
377+
override["value"] = git_hostname
378+
else:
379+
# Remove the remoteHosts placeholder for public hosts
380+
certs_overrides[:] = [
381+
o
382+
for o in certs_overrides
383+
if not (
384+
o.get("name") == "customCA.remoteHosts[0]"
385+
and str(o.get("value")) == GIT_HOSTNAME_PLACEHOLDER
386+
)
387+
]
388+
345389

346390
def generate_variant(
347391
base_path,
@@ -384,8 +428,10 @@ def generate_variant(
384428
_substitute_repository_placeholders(base, org=org, image_name=image_name)
385429

386430
if git_repo_url:
387-
git_host, git_auth_type = _parse_git_repo_url(git_repo_url)
388-
_substitute_git_overrides(base, git_repo_url, git_host, git_auth_type)
431+
git_host, git_auth_type, git_hostname = _parse_git_repo_url(git_repo_url)
432+
_substitute_git_overrides(
433+
base, git_repo_url, git_host, git_auth_type, git_hostname
434+
)
389435

390436
validate_output(base)
391437
cg = base.get("clusterGroup")

values-hub.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,12 +615,17 @@ clusterGroup:
615615
# # Protected repository credentials (Vault + ExternalSecret)
616616
# # - name: git.credentials.enabled
617617
# # value: "true"
618+
# # - name: git.credentials.authType
619+
# # value: "https" # or "ssh"
618620
# # - name: git.credentials.host
619621
# # value: "https://github.com"
620622
# # - name: git.credentials.vaultPath
621623
# # value: "secret/data/hub/supply-chain/git-credentials"
622624
# # - name: qtodo.repository
623625
# # value: "https://github.com/your-org/qtodo.git"
626+
# # Corporate CA trust for internal Git hosts (HTTPS only)
627+
# # - name: git.sslCABundle.enabled
628+
# # value: "true"
624629
#
625630
# ACS Central Services
626631
acs-central:

0 commit comments

Comments
 (0)