Files
infra-librechat/k8s/librechat/deployment.yaml
T
Stéphane Tailland 26aa89643c fix: serve a leaf certificate, not the CA
LibreChat served the lab CA's own certificate as its server certificate — a
CA:TRUE certificate valid for the whole *.sttlab.local wildcard, from a
hand-made tls.yaml that also carried the CA's private key. Gitignored, so never
leaked, but the only service in the estate still copying the CA around instead
of being issued a leaf.

Now a cert-manager Certificate for librechat.sttlab.local: CA:FALSE, one
dnsNames, key generated in the cluster and rotated there.

The two uses of that Secret had to be separated first, which is the part worth
knowing: sttlab-tls was both the ingress certificate and the CA bundle mounted
as NODE_EXTRA_CA_CERTS for outbound calls. Replacing it with a leaf alone would
have left LibreChat unable to verify Keycloak. The CA certificate now comes from
ca-configmap.yaml — the certificate, never the key, committed like the agents'.

Verified: the ingress serves CA:FALSE with SAN librechat.sttlab.local, the pod's
outbound call to Keycloak returns 200 with TLS verified, and the old secret is
deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 18:01:00 +02:00

179 lines
6.2 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: librechat
namespace: librechat
spec:
replicas: 1
selector:
matchLabels:
app: librechat
template:
metadata:
labels:
app: librechat
annotations:
# Prometheus' dedicated `librechat` job (bearer token) targets this
# pod; the annotation is a selection marker, not a scrape config.
prometheus.io/scrape-with-auth: "true"
prometheus.io/port: "3080"
spec:
# *.sttlab.local are lab-only hostnames (self-signed cert), not real
# DNS — this pod needs an explicit mapping to each Gateway's stable
# in-cluster ClusterIP to resolve them at all.
# catalog-dev: kong-v2 catalog-api Gateway (dataplane-ingress-catalog-dev-v4bkm-xqq6v), REST+MCP share this hostname now.
# llm-dev: kong-v2's platform-llm Gateway (dataplane-ingress-llm-dev-rlrxf-7fkb7).
hostAliases:
- ip: "192.168.194.225"
hostnames:
- "catalog-dev.sttlab.local"
- ip: "192.168.194.153"
hostnames:
- "llm-dev.sttlab.local"
initContainers:
- name: wait-for-mongodb
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z mongodb 27017; do
echo "Waiting for MongoDB..."; sleep 3;
done
- name: wait-for-meilisearch
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z meilisearch 7700; do
echo "Waiting for MeiliSearch..."; sleep 3;
done
containers:
- name: librechat
image: ghcr.io/danny-avila/librechat:latest
ports:
- containerPort: 3080
envFrom:
- configMapRef:
name: librechat-config
env:
- name: MONGO_URI
valueFrom:
secretKeyRef:
name: librechat-secrets
key: MONGO_URI
- name: MEILI_MASTER_KEY
valueFrom:
secretKeyRef:
name: librechat-secrets
key: MEILI_MASTER_KEY
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: librechat-secrets
key: JWT_SECRET
- name: JWT_REFRESH_SECRET
valueFrom:
secretKeyRef:
name: librechat-secrets
key: JWT_REFRESH_SECRET
- name: CREDS_KEY
valueFrom:
secretKeyRef:
name: librechat-secrets
key: CREDS_KEY
- name: CREDS_IV
valueFrom:
secretKeyRef:
name: librechat-secrets
key: CREDS_IV
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: librechat-secrets
key: OPENAI_API_KEY
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: librechat-secrets
key: ANTHROPIC_API_KEY
- name: PLATFORM_LLM_LIBRECHAT_APIKEY
valueFrom:
secretKeyRef:
name: librechat-secrets
key: PLATFORM_LLM_LIBRECHAT_APIKEY
- name: LIBRECHAT_OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: librechat-secrets
key: LIBRECHAT_OIDC_CLIENT_SECRET
# Same Keycloak client secret as LIBRECHAT_OIDC_CLIENT_SECRET
# above (one Keycloak client, "librechat", used both for the
# app's own SSO login and for each MCP server's OAuth flow) —
# LibreChat's OIDC-login code path reads this specific env var
# name, so it's duplicated rather than referenced once.
- name: OPENID_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: librechat-secrets
key: LIBRECHAT_OIDC_CLIENT_SECRET
- name: OPENID_SESSION_SECRET
valueFrom:
secretKeyRef:
name: librechat-secrets
key: OPENID_SESSION_SECRET
# Enables /metrics (401 without it). Scraped by the Prometheus in
# the observability namespace using a Bearer token.
- name: METRICS_SECRET
valueFrom:
secretKeyRef:
name: librechat-secrets
key: METRICS_SECRET
# jira-mcp.sttlab.local / konggw.sttlab.local serve a self-signed
# cert (see hostAliases comment above) — Node rejects it by
# default; NODE_EXTRA_CA_CERTS trusts just this one CA, unlike
# NODE_TLS_REJECT_UNAUTHORIZED=0 which would disable TLS
# verification globally.
- name: NODE_EXTRA_CA_CERTS
value: /etc/sttlab-ca/ca.crt
# 512Mi was too tight for LibreChat under real use (MCP clients,
# OTel instrumentation) and caused repeated V8 heap-limit crashes.
# Cap old-space explicitly, leaving headroom under the new
# container limit for non-heap memory (buffers, native modules).
- name: NODE_OPTIONS
value: "--max-old-space-size=1200"
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: 500m
memory: 1536Mi
livenessProbe:
httpGet:
path: /health
port: 3080
initialDelaySeconds: 30
periodSeconds: 20
readinessProbe:
httpGet:
path: /health
port: 3080
initialDelaySeconds: 20
periodSeconds: 10
volumeMounts:
- name: app-config
mountPath: /app/librechat.yaml
subPath: librechat.yaml
- name: sttlab-ca
mountPath: /etc/sttlab-ca
readOnly: true
volumes:
- name: app-config
configMap:
name: librechat-app-config
- name: sttlab-ca
configMap:
name: sttlab-ca