diff --git a/.github/workflows/superlinter.yml b/.github/workflows/superlinter.yml index de22a7f3..87d10257 100644 --- a/.github/workflows/superlinter.yml +++ b/.github/workflows/superlinter.yml @@ -31,6 +31,8 @@ jobs: VALIDATE_ALL_CODEBASE: true DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Exclude bash scripts with Helm templating (they contain {{ }} syntax) + FILTER_REGEX_EXCLUDE: .*\.sh\.tpl$ # These are the validation we disable atm VALIDATE_ANSIBLE: false VALIDATE_BASH: false diff --git a/charts/qtodo/files/spiffe-vault-client.py b/charts/qtodo/files/spiffe-vault-client.py index 9a726ed8..e07255d3 100644 --- a/charts/qtodo/files/spiffe-vault-client.py +++ b/charts/qtodo/files/spiffe-vault-client.py @@ -28,6 +28,12 @@ def __init__(self): self.credentials_file = os.getenv( "CREDENTIALS_FILE", "/etc/credentials.properties" ) + # ZTVP trusted CA bundle (preferred) + self.ztvp_ca_bundle = os.getenv( + "ZTVP_CA_BUNDLE", + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + ) + # Service CA (fallback for backward compatibility) self.service_ca_file = os.getenv( "SERVICE_CA_FILE", "/run/secrets/kubernetes.io/serviceaccount/service-ca.crt", @@ -54,6 +60,7 @@ def __init__(self): logger.info(" VAULT_ROLE: %s", self.vault_role) logger.info(" DB_USERNAME: %s", self.db_username) logger.info(" CREDENTIALS_FILE: %s", self.credentials_file) + logger.info(" ZTVP_CA_BUNDLE: %s", self.ztvp_ca_bundle) logger.info(" SERVICE_CA_FILE: %s", self.service_ca_file) logger.info(" JWT_TOKEN_FILE: %s", self.jwt_token_file) @@ -63,10 +70,22 @@ def __init__(self): # Setup SSL context for CA verification self.ssl_context = ssl.create_default_context() - if os.path.exists(self.service_ca_file): + + # Try ZTVP CA bundle first (contains both ingress and service CAs) + if os.path.exists(self.ztvp_ca_bundle): + self.ssl_context.load_verify_locations(self.ztvp_ca_bundle) + logger.info("Loaded ZTVP trusted CA bundle from: %s", self.ztvp_ca_bundle) + # Fallback to service CA only (for backward compatibility) + elif os.path.exists(self.service_ca_file): self.ssl_context.load_verify_locations(self.service_ca_file) + logger.info("Loaded service CA from: %s", self.service_ca_file) else: - logger.warning("Service CA file not found, using default SSL context") + logger.warning( + "No CA certificates found at %s or %s. " + "Using default SSL context. SSL verification may fail.", + self.ztvp_ca_bundle, + self.service_ca_file, + ) def _make_http_request( self, url, method="GET", data=None, headers=None, timeout=30 @@ -111,11 +130,11 @@ def _make_http_request( "text": error_data, "json": lambda: (json.loads(error_data) if error_data else {}), } - except URLError as e: - logger.error("URL Error: %s", e) + except URLError: + logger.error("URL Error occurred") raise - except Exception as e: - logger.error("Request error: %s", e) + except Exception: + logger.error("Request error occurred") raise def get_spiffe_token(self): @@ -125,8 +144,8 @@ def get_spiffe_token(self): jwt_svid = source.read() logger.info("Successfully retrieved SPIFFE JWT token") return jwt_svid - except Exception as e: - logger.error("Failed to retrieve SPIFFE token: %s", e) + except Exception: + logger.error("Failed to retrieve SPIFFE token") raise def authenticate_with_vault(self): @@ -167,8 +186,8 @@ def authenticate_with_vault(self): return True - except Exception as e: - logger.error("Vault authentication error: %s", e) + except Exception: + logger.error("Vault authentication error occurred") raise def retrieve_vault_secret(self): @@ -204,8 +223,8 @@ def retrieve_vault_secret(self): return secret_data - except Exception as e: - logger.error("Secret retrieval error: %s", e) + except Exception: + logger.error("Secret retrieval error occurred") raise def extract_credentials(self, secret_data): @@ -222,8 +241,8 @@ def extract_credentials(self, secret_data): credentials["db-username"] = self.db_username return credentials - except Exception as e: - logger.error("Credential extraction error: %s", e) + except Exception: + logger.error("Credential extraction error occurred") raise def write_properties_file(self, credentials): @@ -243,8 +262,8 @@ def write_properties_file(self, credentials): logger.info("Credentials written to %s", self.credentials_file) - except Exception as e: - logger.error("Error writing properties file: %s", e) + except Exception: + logger.error("Error writing properties file") raise def is_token_renewal_needed(self): @@ -291,8 +310,8 @@ def renew_vault_token(self): ) return False - except Exception as e: - logger.warning("Token renewal error: %s. Re-authenticating...", e) + except Exception: + logger.warning("Token renewal error occurred. Re-authenticating...") return False def run(self, init=False): @@ -329,8 +348,8 @@ def run(self, init=False): except KeyboardInterrupt: logger.info("Received interrupt signal, shutting down...") break - except Exception as e: - logger.error("Error in main loop: %s", e) + except Exception: + logger.error("Error in main loop") logger.info("Retrying in 60 seconds...") time.sleep(60) @@ -352,7 +371,7 @@ def main(): manager = VaultCredentialManager() manager.run(args.init) except Exception as e: - logger.error("Failed to start credential manager: %s", e) + logger.error("Failed to start credential manager") raise SystemExit(1) from e diff --git a/charts/qtodo/templates/app-config-env.yaml b/charts/qtodo/templates/app-config-env.yaml index 045faa92..441ce5b1 100644 --- a/charts/qtodo/templates/app-config-env.yaml +++ b/charts/qtodo/templates/app-config-env.yaml @@ -4,8 +4,8 @@ metadata: name: qtodo-config-env namespace: {{ .Release.Namespace }} data: -{{- if eq .Values.app.oidc.enabled true }} - QUARKUS_OIDC_ENABLED: "{{ .Values.app.oidc.enabled }}" +{{- if eq .Values.app.spire.enabled true }} + QUARKUS_OIDC_ENABLED: "true" QUARKUS_OIDC_AUTH_SERVER_URL: "{{ default (printf "https://keycloak.%s/realms/ztvp" .Values.global.localClusterDomain) .Values.app.oidc.authServerUrl }}" QUARKUS_OIDC_CLIENT_ID: "{{ .Values.app.oidc.clientId }}" QUARKUS_OIDC_APPLICATION_TYPE: "{{ .Values.app.oidc.applicationType }}" diff --git a/charts/qtodo/templates/app-deployment.yaml b/charts/qtodo/templates/app-deployment.yaml index 249581ae..b5973439 100644 --- a/charts/qtodo/templates/app-deployment.yaml +++ b/charts/qtodo/templates/app-deployment.yaml @@ -5,6 +5,7 @@ metadata: argocd.argoproj.io/sync-wave: '20' labels: app: qtodo + ztvp.io/uses-certificates: "true" name: qtodo namespace: qtodo spec: @@ -28,6 +29,107 @@ spec: spec: {{- if eq .Values.app.spire.enabled true }} initContainers: + - name: init-ca-truststore + image: registry.redhat.io/ubi9/openjdk-17:latest + imagePullPolicy: IfNotPresent + command: + - /bin/bash + - -c + - | + set -e + echo "Converting CA bundle to PKCS12 truststore..." + + # Validate password is provided + if [ -z "$TRUSTSTORE_PASSWORD" ]; then + echo "ERROR: TRUSTSTORE_PASSWORD not set" + exit 1 + fi + + CA_BUNDLE="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" + TRUSTSTORE_PATH="/run/secrets/truststore/truststore.p12" + TEMP_DIR="/tmp/certs" + + # Verify CA bundle exists + if [ ! -f "$CA_BUNDLE" ]; then + echo "ERROR: CA bundle not found at $CA_BUNDLE" + exit 1 + fi + + # Count certificates in bundle + CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" "$CA_BUNDLE" || echo "0") + echo "Found $CERT_COUNT certificates in CA bundle" + + # Create temp directory for individual certs + mkdir -p "$TEMP_DIR" + + # Split the CA bundle into individual certificates + csplit -f "$TEMP_DIR/cert-" -b "%03d.pem" -z "$CA_BUNDLE" '/-----BEGIN CERTIFICATE-----/' '{*}' 2>/dev/null || true + + # Import each certificate into PKCS12 truststore using keytool + IMPORTED=0 + for cert in "$TEMP_DIR"/cert-*.pem; do + if [ -f "$cert" ] && grep -q "BEGIN CERTIFICATE" "$cert"; then + ALIAS="ztvp-ca-$(basename "$cert" .pem)" + if keytool -importcert -storetype PKCS12 \ + -keystore "$TRUSTSTORE_PATH" \ + -storepass "$TRUSTSTORE_PASSWORD" \ + -file "$cert" \ + -alias "$ALIAS" \ + -noprompt 2>/dev/null; then + IMPORTED=$((IMPORTED + 1)) + fi + fi + done + + # Cleanup + rm -rf "$TEMP_DIR" + + echo "Successfully created PKCS12 truststore with $IMPORTED certificates" + ls -lh "$TRUSTSTORE_PATH" + env: + - name: TRUSTSTORE_PASSWORD + valueFrom: + secretKeyRef: + name: qtodo-truststore-secret + key: truststore-password + volumeMounts: + - name: ztvp-trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true + - name: truststore + mountPath: /run/secrets/truststore + - name: wait-for-keycloak + image: registry.redhat.io/ubi9/ubi-minimal:latest + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - | + echo "Waiting for Keycloak OIDC endpoint to be available..." + KEYCLOAK_URL="{{ default (printf "https://keycloak.%s/realms/ztvp/.well-known/openid-configuration" .Values.global.localClusterDomain) .Values.app.oidc.authServerUrl }}/.well-known/openid-configuration" + # Remove duplicate .well-known if authServerUrl already contains realm + KEYCLOAK_URL=$(echo "$KEYCLOAK_URL" | sed 's|/.well-known/openid-configuration/.well-known/openid-configuration|/.well-known/openid-configuration|') + + MAX_RETRIES=60 + RETRY_INTERVAL=5 + RETRY_COUNT=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + if curl -sf --cacert /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem "$KEYCLOAK_URL" > /dev/null 2>&1; then + echo "Keycloak is available!" + exit 0 + fi + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Keycloak not ready yet (attempt $RETRY_COUNT/$MAX_RETRIES). Retrying in ${RETRY_INTERVAL}s..." + sleep $RETRY_INTERVAL + done + + echo "WARNING: Keycloak not available after $MAX_RETRIES attempts. Continuing anyway..." + exit 0 + volumeMounts: + - name: ztvp-trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true - name: init-spiffe-helper image: {{ template "qtodo.image" .Values.app.images.spiffeHelper }} imagePullPolicy: {{ .Values.app.images.spiffeHelper.pullPolicy }} @@ -65,6 +167,8 @@ spec: value: /run/secrets/db-credentials/credentials.properties - name: JWT_TOKEN_FILE value: /svids/jwt.token + - name: ZTVP_CA_BUNDLE + value: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem volumeMounts: - name: svids mountPath: /svids @@ -72,6 +176,9 @@ spec: mountPath: /run/secrets/db-credentials - name: spiffe-vault-client mountPath: /opt/app-root/src + - name: ztvp-trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true {{- end }} containers: {{- if and .Values.app.spire.enabled .Values.app.spire.sidecars }} @@ -113,6 +220,8 @@ spec: value: /run/secrets/db-credentials/credentials.properties - name: JWT_TOKEN_FILE value: /svids/jwt.token + - name: ZTVP_CA_BUNDLE + value: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem volumeMounts: - name: svids mountPath: /svids @@ -122,6 +231,9 @@ spec: - name: spiffe-vault-client mountPath: /opt/app-root/src readOnly: true + - name: ztvp-trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true {{- end }} - name: qtodo image: {{ template "qtodo.image" .Values.app.images.main }} @@ -149,7 +261,7 @@ spec: value: '0.0.0.0' - name: QUARKUS_HTTP_PORT value: '8080' - - name: QUARKUS_HIBERNATE_ORM_DATABASE_GENERATION + - name: QUARKUS_HIBERNATE_ORM_SCHEMA_MANAGEMENT_STRATEGY value: 'drop-and-create' {{- if eq .Values.app.spire.enabled false }} - name: QUARKUS_DATASOURCE_USERNAME @@ -170,11 +282,29 @@ spec: secretKeyRef: name: oidc-client-secret key: client-secret +{{- if eq .Values.app.truststore.enabled true }} + # Truststore password from Vault secret + - name: TRUSTSTORE_PASSWORD + valueFrom: + secretKeyRef: + name: qtodo-truststore-secret + key: truststore-password + # JVM-level truststore configuration for all SSL connections (PKCS12 format) + - name: JAVA_TOOL_OPTIONS + value: "-Djavax.net.ssl.trustStore=/run/secrets/truststore/truststore.p12 -Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStorePassword=$(TRUSTSTORE_PASSWORD)" +{{- end }} {{- end }} {{- if eq .Values.app.spire.enabled true }} volumeMounts: - name: db-credentials mountPath: /run/secrets/db-credentials + - name: truststore + mountPath: /run/secrets/truststore + readOnly: true + # Mount ZTVP CA bundle for non-Java tools (curl, openssl, etc.) + - name: ztvp-trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true {{- end }} resources: {} serviceAccountName: qtodo @@ -195,4 +325,10 @@ spec: - name: spiffe-vault-client configMap: name: spiffe-vault-client + - name: ztvp-trusted-ca + configMap: + name: ztvp-trusted-ca + defaultMode: 420 + - name: truststore + emptyDir: {} {{- end }} diff --git a/charts/qtodo/templates/truststore-secret-external-secret.yaml b/charts/qtodo/templates/truststore-secret-external-secret.yaml new file mode 100644 index 00000000..7b641acd --- /dev/null +++ b/charts/qtodo/templates/truststore-secret-external-secret.yaml @@ -0,0 +1,25 @@ +{{- if .Values.app.spire.enabled }} +apiVersion: "external-secrets.io/v1beta1" +kind: ExternalSecret +metadata: + name: qtodo-truststore-secret + namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: '5' +spec: + refreshInterval: 15s + secretStoreRef: + name: {{ .Values.global.secretStore.name }} + kind: {{ .Values.global.secretStore.kind }} + target: + name: qtodo-truststore-secret + template: + type: Opaque + data: + truststore-password: "{{ `{{ .truststore_password }}` }}" + data: + - secretKey: truststore_password + remoteRef: + key: {{ .Values.app.truststore.vaultPath }} + property: truststore-password +{{- end }} diff --git a/charts/qtodo/values.yaml b/charts/qtodo/values.yaml index f2dd1e33..3d0921f5 100644 --- a/charts/qtodo/values.yaml +++ b/charts/qtodo/values.yaml @@ -63,6 +63,11 @@ app: enabled: true name: "oidc-client-secret" vaultPath: "secret/data/global/oidc-client-secret" + + # Truststore configuration for Java CA certificates + truststore: + enabled: true + vaultPath: "secret/data/global/qtodo-truststore" # PostgreSQL database configuration postgresql: diff --git a/charts/ztvp-certificates/Chart.yaml b/charts/ztvp-certificates/Chart.yaml new file mode 100644 index 00000000..0da9933d --- /dev/null +++ b/charts/ztvp-certificates/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: ztvp-certificates +description: Global CA certificate management for ZTVP pattern components +type: application +version: 1.0.0 +appVersion: "1.0.0" +keywords: + - certificates + - tls + - ssl + - ca-bundle + - zero-trust +home: https://github.com/validatedpatterns/layered-zero-trust +sources: + - https://github.com/validatedpatterns/layered-zero-trust +maintainers: + - name: Zero Trust Validated Patterns Team + email: ztvp-arch-group@redhat.com + diff --git a/charts/ztvp-certificates/files/extract-certificates.sh.tpl b/charts/ztvp-certificates/files/extract-certificates.sh.tpl new file mode 100644 index 00000000..5db24275 --- /dev/null +++ b/charts/ztvp-certificates/files/extract-certificates.sh.tpl @@ -0,0 +1,400 @@ +#!/bin/bash +# shellcheck disable=SC1091,SC2050,SC2154 +# This is a Helm template file - shellcheck will fail on Helm syntax +set -e +{{- if .Values.debug.verbose }} +set -x +{{- end }} + +# Logging functions +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } +error() { echo "[ERROR] $*" >&2; } + +# Initialize variables +INGRESS_CA_FOUND=false +SERVICE_CA_FOUND=false +CUSTOM_CA_FOUND=false +CLUSTER_CA_FOUND=false +TEMP_DIR=$(mktemp -d) + +log "===========================================" +log "ZTVP CA Certificate Extraction" +log "===========================================" +log "Auto-detect: {{ .Values.autoDetect }}" +log "Custom CA: {{ .Values.customCA.secretRef.enabled }}" +log "Namespace: {{ .Values.global.namespace }}" +log "ConfigMap: {{ .Values.configMapName }}" + +# =================================================================== +# PHASE 1: Extract Custom CA (if configured) +# =================================================================== + +{{- if .Values.customCA.secretRef.enabled }} +log "Extracting custom CA from secret: {{ .Values.customCA.secretRef.namespace }}/{{ .Values.customCA.secretRef.name }}" +if oc get secret {{ .Values.customCA.secretRef.name }} -n {{ .Values.customCA.secretRef.namespace }} &>/dev/null; then + KEY="{{ .Values.customCA.secretRef.key }}" + ESCAPED_KEY="${KEY//./\\.}" + oc get secret {{ .Values.customCA.secretRef.name }} \ + -n {{ .Values.customCA.secretRef.namespace }} \ + -o "jsonpath={.data.${ESCAPED_KEY}}" | \ + base64 -d > "${TEMP_DIR}/custom-ca.crt" + CUSTOM_CA_FOUND=true + log "Custom CA extracted from secret successfully" +else + error "Custom secret not found: {{ .Values.customCA.secretRef.namespace }}/{{ .Values.customCA.secretRef.name }}" + exit 1 +fi +{{- end }} + +# =================================================================== +# PHASE 2: Extract Ingress CA (if auto-detect enabled) +# =================================================================== + +{{- if .Values.autoDetect }} +# Auto-detect from OpenShift +log "Auto-detecting ingress CA certificate" + +{{- if .Values.customSource.ingressCA.secretName }} +# Use custom source location +log "Using custom source: {{ .Values.customSource.ingressCA.secretNamespace }}/{{ .Values.customSource.ingressCA.secretName }}" +if oc get secret {{ .Values.customSource.ingressCA.secretName }} -n {{ .Values.customSource.ingressCA.secretNamespace }} &>/dev/null; then + oc get secret {{ .Values.customSource.ingressCA.secretName }} \ + -n {{ .Values.customSource.ingressCA.secretNamespace }} \ + -o jsonpath='{.data.{{ .Values.customSource.ingressCA.secretKey }}}' | \ + base64 -d > "${TEMP_DIR}/ingress-ca.crt" + INGRESS_CA_FOUND=true + log "Ingress CA extracted from custom source" +else + error "Custom ingress CA secret not found" +fi +{{- else }} +# Standard auto-detection logic +# Loop through all IngressControllers +INGRESSCONTROLLERS=$(oc get ingresscontroller -n openshift-ingress-operator -o name 2>/dev/null || echo "") + +if [[ -n "$INGRESSCONTROLLERS" ]]; then + INGRESS_INDEX=0 + for ic in $INGRESSCONTROLLERS; do + IC_NAME=$(echo "$ic" | cut -d'/' -f2) + log "Checking IngressController: $IC_NAME" + + # Check for custom certificate reference + CUSTOM_CERT=$(oc get ingresscontroller "$IC_NAME" -n openshift-ingress-operator \ + -o jsonpath='{.spec.defaultCertificate.name}' 2>/dev/null || echo "") + + if [[ -n "$CUSTOM_CERT" ]]; then + log "Found custom certificate reference: $CUSTOM_CERT" + if oc get secret "$CUSTOM_CERT" -n openshift-ingress &>/dev/null; then + oc get secret "$CUSTOM_CERT" -n openshift-ingress \ + -o jsonpath='{.data.tls\.crt}' | base64 -d > "${TEMP_DIR}/ingress-ca-${IC_NAME}.crt" + INGRESS_CA_FOUND=true + INGRESS_INDEX=$((INGRESS_INDEX + 1)) + log "Extracted ingress CA from $IC_NAME" + fi + else + # Try default router-certs secret for this IngressController + SECRET_NAME="router-certs-${IC_NAME}" + if oc get secret "$SECRET_NAME" -n openshift-ingress &>/dev/null; then + log "Found router secret: $SECRET_NAME" + oc get secret "$SECRET_NAME" -n openshift-ingress \ + -o jsonpath='{.data.tls\.crt}' | base64 -d > "${TEMP_DIR}/ingress-ca-${IC_NAME}.crt" + INGRESS_CA_FOUND=true + INGRESS_INDEX=$((INGRESS_INDEX + 1)) + fi + fi + done + + log "Extracted certificates from $INGRESS_INDEX IngressController(s)" +else + log "WARNING: No IngressControllers found" +fi +{{- end }} +{{- end }} + +# =================================================================== +# PHASE 3: Extract Service CA (if auto-detect enabled) +# =================================================================== + +{{- if .Values.autoDetect }} +log "Extracting OpenShift service CA" + +{{- if .Values.customSource.serviceCA.configMapName }} +# Use custom source location +log "Using custom service CA source: {{ .Values.customSource.serviceCA.configMapNamespace }}/{{ .Values.customSource.serviceCA.configMapName }}" +if oc get configmap {{ .Values.customSource.serviceCA.configMapName }} -n {{ .Values.customSource.serviceCA.configMapNamespace }} &>/dev/null; then + oc get configmap {{ .Values.customSource.serviceCA.configMapName }} \ + -n {{ .Values.customSource.serviceCA.configMapNamespace }} \ + -o jsonpath='{.data.{{ .Values.customSource.serviceCA.configMapKey }}}' \ + > "${TEMP_DIR}/service-ca.crt" + SERVICE_CA_FOUND=true + log "Service CA extracted from custom source" +else + log "WARNING: Custom service CA ConfigMap not found" +fi +{{- else }} +# Standard location +if oc get configmap openshift-service-ca.crt -n openshift-config &>/dev/null; then + oc get configmap openshift-service-ca.crt -n openshift-config \ + -o jsonpath='{.data.service-ca\.crt}' > "${TEMP_DIR}/service-ca.crt" + SERVICE_CA_FOUND=true + log "Service CA extracted successfully" +else + log "WARNING: Service CA not found (this may be expected in some environments)" +fi +{{- end }} +{{- end }} + +# =================================================================== +# PHASE 4: Extract Proxy/Cluster-wide Trusted CA Bundle +# =================================================================== + +{{- if .Values.autoDetect }} +log "Checking for cluster-wide trusted CA bundle" + +# Extract from openshift-config-managed/trusted-ca-bundle +if oc get configmap trusted-ca-bundle -n openshift-config-managed &>/dev/null; then + log "Found trusted-ca-bundle in openshift-config-managed" + oc get configmap trusted-ca-bundle -n openshift-config-managed \ + -o jsonpath='{.data.ca-bundle\.crt}' > "${TEMP_DIR}/trusted-ca-bundle.crt" 2>/dev/null || true + + # Check if we got valid content + if [[ -s "${TEMP_DIR}/trusted-ca-bundle.crt" ]]; then + CLUSTER_CA_FOUND=true + log "Cluster-wide trusted CA bundle extracted successfully" + else + log "trusted-ca-bundle ConfigMap exists but has no ca-bundle.crt data" + rm -f "${TEMP_DIR}/trusted-ca-bundle.crt" + fi +else + log "No cluster-wide trusted-ca-bundle found (this is normal for clusters without proxy configuration)" +fi +{{- end }} + +# =================================================================== +# PHASE 5: Load Additional Certificates (if configured) +# =================================================================== + +{{- if .Values.customCA.additionalCertificates }} +log "Loading {{ len .Values.customCA.additionalCertificates }} additional certificate(s) from secrets" + +{{- range $cert := .Values.customCA.additionalCertificates }} +log "Loading additional certificate: {{ $cert.name }}" +if oc get secret {{ $cert.secretRef.name }} -n {{ $cert.secretRef.namespace }} &>/dev/null; then + ADDL_KEY="{{ $cert.secretRef.key }}" + ADDL_ESCAPED_KEY="${ADDL_KEY//./\\.}" + oc get secret {{ $cert.secretRef.name }} \ + -n {{ $cert.secretRef.namespace }} \ + -o "jsonpath={.data.${ADDL_ESCAPED_KEY}}" | \ + base64 -d > "${TEMP_DIR}/{{ $cert.name }}.crt" + log "OK: Loaded additional certificate: {{ $cert.name }}" +else + log "WARNING: Additional certificate secret not found: {{ $cert.secretRef.namespace }}/{{ $cert.secretRef.name }}" + log "WARNING: Skipping {{ $cert.name }} and continuing with other certificates" +fi +{{- end }} +{{- end }} + +# =================================================================== +# PHASE 6: Validate Certificates +# =================================================================== + +{{- if .Values.validation.enabled }} +log "Validating extracted certificates" + +CERT_COUNT=0 +for cert_file in "${TEMP_DIR}"/*.crt; do + [[ -f "$cert_file" ]] || continue + + CERT_COUNT=$((CERT_COUNT + 1)) + + # Check minimum size + CERT_SIZE=$(wc -c < "$cert_file" 2>/dev/null || echo 0) + if [[ $CERT_SIZE -lt {{ .Values.validation.minSize }} ]]; then + error "Certificate too small: $cert_file ($CERT_SIZE bytes)" + exit 1 + fi + + {{- if .Values.validation.parseCheck }} + # Verify certificate can be parsed + if ! openssl x509 -in "$cert_file" -noout 2>/dev/null; then + error "Invalid certificate format: $cert_file" + exit 1 + fi + log "OK: Valid certificate: $(basename $cert_file)" + {{- end }} +done + +log "Validated $CERT_COUNT certificate(s)" +{{- end }} + +# =================================================================== +# PHASE 7: Combine into Bundle +# =================================================================== + +log "Creating combined CA bundle" + +# Combine all certificates +> "${TEMP_DIR}/tls-ca-bundle.pem" +for cert_file in "${TEMP_DIR}"/*.crt; do + [[ -f "$cert_file" ]] || continue + cat "$cert_file" >> "${TEMP_DIR}/tls-ca-bundle.pem" + echo "" >> "${TEMP_DIR}/tls-ca-bundle.pem" # Add blank line between certs +done + +# Verify bundle is not empty +BUNDLE_SIZE=$(wc -c < "${TEMP_DIR}/tls-ca-bundle.pem" 2>/dev/null || echo 0) +if [[ $BUNDLE_SIZE -lt 100 ]]; then + error "==================================================================" + error "No certificates found or bundle too small" + error "==================================================================" + error "" + error "Certificate Status:" + error " Custom CA found: $CUSTOM_CA_FOUND" + error " Ingress CA found: $INGRESS_CA_FOUND" + error " Service CA found: $SERVICE_CA_FOUND" + error " Bundle size: $BUNDLE_SIZE bytes" + error "" + error "To resolve this, provide a custom CA certificate via secret:" + error "" + error "1. Create a secret with your certificate:" + error " oc create secret generic custom-ca-bundle \\" + error " --from-file=ca.crt=/path/to/ca.crt \\" + error " -n openshift-config" + error "" + error "2. Configure values-hub.yaml:" + error " ztvp-certificates:" + error " overrides:" + error " - name: customCA.secretRef.enabled" + error " value: \"true\"" + error " - name: customCA.secretRef.name" + error " value: custom-ca-bundle" + error " - name: customCA.secretRef.namespace" + error " value: openshift-config" + error "" + error "For more information, see the chart documentation." + error "==================================================================" + exit 1 +fi + +log "Combined CA bundle created: $BUNDLE_SIZE bytes" + +# =================================================================== +# PHASE 8: Create ConfigMap +# =================================================================== + +log "Creating ConfigMap: {{ .Values.global.namespace }}/{{ .Values.configMapName }}" + +cat </dev/null; then + VERIFY_SIZE=$(oc get configmap {{ .Values.configMapName }} -n {{ .Values.global.namespace }} \ + -o jsonpath='{.data.tls-ca-bundle\.pem}' 2>/dev/null | wc -c || echo 0) + + if [[ $VERIFY_SIZE -lt 100 ]]; then + error "ConfigMap created but certificate data is missing or too small ($VERIFY_SIZE bytes)" + exit 1 + fi + + log "===========================================" + log "SUCCESS: CA certificate bundle configured" + log "===========================================" + log "ConfigMap: {{ .Values.global.namespace }}/{{ .Values.configMapName }}" + log "Bundle size: $VERIFY_SIZE bytes" + log "Custom CA: $CUSTOM_CA_FOUND" + log "Ingress CA: $INGRESS_CA_FOUND" + log "Service CA: $SERVICE_CA_FOUND" + log "Cluster CA: $CLUSTER_CA_FOUND" + log "===========================================" +else + error "Failed to create ConfigMap" + exit 1 +fi + +# =================================================================== +# PHASE 9: Automatic Rollout (if enabled) +# =================================================================== + +{{- if .Values.rollout.enabled }} +log "Triggering automatic rollout for consuming applications" +log "Rollout strategy: {{ .Values.rollout.strategy }}" + +ROLLOUT_COUNT=0 + +{{- if eq .Values.rollout.strategy "specific" }} +# Strategy: specific - restart named resources +{{- range .Values.rollout.targets }} +log "Restarting {{ .kind }}/{{ .name }} in namespace {{ .namespace }}" +if oc get {{ .kind | lower }} {{ .name }} -n {{ .namespace }} &>/dev/null; then + if oc rollout restart {{ .kind | lower }}/{{ .name }} -n {{ .namespace }}; then + log "OK: Successfully triggered rollout for {{ .kind }}/{{ .name }}" + ROLLOUT_COUNT=$((ROLLOUT_COUNT + 1)) + else + log "WARNING: Failed to restart {{ .kind }}/{{ .name }} (continuing anyway)" + fi +else + log "WARNING: {{ .kind }}/{{ .name }} not found in namespace {{ .namespace }}" +fi +{{- end }} + +{{- else }} +# Strategy: all or labeled - restart resources in distribution target namespaces +{{- range $ns := .Values.distribution.targetNamespaces }} +log "Processing namespace: {{ $ns }}" + +{{- range $kind := $.Values.rollout.resourceKinds }} +{{- if eq $.Values.rollout.strategy "labeled" }} +# Get resources with specific labels +LABEL_SELECTOR="{{ range $key, $value := $.Values.rollout.labelSelector }}{{ $key }}={{ $value }}{{ end }}" +log "Finding {{ $kind }}s with labels: $LABEL_SELECTOR" +RESOURCES=$(oc get {{ $kind | lower }} -n {{ $ns }} -l "$LABEL_SELECTOR" -o name 2>/dev/null || true) +{{- else }} +# Get all resources +log "Finding all {{ $kind }}s in namespace {{ $ns }}" +RESOURCES=$(oc get {{ $kind | lower }} -n {{ $ns }} -o name 2>/dev/null || true) +{{- end }} + +if [[ -n "$RESOURCES" ]]; then + for resource in $RESOURCES; do + log "Restarting $resource in namespace {{ $ns }}" + if oc rollout restart "$resource" -n {{ $ns }}; then + log "OK: Successfully triggered rollout for $resource" + ROLLOUT_COUNT=$((ROLLOUT_COUNT + 1)) + else + log "WARNING: Failed to restart $resource (continuing anyway)" + fi + done +else + log "No {{ $kind }}s found in namespace {{ $ns }}" +fi +{{- end }} +{{- end }} +{{- end }} + +log "Automatic rollout completed: $ROLLOUT_COUNT resource(s) restarted" +{{- end }} + diff --git a/charts/ztvp-certificates/templates/_helpers.tpl b/charts/ztvp-certificates/templates/_helpers.tpl new file mode 100644 index 00000000..6480f983 --- /dev/null +++ b/charts/ztvp-certificates/templates/_helpers.tpl @@ -0,0 +1,66 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ztvp-certificates.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "ztvp-certificates.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ztvp-certificates.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ztvp-certificates.labels" -}} +helm.sh/chart: {{ include "ztvp-certificates.chart" . }} +{{ include "ztvp-certificates.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ztvp-certificates.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ztvp-certificates.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Service account name +*/}} +{{- define "ztvp-certificates.serviceAccountName" -}} +{{- printf "%s-ca-extractor" (include "ztvp-certificates.fullname" .) }} +{{- end }} + +{{/* +ACM hub template for reading ConfigMap data +Usage: include "ztvp-certificates.acmHubFromConfigMap" (dict "namespace" "openshift-config" "configmap" "ztvp-trusted-ca" "key" "tls-ca-bundle.pem") +Outputs: {{hub fromConfigMap "namespace" "configmap" "key" | autoindent hub}} +*/}} +{{- define "ztvp-certificates.acmHubFromConfigMap" -}} +{{- printf "{{hub fromConfigMap \"%s\" \"%s\" \"%s\" | autoindent hub}}" .namespace .configmap .key }} +{{- end }} + diff --git a/charts/ztvp-certificates/templates/ca-extraction-cronjob.yaml b/charts/ztvp-certificates/templates/ca-extraction-cronjob.yaml new file mode 100644 index 00000000..f18b2bd3 --- /dev/null +++ b/charts/ztvp-certificates/templates/ca-extraction-cronjob.yaml @@ -0,0 +1,68 @@ +{{- if and .Values.enabled .Values.cronJob.enabled }} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-ca-extractor + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-8" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +spec: + schedule: {{ .Values.cronJob.schedule | quote }} + successfulJobsHistoryLimit: {{ .Values.cronJob.successfulJobsHistoryLimit }} + failedJobsHistoryLimit: {{ .Values.cronJob.failedJobsHistoryLimit }} + concurrencyPolicy: {{ .Values.cronJob.concurrencyPolicy }} + {{- if .Values.cronJob.startingDeadlineSeconds }} + startingDeadlineSeconds: {{ .Values.cronJob.startingDeadlineSeconds }} + {{- end }} + suspend: {{ .Values.cronJob.suspend }} + jobTemplate: + metadata: + labels: + {{- include "ztvp-certificates.labels" . | nindent 8 }} + spec: + {{- if not .Values.debug.keepFailedJobs }} + ttlSecondsAfterFinished: {{ .Values.job.ttlSecondsAfterFinished }} + {{- end }} + backoffLimit: {{ .Values.job.backoffLimit }} + template: + metadata: + labels: + {{- include "ztvp-certificates.selectorLabels" . | nindent 12 }} + spec: + serviceAccountName: {{ include "ztvp-certificates.serviceAccountName" . }} + restartPolicy: OnFailure + securityContext: + runAsNonRoot: true + runAsUser: 1001 + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault + volumes: + - name: script + configMap: + name: {{ include "ztvp-certificates.fullname" . }}-script + defaultMode: 0755 + containers: + - name: ca-extractor + image: {{ .Values.job.image.registry }}/{{ .Values.job.image.repository }}:{{ .Values.job.image.tag }} + imagePullPolicy: {{ .Values.job.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + runAsUser: 1001 + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + {{- toYaml .Values.job.resources | nindent 14 }} + volumeMounts: + - name: script + mountPath: /scripts + readOnly: true + command: + - /bin/bash + - /scripts/extract-certificates.sh +{{- end }} diff --git a/charts/ztvp-certificates/templates/ca-extraction-job-initial.yaml b/charts/ztvp-certificates/templates/ca-extraction-job-initial.yaml new file mode 100644 index 00000000..d867cfc7 --- /dev/null +++ b/charts/ztvp-certificates/templates/ca-extraction-job-initial.yaml @@ -0,0 +1,60 @@ +{{- if .Values.enabled }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-ca-extractor-initial + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-8" + # Run as regular sync resource (after RBAC at -9, before Policy at -5) + # Using Prune=false prevents OutOfSync after TTL deletes the completed Job + argocd.argoproj.io/sync-options: Prune=false + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} + app.kubernetes.io/component: initial-extraction +spec: + {{- if not .Values.debug.keepFailedJobs }} + ttlSecondsAfterFinished: {{ .Values.job.ttlSecondsAfterFinished }} + {{- end }} + backoffLimit: {{ .Values.job.backoffLimit }} + template: + metadata: + labels: + {{- include "ztvp-certificates.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: initial-extraction + spec: + serviceAccountName: {{ include "ztvp-certificates.serviceAccountName" . }} + restartPolicy: OnFailure + securityContext: + runAsNonRoot: true + runAsUser: 1001 + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault + volumes: + - name: script + configMap: + name: {{ include "ztvp-certificates.fullname" . }}-script + defaultMode: 0755 + containers: + - name: ca-extractor + image: {{ .Values.job.image.registry }}/{{ .Values.job.image.repository }}:{{ .Values.job.image.tag }} + imagePullPolicy: {{ .Values.job.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + runAsUser: 1001 + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + {{- toYaml .Values.job.resources | nindent 10 }} + volumeMounts: + - name: script + mountPath: /scripts + readOnly: true + command: + - /bin/bash + - /scripts/extract-certificates.sh +{{- end }} diff --git a/charts/ztvp-certificates/templates/configmap-script.yaml b/charts/ztvp-certificates/templates/configmap-script.yaml new file mode 100644 index 00000000..802fda93 --- /dev/null +++ b/charts/ztvp-certificates/templates/configmap-script.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-script + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} + app.kubernetes.io/component: extraction-script +data: + extract-certificates.sh: | + {{- tpl (.Files.Get "files/extract-certificates.sh.tpl") . | nindent 4 }} +{{- end }} + diff --git a/charts/ztvp-certificates/templates/distribution-policy.yaml b/charts/ztvp-certificates/templates/distribution-policy.yaml new file mode 100644 index 00000000..a3f5a3c5 --- /dev/null +++ b/charts/ztvp-certificates/templates/distribution-policy.yaml @@ -0,0 +1,82 @@ +{{- if .Values.distribution.enabled }} +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: Policy +metadata: + name: ztvp-certificates-distribution + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-5" + policy.open-cluster-management.io/standards: NIST-CSF + policy.open-cluster-management.io/categories: PR.DS Data Security + policy.open-cluster-management.io/controls: PR.DS-2 Data-in-transit +spec: + disabled: false + remediationAction: enforce + policy-templates: + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: ztvp-ca-distribution + spec: + remediationAction: enforce + severity: high + namespaceSelector: + include: +{{- range .Values.distribution.targetNamespaces }} + - {{ . }} +{{- end }} + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: ztvp-trusted-ca + labels: + app.kubernetes.io/managed-by: ztvp-certificates + ztvp.io/certificate-bundle: "true" + annotations: + ztvp.io/source: "openshift-config/ztvp-trusted-ca" + ztvp.io/distribution-method: "acm-policy" + data: + tls-ca-bundle.pem: | + {{ `{{hub fromConfigMap "` }}{{ $.Values.global.namespace }}{{ `" "` }}{{ $.Values.configMapName }}{{ `" "tls-ca-bundle.pem" | autoindent hub}}` }} +--- +apiVersion: policy.open-cluster-management.io/v1 +kind: PlacementBinding +metadata: + name: ztvp-certificates-distribution-binding + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-5" +bindingOverrides: + remediationAction: enforce +placementRef: + apiGroup: cluster.open-cluster-management.io + kind: Placement + name: ztvp-certificates-distribution-placement +subjects: + - apiGroup: policy.open-cluster-management.io + kind: Policy + name: ztvp-certificates-distribution +--- +apiVersion: cluster.open-cluster-management.io/v1beta1 +kind: Placement +metadata: + name: ztvp-certificates-distribution-placement + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-5" +spec: + predicates: + - requiredClusterSelector: + labelSelector: + matchExpressions: + - key: local-cluster + operator: In + values: + - "true" +{{- end }} + diff --git a/charts/ztvp-certificates/templates/managedclusterset-binding.yaml b/charts/ztvp-certificates/templates/managedclusterset-binding.yaml new file mode 100644 index 00000000..82ccf07f --- /dev/null +++ b/charts/ztvp-certificates/templates/managedclusterset-binding.yaml @@ -0,0 +1,17 @@ +{{- if .Values.distribution.enabled }} +{{- if eq .Values.distribution.method "acm-policy" }} +--- +apiVersion: cluster.open-cluster-management.io/v1beta2 +kind: ManagedClusterSetBinding +metadata: + name: default + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-6" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +spec: + clusterSet: default +{{- end }} +{{- end }} + diff --git a/charts/ztvp-certificates/templates/rbac.yaml b/charts/ztvp-certificates/templates/rbac.yaml new file mode 100644 index 00000000..94949dc7 --- /dev/null +++ b/charts/ztvp-certificates/templates/rbac.yaml @@ -0,0 +1,117 @@ +{{- if .Values.enabled }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ztvp-certificates.serviceAccountName" . }} + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "ztvp-certificates.serviceAccountName" . }} + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "create", "update", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "ztvp-certificates.serviceAccountName" . }} + namespace: {{ .Values.global.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "ztvp-certificates.serviceAccountName" . }} +subjects: +- kind: ServiceAccount + name: {{ include "ztvp-certificates.serviceAccountName" . }} + namespace: {{ .Values.global.namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-ca-reader + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +rules: +# Read ingress CA from router secrets (dynamically discovered from any IngressController) +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] +# Read OpenShift service CA and cluster-wide trusted-ca-bundle +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list"] +# Read ingress controller configuration to detect custom certs +- apiGroups: ["operator.openshift.io"] + resources: ["ingresscontrollers"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-ca-reader + 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" . }}-ca-reader +subjects: +- kind: ServiceAccount + name: {{ include "ztvp-certificates.serviceAccountName" . }} + namespace: {{ .Values.global.namespace }} +{{- if .Values.rollout.enabled }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-rollout + annotations: + argocd.argoproj.io/sync-wave: "-9" + labels: + {{- include "ztvp-certificates.labels" . | nindent 4 }} +rules: +# Allow triggering rollout restart for consuming applications +- apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "daemonsets"] + verbs: ["get", "list", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "ztvp-certificates.fullname" . }}-rollout + 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" . }}-rollout +subjects: +- kind: ServiceAccount + name: {{ include "ztvp-certificates.serviceAccountName" . }} + namespace: {{ .Values.global.namespace }} +{{- end }} +{{- end }} + diff --git a/charts/ztvp-certificates/values.yaml b/charts/ztvp-certificates/values.yaml new file mode 100644 index 00000000..354ee4c7 --- /dev/null +++ b/charts/ztvp-certificates/values.yaml @@ -0,0 +1,186 @@ +# Global certificate configuration for ZTVP pattern +# Provides CA certificates for SSL/TLS verification across all components + +# Global settings +global: + # Namespace where the source ConfigMap will be created + # Using openshift-config namespace (standard location for cluster-wide configuration) + namespace: openshift-config + +# Enable/disable certificate management +enabled: true + +# Automatically detect and extract CA certificates from OpenShift +# Recommended: true (works for most scenarios) +autoDetect: true + +# Custom CA certificate configuration +customCA: + # Primary custom CA: Reference an existing Kubernetes secret containing CA certificates + # To create the secret: + # Single cert: oc create secret generic custom-ca-bundle --from-file=ca.crt=/path/to/ca.crt -n openshift-config + # Multiple certs: cat cert1.crt cert2.crt cert3.crt > combined.crt && oc create secret generic custom-ca-bundle --from-file=ca.crt=combined.crt -n openshift-config + secretRef: + enabled: false + name: "" # Name of secret containing CA certificate(s) + namespace: "" # Namespace where secret is located + key: "ca.crt" # Key in secret containing the certificate(s) + # Example: + # secretRef: + # enabled: true + # name: custom-ca-bundle + # namespace: openshift-config + # key: ca.crt + + # Additional CA certificates: Reference multiple secrets + # Useful when you have multiple CA certificates in separate secrets + # Create secrets first: + # oc create secret generic --from-file=ca.crt=/path/to/cert.crt -n openshift-config + # Configure via overrides/values-ztvp-certificates.yaml (using extraValueFiles) + additionalCertificates: [] + # Example: + # additionalCertificates: + # - name: corporate-root-ca + # secretRef: + # name: corporate-root-ca + # namespace: openshift-config + # key: ca.crt + +# Custom source locations for auto-detection +# Override default locations where CA certificates are found +customSource: + # Where to find ingress CA certificate + ingressCA: + # Default: router-certs-default secret in openshift-ingress + secretName: "" # Leave empty to use auto-detection + secretNamespace: "" + secretKey: "tls.crt" + + # Where to find OpenShift service CA certificate + serviceCA: + # Default: openshift-service-ca.crt ConfigMap in openshift-config + configMapName: "" # Leave empty to use auto-detection + configMapNamespace: "" + configMapKey: "service-ca.crt" + +# CronJob configuration for automatic certificate extraction and rotation +# A one-time Job runs during initial installation (ArgoCD PreSync hook) +# The CronJob handles automatic rotation on a schedule +cronJob: + # Set to false to disable the CronJob (for debugging) + enabled: true + + # Schedule for certificate extraction (cron format) + # Default: Every day at 2 AM + # Examples: + # "0 2 * * *" - Daily at 2 AM + # "0 */6 * * *" - Every 6 hours + # "0 0 * * 0" - Weekly on Sunday at midnight + # "0 2 1 * *" - Monthly on 1st at 2 AM + schedule: "0 2 * * *" + + # CronJob specific settings + successfulJobsHistoryLimit: 3 # Keep last 3 successful jobs + failedJobsHistoryLimit: 1 # Keep last 1 failed job + concurrencyPolicy: Forbid # Don't allow concurrent runs + startingDeadlineSeconds: 300 # Start within 5 minutes of scheduled time + suspend: false # Set to true to temporarily disable + +# Job configuration (used by both Job and CronJob) +job: + # Container image for CA extraction + image: + registry: registry.redhat.io + repository: openshift4/ose-cli + tag: latest + pullPolicy: IfNotPresent + + # Resource limits + # Note: Processing 150+ certificates requires sufficient memory + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + + # Job retry policy + backoffLimit: 3 + + # TTL for completed jobs (cleanup) + ttlSecondsAfterFinished: 300 # 5 minutes + +# ConfigMap name that will be created in global.namespace +configMapName: ztvp-trusted-ca + +# Automatic rollout configuration +# After certificate extraction, automatically restart consuming applications +rollout: + enabled: true + + # Strategy: how to identify resources to restart + # - "all": Restart all Deployments/StatefulSets in target namespaces + # - "labeled": Restart only resources with specific labels + # - "specific": Restart specific named resources (see targets below) + strategy: labeled + + # Label selector for "labeled" strategy + # Only restart resources with these labels + labelSelector: + ztvp.io/uses-certificates: "true" + + # Specific targets for "specific" strategy + # List of deployments/statefulsets to restart after certificate update + targets: + - namespace: qtodo + kind: Deployment # Deployment, StatefulSet, DaemonSet + name: qtodo + + # Resource kinds to restart (applies to "all" and "labeled" strategies) + resourceKinds: + - Deployment + - StatefulSet + +# Annotations to add to the ConfigMap +configMapAnnotations: + argocd.argoproj.io/sync-options: Prune=false + +# Labels to add to the ConfigMap +configMapLabels: + app.kubernetes.io/name: ztvp-certificates + app.kubernetes.io/component: tls-certificates + app.kubernetes.io/managed-by: ztvp-certificate-manager + +# Enable validation of extracted certificates +validation: + enabled: true + # Minimum certificate size (bytes) to consider valid + minSize: 100 + # Verify certificate can be parsed by openssl + parseCheck: true + +# Distribution configuration +# Automatically distribute certificates to multiple namespaces +distribution: + # Enable distribution to target namespaces + enabled: true + + # Target namespaces where certificates should be distributed + # Each namespace will get a ConfigMap named: ztvp-trusted-ca- + targetNamespaces: + - qtodo + # - rhtpa # Keep RHTPA as-is for now; add when ready + # Add more namespaces as needed + + # Distribution method: ACM Policy distributes ConfigMaps to target namespaces + # Requires: ManagedClusterSetBinding in the namespace + method: "acm-policy" + +# Debugging options +debug: + # Enable verbose logging in extraction job + verbose: false + # Keep failed jobs for debugging (don't auto-cleanup) + keepFailedJobs: false + diff --git a/overrides/values-ztvp-certificates.yaml b/overrides/values-ztvp-certificates.yaml new file mode 100644 index 00000000..9ff55b00 --- /dev/null +++ b/overrides/values-ztvp-certificates.yaml @@ -0,0 +1,34 @@ +# Additional CA certificates for ztvp-certificates chart +# This file is referenced via extraValueFiles in values-hub.yaml +# +# To add additional certificates: +# 1. Create the secret: oc create secret generic --from-file=ca.crt=/path/to/cert.crt -n openshift-config +# 2. Uncomment and update additionalCertificates below +# 3. Trigger ArgoCD sync + +# Additional CA certificates (uncomment and configure as needed) +# customCA: +# additionalCertificates: +# - name: corporate-root-ca +# secretRef: +# name: corporate-root-ca +# namespace: openshift-config +# key: ca.crt +# - name: partner-ca +# secretRef: +# name: partner-ca +# namespace: openshift-config +# key: ca.crt + +# Automatic rollout configuration for applications consuming certificates +# When enabled, deployments with the label 'ztvp.io/uses-certificates: true' +# will be automatically restarted when certificates are updated +rollout: + enabled: true + strategy: labeled + labelSelector: + ztvp.io/uses-certificates: "true" + resourceKinds: + - Deployment + - StatefulSet + diff --git a/values-hub.yaml b/values-hub.yaml index 65b36456..d23cc8ce 100644 --- a/values-hub.yaml +++ b/values-hub.yaml @@ -156,6 +156,61 @@ clusterGroup: # - '/overrides/values-{{ $.Values.global.hubClusterDomain }}.yaml' # - '/overrides/values-{{ $.Values.global.localClusterDomain }}.yaml' applications: + ztvp-certificates: + name: ztvp-certificates + namespace: openshift-config + project: hub + path: charts/ztvp-certificates + annotations: + argocd.argoproj.io/sync-wave: "-10" + # Ignore the ACM-replicated policy in local-cluster namespace + # ACM automatically creates policy replicas with name pattern: . + ignoreDifferences: + - group: policy.open-cluster-management.io + kind: Policy + name: openshift-config.ztvp-certificates-distribution + namespace: local-cluster + jsonPointers: + - / + # Use extraValueFiles for complex nested structures like additionalCertificates + # The validated patterns framework only processes 'overrides' as --set parameters + # Edit /overrides/values-ztvp-certificates.yaml to configure: + # - Additional CA certificates (additionalCertificates array) + # - Automatic rollout restart for consuming applications + extraValueFiles: + - /overrides/values-ztvp-certificates.yaml + overrides: + # Disable Job TTL to prevent ArgoCD OutOfSync when Kubernetes deletes completed Jobs + # The initial Job runs once during first sync; CronJob handles ongoing extraction + - name: debug.keepFailedJobs + value: "true" + + # Enable verbose logging for troubleshooting (uncomment if needed) + # - name: debug.verbose + # value: "true" + + # Primary custom CA: Use secretRef to reference an existing Kubernetes secret containing CA certificates + # Uncomment to add a primary custom CA: + # Single cert: oc create secret generic custom-ca-bundle --from-file=ca.crt=/path/to/ca.crt -n openshift-config + # Multiple certs: cat corp-root.crt intermediate.crt partner.crt > combined-ca.crt && oc create secret generic custom-ca-bundle --from-file=ca.crt=combined-ca.crt -n openshift-config + # Disabled for now - using auto-detection only + # - name: customCA.secretRef.enabled + # value: "true" + - name: customCA.secretRef.name + value: custom-ca-bundle + - name: customCA.secretRef.namespace + value: openshift-config + - name: customCA.secretRef.key + value: ca.crt + + # Automatic rollout configuration (simple overrides work fine) + - name: rollout.enabled + value: "true" + - name: rollout.strategy + value: labeled + + # Note: additionalCertificates (complex nested array) temporarily disabled + # Need to find proper way to pass complex structures in Validated Patterns acm: name: acm namespace: open-cluster-management diff --git a/values-secret.yaml.template b/values-secret.yaml.template index db02b485..c7762447 100644 --- a/values-secret.yaml.template +++ b/values-secret.yaml.template @@ -47,6 +47,13 @@ secrets: - name: db-password onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy + - name: qtodo-truststore + vaultPrefixes: + - global + fields: + - name: truststore-password + onMissingValue: generate + vaultPolicy: alphaNumericPolicy - name: keycloak-users vaultPrefixes: - global