User Tools

Site Tools


iam_production_deployment_guide

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

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:

TypeProtocolPortSources
HTTPTCP80All IPv4, All IPv6
HTTPSTCP443All IPv4, All IPv6
SSHTCP22
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.

iam_production_deployment_guide.txt · Last modified: by pradnya