Compare commits

..

4 Commits

Author SHA1 Message Date
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
Stéphane Tailland bfc748a925 refactor: request the neutrally-named token-exchange broker
Follows the rename in the realm. The client that performs the on-behalf-of
exchange is no longer named after the middleware that calls it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 07:53:00 +02:00
Stéphane Tailland 7892f0f8f5 Remove the Grafana MCP server
Reverts the declaration. The approach does not hold up in practice: an MCP
client resends the whole tool list every turn, so simply having this server
connected cost ~19k prompt tokens per request — and ~5.4k even after
trimming the tool set by 72%. On top of that its metric-name listing
returns 3330 entries, 137 KiB, which the assistant called twice while
exploring. Conversations tripped Kong's 50k-tokens-per-minute AI rate limit
and sat in a 429 retry loop.

The deployment, the Vault-backed secret and the Grafana service account
stay in place; only the LibreChat wiring goes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 18:18:36 +02:00
Stéphane Tailland 2a695da0dd Declare the Grafana MCP server
Prometheus, Loki and Tempo become tools the assistant can call, so the
telemetry can be questioned rather than browsed — which is where the trace
dashboard kept running out of road. Verified: LibreChat initialises the
server in 168ms and loads its tool list.

No oauth block, unlike every other server here. The others sit behind a
Kong gateway enforcing openid-connect; this one is reached directly on the
cluster network and authenticates to Grafana with a service account token
of its own. LibreChat passes no user identity along, so there is no
per-user scoping: every LibreChat user gets the same full read of all
telemetry, including the Kong audit records and the LLM prompts now
captured there.

That is stated in the config rather than left to be discovered, and it is
the reason to move this server behind a gateway like the others.

