TLS


https://www.keycloak.org/server/enabletls
https://www.keycloak.org/server/hostname
https://www.keycloak.org/server/reverseproxy
https://www.keycloak.org/server/db
https://www.keycloak.org/server/all-config

1. Important Points#

Keycloak 企业部署里 TLS 不只是浏览器访问 HTTPS。至少有 4 条链路要明确:

browser / app -> Keycloak:
    public HTTPS endpoint

reverse proxy / ingress -> Keycloak:
    edge termination or re-encrypt

Keycloak -> PostgreSQL:
    JDBC TLS and CA verification

Keycloak -> LDAP / AD / upstream IdP / SMTP:
    LDAPS / HTTPS / SMTP TLS

production principles:

use trusted CA certificates
keep hostname stable
verify certificates and hostnames
monitor certificate expiry
rotate certs before expiry
do not use insecure TLS flags in production

2. Server Configuration#

TLS terminated at Keycloak#

hostname=sso.example.com
http-enabled=false
https-certificate-file=/etc/keycloak/tls/tls.crt
https-certificate-key-file=/etc/keycloak/tls/tls.key
health-enabled=true
metrics-enabled=true

File layout:

/etc/keycloak/tls/
    tls.crt
    tls.key
    ca.crt

permissions:
    tls.key readable only by keycloak user
    certificate files managed by cert-manager / ACME / secret manager

TLS terminated at reverse proxy#

hostname=sso.example.com
http-enabled=true
proxy-headers=xforwarded

reverse proxy checklist:

edge:
    TLS certificate belongs to sso.example.com
    redirect HTTP to HTTPS
    set X-Forwarded-For
    set X-Forwarded-Proto=https
    set X-Forwarded-Host=sso.example.com

Keycloak:
    trusts forwarded headers only from controlled proxy
    hostname matches external URL
    admin console tested behind proxy

Do not expose the internal HTTP port directly to users when TLS is terminated at the proxy.

3. Client Configuration / Verify#

OIDC discovery:

curl -fsS https://sso.example.com/realms/prod/.well-known/openid-configuration

Certificate verify:

openssl s_client -connect sso.example.com:443 -servername sso.example.com </dev/null

Expected:

Verify return code: 0 (ok)
issuer is expected company/public CA
subject/SAN contains sso.example.com
certificate expires later than rotation window

Database TLS:

db=postgres
db-url=jdbc:postgresql://postgres.example.com:5432/keycloak?sslmode=verify-full&sslrootcert=/etc/keycloak/db-ca/ca.crt
db-username=keycloak
db-password=${KC_DB_PASSWORD}

LDAP TLS:

LDAP URL:
    ldaps://ad.example.com:636

trust:
    AD server certificate chains to trusted CA
    hostname matches certificate SAN
    bind account has least privilege

4. Java#

Spring Security OIDC resource server example:

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://sso.example.com/realms/prod

Custom CA:

keytool -importcert \
  -alias company-root-ca \
  -file company-root-ca.crt \
  -keystore truststore.p12 \
  -storetype PKCS12

java \
  -Djavax.net.ssl.trustStore=truststore.p12 \
  -Djavax.net.ssl.trustStorePassword=changeit \
  -jar order-api.jar

Do not disable hostname verification in production.

5. Python#

Python requests with CA bundle:

import requests

issuer = "https://sso.example.com/realms/prod"
resp = requests.get(
    f"{issuer}/.well-known/openid-configuration",
    timeout=5,
    verify="/etc/ssl/company-ca.pem",
)
resp.raise_for_status()
print(resp.json()["issuer"])

Runtime:

export SSL_CERT_FILE=/etc/ssl/company-ca.pem
export REQUESTS_CA_BUNDLE=/etc/ssl/company-ca.pem

Never use verify=False in production.

6. Go#

Go custom CA:

package main

import (
	"crypto/tls"
	"crypto/x509"
	"net/http"
	"os"
)

func clientWithCA(caPath string) (*http.Client, error) {
	pem, err := os.ReadFile(caPath)
	if err != nil {
		return nil, err
	}
	roots := x509.NewCertPool()
	roots.AppendCertsFromPEM(pem)

	return &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				MinVersion: tls.VersionTLS12,
				RootCAs:    roots,
			},
		},
	}, nil
}

Do not set InsecureSkipVerify: true in production.

7. Node.js#

Node.js with custom CA:

export NODE_EXTRA_CA_CERTS=/etc/ssl/company-ca.pem
node server.js

Fetch OIDC discovery:

const issuer = "https://sso.example.com/realms/prod";

const resp = await fetch(`${issuer}/.well-known/openid-configuration`);
if (!resp.ok) {
  throw new Error(`OIDC discovery failed: ${resp.status}`);
}

const discovery = await resp.json();
console.log(discovery.issuer);

Never use NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized: false in production.

8. Rotation Checklist#

before rotation:
    check all clients trust the new CA/intermediate
    update Keycloak TLS secret or certificate files
    update DB / LDAP CA bundles if needed
    confirm certificate SAN includes hostname

during rotation:
    apply secret/certificate
    restart or reload component if needed
    verify OIDC discovery
    verify browser login
    verify app token validation

after rotation:
    monitor login failures and TLS errors
    remove old cert after overlap window
    update expiry dashboard