====== 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 ====
{{:iambootprocessandbackup.jpg?nolink&|iambootprocessandbackup.jpg}}
===== 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]
**Step 3: Create HTTPS proxy config**
bash
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.