Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/superlinter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 40 additions & 21 deletions charts/qtodo/files/spiffe-vault-client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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


Expand Down
4 changes: 2 additions & 2 deletions charts/qtodo/templates/app-config-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand Down
142 changes: 141 additions & 1 deletion charts/qtodo/templates/app-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ metadata:
argocd.argoproj.io/sync-wave: '20'
labels:
app: qtodo
ztvp.io/uses-certificates: "true"
name: qtodo
namespace: qtodo
spec:
Expand All @@ -28,6 +29,111 @@ spec:
spec:
{{- if eq .Values.app.spire.enabled true }}
initContainers:
- name: init-ca-truststore
Comment thread
mlorenzofr marked this conversation as resolved.
image: registry.redhat.io/ubi9/openjdk-17:latest
imagePullPolicy: IfNotPresent
command:
Comment thread
mlorenzofr marked this conversation as resolved.
- /bin/bash
- -c
- |
set -e
echo "Converting CA bundle to PKCS12 truststore using Java KeyStore API..."

# 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"

# 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"

# Use jshell to bulk-import all certificates (single JVM, much faster than keytool loop)
jshell -R-Dpassword="$TRUSTSTORE_PASSWORD" -R-Dbundle="$CA_BUNDLE" -R-Doutput="$TRUSTSTORE_PATH" <<'JSHELL'
import java.security.*;
import java.security.cert.*;
import java.io.*;
import java.nio.file.*;

String password = System.getProperty("password");
String bundlePath = System.getProperty("bundle");
String outputPath = System.getProperty("output");

KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, password.toCharArray());

CertificateFactory cf = CertificateFactory.getInstance("X.509");
byte[] pemBytes = Files.readAllBytes(Path.of(bundlePath));

var certs = cf.generateCertificates(new ByteArrayInputStream(pemBytes));
int i = 0;
for (var cert : certs) {
ks.setCertificateEntry("ztvp-ca-" + String.format("%03d", i++), cert);
}

try (FileOutputStream fos = new FileOutputStream(outputPath)) {
ks.store(fos, password.toCharArray());
}
System.out.println("Imported " + i + " certificates into PKCS12 truststore");
/exit
JSHELL

echo "Successfully created PKCS12 truststore"
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/openshift4/ose-tools-rhel9: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 }}
Expand Down Expand Up @@ -65,13 +171,18 @@ 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
- name: db-credentials
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 }}
Expand Down Expand Up @@ -113,6 +224,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
Expand All @@ -122,6 +235,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 }}
Expand Down Expand Up @@ -149,7 +265,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
Expand All @@ -170,11 +286,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
Expand All @@ -195,4 +329,10 @@ spec:
- name: spiffe-vault-client
configMap:
name: spiffe-vault-client
- name: ztvp-trusted-ca
configMap:
name: ztvp-trusted-ca
defaultMode: 420
Comment thread
mlorenzofr marked this conversation as resolved.
- name: truststore
emptyDir: {}
{{- end }}
Loading