Skip to content
Merged
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
50 changes: 24 additions & 26 deletions docs/REDIS_TLS_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,26 @@ This document explains the Redis TLS configuration for the status-list-server de

## Quick Start

**For Redis TLS with HAProxy:**
**For Redis TLS with HAProxy (automated sync):**

1. **Create HAProxy TLS secret:**
1. **Deploy / upgrade the chart (CronJob + RBAC included):**

```bash
# Extract certificate and key from existing secret for HAProxy
CRT=$(kubectl get secret statuslist-tls -n statuslist -o jsonpath='{.data.tls\.crt}' | base64 -d)
KEY=$(kubectl get secret statuslist-tls -n statuslist -o jsonpath='{.data.tls\.key}' | base64 -d)
# Combine cert and key into single PEM file for HAProxy
printf "%s\n%s\n" "$CRT" "$KEY" > redis.pem
# Create new secret for HAProxy with combined PEM
kubectl create secret generic statuslist-haproxy-tls -n statuslist --from-file=redis.pem=redis.pem
helm upgrade statuslist ./helm/status-list-server-chart --namespace statuslist
```

2. **Deploy:**
This will:

```bash
helm upgrade statuslist ./helm/status-list-server-chart --namespace statuslist
```
- Ensure the wildcard certificate `statuslist-tls` is managed by cert-manager
- Install a `CronJob` that automatically syncs `statuslist-tls` into `statuslist-haproxy-tls`
- Only update `statuslist-haproxy-tls` when the certificate actually changes
Comment thread
Hermann-Core marked this conversation as resolved.

3. **Verify:**
2. **Verify:**

```bash
kubectl get pods -n statuslist
kubectl logs statuslist-status-list-server-deployment-<pod-id> -n statuslist
kubectl logs cronjob/redis-cert-sync -n statuslist
```

## Why This Setup?
Expand Down Expand Up @@ -85,23 +80,19 @@ redis-ha:

### 2. HAProxy TLS Termination

**Challenge**: HAProxy needed TLS termination with proper certificate handling.
**Challenge**: HAProxy needs TLS termination with proper certificate handling.

**Problem**: HAProxy expects a single PEM file (cert + key), but Kubernetes TLS secrets store them separately.
**Problem**: HAProxy expects a single PEM file (certificate + key), but Kubernetes TLS secrets store them as separate fields.

**Solution**: Created a combined PEM secret for HAProxy:
**Solution**: Automate the combined PEM secret creation for HAProxy using a Kubernetes CronJob.

```bash
# Extract certificate and key from existing secret
CRT=$(kubectl get secret statuslist-tls -n statuslist -o jsonpath='{.data.tls\.crt}' | base64 -d)
KEY=$(kubectl get secret statuslist-tls -n statuslist -o jsonpath='{.data.tls\.key}' | base64 -d)
The CronJob is defined in the Helm chart templates and is responsible for:

# Create combined PEM file
printf "%s\n%s\n" "$CRT" "$KEY" > redis.pem
- Reading the `statuslist-tls` secret (tls.crt + tls.key)
- Concatenating them into a single `redis.pem` file
- Creating or updating the `statuslist-haproxy-tls` secret used by HAProxy

# Create new secret for HAProxy
kubectl create secret generic statuslist-haproxy-tls -n statuslist --from-file=redis.pem=redis.pem
```
Please refer to the Helm template for the authoritative implementation: [redis-ha-cert-sync.yaml](../helm/chart/templates/redis-ha-cert-sync.yaml)

**HAProxy Configuration**:

Expand Down Expand Up @@ -193,6 +184,13 @@ env:
- HAProxy uses this certificate for TLS termination
- App validates certificate against hostname `redis.eudi-adorsys.com`

We intentionally **derive the Redis/HAProxy certificate from the same wildcard certificate used by the status-list-server ingress** (`statuslist-tls`) so that:

- We only manage **one ACME certificate** for the entire `*.eudi-adorsys.com` namespace.
- cert-manager handles issuance and renewal in a single place.
- Both HTTP (`statuslist.eudi-adorsys.com`) and Redis (`redis.eudi-adorsys.com`) endpoints present certificates that are consistent and valid for their hostnames.
- We avoid self-signed or cluster-internal certificates on the Redis endpoint, which would fail TLS validation in the Rust client unless we shipped and configured custom root CAs.

**Certificate Flow**:

