Skip to content

Keycloak and NGINX setup for testing environment

Overview

This setup provides: - www.tripqlub.com: Public web application (no auth required for UI, API key for API) - admin.tripqlub.com: Private admin application (OAuth2/OIDC authentication via Keycloak) - Two separate Keycloak realms for different access levels - OAuth2-Proxy containers handling authentication flows - SSL offloading at front-end NGINX

Architecture Flow

[User] → [Front-end NGINX (SSL)] → [Back-end NGINX] → [Web Apps]
                 ↓                          ↓
           [OAuth2-Proxy] ← → [Keycloak (authdev.perunapps.com)]
           [API Server (11080)]

Prerequisites

  1. SSL Certificates: Ensure certificates exist for:

    • tripqlub.com (including www subdomain)
    • admin.tripqlub.com
  2. DNS Configuration:

    • www.tripqlub.com → Front-end NGINX server
    • admin.tripqlub.com → Front-end NGINX server
    • authdev.perunapps.com → Keycloak NGINX server
  3. Keycloak Realms:

    • tqpro-adm: For admin application
    • tqpro-web: For public application (if optional authentication needed)

Keycloak Installation

Keycloak 24+ requires Java 17+ and can be deployed either as a bare-metal server process or as a container. Both approaches are documented below.

Option A: Bare-Metal Installation

A1. Install Java 17

# Ubuntu/Debian
sudo apt update
sudo apt install -y openjdk-17-jre-headless

# RHEL/Rocky/AlmaLinux
sudo dnf install -y java-17-openjdk-headless

# Verify
java -version

A2. Download and Extract Keycloak

# Download latest Keycloak (check https://www.keycloak.org/downloads for current version)
KC_VERSION=24.0.5
cd /opt
sudo wget https://github.com/keycloak/keycloak/releases/download/${KC_VERSION}/keycloak-${KC_VERSION}.tar.gz
sudo tar -xzf keycloak-${KC_VERSION}.tar.gz
sudo ln -s /opt/keycloak-${KC_VERSION} /opt/keycloak

A3. Create Keycloak System User

sudo groupadd keycloak
sudo useradd -r -g keycloak -d /opt/keycloak -s /sbin/nologin keycloak
sudo chown -R keycloak:keycloak /opt/keycloak-${KC_VERSION}

A4. Configure Keycloak

Edit /opt/keycloak/conf/keycloak.conf:

# Database — use PostgreSQL for production (H2 is dev-only)
db=postgres
db-url=jdbc:postgresql://localhost:5432/keycloak
db-username=keycloak
db-password=<strong-password>

# Hostname
hostname=https://authdev.perunapps.com
hostname-strict=false

# HTTP (Keycloak behind reverse proxy)
http-enabled=true
http-port=8080
proxy-headers=xforwarded

# Health and metrics
health-enabled=true
metrics-enabled=true

Create the PostgreSQL database:

CREATE USER keycloak WITH PASSWORD '<strong-password>';
CREATE DATABASE keycloak OWNER keycloak;

A5. Build Keycloak for Production

Keycloak must be "built" before starting in production mode. This pre-compiles the configuration and optimizes startup:

sudo -u keycloak /opt/keycloak/bin/kc.sh build

This step is required after any change to keycloak.conf (database, features, etc.).

A6. Create Initial Admin User

The admin user must be created before starting in production mode. Production mode does not serve the admin creation page on non-localhost interfaces (you will see "Local access required" error).

Method 1 — Bootstrap command (Keycloak 24+, recommended):

sudo -u keycloak /opt/keycloak/bin/kc.sh bootstrap-admin user \
  --username admin \
  --password <strong-admin-password>

Method 2 — Start in dev mode once to create the admin, then stop:

export KEYCLOAK_ADMIN=admin
export KEYCLOAK_ADMIN_PASSWORD=<strong-admin-password>

# Start in dev mode (creates admin in the database)
sudo -u keycloak /opt/keycloak/bin/kc.sh start-dev &

# Wait ~30 seconds for startup, then stop
kill %1

# The admin user is now persisted — subsequent starts use production mode

A7. Create systemd Service

Create /etc/systemd/system/keycloak.service:

[Unit]
Description=Keycloak Authorization Server
After=network.target postgresql.service

[Service]
Type=simple
User=keycloak
Group=keycloak
ExecStart=/opt/keycloak/bin/kc.sh start --optimized
Restart=on-failure
RestartSec=10
LimitNOFILE=65536

# Environment
Environment=JAVA_OPTS=-Xms512m -Xmx1024m

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable keycloak
sudo systemctl start keycloak