allowedDomains gains the first http:// and first cluster-internal entry;
without it LibreChat's SSRF protection refuses the address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:29:17 +02:00
8 changed files with 91 additions and 28 deletions
+3 -1
View File
@@ -1,6 +1,8 @@
# Secrets — ne jamais committer de valeurs réelles # Secrets — ne jamais committer de valeurs réelles
secret.yaml secret.yaml
tls.yaml # tls.yaml : plus de fichier TLS local. Le certificat de service est émis par
# cert-manager (ingress/certificate.yaml) et sa clé reste dans le cluster ; le
# certificat du CA, lui, est public et versionné dans ca-configmap.yaml.
# Certificats TLS # Certificats TLS
*.crt *.crt
+15 -11
View File
@@ -44,7 +44,8 @@ k8s/
│ └── service.yaml │ └── service.yaml
└── ingress/ └── ingress/
├── ingress.yaml ├── ingress.yaml
└── tls.yaml TLS secret (gitignored — never commit) ├── certificate.yaml cert-manager Certificate → librechat-tls
└── (no TLS file: the key is generated in the cluster)
``` ```
## Prerequisites ## Prerequisites
@@ -72,19 +73,22 @@ openssl rand -hex 8 # CREDS_IV
mongodb://librechat:<password>@mongodb:27017/LibreChat?authSource=admin mongodb://librechat:<password>@mongodb:27017/LibreChat?authSource=admin
``` ```
### 2. Create the TLS secret ### 2. TLS
```bash Nothing to create. `ingress/certificate.yaml` asks cert-manager for a leaf
kubectl apply -f k8s/namespace.yaml certificate for `librechat.sttlab.local`, issued by the `sttlab-local-ca`
ClusterIssuer into the `librechat-tls` Secret. The private key is generated in
the cluster, rotated automatically, and never exists as a file.
kubectl create secret tls sttlab-tls \ This replaced a hand-made `tls.yaml` holding the lab CA's own certificate *and
--cert=$HOME/tls/sttlab.pc.crt \ private key*, which LibreChat then served as its server certificate — a
--key=$HOME/tls/sttlab.pc.key \ `CA:TRUE` certificate valid for the whole `*.sttlab.local` wildcard where a leaf
--namespace=librechat \ for one name is what a server needs.
--dry-run=client -o yaml > k8s/ingress/tls.yaml
kubectl apply -f k8s/ingress/tls.yaml The CA **certificate** is still needed, for the outbound calls LibreChat makes
``` to Keycloak and the domain gateways. It lives in `ca-configmap.yaml`, is mounted
as `NODE_EXTRA_CA_CERTS`, and is committed — a certificate is public by
construction; the key is what must never be in a file.
### 3. Deploy ### 3. Deploy
+38
View File
@@ -0,0 +1,38 @@
# The lab CA, so LibreChat can verify keycloak.sttlab.local and the domain
# gateways rather than disabling verification.
#
# The certificate, never the key — which is the whole difference from the
# `ingress/tls.yaml` this replaces. A CA certificate is public by construction, so it is
# committed and the deployment has no hidden prerequisite.
#
# Issued by the sttlab-local-ca ClusterIssuer, valid until 2026-11-24.
# Regenerate on rotation:
# kubectl -n cert-manager get secret sttlab-local-ca \
# -o jsonpath='{.data.tls\.crt}' | base64 -d
apiVersion: v1
kind: ConfigMap
metadata:
name: sttlab-ca
namespace: librechat
data:
ca.crt: |
-----BEGIN CERTIFICATE-----
MIIDPDCCAiSgAwIBAgIUd/utDf+f0yQR3v3qTjZqx+wvTZMwDQYJKoZIhvcNAQEL
BQAwGTEXMBUGA1UEAwwOKi5zdHRsYWIubG9jYWwwHhcNMjUxMTI0MTUzODQ1WhcN
MjYxMTI0MTUzODQ1WjAZMRcwFQYDVQQDDA4qLnN0dGxhYi5sb2NhbDCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALj0sryq8qXtkShT/B+/EjVJNN/TLHIn
QJYc6xuUH/f7q3dYdLm4iH0XnFgrSQLUo4iqd/Mczef5iWCYwT5FFa7P17/TcLVf
gso0j41qNLBjGxDQGQ6Z1VCxi6oan8ow98rumjVj0E0Qgs5CX/rN+gcEmZ/TZFWU
qudVoztpeiVqJ0dxCIq+KMutzd1BU7BXx4VRO9Y+t1idUc7oY7BPmS2qPAN7TtXP
j1KhH2BgLsmF5GRhYHNS578eCltcmS2sbQ3c7GwOV96lpgF9TixEvFXeN4xk43E6
1COUD7CRiWAZ0XI8FMlpZGeNZeV4bbV/MqVig1ybWNOz9MDKYj3eoXUCAwEAAaN8
MHowHQYDVR0OBBYEFDbzYWOPr4vxa+VONs7xHbcNx70YMB8GA1UdIwQYMBaAFDbz
YWOPr4vxa+VONs7xHbcNx70YMA8GA1UdEwEB/wQFMAMBAf8wJwYDVR0RBCAwHoIO
Ki5zdHRsYWIubG9jYWyCDHN0dGxhYi5sb2NhbDANBgkqhkiG9w0BAQsFAAOCAQEA
tU4p5/ONXm7x4YDzSeL8NdThVLguiFur0jcTxvvPUS4NFNsnQJk4OXxBgjy1YwpL
3i56/4mCEd9OCQF/Hl0eFwGZBzNKlMoTA08AC/lBDvPrnVjuUu0qe/b/2KOkhESa
x8rQNp0o8pcj0f29nu0JfRqYGkzy0Lh2qgXhPu2iC+t2PfdleRAky1tt68wogdFL
TvHDsJbVo/ihsPpONlSyHfxG6bb/0Mkz64PH46TTt9GvJflEKMqDwivNFUA1P5S/
U4Ic8Qapnxc7h2+WbRjmYrnuygC2wMBE6rL9OF3gwCr1fkgGGRr5lVRUq2X2HZV5
5jqViv6eurq8O61N3vWGCQ==
-----END CERTIFICATE-----
+21
View File
@@ -0,0 +1,21 @@
# cert-manager issues this into the `librechat-tls` Secret.
#
# It replaces a hand-made `tls.yaml` that carried the lab CA's own certificate
# and private key, which LibreChat then served as its server certificate — a
# CA:TRUE certificate valid for the whole `*.sttlab.local` wildcard, where a
# leaf for one name is what a server needs.
#
# The key is generated in the cluster, rotated by cert-manager, and never
# reaches a file anyone has to remember not to commit.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: librechat-tls
namespace: librechat
spec:
secretName: librechat-tls
dnsNames:
- librechat.sttlab.local
issuerRef:
name: sttlab-local-ca
kind: ClusterIssuer
+1 -1
View File
@@ -25,7 +25,7 @@ spec:
tls: tls:
- hosts: - hosts:
- librechat.sttlab.local - librechat.sttlab.local
secretName: sttlab-tls secretName: librechat-tls
rules: rules:
- host: librechat.sttlab.local - host: librechat.sttlab.local
http: http:
+2 -1
View File
@@ -5,6 +5,7 @@ resources:
- namespace.yaml - namespace.yaml
- secret.yaml - secret.yaml
- configmap.yaml - configmap.yaml
- ca-configmap.yaml
- mongodb/pvc.yaml - mongodb/pvc.yaml
- mongodb/statefulset.yaml - mongodb/statefulset.yaml
- mongodb/service.yaml - mongodb/service.yaml
@@ -14,5 +15,5 @@ resources:
- librechat/configmap-app.yaml - librechat/configmap-app.yaml
- librechat/deployment.yaml - librechat/deployment.yaml
- librechat/service.yaml - librechat/service.yaml
- ingress/tls.yaml - ingress/certificate.yaml
- ingress/ingress.yaml - ingress/ingress.yaml
+5 -5
View File
@@ -24,7 +24,7 @@ data:
client_id: librechat client_id: librechat
client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}' client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}'
redirect_uri: https://librechat.sttlab.local/api/mcp/catalog-mcp/oauth/callback redirect_uri: https://librechat.sttlab.local/api/mcp/catalog-mcp/oauth/callback
scope: 'openid mcp-catalog-audience kong-token-exchange-broker-audience' scope: 'openid mcp-catalog-audience token-exchange-broker-audience'
grant_types_supported: ['authorization_code', 'refresh_token'] grant_types_supported: ['authorization_code', 'refresh_token']
token_endpoint_auth_methods_supported: ['client_secret_post'] token_endpoint_auth_methods_supported: ['client_secret_post']
customers-mcp: customers-mcp:
@@ -36,7 +36,7 @@ data:
client_id: librechat client_id: librechat
client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}' client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}'
redirect_uri: https://librechat.sttlab.local/api/mcp/customers-mcp/oauth/callback redirect_uri: https://librechat.sttlab.local/api/mcp/customers-mcp/oauth/callback
scope: 'openid mcp-customers-audience kong-token-exchange-broker-audience' scope: 'openid mcp-customers-audience token-exchange-broker-audience'
grant_types_supported: ['authorization_code', 'refresh_token'] grant_types_supported: ['authorization_code', 'refresh_token']
token_endpoint_auth_methods_supported: ['client_secret_post'] token_endpoint_auth_methods_supported: ['client_secret_post']
payments-mcp: payments-mcp:
@@ -48,7 +48,7 @@ data:
client_id: librechat client_id: librechat
client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}' client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}'
redirect_uri: https://librechat.sttlab.local/api/mcp/payments-mcp/oauth/callback redirect_uri: https://librechat.sttlab.local/api/mcp/payments-mcp/oauth/callback
scope: 'openid mcp-payments-audience kong-token-exchange-broker-audience' scope: 'openid mcp-payments-audience token-exchange-broker-audience'
grant_types_supported: ['authorization_code', 'refresh_token'] grant_types_supported: ['authorization_code', 'refresh_token']
token_endpoint_auth_methods_supported: ['client_secret_post'] token_endpoint_auth_methods_supported: ['client_secret_post']
sales-mcp: sales-mcp:
@@ -60,7 +60,7 @@ data:
client_id: librechat client_id: librechat
client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}' client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}'
redirect_uri: https://librechat.sttlab.local/api/mcp/sales-mcp/oauth/callback redirect_uri: https://librechat.sttlab.local/api/mcp/sales-mcp/oauth/callback
scope: 'openid mcp-sales-audience kong-token-exchange-broker-audience' scope: 'openid mcp-sales-audience token-exchange-broker-audience'
grant_types_supported: ['authorization_code', 'refresh_token'] grant_types_supported: ['authorization_code', 'refresh_token']
token_endpoint_auth_methods_supported: ['client_secret_post'] token_endpoint_auth_methods_supported: ['client_secret_post']
workforce-mcp: workforce-mcp:
@@ -72,7 +72,7 @@ data:
client_id: librechat client_id: librechat
client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}' client_secret: '${LIBRECHAT_OIDC_CLIENT_SECRET}'
redirect_uri: https://librechat.sttlab.local/api/mcp/workforce-mcp/oauth/callback redirect_uri: https://librechat.sttlab.local/api/mcp/workforce-mcp/oauth/callback
scope: 'openid mcp-workforce-audience kong-token-exchange-broker-audience' scope: 'openid mcp-workforce-audience token-exchange-broker-audience'
grant_types_supported: ['authorization_code', 'refresh_token'] grant_types_supported: ['authorization_code', 'refresh_token']
token_endpoint_auth_methods_supported: ['client_secret_post'] token_endpoint_auth_methods_supported: ['client_secret_post']
endpoints: endpoints:
+6 -9
View File
@@ -136,7 +136,7 @@ spec:
# NODE_TLS_REJECT_UNAUTHORIZED=0 which would disable TLS # NODE_TLS_REJECT_UNAUTHORIZED=0 which would disable TLS
# verification globally. # verification globally.
- name: NODE_EXTRA_CA_CERTS - name: NODE_EXTRA_CA_CERTS
value: /etc/sttlab-tls/tls.crt value: /etc/sttlab-ca/ca.crt
# 512Mi was too tight for LibreChat under real use (MCP clients, # 512Mi was too tight for LibreChat under real use (MCP clients,
# OTel instrumentation) and caused repeated V8 heap-limit crashes. # OTel instrumentation) and caused repeated V8 heap-limit crashes.
# Cap old-space explicitly, leaving headroom under the new # Cap old-space explicitly, leaving headroom under the new
@@ -166,16 +166,13 @@ spec:
- name: app-config - name: app-config
mountPath: /app/librechat.yaml mountPath: /app/librechat.yaml
subPath: librechat.yaml subPath: librechat.yaml
- name: sttlab-tls-cert - name: sttlab-ca
mountPath: /etc/sttlab-tls mountPath: /etc/sttlab-ca
readOnly: true readOnly: true
volumes: volumes:
- name: app-config - name: app-config
configMap: configMap:
name: librechat-app-config name: librechat-app-config
- name: sttlab-tls-cert - name: sttlab-ca
secret: configMap:
secretName: sttlab-tls name: sttlab-ca
items:
- key: tls.crt
path: tls.crt