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¶
-
SSL Certificates: Ensure certificates exist for:
- tripqlub.com (including www subdomain)
- admin.tripqlub.com
-
DNS Configuration:
- www.tripqlub.com → Front-end NGINX server
- admin.tripqlub.com → Front-end NGINX server
- authdev.perunapps.com → Keycloak NGINX server
-
Keycloak Realms:
tqpro-adm: For admin applicationtqpro-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:
A5. Build Keycloak for Production¶
Keycloak must be "built" before starting in production mode. This pre-compiles the configuration and optimizes startup:
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):
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_dumpof thekeycloakdatabase. - Connection pooling — Keycloak uses Agroal; tune
db-pool-initial-size,db-pool-min-size,db-pool-max-sizeinkeycloak.confor viaKC_DB_POOL_*environment variables.
Startup Mode¶
- Always use
start(notstart-dev) in production. Thestart-devmode disables caching, enables live-reload, and runs with development-grade security settings. - Run
kc.sh buildbeforestart --optimizedto pre-compile configuration. This significantly reduces startup time and memory usage.
Hostname and Proxy¶
- Set
hostnameto the public HTTPS URL (e.g.,https://auth.example.com). - Set
proxy-headers=xforwardedwhen behind a reverse proxy (NGINX). - Set
http-enabled=trueonly 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 -Xmx1024mfor 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-adminto 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(notDEBUG): - 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¶
- Login to Keycloak at https://authdev.perunapps.com
- Create realm
tqpro-adm - Create client
tqweb-adm:- Client Protocol: openid-connect
- Access Type: confidential
- Valid Redirect URIs:
https://admin.tripqlub.com/oauth2/callbackhttps://admin.tripqlub.com/*
- Web Origins:
https://admin.tripqlub.com - Copy the client secret from Credentials tab
Create Realm: tqpro-web (Optional)¶
- Create realm
tqpro-web - Create client
tqweb-public:- Client Protocol: openid-connect
- Access Type: confidential
- Valid Redirect URIs:
https://www.tripqlub.com/oauth2/callbackhttps://www.tripqlub.com/*
- Web Origins:
https://www.tripqlub.com - Copy the client secret
Create Users¶
- In each realm, create test users
- Set passwords
- Assign appropriate roles/groups
2. OAuth2-Proxy Configuration¶
Update oauth2-proxy-adm.cfg¶
- Replace
oidc_issuer_urlwith HTTPS URL - Update
redirect_urlto production domain - Update
cookie_domainsto 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¶
-
Cookie Security:
- Always use
cookie_secure = truein production - Use
cookie_httponly = trueto prevent XSS - Set appropriate
cookie_samesitevalue
- Always use
-
Secrets Management:
- Rotate
cookie_secretregularly - Keep
client_secretsecure and never commit to git - Use environment variables for sensitive data
- Rotate
-
API Keys:
- Implement rate limiting
- Rotate keys periodically
- Use different keys per client/environment
-
HTTPS Enforcement:
- Always redirect HTTP to HTTPS
- Use HSTS headers
- Ensure all internal communication considers security
-
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