Configure Keycloak HTTPS/TLS with SSL Certificates

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.io/keycloak/keycloak:26.7.3
openssl-3.5.5-6.el10_2.x86_64
Applies to Any Linux host running the official Quay image or ZIP distribution — HTTPS options are the same whether you use kc.sh or container KC_* variables
Privilege Normal user for rootless Podman lab commands
Scope Direct HTTPS on Keycloak (8443) with PEM or PKCS12/JKS, hostname URL alignment, certificate reload, TLS protocol and port options, and HTTPS verification.
Related guides Configure Keycloak behind a reverse proxy
Keycloak keycloak.conf guide
Keycloak hostname
Short on time?Beta Get the gist of this 9-minute read in seconds, then ask follow-up questions

In this article we will configure Keycloak HTTPS/TLS with PEM or PKCS12 certificates on Keycloak 26.7.3.

OIDC clients and browsers care about the URL scheme, hostname, and port Keycloak advertises. The steps below show the universal kc.sh options first, then a tested Podman lab that sets the same values with KC_* environment variables. If TLS terminates at a reverse proxy instead, Keycloak often runs with KC_HTTP_ENABLED=true and KC_PROXY_HEADERS; use the reverse-proxy sibling in Related guides for that layout.


What this TLS lab teaches

This guide focuses on HTTPS configured on Keycloak itself.

At the end you will understand:

  • How to enable HTTPS with PEM files or a PKCS12/JKS keystore
  • How to use a full KC_HOSTNAME URL when Keycloak must advertise a specific public scheme or port
  • How certificate reload works without restarting Keycloak
  • How to change the HTTPS port and restrict TLS protocols
Direct TLS on Keycloak

  ┌────────┐  TLS   ┌───────────────┐
  │ Client │ ─────► │ Keycloak :8443│  KC_HTTPS_* + KC_HOSTNAME (full URL)
  └────────┘        └───────────────┘
Purpose CLI Environment
Certificate (PEM) --https-certificate-file KC_HTTPS_CERTIFICATE_FILE
Private key (PEM) --https-certificate-key-file KC_HTTPS_CERTIFICATE_KEY_FILE
Keystore --https-key-store-file KC_HTTPS_KEY_STORE_FILE
Keystore password --https-key-store-password KC_HTTPS_KEY_STORE_PASSWORD
HTTPS port --https-port KC_HTTPS_PORT
TLS protocols --https-protocols KC_HTTPS_PROTOCOLS
Certificate reload period --https-certificates-reload-period KC_HTTPS_CERTIFICATES_RELOAD_PERIOD

Generate a lab PEM certificate

Create a self-signed certificate whose Subject Alternative Name (SAN) matches the hostname clients will use. These steps extend the base layout from Install Keycloak with Podman.

bash
mkdir -p /tmp/kc-tls-lab/conf
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout /tmp/kc-tls-lab/conf/server.key.pem \
  -out /tmp/kc-tls-lab/conf/server.crt.pem \
  -days 365 \
  -subj '/CN=auth.lab.local' \
  -addext 'subjectAltName=DNS:auth.lab.local,DNS:localhost'

The Keycloak process must read both files. If the private key is mode 600 owned by another user, startup fails with AccessDeniedException. For this temporary lab the files are made readable by the container user. Do not make a production private key world-readable; instead set ownership, group, or ACLs so only the Keycloak process can read it:

bash
chmod 644 /tmp/kc-tls-lab/conf/server.crt.pem /tmp/kc-tls-lab/conf/server.key.pem

Configure Keycloak HTTPS with a PEM certificate

On a ZIP or package install, start Keycloak with the certificate paths and a full public hostname URL:

bash
bin/kc.sh start \
  --hostname=https://auth.example.com \
  --https-certificate-file=/path/to/server.crt.pem \
  --https-certificate-key-file=/path/to/server.key.pem

The equivalent container configuration uses KC_HTTPS_CERTIFICATE_FILE and KC_HTTPS_CERTIFICATE_KEY_FILE.

Podman (or Docker) port publishing maps container 8443 to host 18443, but that mapping does not change the public URL Keycloak advertises. When clients reach Keycloak on a non-default port, fix the scheme and port in KC_HOSTNAME:

bash
podman run -d --name kc-tls-direct --replace \
  -v /tmp/kc-tls-lab/conf:/opt/keycloak/conf:Z \
  -p 127.0.0.1:18443:8443 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  -e KC_HOSTNAME=https://auth.lab.local:18443 \
  -e KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/server.crt.pem \
  -e KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/server.key.pem \
  -e KC_HTTP_ENABLED=false \
  quay.io/keycloak/keycloak:26.7.3 start

start disables HTTP by default, so you must configure HTTPS on Keycloak for this lab. The same PEM paths work in keycloak.conf as https-certificate-file and https-certificate-key-file — see the keycloak.conf sibling under Related guides for file precedence.

