DEV Community

Yaroslav
Yaroslav

Posted on

Setting up Keycloak as a central identity provider

This is the first article in a series about Keycloak. We will set up Keycloak as an authentication provider and later connect it to internal services. In the end we get a single entry point and a single user provider for all of them.

In this article we set up Keycloak itself, get the admin console running, and lock it down properly.

Installation

We run Keycloak with Docker, behind an existing nginx reverse proxy. This guide assumes such a proxy (e.g. jwilder/nginx-proxy with an ACME companion for SSL) is already running on the host and shared across services, listening on the webproxy docker network.

Make sure Docker and the Docker Compose plugin are installed on the server first:

docker --version
docker compose version
Enter fullscreen mode Exit fullscreen mode

If these commands fail, install Docker using the official instructions for your OS. Once Docker is ready, create a folder on the server for the project, e.g. /opt/docker.keycloak. We will place the files there step by step: docker-compose.yml, .env, nginx/vhost.conf, and deploy.sh.

Configuration

docker-compose.yml:

version: '3.8'

services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.2
    container_name: keycloak
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: ${KC_DB_USERNAME:-keycloak}
      KC_DB_PASSWORD: ${KC_DB_PASSWORD}
      KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN:-admin}
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      KC_HOSTNAME: ${DOMAIN}
      KC_HTTP_ENABLED: "true"
      KC_PROXY_HEADERS: xforwarded
      KC_HOSTNAME_STRICT: "false"
      KC_HEALTH_ENABLED: "true"
      JAVA_OPTS_APPEND: ${KC_JAVA_OPTS:--Xms512m -Xmx512m}
      VIRTUAL_HOST: ${DOMAIN}
      VIRTUAL_PORT: 8080
      LETSENCRYPT_HOST: ${DOMAIN}
      LETSENCRYPT_EMAIL: ${EMAIL}
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - keycloak
      - webproxy
    healthcheck:
      test: [ "CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000 && printf 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && cat <&3 | grep -q '\"status\": \"UP\"'" ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 90s
    deploy:
      resources:
        limits:
          memory: 1g
    restart: unless-stopped

  postgres:
    image: postgres:17
    container_name: keycloak_postgres
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: ${KC_DB_USERNAME:-keycloak}
      POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
    volumes:
      - keycloak_data:/var/lib/postgresql/data
    networks:
      - keycloak
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 512m
    restart: unless-stopped

networks:
  keycloak:
    driver: bridge
  webproxy:
    external: true

volumes:
  keycloak_data:

Enter fullscreen mode Exit fullscreen mode

The setup has two containers: Keycloak and Postgres. Keycloak stores realm and user data in Postgres, so the database needs to be healthy before Keycloak starts. The webproxy network connects Keycloak to an external nginx reverse proxy.

Environment variables

.env:

DOMAIN=localhost
EMAIL=admin@example.com

KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=

KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode
  • DOMAIN: the domain for Keycloak (e.g. auth.example.com)
  • EMAIL: email for the Let's Encrypt SSL certificate
  • KEYCLOAK_ADMIN: Keycloak admin username
  • KEYCLOAK_ADMIN_PASSWORD: admin password
  • KC_DB_USERNAME: Postgres username (default keycloak)
  • KC_DB_PASSWORD: Postgres password

Fill in DOMAIN, EMAIL, and both passwords before the first run.

Restricting admin access

The admin console and the master realm give full control over Keycloak. If this is open to the web, it becomes a direct target for brute force and credential stuffing. The safe option is to close it from the outside and allow access only from 127.0.0.1, then reach it through an SSH tunnel when needed.

nginx/vhost.conf:

# Keycloak admin access restriction.
# Template: ${DOMAIN} is substituted by deploy.sh via envsubst.
#
# Admin interface is accessible only via SSH tunnel from localhost.

location /auth/admin/ {
    allow 127.0.0.1;
    deny all;

    proxy_pass         http://${DOMAIN};
    proxy_set_header   Host              $http_host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Forwarded-Port  $server_port;
    proxy_set_header   Proxy             "";
    proxy_http_version 1.1;
}

location /auth/realms/master/ {
    allow 127.0.0.1;
    deny all;

    proxy_pass         http://${DOMAIN};
    proxy_set_header   Host              $http_host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Forwarded-Port  $server_port;
    proxy_set_header   Proxy             "";
    proxy_http_version 1.1;
}
Enter fullscreen mode Exit fullscreen mode

This config blocks the admin console and the master realm from the public internet. Only requests from 127.0.0.1 get through. This means the admin panel is reachable only through an SSH tunnel, which we set up below.

Deployment

With the config files ready, one script publishes the nginx vhost and starts Keycloak.

deploy.sh:

#!/bin/bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VHOSTS_DIR="/srv/nginx-vhosts"

# Load .env
set -a
source "${SCRIPT_DIR}/.env"
set +a

if [[ -z "${DOMAIN:-}" ]]; then
    echo "Error: DOMAIN is not set in .env" >&2
    exit 1
fi

# Publish nginx vhost config
echo "Publishing nginx config for ${DOMAIN}..."
mkdir -p "${VHOSTS_DIR}"
envsubst '${DOMAIN}' < "${SCRIPT_DIR}/nginx/vhost.conf" > "${VHOSTS_DIR}/${DOMAIN}"

# Reload nginx
if docker inspect nginx_proxy &>/dev/null; then
    docker exec nginx_proxy nginx -s reload
    echo "nginx reloaded."
else
    echo "Warning: nginx_proxy container not found, skipping reload."
fi

# Start Keycloak
echo "Starting Keycloak..."
docker compose -f "${SCRIPT_DIR}/docker-compose.yml" up -d

Enter fullscreen mode Exit fullscreen mode

Run it with:

./deploy.sh
Enter fullscreen mode Exit fullscreen mode

The script does three things:

  1. Fills DOMAIN from .env into nginx/vhost.conf and writes the result to /srv/nginx-vhosts/${DOMAIN}.
  2. Reloads nginx_proxy.
  3. Starts Keycloak with docker compose up -d.

Admin console access

/auth/admin/ and /auth/realms/master/ are closed to the outside world. Only 127.0.0.1 can reach them.

Open an SSH tunnel from your local machine:

ssh -L 8443:127.0.0.1:443 user@server -N
Enter fullscreen mode Exit fullscreen mode

Add this line to /etc/hosts:

127.0.0.1 auth.example.com
Enter fullscreen mode Exit fullscreen mode

Open in the browser:

https://auth.example.com:8443/auth/admin/
Enter fullscreen mode Exit fullscreen mode

Log in with the credentials from KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD. Close the tunnel with Ctrl+C when done.

Testing

After Keycloak starts, open the admin console through the SSH tunnel. You will see the Keycloak login screen.

Log in with the credentials from KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD.

After logging in, you start in the master realm. Do not use it for your applications and users: master controls Keycloak itself, including the admin account, and a leaked token from your applications should never reach it.

Create a separate realm for your organization instead. Click Create realm, give it a name (e.g. your company name), and save.

From this point, the realm is ready for clients, users, and roles. Everything you connect to Keycloak goes into this realm, not into master.

Conclusion

We now have a working Keycloak instance with a Postgres database and a locked down admin console. It can create realms, manage users, and issue tokens, which is everything a service needs to delegate its login screen to Keycloak.

Top comments (0)