# Check status
sudo systemctl status keycloak
journalctl -u keycloak -f

A8. Verify Bare-Metal Installation

# Health check
curl http://localhost:8080/health/ready

# OIDC discovery (through reverse proxy)
curl https://authdev.perunapps.com/realms/master/.well-known/openid-configuration

Option B: Docker / Container Installation

B1. Prerequisites

# Install Docker and Docker Compose
sudo apt update
sudo apt install -y docker.io docker-compose-plugin

# Or for Docker Compose V2 (standalone)
sudo apt install -y docker-compose

# Add your user to docker group
sudo usermod -aG docker $USER

B2. Development Setup (Quick Start)

For development/testing, Keycloak can run with its embedded H2 database using start-dev:

docker run -d --name keycloak \
  -p 8081:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak start-dev \
  --hostname=https://authdev.perunapps.com \
  --hostname-strict=false \
  --proxy-headers=xforwarded \
  --http-enabled=true

Or use the docker-compose setup described in Section 3: Docker Deployment below, which includes OAuth2-Proxy and Redis.

B3. Production Container Setup

For production, use PostgreSQL as the database, run in start mode (not start-dev), and enable health checks.

Create docker-compose.yaml:

services:
  keycloak-db:
    image: postgres:16-alpine
    container_name: keycloak-db
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
    volumes:
      - keycloak-db-data:/var/lib/postgresql/data
    networks:
      - authnet
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak"]
      interval: 10s
      timeout: 5s
      retries: 5

  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    container_name: keycloak
    command:
      - start
      - --optimized
      - --hostname=https://authdev.perunapps.com
      - --hostname-strict=false
      - --proxy-headers=xforwarded
      - --http-enabled=true
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${KC_DB_PASSWORD}
      KC_HEALTH_ENABLED: "true"
      KC_METRICS_ENABLED: "true"
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
    ports:
      - "8081:8080"
    depends_on:
      keycloak-db:
        condition: service_healthy
    networks:
      - authnet
    healthcheck:
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080; echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3; timeout 1 cat <&3 | grep -q '\"status\":\"UP\"'"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

volumes:
  keycloak-db-data:

networks:
  authnet:
    driver: bridge

Create .env file (never commit to git):

KC_DB_PASSWORD=<strong-database-password>
KC_ADMIN_PASSWORD=<strong-admin-password>

Important: For the production container, you must first build the Keycloak image with your configuration baked in. Create a custom Dockerfile:

FROM quay.io/keycloak/keycloak:24.0 AS builder
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:24.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/

Then update docker-compose.yaml to use build: . instead of the image directly.

B4. Start and Verify

docker compose up -d
docker compose logs -f keycloak

# Wait for "Keycloak started in X seconds" message
# Then verify:
curl http://localhost:8081/health/ready

Production Hardening Notes

Regardless of deployment method (bare-metal or container), apply these production best practices:

Database

  • Never use H2 in production — always use PostgreSQL (or MariaDB/MySQL). H2 is embedded, single-process, and not crash-safe.
  • Regular backups — schedule pg_dump of the keycloak database.
  • Connection pooling — Keycloak uses Agroal; tune db-pool-initial-size, db-pool-min-size, db-pool-max-size in keycloak.conf or via KC_DB_POOL_* environment variables.

Startup Mode

  • Always use start (not start-dev) in production. The start-dev mode disables caching, enables live-reload, and runs with development-grade security settings.
  • Run kc.sh build before start --optimized to pre-compile configuration. This significantly reduces startup time and memory usage.