First boot can take 30–90 seconds. When logs show https://0.0.0.0:8443, probe the published port (use curl --noproxy '*' if your shell sets https_proxy):

bash
curl --noproxy '*' -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1:18443/

An HTTP 200 or redirect (302) confirms TLS is listening.


Configure Keycloak HTTPS with a PKCS12 or JKS keystore

If both PEM certificate options and a keystore are configured, Keycloak uses the PEM certificate/key configuration.

Some teams ship a PKCS12 (.p12) or JKS keystore instead of separate PEM files:

bash
bin/kc.sh start \
  --hostname=https://auth.example.com \
  --https-key-store-file=/path/to/keystore.p12 \
  --https-key-store-password=changeit \
  --https-key-store-type=PKCS12
Variable Example
KC_HTTPS_KEY_STORE_FILE /opt/keycloak/conf/keystore.p12
KC_HTTPS_KEY_STORE_PASSWORD changeit
KC_HTTPS_KEY_STORE_TYPE PKCS12 (or JKS)

Build a PKCS12 file from the lab PEM material:

bash
openssl pkcs12 -export \
  -in /tmp/kc-tls-lab/conf/server.crt.pem \
  -inkey /tmp/kc-tls-lab/conf/server.key.pem \
  -out /tmp/kc-tls-lab/conf/keystore.p12 \
  -name keycloak \
  -passout pass:changeit

A PKCS12 file contains private key material. For this temporary lab make it readable by the container user the same way as the PEM key. Do not leave a production keystore world-readable:

bash
chmod 644 /tmp/kc-tls-lab/conf/keystore.p12

Start with the keystore instead of PEM paths. Use a different host port so this container does not collide with the PEM example:

bash
podman run -d --name kc-tls-keystore --replace \
  -v /tmp/kc-tls-lab/conf:/opt/keycloak/conf:Z \
  -p 127.0.0.1:18444:8443 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  -e KC_HOSTNAME=https://auth.lab.local:18444 \
  -e KC_HTTPS_KEY_STORE_FILE=/opt/keycloak/conf/keystore.p12 \
  -e KC_HTTPS_KEY_STORE_PASSWORD=changeit \
  -e KC_HTTPS_KEY_STORE_TYPE=PKCS12 \
  quay.io/keycloak/keycloak:26.7.3 start

After startup, curl --noproxy '*' -sk against https://127.0.0.1:18444/ should return HTTP 200. Use PKCS12 when your CA or platform already distributes a single keystore bundle; PEM files are simpler for Kubernetes Secrets split into tls.crt and tls.key.


Hostname and public URLs

Certificate identity and Keycloak's advertised frontend URL must describe a URL clients can actually reach.

