diff --git a/openstack/barbican/Chart.yaml b/openstack/barbican/Chart.yaml
index 9ec9f113b0e..2cb45f44917 100644
--- a/openstack/barbican/Chart.yaml
+++ b/openstack/barbican/Chart.yaml
@@ -4,7 +4,7 @@ appVersion: flamingo
description: A Helm chart for Openstack Barbican
icon: https://www.openstack.org/themes/openstack/images/project-mascots/Barbican/OpenStack_Project_Barbican_vertical.png
name: barbican
-version: 0.9.2
+version: 0.10.0
dependencies:
- condition: mariadb.enabled
name: mariadb
diff --git a/openstack/barbican/alerts/kubernetes/openstack-barbican.alerts b/openstack/barbican/alerts/kubernetes/openstack-barbican.alerts
index 8fe19921a6a..d676d78db38 100644
--- a/openstack/barbican/alerts/kubernetes/openstack-barbican.alerts
+++ b/openstack/barbican/alerts/kubernetes/openstack-barbican.alerts
@@ -17,3 +17,38 @@ groups:
annotations:
description: 'barbican-api pod {{ $labels.pod }} not ready; recreate pod. See playbook.'
summary: 'barbican-api pod not ready'
+
+ - alert: OpenstackBarbicanTLSCertificateExpiresCritical
+ expr: (probe_ssl_earliest_cert_expiry{instance=~"https://keymanager-3\\..*"} - time()) / 86400 < 7
+ for: 1h
+ labels:
+ no_alert_on_absence: "true"
+ context: security
+ dashboard: barbican
+ service: barbican
+ severity: critical
+ tier: os
+ support_group: identity
+ playbook: 'docs/support/playbook/barbican/alerts/tls-certificate-expiry/#openstackbarbicantlscertificateexpirescritical'
+ annotations:
+ description: The TLS certificate served by Barbican at {{ $labels.instance }} expires in less than 7 days. External HTTPS access will break when it expires. Check the rotation CronJob and cert-manager.
+ summary: Barbican served TLS certificate expires within 7 days
+
+ - alert: OpenstackBarbicanTLSRotationStale
+ expr: >
+ time() - kube_cronjob_created{cronjob="barbican-tls-rotation"} >= 8*24*60*60
+ and on(cronjob) (time() - kube_cronjob_status_last_successful_time{cronjob="barbican-tls-rotation"}
+ or on(cronjob) kube_cronjob_status_last_schedule_time{cronjob="barbican-tls-rotation"}) >= 8*24*60*60
+ for: 1h
+ labels:
+ no_alert_on_absence: "true"
+ context: security
+ dashboard: barbican
+ service: barbican
+ severity: warning
+ tier: os
+ support_group: identity
+ playbook: 'docs/support/playbook/barbican/alerts/tls-certificate-expiry/#openstackbarbicantlsrotationstale'
+ annotations:
+ description: The Barbican TLS certificate rotation CronJob has not completed successfully in over 8 days. If not resolved, the certificate will expire and external HTTPS access will break. Check the CronJob and its Jobs.
+ summary: Barbican TLS certificate rotation is stale
diff --git a/openstack/barbican/alerts/openstack/openstack-barbican.alerts b/openstack/barbican/alerts/openstack/openstack-barbican.alerts
index 368ec46d229..f95bba3ef4c 100644
--- a/openstack/barbican/alerts/openstack/openstack-barbican.alerts
+++ b/openstack/barbican/alerts/openstack/openstack-barbican.alerts
@@ -116,3 +116,23 @@ groups:
annotations:
description: All barbican-api server pods are down.
summary: barbican is unavailable.
+
+ - alert: OpenstackBarbicanTLSRotationFailed
+ expr: >
+ kube_job_status_failed{job_name=~"barbican-tls-rotation.*"} > 0
+ and on (namespace, job_name)
+ kube_job_created{job_name=~"barbican-tls-rotation.*"}
+ == on (namespace) group_left()
+ max by (namespace) (kube_job_created{job_name=~"barbican-tls-rotation.*"})
+ for: 1h
+ labels:
+ context: security
+ dashboard: barbican
+ service: barbican
+ severity: warning
+ tier: os
+ support_group: identity
+ no_alert_on_absence: "true"
+ annotations:
+ description: The weekly TLS certificate rotation CronJob for Barbican has failed. If not resolved, the TLS certificate will expire and external HTTPS access will break. Check the CronJob logs.
+ summary: Barbican TLS certificate rotation failed
diff --git a/openstack/barbican/templates/_helpers.tpl b/openstack/barbican/templates/_helpers.tpl
index fe785f70456..b6b858cd389 100644
--- a/openstack/barbican/templates/_helpers.tpl
+++ b/openstack/barbican/templates/_helpers.tpl
@@ -15,6 +15,11 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
{{- printf "%s-%s" .Release.Name $name | trunc 63 | replace "_" "-" | trimSuffix "-" -}}
{{- end -}}
+{{- define "barbican.external_ip" -}}
+{{- $svc := .Values.services | default dict -}}
+{{- .Values.global.barbican_external_ip | default $svc.externalip -}}
+{{- end -}}
+
{{- define "barbican.db_service" }}
{{- include "utils.db_host" . }}
{{- end }}
@@ -39,3 +44,24 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
{{- .Release.Name }}-{{ $name }}-{{ substr 0 4 $hash }}-{{ .Values.imageVersionBarbicanApi | required "Please set barbican.imageVersionBarbicanApi" }}
{{- end }}
{{- end }}
+
+{{- define "barbican.tls.validate" -}}
+{{- if .Values.tls.enabled }}
+ {{- if not .Values.tls.keyGeneration }}
+ {{- fail "tls.keyGeneration is required when tls.enabled (options: go-crypto, hsm-entropy, hsm-full, tpm-entropy)" }}
+ {{- end }}
+ {{- if not .Values.tls.keyWrapping }}
+ {{- fail "tls.keyWrapping is required when tls.enabled (options: none, vault-transit, hsm, tpm)" }}
+ {{- end }}
+ {{- if not .Values.tls.keyStorage }}
+ {{- fail "tls.keyStorage is required when tls.enabled (options: internal-k8s-secret, k8s-secret, vault-secret)" }}
+ {{- end }}
+ {{- if and (eq .Values.tls.keyWrapping "none") (eq .Values.tls.keyStorage "k8s-secret") (not .Values.tls.allowInsecureStorage) }}
+ {{- fail "tls: unwrapped keys cannot be stored as plain-text K8s Secrets. Set tls.keyWrapping or tls.keyStorage, or set tls.allowInsecureStorage: true to acknowledge." }}
+ {{- end }}
+ {{- $svc := .Values.services | default dict }}
+ {{- if not (.Values.global.barbican_external_ip | default $svc.externalip) }}
+ {{- fail "tls.enabled requires an external IP (global.barbican_external_ip or services.externalip): enabling TLS removes the ingress and the public TLS Service only renders once the external IP is set." }}
+ {{- end }}
+{{- end }}
+{{- end }}
diff --git a/openstack/barbican/templates/api-deployment.yaml b/openstack/barbican/templates/api-deployment.yaml
index fc6b81ed2f2..f8805e94351 100644
--- a/openstack/barbican/templates/api-deployment.yaml
+++ b/openstack/barbican/templates/api-deployment.yaml
@@ -1,3 +1,4 @@
+{{- include "barbican.tls.validate" . }}
kind: Deployment
apiVersion: apps/v1
@@ -12,7 +13,7 @@ metadata:
type: api
component: barbican
annotations:
- secret.reloader.stakater.com/reload: "{{ .Release.Name }}-secrets"
+ secret.reloader.stakater.com/reload: "{{ .Release.Name }}-secrets{{- if .Values.tls.enabled }},{{ .Values.tls.secretName | default "barbican-tls" }}{{- end }}"
deployment.reloader.stakater.com/pause-period: "60s"
spec:
replicas: {{ required ".Values.api.replicas is missing" .Values.api.replicas }}
@@ -39,12 +40,23 @@ spec:
annotations:
configmap-etc-hash: {{ include (print $.Template.BasePath "/etc-configmap.yaml") . | sha256sum }}
configmap-bin-hash: {{ include (print $.Template.BasePath "/configmap-bin.yaml") . | sha256sum }}
+ {{- if .Values.tls.enabled }}
+ config.linkerd.io/skip-inbound-ports: "443"
+ {{- end }}
{{- if .Values.proxysql.mode }}
prometheus.io/scrape: "true"
prometheus.io/targets: {{ required ".Values.alerts.prometheus missing" .Values.alerts.prometheus | quote }}
{{- end }}
{{- include "utils.linkerd.pod_and_service_annotation" . | indent 8 }}
spec:
+ {{- if .Values.tls.enabled }}
+ securityContext:
+ # distroless "nonroot" gid of the tls-lifecycle-manager image; lets the
+ # non-root tls-init container read the 0440 tls-input Secret while keeping
+ # the key group-restricted (not world-readable) per the BSI least-privilege
+ # requirement (Sicherheitsarchitektur 4.1: non-root, restriktive Berechtigungen).
+ fsGroup: 65532
+ {{- end }}
{{- if .Values.rbac.enabled }}
serviceAccountName: {{ .Release.Name }}
{{- end }}
@@ -59,6 +71,22 @@ spec:
{{- if .Values.proxysql.native_sidecar }}
{{- include "utils.proxysql.container" . | indent 6 }}
{{- end }}
+ {{- if .Values.tls.enabled }}
+ - name: tls-init
+ image: {{ .Values.tls.lifecycle.image }}:{{ .Values.tls.lifecycle.imageTag }}
+ imagePullPolicy: IfNotPresent
+ args:
+ - --mode=unwrap
+ - --input-dir=/tls-input
+ - --output-dir=/mnt/secrets
+ - --key-wrapping={{ required "tls.keyWrapping is required" .Values.tls.keyWrapping }}
+ volumeMounts:
+ - name: tls-input
+ mountPath: /tls-input
+ readOnly: true
+ - name: tls-secrets
+ mountPath: /mnt/secrets
+ {{- end }}
containers:
- name: barbican-api
image: {{required ".Values.global.registry is missing" .Values.global.registry }}/loci-barbican:{{required "Values.imageVersionBarbicanApi is missing" .Values.imageVersionBarbicanApi}}
@@ -115,6 +143,10 @@ spec:
ports:
- name: barbican-api
containerPort: {{.Values.api_port_internal}}
+ {{- if .Values.tls.enabled }}
+ - name: barbican-tls
+ containerPort: 443
+ {{- end }}
volumeMounts:
- name: etcbarbican
mountPath: /etc/barbican
@@ -138,6 +170,23 @@ spec:
mountPath: /etc/barbican/logging.ini
subPath: logging.ini
readOnly: true
+ {{- if .Values.tls.enabled }}
+ - name: tls-secrets
+ mountPath: /mnt/secrets
+ readOnly: true
+ - name: barbican-etc
+ mountPath: /etc/apache2/conf-enabled/00-ssl-load.conf
+ subPath: ssl-load.conf
+ readOnly: true
+ - name: barbican-etc
+ mountPath: /etc/apache2/conf-enabled/tls-hardening.conf
+ subPath: tls-hardening.conf
+ readOnly: true
+ - name: barbican-etc
+ mountPath: /etc/apache2/conf-enabled/apache-hardening.conf
+ subPath: apache-hardening.conf
+ readOnly: true
+ {{- end }}
- mountPath: /etc/barbican/barbican.conf.d
name: barbican-etc-confd
readOnly: true
@@ -334,3 +383,13 @@ spec:
{{- end }}
{{- include "utils.proxysql.volumes" . | indent 8 }}
{{- include "utils.trust_bundle.volumes" . | indent 8 }}
+ {{- if .Values.tls.enabled }}
+ - name: tls-input
+ secret:
+ secretName: {{ .Values.tls.secretName | default "barbican-tls" }}
+ defaultMode: 0440
+ - name: tls-secrets
+ emptyDir:
+ medium: Memory
+ sizeLimit: 1Mi
+ {{- end }}
diff --git a/openstack/barbican/templates/cronjob-tls-rotation.yaml b/openstack/barbican/templates/cronjob-tls-rotation.yaml
new file mode 100644
index 00000000000..9a59f3b3cb3
--- /dev/null
+++ b/openstack/barbican/templates/cronjob-tls-rotation.yaml
@@ -0,0 +1,46 @@
+{{- if .Values.tls.enabled }}
+{{- include "barbican.tls.validate" . }}
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: {{ .Release.Name }}-tls-rotation
+ labels:
+ app: {{ template "fullname" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ component: barbican
+ type: tls-rotation
+spec:
+ schedule: {{ .Values.tls.rotationSchedule | default "0 3 * * 1" | quote }}
+ concurrencyPolicy: Forbid
+ successfulJobsHistoryLimit: 3
+ failedJobsHistoryLimit: 3
+ jobTemplate:
+ spec:
+ template:
+ spec:
+ restartPolicy: Never
+ serviceAccountName: {{ .Release.Name }}-tls-manager
+ containers:
+ - name: tls-rotation
+ image: {{ .Values.tls.lifecycle.image }}:{{ .Values.tls.lifecycle.imageTag }}
+ imagePullPolicy: IfNotPresent
+ args:
+ - --mode=rotation
+ - --namespace={{ .Release.Namespace }}
+ - --secret-name={{ .Values.tls.secretName | default "barbican-tls" }}
+ - --key-algorithm={{ .Values.tls.keyAlgorithm | default "ECDSAP256" }}
+ - --issuer-name={{ .Values.tls.issuerName | default "digicert-issuer" }}
+ - --issuer-kind={{ .Values.tls.issuerKind | default "ClusterIssuer" }}
+ - --issuer-group={{ .Values.tls.issuerGroup | default "certmanager.cloud.sap" }}
+ - --common-name={{ include "barbican_api_endpoint_host_public" . }}
+ - --san-dns={{ include "barbican_api_endpoint_host_public" . }}
+ - --renewal-threshold-days=30
+ - --key-generation={{ required "tls.keyGeneration is required" .Values.tls.keyGeneration }}
+ - --key-wrapping={{ required "tls.keyWrapping is required" .Values.tls.keyWrapping }}
+ - --key-storage={{ required "tls.keyStorage is required" .Values.tls.keyStorage }}
+ {{- if .Values.tls.allowInsecureStorage }}
+ - --allow-insecure-storage
+ {{- end }}
+ backoffLimit: 3
+{{- end }}
diff --git a/openstack/barbican/templates/etc-configmap.yaml b/openstack/barbican/templates/etc-configmap.yaml
index e735e25c787..237a5eb7a96 100644
--- a/openstack/barbican/templates/etc-configmap.yaml
+++ b/openstack/barbican/templates/etc-configmap.yaml
@@ -11,6 +11,14 @@ data:
{{ include (print .Template.BasePath "/etc/_barbican-api-paste.ini.tpl") . | indent 4 }}
barbican.conf: |
{{ include (print .Template.BasePath "/etc/_barbican.conf.tpl") . | indent 4 }}
+{{- if .Values.tls.enabled }}
+ ssl-load.conf: |
+{{ include (print .Template.BasePath "/etc/_ssl-load.conf.tpl") . | indent 4 }}
+ tls-hardening.conf: |
+{{ include (print .Template.BasePath "/etc/_tls-hardening.conf.tpl") . | indent 4 }}
+ apache-hardening.conf: |
+{{ include (print .Template.BasePath "/etc/_apache-hardening.conf.tpl") . | indent 4 }}
+{{- end }}
policy.yaml: |
{{ include (print .Template.BasePath "/etc/_barbican-policy.yaml.tpl") . | indent 4 }}
logging.ini: |
diff --git a/openstack/barbican/templates/etc/_apache-hardening.conf.tpl b/openstack/barbican/templates/etc/_apache-hardening.conf.tpl
new file mode 100644
index 00000000000..8ff2fa1a4db
--- /dev/null
+++ b/openstack/barbican/templates/etc/_apache-hardening.conf.tpl
@@ -0,0 +1,17 @@
+{{/*
+Server-level Apache hardening: suppress the version banner, disable the TRACE
+method, and turn off directory listing for the document root. Parsed after the
+packaged apache2.conf so these directives take precedence.
+*/}}
+
+# Suppress server version banner and disable TRACE
+ServerTokens Prod
+ServerSignature Off
+TraceEnable Off
+
+# Disable directory listing for the document root
+
+ Options FollowSymLinks
+ AllowOverride None
+ Require all granted
+
diff --git a/openstack/barbican/templates/etc/_ssl-load.conf.tpl b/openstack/barbican/templates/etc/_ssl-load.conf.tpl
new file mode 100644
index 00000000000..ed96306c1db
--- /dev/null
+++ b/openstack/barbican/templates/etc/_ssl-load.conf.tpl
@@ -0,0 +1,9 @@
+{{/*
+Load mod_ssl and its socache dependency via read-only conf so the module is
+available under a read-only root filesystem without a2enmod. mod_headers is
+required for the HTTP security response headers set in the TLS vhost.
+*/}}
+
+LoadModule socache_shmcb_module /usr/lib/apache2/modules/mod_socache_shmcb.so
+LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
+LoadModule headers_module /usr/lib/apache2/modules/mod_headers.so
diff --git a/openstack/barbican/templates/etc/_tls-hardening.conf.tpl b/openstack/barbican/templates/etc/_tls-hardening.conf.tpl
new file mode 100644
index 00000000000..e85cd57fd4d
--- /dev/null
+++ b/openstack/barbican/templates/etc/_tls-hardening.conf.tpl
@@ -0,0 +1,36 @@
+{{/*
+Apache TLS protocol hardening configuration.
+Configures TLS 1.3/1.2 with compliant cipher suites, PFS, AEAD modes,
+and brainpool curve preference. Shared between Keystone and Barbican.
+*/}}
+
+# TLS Protocol Versions: only 1.3 and 1.2
+SSLProtocol -all +TLSv1.3 +TLSv1.2
+
+# TLS 1.2 Cipher Suites (ECDHE + AEAD only, PFS required)
+SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256
+
+# TLS 1.3 Cipher Suites (AES-GCM and AES-CCM)
+SSLCipherSuite TLSv1.3 TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256
+
+# Server chooses cipher order
+SSLHonorCipherOrder on
+
+# ECDHE Curve Preference: brainpool first, NIST as fallback
+SSLOpenSSLConfCmd Curves brainpoolP256r1:brainpoolP384r1:prime256v1:secp384r1
+
+# Signature algorithms preference
+SSLOpenSSLConfCmd SignatureAlgorithms ecdsa_secp256r1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha256:rsa_pss_rsae_sha384
+
+# Session cache
+SSLSessionCache shmcb:/run/apache2/ssl_scache(512000)
+SSLSessionCacheTimeout 300
+
+# Disable session tickets (for PFS)
+SSLSessionTickets off
+
+# Strict SNI
+SSLStrictSNIVHostCheck on
+
+# Disable TLS compression (CRIME mitigation)
+SSLCompression off
diff --git a/openstack/barbican/templates/etc/_wsgi-barbican.conf.tpl b/openstack/barbican/templates/etc/_wsgi-barbican.conf.tpl
index 2a4441db1a5..8ed55d79525 100644
--- a/openstack/barbican/templates/etc/_wsgi-barbican.conf.tpl
+++ b/openstack/barbican/templates/etc/_wsgi-barbican.conf.tpl
@@ -16,6 +16,49 @@ WSGIDaemonProcess barbican-api processes={{ .Values.api.processes | default 1 }}
Listen 0.0.0.0:{{ .Values.api_port_internal }}
+{{- if .Values.tls.enabled }}
+# External HTTPS endpoint
+# mod_ssl is loaded via a conf-enabled snippet, which is parsed after
+# ports.conf, so its ssl_module-gated Listen 443 does not apply here.
+Listen 0.0.0.0:443
+
+
+ SSLEngine on
+ SSLCertificateFile /mnt/secrets/tls.crt
+ SSLCertificateKeyFile /mnt/secrets/tls.key
+
+ # HTTP security response headers
+ Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
+ Header always set X-Content-Type-Options "nosniff"
+ Header always set X-Frame-Options "DENY"
+ Header always set Content-Security-Policy "default-src 'self'"
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
+
+ WSGIProcessGroup barbican-api
+ WSGIScriptAlias / /var/www/cgi-bin/barbican/barbican-wsgi-api
+ WSGIApplicationGroup %{GLOBAL}
+ WSGIPassAuthorization On
+ LimitRequestBody 114688
+
+ = 2.4>
+ ErrorLogFormat "%{cu}t %M"
+
+ ErrorLog /dev/stderr
+ {{- if .Values.use_json }}
+ SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded
+ CustomLog /dev/stdout json_combined env=!forwarded
+ CustomLog /dev/stdout json_proxy env=forwarded
+ {{- else }}
+ SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded
+ CustomLog /dev/stdout combined env=!forwarded
+ CustomLog /dev/stdout proxy env=forwarded
+ {{- end }}
+
+ KeepAliveTimeout 61
+
+{{- end }}
+
+# Internal HTTP endpoint (protected by Linkerd mTLS at the network layer)
ServerName {{ include "barbican_api_endpoint_host_public" . }}
diff --git a/openstack/barbican/templates/ingress.yaml b/openstack/barbican/templates/ingress.yaml
index 20ebb94a242..94d26106dd2 100644
--- a/openstack/barbican/templates/ingress.yaml
+++ b/openstack/barbican/templates/ingress.yaml
@@ -1,3 +1,4 @@
+{{- if not .Values.tls.enabled }}
kind: Ingress
apiVersion: networking.k8s.io/v1
@@ -39,3 +40,4 @@ spec:
name: barbican-api
port:
number: {{.Values.api_port_internal}}
+{{- end }}
diff --git a/openstack/barbican/templates/job-tls-bootstrap.yaml b/openstack/barbican/templates/job-tls-bootstrap.yaml
new file mode 100644
index 00000000000..99938947c3e
--- /dev/null
+++ b/openstack/barbican/templates/job-tls-bootstrap.yaml
@@ -0,0 +1,43 @@
+{{- if .Values.tls.enabled }}
+{{- include "barbican.tls.validate" . }}
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ .Release.Name }}-tls-bootstrap
+ labels:
+ app: {{ template "fullname" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ component: barbican
+ type: tls-bootstrap
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-5"
+ "helm.sh/hook-delete-policy": before-hook-creation
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ serviceAccountName: {{ .Release.Name }}-tls-manager
+ containers:
+ - name: tls-bootstrap
+ image: {{ .Values.tls.lifecycle.image }}:{{ .Values.tls.lifecycle.imageTag }}
+ imagePullPolicy: IfNotPresent
+ args:
+ - --mode=bootstrap
+ - --namespace={{ .Release.Namespace }}
+ - --secret-name={{ .Values.tls.secretName | default "barbican-tls" }}
+ - --key-algorithm={{ .Values.tls.keyAlgorithm | default "ECDSAP256" }}
+ - --issuer-name={{ .Values.tls.issuerName | default "digicert-issuer" }}
+ - --issuer-kind={{ .Values.tls.issuerKind | default "ClusterIssuer" }}
+ - --issuer-group={{ .Values.tls.issuerGroup | default "certmanager.cloud.sap" }}
+ - --common-name={{ include "barbican_api_endpoint_host_public" . }}
+ - --san-dns={{ include "barbican_api_endpoint_host_public" . }}
+ - --key-generation={{ required "tls.keyGeneration is required" .Values.tls.keyGeneration }}
+ - --key-wrapping={{ required "tls.keyWrapping is required" .Values.tls.keyWrapping }}
+ - --key-storage={{ required "tls.keyStorage is required" .Values.tls.keyStorage }}
+ {{- if .Values.tls.allowInsecureStorage }}
+ - --allow-insecure-storage
+ {{- end }}
+ backoffLimit: 3
+{{- end }}
diff --git a/openstack/barbican/templates/pdb.yaml b/openstack/barbican/templates/pdb.yaml
new file mode 100644
index 00000000000..54931bc33db
--- /dev/null
+++ b/openstack/barbican/templates/pdb.yaml
@@ -0,0 +1,17 @@
+{{- if .Values.api.pdb.minAvailable }}
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+ name: {{ .Release.Name }}-api
+ labels:
+ app: {{ template "fullname" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ component: barbican
+ type: api
+spec:
+ minAvailable: {{ .Values.api.pdb.minAvailable }}
+ selector:
+ matchLabels:
+ name: barbican-api
+{{- end }}
diff --git a/openstack/barbican/templates/probe-tls.yaml b/openstack/barbican/templates/probe-tls.yaml
new file mode 100644
index 00000000000..b2ef0eb6601
--- /dev/null
+++ b/openstack/barbican/templates/probe-tls.yaml
@@ -0,0 +1,22 @@
+{{- if and .Values.tls.enabled .Values.tls.probe.enabled (include "barbican.external_ip" .) }}
+apiVersion: monitoring.coreos.com/v1
+kind: Probe
+metadata:
+ name: {{ .Release.Name }}-tls
+ labels:
+ prometheus: {{ .Values.tls.probe.prometheus }}
+spec:
+ jobName: {{ .Release.Name }}-tls
+ prober:
+ url: prober.{{ .Values.global.region }}.cloud.sap
+ scheme: https
+ module: {{ .Values.tls.probe.module }}
+ interval: {{ .Values.tls.probe.interval }}
+ scrapeTimeout: {{ .Values.tls.probe.scrapeTimeout }}
+ targets:
+ staticConfig:
+ static:
+ - https://{{ include "barbican_api_endpoint_host_public" . }}
+ labels:
+ region: {{ .Values.global.region }}
+{{- end }}
diff --git a/openstack/barbican/templates/rbac-tls-manager.yaml b/openstack/barbican/templates/rbac-tls-manager.yaml
new file mode 100644
index 00000000000..e82c13fe51d
--- /dev/null
+++ b/openstack/barbican/templates/rbac-tls-manager.yaml
@@ -0,0 +1,49 @@
+{{- if .Values.tls.enabled }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ .Release.Name }}-tls-manager
+ labels:
+ app: {{ template "fullname" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ component: barbican
+ type: tls-manager
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-10"
+ "helm.sh/hook-delete-policy": before-hook-creation
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ .Release.Name }}-tls-manager
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-10"
+ "helm.sh/hook-delete-policy": before-hook-creation
+rules:
+ - apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "create", "update"]
+ - apiGroups: ["cert-manager.io"]
+ resources: ["certificaterequests"]
+ verbs: ["create", "get", "delete", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ .Release.Name }}-tls-manager
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-10"
+ "helm.sh/hook-delete-policy": before-hook-creation
+subjects:
+ - kind: ServiceAccount
+ name: {{ .Release.Name }}-tls-manager
+ namespace: {{ .Release.Namespace }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: {{ .Release.Name }}-tls-manager
+{{- end }}
diff --git a/openstack/barbican/templates/record.yaml b/openstack/barbican/templates/record.yaml
new file mode 100644
index 00000000000..99c02ebb3b4
--- /dev/null
+++ b/openstack/barbican/templates/record.yaml
@@ -0,0 +1,19 @@
+{{- $externalIP := include "barbican.external_ip" . }}
+{{- if and .Values.tls.enabled $externalIP }}
+apiVersion: disco.stable.sap.cc/v1
+kind: Record
+metadata:
+ name: {{ include "barbican_api_endpoint_host_public" . }}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ app: {{ template "fullname" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ heritage: "{{ .Release.Service }}"
+spec:
+ type: A
+ record: {{ $externalIP | quote }}
+ hosts:
+ - {{ include "barbican_api_endpoint_host_public" . }}
+ zoneName: {{ .Values.global.region }}.{{ .Values.global.tld }}
+{{- end }}
diff --git a/openstack/barbican/templates/service-tls.yaml b/openstack/barbican/templates/service-tls.yaml
new file mode 100644
index 00000000000..3d0b88119e4
--- /dev/null
+++ b/openstack/barbican/templates/service-tls.yaml
@@ -0,0 +1,35 @@
+{{- $externalIP := include "barbican.external_ip" . }}
+{{- if and .Values.tls.enabled $externalIP }}
+kind: Service
+apiVersion: v1
+metadata:
+ name: barbican-public-tls
+ namespace: {{ .Release.Namespace }}
+ labels:
+ app: {{ template "fullname" . }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ release: "{{ .Release.Name }}"
+ heritage: "{{ .Release.Service }}"
+ system: openstack
+ type: api-tls
+ component: barbican
+ annotations:
+ {{- if .Values.calicoLoadBalancer }}
+ projectcalico.org/loadBalancerIPs: '["{{ $externalIP }}"]'
+ {{- end }}
+ {{- include "utils.linkerd.pod_and_service_annotation" . | indent 4 }}
+spec:
+ type: LoadBalancer
+ externalTrafficPolicy: Local
+ selector:
+ name: barbican-api
+ ports:
+ - name: barbican-tls
+ protocol: "TCP"
+ port: 443
+ targetPort: 443
+ {{- if not .Values.calicoLoadBalancer }}
+ externalIPs:
+ - {{ $externalIP }}
+ {{- end }}
+{{- end }}
diff --git a/openstack/barbican/values.yaml b/openstack/barbican/values.yaml
index 2993252f39f..78bb19eb441 100644
--- a/openstack/barbican/values.yaml
+++ b/openstack/barbican/values.yaml
@@ -11,6 +11,8 @@ global:
domain_seeds:
skip_hcm_domain: false
linkerd_requested: false
+ # Public /32 for the pod-level TLS LoadBalancer service and DNS record.
+ # barbican_external_ip: "192.168.1.100"
rbac:
enabled: true
@@ -42,6 +44,8 @@ api:
replicas: 2
processes: 1
threads: 1
+ pdb:
+ minAvailable: 1
metrics:
enabled: true
resources:
@@ -472,6 +476,12 @@ sentry:
tlsacme: true
+calicoLoadBalancer: false
+
+services:
+ # Backward-compat alias for global.barbican_external_ip.
+ # externalip: "192.168.1.100"
+
# SCIBPL-209: Ingress hardening — request size cap and connection limiting.
# These annotations are applied to the nginx ingress controller.
# Keystone middleware already enforces authentication; these settings add a
@@ -489,6 +499,41 @@ ingress:
enableConnectionsLimit: true
limitConnections: 20
+# Pod-level TLS termination in Apache
+tls:
+ enabled: false
+ # Name of the K8s Secret containing tls.key and tls.crt (created by TLS bootstrap Job)
+ secretName: barbican-tls
+ # Per-region operator choices — all three are REQUIRED when tls.enabled: true.
+ # There are no defaults; the operator must make an explicit choice.
+ # keyGeneration: go-crypto | hsm-entropy | hsm-full | tpm-entropy
+ # keyWrapping: none | vault-transit | hsm | tpm
+ # keyStorage: internal-k8s-secret | k8s-secret | vault-secret
+ # internal-k8s-secret (recommended): key written directly to the K8s API; stays in-cluster, never in Git
+ # k8s-secret: Secret manifest emitted for Git commit + GitOps operator review
+ # Escape hatch: allowInsecureStorage: true also passes --allow-insecure-storage to the binary,
+ # permitting wrapping=none + storage=k8s-secret (development only)
+ allowInsecureStorage: false
+ # TLS lifecycle manager binary for bootstrap Job and rotation CronJob
+ lifecycle:
+ image: keppel.eu-de-1.cloud.sap/ccloud/tls-lifecycle-manager
+ imageTag: "20260818160021"
+ # cert-manager issuer configuration
+ issuerName: digicert-issuer
+ issuerKind: ClusterIssuer
+ issuerGroup: certmanager.cloud.sap
+ keyAlgorithm: ECDSAP256
+ # Cron schedule for the weekly certificate rotation CronJob. Defaults are
+ # staggered across services (keystone runs an hour after barbican) so their
+ # rotations do not run at the same time and hit issuer rate limits.
+ rotationSchedule: "0 3 * * 1"
+ probe:
+ enabled: false
+ prometheus: kubernetes
+ module: http_200_403
+ interval: 60s
+ scrapeTimeout: 55s
+
# sapcc/openstack-rate-limit-middleware
sapcc_rate_limit:
enabled: true
diff --git a/openstack/keystone/Chart.yaml b/openstack/keystone/Chart.yaml
index 0988fc63e80..1ecf0ddc017 100644
--- a/openstack/keystone/Chart.yaml
+++ b/openstack/keystone/Chart.yaml
@@ -9,7 +9,7 @@ maintainers:
name: keystone
sources:
- https://github.com/sapcc/keystone
-version: 0.13.4
+version: 0.14.0
dependencies:
- condition: mariadb.enabled
name: mariadb
diff --git a/openstack/keystone/alerts/kubernetes/keystone.alerts b/openstack/keystone/alerts/kubernetes/keystone.alerts
index 8bb3d7cb083..bb15841690c 100644
--- a/openstack/keystone/alerts/kubernetes/keystone.alerts
+++ b/openstack/keystone/alerts/kubernetes/keystone.alerts
@@ -51,3 +51,38 @@ groups:
description: The container {{ $labels.pod }}/{{ $labels.container }} is being CPU-throttled
constantly. This is probably impacting performance, so check if we can increase the number
of replicas or the resource requests/limits.
+
+ - alert: OpenstackKeystoneTLSCertificateExpiresCritical
+ expr: (probe_ssl_earliest_cert_expiry{instance=~"https://identity-3\\..*"} - time()) / 86400 < 7
+ for: 1h
+ labels:
+ no_alert_on_absence: "true"
+ context: security
+ dashboard: keystone
+ service: keystone
+ severity: critical
+ tier: os
+ support_group: identity
+ playbook: 'docs/support/playbook/keystone/alerts/tls-certificate-expiry/#openstackkeystonetlscertificateexpirescritical'
+ annotations:
+ description: The TLS certificate served by Keystone at {{ $labels.instance }} expires in less than 7 days. External HTTPS access will break when it expires. Check the rotation CronJob and cert-manager.
+ summary: Keystone served TLS certificate expires within 7 days
+
+ - alert: OpenstackKeystoneTLSRotationStale
+ expr: >
+ time() - kube_cronjob_created{cronjob="keystone-tls-rotation"} >= 8*24*60*60
+ and on(cronjob) (time() - kube_cronjob_status_last_successful_time{cronjob="keystone-tls-rotation"}
+ or on(cronjob) kube_cronjob_status_last_schedule_time{cronjob="keystone-tls-rotation"}) >= 8*24*60*60
+ for: 1h
+ labels:
+ no_alert_on_absence: "true"
+ context: security
+ dashboard: keystone
+ service: keystone
+ severity: warning
+ tier: os
+ support_group: identity
+ playbook: 'docs/support/playbook/keystone/alerts/tls-certificate-expiry/#openstackkeystonetlsrotationstale'
+ annotations:
+ description: The Keystone TLS certificate rotation CronJob has not completed successfully in over 8 days. If not resolved, the certificate will expire and external HTTPS access will break. Check the CronJob and its Jobs.
+ summary: Keystone TLS certificate rotation is stale
diff --git a/openstack/keystone/alerts/openstack/keystone.alerts b/openstack/keystone/alerts/openstack/keystone.alerts
index b9ed5b48d6d..9e806d1073e 100644
--- a/openstack/keystone/alerts/openstack/keystone.alerts
+++ b/openstack/keystone/alerts/openstack/keystone.alerts
@@ -91,3 +91,22 @@ groups:
description: Api Requests to Keystone is {{ $value }}ops/s
summary: Api Requests to Keystone is {{ $value }}ops/s
+ - alert: OpenstackKeystoneTLSRotationFailed
+ expr: >
+ kube_job_status_failed{job_name=~"keystone-tls-rotation.*"} > 0
+ and on (namespace, job_name)
+ kube_job_created{job_name=~"keystone-tls-rotation.*"}
+ == on (namespace) group_left()
+ max by (namespace) (kube_job_created{job_name=~"keystone-tls-rotation.*"})
+ for: 1h
+ labels:
+ context: security
+ dashboard: keystone
+ service: keystone
+ severity: warning
+ tier: os
+ support_group: identity
+ no_alert_on_absence: "true"
+ annotations:
+ description: The weekly TLS certificate rotation CronJob for Keystone has failed. If not resolved, the TLS certificate will expire and external HTTPS access will break. Check the CronJob logs.
+ summary: Keystone TLS certificate rotation failed
diff --git a/openstack/keystone/templates/_helpers.tpl b/openstack/keystone/templates/_helpers.tpl
index 26b71af9dcc..74a350470ed 100644
--- a/openstack/keystone/templates/_helpers.tpl
+++ b/openstack/keystone/templates/_helpers.tpl
@@ -111,3 +111,36 @@ Empty values are skipped so callers may pass "" for optional dependencies.
{{- printf "http://prodel.%s.svc/check-delete_project/%%(project_id)s" $ns -}}
{{- end }}
{{- end }}
+
+{{- define "keystone.tls.validate" -}}
+{{- if .Values.tls.enabled }}
+ {{- if not .Values.tls.keyGeneration }}
+ {{- fail "tls.keyGeneration is required when tls.enabled (options: go-crypto, hsm-entropy, hsm-full, tpm-entropy)" }}
+ {{- end }}
+ {{- if not .Values.tls.keyWrapping }}
+ {{- fail "tls.keyWrapping is required when tls.enabled (options: none, vault-transit, hsm, tpm)" }}
+ {{- end }}
+ {{- if not .Values.tls.keyStorage }}
+ {{- fail "tls.keyStorage is required when tls.enabled (options: internal-k8s-secret, k8s-secret, vault-secret)" }}
+ {{- end }}
+ {{- if and (eq .Values.tls.keyWrapping "none") (eq .Values.tls.keyStorage "k8s-secret") (not .Values.tls.allowInsecureStorage) }}
+ {{- fail "tls: unwrapped keys cannot be stored as plain-text K8s Secrets. Set tls.keyWrapping or tls.keyStorage, or set tls.allowInsecureStorage: true to acknowledge." }}
+ {{- end }}
+ {{- if not .Values.global.keystone_external_ip }}
+ {{- fail "tls.enabled requires global.keystone_external_ip: enabling TLS removes the ingress and the public TLS Service only renders once the external IP is set." }}
+ {{- end }}
+ {{- /* Guard the CSP script-src hash against drift: the TLS vhost pins a
+ sha256 of the WebSSO callback's inline auto-submit script, and Helm
+ cannot recompute a base64 digest, so fail loudly if the script changes
+ and both pins must be refreshed together. */}}
+ {{- $ssoTpl := include (print $.Template.BasePath "/etc/_sso_callback_template.html.tpl") . }}
+ {{- $ssoParts := splitList "" (index $ssoParts 1)) 0 }}
+ {{- if ne (sha256sum $ssoScript) "a016a19411507a6fa732cd49c20701074dc7cbc9d18797bca8419339cc6602e3" }}
+ {{- fail "SSO callback inline script changed; recompute the CSP script-src hash (sha256/base64 of the