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