Piece Must align
Certificate SAN Host clients type in the browser (auth.lab.local, auth.example.com)
KC_HOSTNAME Full public URL when scheme or port is non-default (https://auth.lab.local:18443)
OIDC issuer https://<hostname>[:port]/realms/<realm>

Prefer a full URL when the externally visible scheme or port must stay fixed:

text
KC_HOSTNAME=https://auth.example.com
KC_HOSTNAME=https://auth.lab.local:18443

With KC_HOSTNAME=https://auth.lab.local:18443 and port 18443 published, fetch discovery using --resolve so the host header matches the certificate:

bash
curl --noproxy '*' -sk --resolve auth.lab.local:18443:127.0.0.1 \
  https://auth.lab.local:18443/realms/master/.well-known/openid-configuration

The issuer field should match that public URL:

output
"issuer":"https://auth.lab.local:18443/realms/master"

A certificate for auth.example.com combined with KC_HOSTNAME=https://auth.lab.local:18443 produces browser warnings and wrong redirect URLs. Hostname v2 rules, admin URL split, and strict mode are covered in the hostname sibling under Related guides.

Production start with HTTP disabled and no certificate paths fails fast — run it once without TLS to see the message:

bash
podman run --rm -e KC_HTTP_ENABLED=false quay.io/keycloak/keycloak:26.7.3 start
output
Key material not provided to setup HTTPS. Please configure your keys/certificates, or if HTTPS access is not needed see the `http-enabled` option. If you meant to start the server in development mode, see the `start-dev` command.

That failure is why production start needs hostname and TLS planning up front, as covered in Keycloak start vs start-dev.


Change the HTTPS port and TLS protocols

Keycloak listens for HTTPS on port 8443 by default. Change the listener with:

bash
bin/kc.sh start --https-port=9443 ...

Or in a container:

bash
-e KC_HTTPS_PORT=9443

Remember that KC_HOSTNAME advertises the public URL. If clients still reach Keycloak through a published host port or a reverse proxy on 443, the hostname URL must reflect that public address — not only the container listen port.

By default Keycloak enables modern TLS protocols and does not turn on deprecated ones. To restrict the list explicitly:

bash
-e KC_HTTPS_PROTOCOLS=TLSv1.3

Or allow both current defaults:

bash
-e KC_HTTPS_PROTOCOLS=TLSv1.3,TLSv1.2

Reload TLS certificates without restarting Keycloak

Keycloak automatically reloads certificate, key, and keystore material configured through the https-* options. The default reload period is one hour.

Override it with https-certificates-reload-period or the environment form:

bash
-e KC_HTTPS_CERTIFICATES_RELOAD_PERIOD=10m

Use a supported duration greater than 30 seconds, such as 10m or 1h. Set -1 to disable automatic reload. After you replace the PEM files or keystore on the mounted volume, wait for the next reload window (or restart) before expecting clients to see the new certificate.


HTTPS and management ports

Port Purpose Reverse-proxy this port?
8443 (or 8080 when HTTP enabled) Admin Console, Account Console, OIDC/SAML endpoints, Admin REST API Yes — application front door
9000 Management — health, metrics, optional management TLS No — keep internal

HTTPS application traffic uses 8443 by default. Port 9000 is the separate management interface used when features such as health or metrics are enabled. When management TLS is not configured separately, its settings can inherit from the main HTTP server. Enabling health checks and probing /health/ready belongs in the dedicated health guide when published — this page stays on certificate configuration.


Verify TLS correctly

Walk through these checks after any HTTPS change.

Certificate and SAN

Pipe openssl s_client into x509 to print the subject and SAN extension for the name clients will use:

bash
echo | openssl s_client -connect 127.0.0.1:18443 -servername auth.lab.local 2>/dev/null \
  | openssl x509 -noout -subject -ext subjectAltName

Confirm DNS:auth.lab.local (or your real FQDN) appears in the SAN list.

HTTPS response on the application port

Send a HEAD request to the published HTTPS port and confirm TLS completes:

bash
curl --noproxy '*' -sk -I https://127.0.0.1:18443/

Expect HTTP/1.1 200 or 302 — not connection refused or TLS handshake errors.

OIDC discovery issuer

Fetch discovery JSON with --resolve so the hostname matches the certificate, then grep the issuer:

bash
curl --noproxy '*' -sk --resolve auth.lab.local:18443:127.0.0.1 \
  https://auth.lab.local:18443/realms/master/.well-known/openid-configuration \
  | grep -o '"issuer":"[^"]*"'

The issuer must match what client applications configure — scheme, host, and port included.


Common problems

Symptom Likely cause Fix
AccessDeniedException on server.key.pem or keystore.p12 Key file not readable by Keycloak Make the file readable by the Keycloak user (lab: chmod 644; production: ownership/group/ACLs, not world-readable keys)
Production start exits immediately No TLS material and HTTP disabled Add KC_HTTPS_* certificate or keystore options
Browser certificate warning Self-signed lab cert or SAN mismatch Use a CA-signed cert; match SAN to the host in KC_HOSTNAME
Issuer missing published host port KC_HOSTNAME set to hostname only while clients use a mapped port Set KC_HOSTNAME to the full URL (https://auth.lab.local:18443)
Admin Console redirect loop Hostname does not match public URL Align KC_HOSTNAME with the URL clients reach
OIDC issuer shows internal host:port KC_HOSTNAME wrong or incomplete Fix the full hostname URL; re-check discovery JSON

Proxy-related symptoms such as 403 Forbidden, redirects to http://, or wrong issuer behind Nginx belong in the reverse-proxy guide linked under Related guides.


Summary

Direct Keycloak HTTPS puts certificates on Keycloak itself (--https-certificate-file / KC_HTTPS_CERTIFICATE_FILE, or a keystore) and exposes the application on port 8443. Set KC_HOSTNAME to the full public URL when the scheme or port clients use differs from Keycloak's internal listener. Certificate identity, KC_HOSTNAME, and the OIDC issuer URL must agree; openssl s_client and the discovery document are quick proofs after every change.

Certificate reload defaults to one hour and can be shortened with KC_HTTPS_CERTIFICATES_RELOAD_PERIOD. Management traffic on port 9000 stays separate from user-facing HTTPS. For edge termination, re-encrypt, passthrough, and Nginx or HAProxy configs, continue with the reverse-proxy sibling.


References


Frequently Asked Questions

What port does Keycloak HTTPS use?

The default HTTPS application port is 8443. Change it with --https-port or KC_HTTPS_PORT. Management health and metrics use port 9000 on a separate interface when those features are enabled.

Can I use PEM and a keystore at the same time?

If both are configured, Keycloak uses the PEM certificate and key. Configure only one style unless you intentionally want PEM to win.

Why does OIDC discovery show the wrong port?

The issuer URL follows KC_HOSTNAME. When clients reach Keycloak on a published host port that differs from container 8443, set KC_HOSTNAME to the full URL including that port.

Should I put Keycloak behind a reverse proxy instead?

Often yes in production. Direct HTTPS is still useful for labs, internal services, and re-encrypt or passthrough backends. When the proxy terminates TLS for clients, configure KC_HTTP_ENABLED and KC_PROXY_HEADERS on Keycloak and put Nginx or HAProxy in front.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)