Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
138 changes: 137 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,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 }}
Expand Down Expand Up @@ -65,13 +167,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 +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
Expand All @@ -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 }}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 }}
25 changes: 25 additions & 0 deletions charts/qtodo/templates/truststore-secret-external-secret.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
Loading