Table of Contents
IAM Production Deployment Guide
Keycloak + PostgreSQL + SOPS (AlmaLinux 9)
Target Stack
- OS: AlmaLinux 9
- Containers: Docker Engine + Compose
- IAM: Keycloak
- DB: PostgreSQL 16
- Secret Encryption: Mozilla SOPS
- Key Backend: age
- SELinux: Enforcing mode
Deployment Strategy
1. Install Dependencies
Update System
# This command requires server reboot. sudo dnf update -y sudo dnf install -y epel-release
Install Docker
sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Enable:
sudo systemctl enable docker sudo systemctl start docker
Verify:
docker --version docker compose version
Install SOPS & AGE
sudo dnf install -y sops age
Verify:
sops --version age --version
2. Create Production Directory Structure
sudo mkdir -p /opt/cotrav/iam/{data/postgres,secrets,logs,keycloak-backups}
sudo mkdir -p /etc/cotrav/sops/age
Set permissions:
sudo chown -R $USER:$USER /opt/cotrav sudo chown -R root:root /etc/cotrav/sops sudo chmod 700 /etc/cotrav/sops
3. Generate AGE Key (One-Time Task)
sudo age-keygen -o /etc/cotrav/sops/age/keys.txt sudo chmod 600 /etc/cotrav/sops/age/keys.txt
Extract public key:
grep "public key:" /etc/cotrav/sops/age/keys.txt
Save that public key value.
4. Create & Encrypt Secrets
Navigate:
cd /opt/cotrav/iam/secrets
Create temporary plaintext:
nano secrets.yaml
Secrets:
db_password: StrongDBPass@2026! admin_password: StrongAdminPass@2026!
Set key:
export AGE_PUBLIC_KEY=$(grep "public key:" /etc/cotrav/sops/age/keys.txt | awk '{print $4}')
# Verify value
echo $AGE_PUBLIC_KEY
Encrypt:
sops --encrypt \ --age "$AGE_PUBLIC_KEY" \ --encrypted-regex '^(db_password|admin_password)$' \ secrets.yaml> secrets.enc.yaml
Secure delete plaintext:
shred -u secrets.yaml
Only secrets.enc.yaml remains.
5. Docker Compose (Production Version)
/opt/cotrav/iam/docker-compose.yml
services:
postgres:
image: postgres:16
container_name: keycloak_db
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak_user
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- /opt/cotrav/iam/data/postgres:/var/lib/postgresql/data:Z
- /dev/shm/iam-secrets/db_password:/run/secrets/db_password:ro,Z
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak_user -d keycloak"]
interval: 5s
networks:
- iam_network
keycloak:
image: quay.io/keycloak/keycloak:26.1.0
container_name: keycloak_app
command: start
ports:
- "8080:8080"
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://keycloak_db:5432/keycloak
KC_DB_USERNAME: keycloak_user
KC_DB_PASSWORD_FILE: /run/secrets/db_password
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD_FILE: /run/secrets/admin_password
KC_HTTP_ENABLED: "true"
volumes:
- /dev/shm/iam-secrets/db_password:/run/secrets/db_password:ro,Z
- /dev/shm/iam-secrets/admin_password:/run/secrets/admin_password:ro,Z
- /opt/cotrav/iam/logs:/opt/keycloak/data/log:Z
depends_on:
postgres:
condition: service_healthy
networks:
- iam_network
networks:
iam_network:
driver: bridge
6. Create Secure start-iam.sh
/opt/cotrav/iam/start-iam.sh
#!/bin/bash set -e echo "Setting SOPS key..." export SOPS_AGE_KEY_FILE=/etc/cotrav/sops/age/keys.txt echo "Creating RAM directory..." mkdir -p /dev/shm/iam-secrets chmod 700 /dev/shm/iam-secrets echo "Decrypting secrets to RAM..." sops --decrypt --extract '["db_password"]' \ /opt/cotrav/iam/secrets/secrets.enc.yaml \ > /dev/shm/iam-secrets/db_password sops --decrypt --extract '["admin_password"]' \ /opt/cotrav/iam/secrets/secrets.enc.yaml \ > /dev/shm/iam-secrets/admin_password chmod 600 /dev/shm/iam-secrets/* echo "Starting Docker..." cd /opt/cotrav/iam docker compose up -d echo "IAM stack started."
Make executable:
chmod +x /opt/cotrav/iam/start-iam.sh
7. systemd Auto-Start Service
/etc/systemd/system/cotrav-iam.service
[Unit] Description=Cotrav IAM Stack After=docker.service Requires=docker.service [Service] Type=oneshot RemainAfterExit=yes ExecStart=/opt/cotrav/iam/start-iam.sh ExecStop=/usr/bin/docker compose -f /opt/cotrav/iam/docker-compose.yml down [Install] WantedBy=multi-user.target
Enable:
sudo systemctl daemon-reload sudo systemctl enable cotrav-iam sudo systemctl start cotrav-iam
8. SELinux Configuration (AlmaLinux 9)
sudo semanage fcontext -a -t container_file_t "/opt/travsetup(/.*)?"\\ sudo restorecon -Rv /opt/travsetup\\
Verify:
ls -Z /opt/travsetup/iam
Must show container_file_t.
9. Automated PostgreSQL Backup
/opt/travsetup/iam/backup-postgres.sh
#!/bin/bash\ DATE=$(date +%F-%H%M) docker exec keycloak_db \ pg_dump -U keycloak_user keycloak \ > /opt/travsetup/iam/keycloak-backups/keycloak-$DATE.sql
Make executable:
chmod +x /opt/travsetup/iam/backup-postgres.sh\
Add cron job:
crontab -e\
Add:
0 2 * * * /opt/travsetup/iam/backup-postgres.sh>> /opt/travsetup/iam/logs/backup.log 2>&1\
Proxy through NGINX - Droplet FIX
To resolve on browser error “We are sorry… HTTPS required”
Step 1: Install Nginx on Alma Linux
bash
dnf install -y nginx systemctl enable --now nginx
Step 2: Generate a self-signed certificate
bash
mkdir -p /etc/nginx/ssl openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/keycloak.key \ -out /etc/nginx/ssl/keycloak.crt \ -subj "/CN=64.227.190.56"
Step 3: Create Nginx config for Keycloak
bash
nano /etc/nginx/conf.d/keycloak.conf
server {
listen 443 ssl;
server_name 64.227.190.56;
ssl_certificate /etc/nginx/ssl/keycloak.crt;
ssl_certificate_key /etc/nginx/ssl/keycloak.key;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $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_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
server {
listen 80;
server_name 64.227.190.56;
return 301 https://$host$request_uri;
}
Modified docker-compose.yml as follows
keycloak: image: quay.io/keycloak/keycloak:26.1.0 container_name: keycloak_app command: start ports: - "8080:8080" environment: KC_DB: postgres KC_DB_URL: jdbc:postgresql://keycloak_db:5432/keycloak KC_DB_USERNAME: keycloak_user KC_DB_PASSWORD: KC_BOOTSTRAP_ADMIN_USERNAME: admin KC_BOOTSTRAP_ADMIN_PASSWORD: KC_HTTP_ENABLED: "true" KC_HTTP_PORT: "8080" KC_PROXY_HEADERS: xforwarded KC_HOSTNAME: "https://64.227.190.56" KC_HOSTNAME_STRICT: "false"
Start NginX
systemctl restart nginx
Check/Configure Firewall rules for URL as follows
Configure Inbound Rules
Add these inbound rules:
| Type | Protocol | Port | Sources |
|---|---|---|---|
| HTTP | TCP | 80 | All IPv4, All IPv6 |
| HTTPS | TCP | 443 | All IPv4, All IPv6 |
| SSH | TCP | 22 | All IPv4, All IPv6 |
Allow Nginx to connect to local ports
setsebool -P httpd_can_network_connect 1 #or setenforce 1
Stop docker and NginX and start again.
cPanel's userdata include
Step 1: Create the userdata directories
bash
mkdir -p /etc/apache2/conf.d/userdata/std/2_4/ctapi/kcloak.ctapi.in/ mkdir -p /etc/apache2/conf.d/userdata/ssl/2_4/ctapi/kcloak.ctapi.in/
Step 2: Create HTTP proxy config
bash
nano /etc/apache2/conf.d/userdata/std/2_4/ctapi/kcloak.ctapi.in/proxy.conf
Add:
RewriteEngine On RewriteRule ^(.*)$ https://kcloak.ctapi.in$1 [R=301,L]<code> **Step 3: Create HTTPS proxy config** bash <code>nano /etc/apache2/conf.d/userdata/ssl/2_4/ctapi/kcloak.ctapi.in/proxy.conf
Add:
ProxyPreserveHost On\ ProxyPass / http://127.0.0.1:8080/\ ProxyPassReverse / http://127.0.0.1:8080/\ RequestHeader set X-Forwarded-Proto "https"\ RequestHeader set X-Forwarded-Port "443"
Step 4: Rebuild Apache config and restart
bash
/scripts/rebuildhttpdconf httpd -t systemctl restart httpd
Then test:
bash
curl -I https://kcloak.ctapi.in
Expected result:
curl -I [[https://kcloak.ctapi.in/|https://kcloak.ctapi.in]] HTTP/1.1 302 Found Date: Thu, 26 Feb 2026 11:22:25 GMT Server: Apache Location: [[https://kcloak.ctapi.in/admin/|https://kcloak.ctapi.in/admin/]] Referrer-Policy: no-referrer Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block
Check for Location: https://kcloak.ctapi.in/admin/|https://kcloak.ctapi.in/admin/
This is poining to correct directory and not apache direcoty with cgi folder.