Hostname and Proxy

  • Set hostname to the public HTTPS URL (e.g., https://auth.example.com).
  • Set proxy-headers=xforwarded when behind a reverse proxy (NGINX).
  • Set http-enabled=true only on the internal interface (the proxy terminates SSL).
  • Never expose port 8080 directly to the internet — always use NGINX with SSL in front.

Java Tuning

  • Set JVM heap: -Xms512m -Xmx1024m for small deployments, scale up for larger user bases.
  • For containers, set memory limits in docker-compose: deploy.resources.limits.memory: 1536m.

Admin Console

  • Change the default admin password immediately after first login.
  • Consider disabling the admin console on the public hostname in production by setting hostname-admin to an internal URL.

Session and Token Configuration

  • In each realm, configure appropriate token lifespans:
  • Access Token Lifespan: 5-15 minutes
  • SSO Session Idle: 30 minutes
  • SSO Session Max: 8-12 hours
  • Enable "Revoke Refresh Token" for tighter security.

Realm Export/Import

# Export (bare-metal)
/opt/keycloak/bin/kc.sh export --dir /tmp/kc-export --realm tqpro-adm

# Export (container)
docker exec -it keycloak /opt/keycloak/bin/kc.sh export --dir /tmp/kc-export --realm tqpro-adm
docker cp keycloak:/tmp/kc-export ./keycloak-backup/

# Import on new server
/opt/keycloak/bin/kc.sh import --dir /tmp/kc-export

Logging

  • In production, set log level to INFO (not DEBUG):
    log-level=INFO
    log-console-output=json
    
  • For containers: docker compose logs -f keycloak | jq .

High Availability

For HA deployments: - Run multiple Keycloak instances behind a load balancer. - Use a shared PostgreSQL database. - Enable distributed caching (Infinispan, built into Keycloak) via the cache configuration. - Ensure sticky sessions at the load balancer level (or use the built-in distributed session support).


Step-by-Step Configuration

1. Keycloak Setup

Create Realm: tqpro-adm

  1. Login to Keycloak at https://authdev.perunapps.com
  2. Create realm tqpro-adm
  3. Create client tqweb-adm:
    • Client Protocol: openid-connect
    • Access Type: confidential
    • Valid Redirect URIs:
      • https://admin.tripqlub.com/oauth2/callback
      • https://admin.tripqlub.com/*
    • Web Origins: https://admin.tripqlub.com
    • Copy the client secret from Credentials tab

Create Realm: tqpro-web (Optional)

  1. Create realm tqpro-web
  2. Create client tqweb-public:
    • Client Protocol: openid-connect
    • Access Type: confidential
    • Valid Redirect URIs:
      • https://www.tripqlub.com/oauth2/callback
      • https://www.tripqlub.com/*
    • Web Origins: https://www.tripqlub.com
    • Copy the client secret

Create Users

  1. In each realm, create test users
  2. Set passwords
  3. Assign appropriate roles/groups

2. OAuth2-Proxy Configuration

Update oauth2-proxy-adm.cfg

  • Replace oidc_issuer_url with HTTPS URL
  • Update redirect_url to production domain
  • Update cookie_domains to production domain
  • Ensure cookie_secure = true

Here is a deployed configuration in the AWS environment:

# OIDC provider: Keycloak
provider = "keycloak-oidc"

# URL to the OIDC realm. If working under HTTPS, change this url to https://
oidc_issuer_url = "https://authdev.perunapps.com/realms/tqpro-adm"

# Must match client created in Keycloak
client_id = "tqweb-adm"

# Copy from Keycloak → Credentials
client_secret = "MQ6tm68wn5Kamhok8WkHnePEDPPX1yMi"

code_challenge_method = "S256"

# Where oauth2-proxy listens (all addresses)
http_address = "0.0.0.0:4180"

# Redirect URL after login (your frontend app)
redirect_url = "https://admin.tripqlub.com/oauth2/callback"

# Add whitelist domains for redirects
whitelist_domains = ["admin.tripqlub.com"]

# Skip authentication for sign_out endpoint (optional, if needed)
skip_auth_routes = [
  "^/oauth2/sign_out"
]

# Session cookie settings
cookie_secret = "U0ui7gdP2AyXRgsRWVCke5Lm7asK0xQZEjWCZKOk8wg="  # use: `openssl rand -base64 32`
cookie_secure = true    # must be true for HTTPS
cookie_samesite = "none"
cookie_domains = ["admin.tripqlub.com"]
set_authorization_header = true

# Email domain filtering (allow all for now)
email_domains = ["*"]

# Session duration and refresh
cookie_expire = "8h"
cookie_refresh = "1h"

session_store_type = "redis"
redis_connection_url = "redis://redis:6379/0"

# Upstream destination (local web app)
# upstreams = ["http://127.0.0.1:8081/"]  # optional if handled fully in NGINX

# Disable automatic sign-in redirect (optional)
skip_provider_button = true
set_xauthrequest = true
pass_user_headers = true
pass_access_token = true

# Logging (optional)
standard_logging = true
auth_logging = true
request_logging = true

Update oauth2-proxy-web.cfg

  • Update with tqpro-web realm settings
  • Add new client secret from Keycloak
  • Generate new cookie_secret: openssl rand -base64 32

Here is a deployed configuration file:

# OIDC provider: Keycloak
provider = "keycloak-oidc"

# URL to the OIDC realm. If working under HTTPS, change this url to https://
oidc_issuer_url = "https://authdev.perunapps.com/realms/tqpro-web"

# Must match client created in Keycloak
client_id = "tqweb-adm"

# Copy from Keycloak → Credentials
client_secret = "JmatUkEWJOhZDQ6j2i1DuIaJlw4dkvok"

# Where oauth2-proxy listens (all addresses)
http_address = "0.0.0.0:4180"

# Redirect URL after login (your frontend app)
redirect_url = "http://www.tripqlub.com/oauth2/callback"

# Add whitelist domains for redirects
whitelist_domains = ["tripqlub.com"]

# Skip authentication for sign_out endpoint (optional, if needed)
skip_auth_routes = [
  "^/oauth2/sign_out"
]

# Session cookie settings
cookie_secret = "U0ui7gdP2AyXRgsRWVCke5Lm7asK0xQZEjWCZKOk8wg="  # use: `openssl rand -base64 32`
cookie_secure = true    # must be true for HTTPS
cookie_samesite = "none"
cookie_domains = ["tripqlub.com"]
set_authorization_header = true

# Email domain filtering (allow all for now)
email_domains = ["*"]

# Session duration and refresh
cookie_expire = "8h"
cookie_refresh = "1h"

# Upstream destination (local web app)
# upstreams = ["http://127.0.0.1:8081/"]  # optional if handled fully in NGINX

# Disable automatic sign-in redirect (optional)
skip_provider_button = true
set_xauthrequest = true
pass_user_headers = true
pass_access_token = true

# Logging (optional)
standard_logging = true
auth_logging = true
request_logging = true

3. Docker Deployment

This is the docker-compose file that manages the required containers.

services:
  keycloak:
    image: quay.io/keycloak/keycloak
    container_name: keycloak
    command: [ 'start-dev', '--hostname=https://authdev.perunapps.com', '--hostname-strict=false', '--proxy-headers=xforwarded', '--http-enabled=true' ]
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
    # expose the internal 8080 on host 8081 to avoid clashes
    ports:
      - "8081:8080"
    networks:
      - authnet


  redis:
    image: redis:7-alpine
    container_name: redis-oauth2proxy
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - ./redisdata:/data
    # No need to publish ports; keep it internal
    networks:
      - authnet

  oauth2-proxy-adm:
    image: quay.io/oauth2-proxy/oauth2-proxy
    container_name: oauth2-proxy-adm
    depends_on:
      - keycloak
      - redis
    volumes:
      - ./oauth2-proxy-adm.cfg:/etc/oauth2-proxy.cfg:ro
    ports:
      - "4181:4180"
    command: [ "--config", "/etc/oauth2-proxy.cfg" ]
    networks:
      - authnet

  oauth2-proxy-web:
    image: quay.io/oauth2-proxy/oauth2-proxy
    container_name: oauth2-proxy-web
    depends_on:
      - keycloak
      - redis
    volumes:
      - ./oauth2-proxy-web.cfg:/etc/oauth2-proxy.cfg:ro
    ports:
      - "4182:4180"
    command: [ "--config", "/etc/oauth2-proxy.cfg" ]
    networks:
      - authnet

networks:
  authnet:
    driver: bridge
# Navigate to docker directory
cd /path/to/docker-compose

# Pull latest images
docker-compose pull

# Start services
docker-compose up -d

# Check logs
docker-compose logs -f

# Verify health
docker-compose ps

4. NGINX Configuration

Front-end NGINX Server

# Copy configuration
sudo cp frontend-nginx.conf /etc/nginx/sites-available/tripqlub-frontend.conf

# Create symbolic link
sudo ln -s /etc/nginx/sites-available/tripqlub-frontend.conf /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload NGINX
sudo systemctl reload nginx

Back-end NGINX Server (dev04.perunapps.com)

# Copy configuration
sudo cp backend-nginx.conf /etc/nginx/sites-available/backend-apps.conf

# Create symbolic link
sudo ln -s /etc/nginx/sites-available/backend-apps.conf /etc/nginx/sites-enabled/

# Create web app directories
sudo mkdir -p /var/www/tripqlub
sudo mkdir -p /var/www/tqadmin

# Test configuration
sudo nginx -t

# Reload NGINX
sudo systemctl reload nginx

5. Web Application Deployment

# Deploy tripqlub (public app)
sudo rsync -avz /path/to/tripqlub/dist/ /var/www/tripqlub/

# Deploy tqadmin (admin app)
sudo rsync -avz /path/to/tqadmin/dist/ /var/www/tqadmin/

# Set permissions
sudo chown -R www-data:www-data /var/www/tripqlub
sudo chown -R www-data:www-data /var/www/tqadmin

Testing

Test Public Application (www.tripqlub.com)

# Should work without authentication
curl -I https://www.tripqlub.com

# API should require key
curl -H "X-Api-Key: your-api-key" https://www.tripqlub.com/api/endpoint

Test Admin Application (admin.tripqlub.com)

# Should redirect to Keycloak login
curl -I https://admin.tripqlub.com

# After login, check authenticated access
# Use browser for full OAuth flow testing

Test OAuth2-Proxy

# Check if oauth2-proxy is running
docker ps | grep oauth2-proxy

# Test ping endpoint
curl http://localhost:4181/ping
curl http://localhost:4182/ping

Test Keycloak

# Check Keycloak health
curl http://localhost:8081/health/ready

# Test OIDC discovery
curl https://authdev.perunapps.com/realms/tqpro-adm/.well-known/openid-configuration

Troubleshooting

Issue: Redirect Loop

Symptoms: Browser keeps redirecting between app and login Solutions: - Check cookie_domains matches actual domain - Verify cookie_secure = true for HTTPS - Check X-Forwarded-Proto header is being set - Clear browser cookies

Issue: 401 Unauthorized on Admin App

Symptoms: Always redirected to login Solutions: - Verify oauth2-proxy can reach Keycloak - Check client_id and client_secret match Keycloak - Verify redirect_url matches Keycloak client settings - Check oauth2-proxy logs: docker logs oauth2-proxy-adm

Issue: CORS Errors

Symptoms: Browser console shows CORS errors Solutions: - Verify Web Origins in Keycloak client settings - Add appropriate CORS headers in NGINX - Check cookie_samesite setting

Issue: Session Expires Too Quickly

Solutions: - Increase cookie_expire in oauth2-proxy config - Adjust Keycloak session timeout settings - Enable cookie_refresh for automatic renewal

Issue: API Key Authentication Not Working

Symptoms: 401 on public API calls Solutions: - Verify API key is sent in X-Api-Key header - Check NGINX is passing the header to backend - Implement API key validation in API server (dev03.perunapps.com:11080)

Security Considerations

  1. Cookie Security:

    • Always use cookie_secure = true in production
    • Use cookie_httponly = true to prevent XSS
    • Set appropriate cookie_samesite value
  2. Secrets Management:

    • Rotate cookie_secret regularly
    • Keep client_secret secure and never commit to git
    • Use environment variables for sensitive data
  3. API Keys:

    • Implement rate limiting
    • Rotate keys periodically
    • Use different keys per client/environment
  4. HTTPS Enforcement:

    • Always redirect HTTP to HTTPS
    • Use HSTS headers
    • Ensure all internal communication considers security
  5. Session Management:

    • Set reasonable session timeouts
    • Implement automatic session refresh
    • Provide clear logout functionality

Monitoring

Log Locations

  • Front-end NGINX: /var/log/nginx/
  • Back-end NGINX: /var/log/nginx/
  • OAuth2-Proxy: docker logs oauth2-proxy-adm / docker logs oauth2-proxy-web
  • Keycloak: docker logs keycloak

Health Checks

# Check all services
docker-compose ps

# Monitor logs in real-time
docker-compose logs -f

# Check NGINX status
sudo systemctl status nginx

Backup and Recovery

Configuration Backup

# Backup all configs
tar -czf tripqlub-config-backup-$(date +%Y%m%d).tar.gz \
  docker-compose.yaml \
  oauth2-proxy-*.cfg \
  /etc/nginx/sites-available/tripqlub-frontend.conf \
  /etc/nginx/sites-available/backend-apps.conf

Keycloak Backup

# Export realms
docker exec -it keycloak /opt/keycloak/bin/kc.sh export \
  --dir /tmp/keycloak-export --realm tqpro-adm

# Copy export from container
docker cp keycloak:/tmp/keycloak-export ./keycloak-backup/

Production Checklist

  • [ ] SSL certificates installed and valid
  • [ ] DNS records configured correctly
  • [ ] Keycloak realms and clients created
  • [ ] OAuth2-Proxy configurations updated with production URLs
  • [ ] Docker containers running and healthy
  • [ ] NGINX configurations deployed and tested
  • [ ] Web applications deployed
  • [ ] Authentication flow tested end-to-end
  • [ ] API key authentication tested
  • [ ] Monitoring and logging configured
  • [ ] Backup procedures documented
  • [ ] Security headers verified
  • [ ] Cookie settings appropriate for production
  • [ ] Error pages customized
  • [ ] Documentation updated with actual values