1. App connects to `redis.eudi-adorsys.com:6379`
Expand Down
2 changes: 1 addition & 1 deletion helm/chart/Chart.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ dependencies:
repository: https://dandydeveloper.github.io/charts
version: 4.35.0
digest: sha256:f1f581f7aee3669dcc25c5b46c7b4d1204621a53635973d48eb59b5266d00549
generated: "2025-10-14T10:50:44.992940427+01:00"
generated: "2026-02-17T13:47:10.337741571+01:00"
139 changes: 139 additions & 0 deletions helm/chart/templates/redis-ha-cert-sync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
{{- /*
Sync the HAProxy TLS secret (statuslist-haproxy-tls) with the main wildcard
certificate secret (statuslist-tls) used by the ingress.

This ensures that when cert-manager renews the wildcard certificate, the Redis
HAProxy endpoint (redis.eudi-adorsys.com) is also updated automatically.

We:
- Read tls.crt and tls.key from statuslist-tls
- Concatenate them into a single redis.pem (cert + key)
- Compare against the existing redis.pem in statuslist-haproxy-tls (if any)
- Only apply a new secret if redis.pem has actually changed
Comment thread
Hermann-Core marked this conversation as resolved.

Comment thread
IngridPuppet marked this conversation as resolved.
The CronJob is scheduled infrequently, because certificates change rarely and
DNS / LB propagation is not instantaneous.
*/ -}}

{{- $redisHA := index .Values "redis-ha" | default dict -}}
{{- $haproxy := index $redisHA "haproxy" | default dict -}}
{{- $haproxyTls := index $haproxy "tls" | default dict -}}
{{- if and (eq (default true (index $haproxy "enabled")) true) (eq (default false (index $haproxyTls "enabled")) true) }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: redis-cert-sync-sa
namespace: {{ .Release.Namespace }}
labels:
{{- include "status-list-server-chart.labels" . | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: redis-cert-sync-role
namespace: {{ .Release.Namespace }}
labels:
{{- include "status-list-server-chart.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames:
- statuslist-tls
- statuslist-haproxy-tls
verbs: ["get", "create", "update", "patch"]
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames:
- {{ .Release.Name }}-redis-ha-haproxy
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: redis-cert-sync-rb
namespace: {{ .Release.Namespace }}
labels:
{{- include "status-list-server-chart.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: redis-cert-sync-role
subjects:
- kind: ServiceAccount
name: redis-cert-sync-sa
namespace: {{ .Release.Namespace }}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: redis-cert-sync
namespace: {{ .Release.Namespace }}
labels:
{{- include "status-list-server-chart.labels" . | nindent 4 }}
spec:
# Run once per week at Sunday 00:00 UTC.
# Certificates are long-lived and cert-manager renews well before expiry,
# so a weekly sync is sufficient while keeping cluster noise low.
schedule: "0 0 * * 0"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 2
jobTemplate:
spec:
template:
metadata:
labels:
{{- include "status-list-server-chart.labels" . | nindent 12 }}
spec:
serviceAccountName: redis-cert-sync-sa
restartPolicy: OnFailure
containers:
- name: redis-cert-sync
image: bitnami/kubectl:latest
imagePullPolicy: IfNotPresent
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
command:
- /bin/bash
- -c
- |
set -euo pipefail

NS="${NAMESPACE:-{{ .Release.Namespace }}}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

echo "Fetching base certificate from secret statuslist-tls in namespace ${NS}..."
kubectl get secret statuslist-tls -n "${NS}" -o jsonpath='{.data.tls\.crt}' | base64 -d > "${TMP_DIR}/tls.crt"
kubectl get secret statuslist-tls -n "${NS}" -o jsonpath='{.data.tls\.key}' | base64 -d > "${TMP_DIR}/tls.key"

cat "${TMP_DIR}/tls.crt" "${TMP_DIR}/tls.key" > "${TMP_DIR}/redis.pem"

# If the HAProxy secret exists, compare the current redis.pem with the new one.
if kubectl get secret statuslist-haproxy-tls -n "${NS}" >/dev/null 2>&1; then
echo "Existing statuslist-haproxy-tls secret found, comparing redis.pem..."
kubectl get secret statuslist-haproxy-tls -n "${NS}" -o jsonpath='{.data.redis\.pem}' | base64 -d > "${TMP_DIR}/existing-redis.pem" || true

if [ -s "${TMP_DIR}/existing-redis.pem" ] && cmp -s "${TMP_DIR}/redis.pem" "${TMP_DIR}/existing-redis.pem"; then
echo "Redis HAProxy certificate is already up to date. No changes applied."
exit 0
fi
else
echo "statuslist-haproxy-tls secret does not exist yet. It will be created."
fi

echo "Applying updated statuslist-haproxy-tls secret..."
kubectl create secret generic statuslist-haproxy-tls \
-n "${NS}" \
--from-file=redis.pem="${TMP_DIR}/redis.pem" \
--dry-run=client -o yaml | kubectl apply -f -

echo "Redis HAProxy TLS secret synced successfully. Restarting HAProxy deployment to pick up new certificate..."
kubectl rollout restart deploy/{{ .Release.Name }}-redis-ha-haproxy -n "${NS}"

echo "Redis HAProxy deployment restart triggered."
{{- end }}