Tropic Host

Установка Keycloak на KVM VPS в Docker Compose: настройка PostgreSQL, Nginx SSL и корпоративного SSO

41 мин чтения
Tropic

Краткий вывод: Для отказоустойчивой работы Keycloak в Docker Compose с бэкендом PostgreSQL 16 и фронтендом Nginx необходим KVM VPS с гарантированными ресурсами (%st = 0.0%), минимум 2 vCPU (3.5+ ГГц), 4 ГБ RAM (с фиксацией лимитов cgroups v2 и кучи JVM -Xms1024m -Xmx2048m), от 40 ГБ NVMe (PCIe 4.0 с показателями от 15 000 IOPS для синхронного fsync WAL-журнала) и портом 1 Гбит/с с алгоритмом TCP BBR. Продакшен-конфигурация требует изоляции контейнеров во внутренней сети Docker, терминации TLS 1.3/HTTP/2 на стороне Nginx с пробросом заголовков X-Forwarded-* (KC_PROXY_HEADERS=xforwarded) и оптимизации пула Agroal (KC_DB_POOL_MAX_SIZE=25) во избежание деградации OIDC/SAML токен-эндпоинтов. Применение тюнинга ядра (vm.overcommit_memory=1, net.core.somaxconn=4096) и пула потоков Quarkus удерживает сетевую задержку аутентификации p99 ниже 100 мс при нагрузке свыше 500 активных SSO-сессий.


Содержание

  1. Аппаратные требования и сайзинг KVM VPS под Java Runtime Keycloak
  2. Подготовка Linux и тюнинг параметров ядра для базы данных
  3. Развертывание Keycloak Quarkus в Docker Compose с PostgreSQL
  4. Настройка Nginx Reverse Proxy с TLS 1.3 и передачей заголовков X-Forwarded
  5. Настройка первого Realm, OpenID Connect клиента и защиты от брутфорса
  6. Регламент Disaster Recovery: бэкап базы данных и экспорт конфигураций realm
  7. Часто задаваемые вопросы (FAQ)

Аппаратные требования и сайзинг KVM VPS под Java Runtime Keycloak

Архитектура развертывания Keycloak на VPS через Docker Compose базируется на рантайме Quarkus (начиная с версии 17+), пришедшем на смену устаревшему WildFly. Переход на Quarkus кардинально снизил время холодного старта и базовое потребление памяти, однако профиль нагрузки сервиса федерации идентификации (SSO/IAM) остается критичным к процессорным тактам и структуре аллокации оперативной памяти. Некорректный сайзинг и пренебрежение механизмами изоляции ядра Linux неминуемо приводят к падению контейнера по OOM (Out Of Memory) или взрывному росту задержки ответа (p99 latency) на криптографических операциях.


Архитектура памяти JVM и риски OOM Killer в cgroups v2

Распространенная ошибка при запуске Keycloak в контейнере — ограничение памяти в docker-compose.yml до значения, эквивалентного параметру -Xmx (Max Heap). В Java-рантайме куча (Heap) представляет лишь часть потребляемого адресного пространства процесса. Полная модель памяти контейнера описывается формулой:

$$\text{RSS}_{\text{total}} = \text{Heap} + \text{Metaspace} + (\text{Threads} \times \text{Xss}) + \text{DirectMemory} + \text{CodeCache} + \text{JVM Native Overhead}$$

  1. Java Heap (-Xms, -Xmx): Хранит пользовательские сессии, кэши сущностей realm/user/client, билеты аутентификации и структуры Infinispan. Для небольших инсталляций достаточно выделить 1024–1536 МБ, для высоконагруженных сред — от 4096 МБ.
  2. Metaspace (-XX:MaxMetaspaceSize): Содержит метаданные загруженных классов (Quarkus core, Hibernate ORM, RESTEasy, криптографические провайдеры BouncyCastle). Стабильная работа требует фиксации лимита на уровне 256–384 МБ во избежание неконтролируемого разрастания native-памяти хоста.
  3. Thread Stacks (-Xss): Стек одного потока в 64-битной Linux JVM по умолчанию равен 1 МБ. Подсистема Netty и пул воркеров Quarkus/Resteasy при пиковой нагрузке порождают 200–400 потоков, что суммарно резервирует до 200–400 МБ вне кучи.
  4. Direct Memory (-XX:MaxDirectMemorySize): Буферы прямого ввода-вывода (I/O) сетевого стека Netty для обработки входящих HTTP/1.1 и HTTP/2 соединений, TLS-терминации и сериализации токенов. Необходимо выделять не менее 256 МБ.
  5. CodeCache и Native Overhead: JIT-компилятор (C1/C2), сборщик мусора (G1GC/Shenandoah) и внутренние структуры HotSpot требуют дополнительно 200–300 МБ.

В ядре Linux с включенной иерархией cgroups v2 лимит оперативной памяти контейнера контролируется интерфейсным файлом memory.max. При достижении границы memory.high ядро принудительно замедляет процесс, активируя reclaim-циклы. Если потребление достигает memory.max и swap отключен либо исчерпан, демон ядра oomd или системный вызов mem_cgroup_out_of_memory() инициируют принудительное завершение основного процесса:

# Диагностика факта завершения JVM процессом OOM Killer в системном журнале ядра
dmesg -T | grep -E -i "oom[-_]killer|killed process.*java"

# Проверка счетчиков OOM и событий сброса страниц в контрольной группе контейнера (cgroups v2)
cat /sys/fs/cgroup/system.slice/docker-<CONTAINER_ID>.scope/memory.events

Если сумма всех сегментов JVM превышает лимит cgroup, контейнер аварийно завершается с кодом 137. Следовательно, системный резерв (memory limit в Docker) обязан превышать -Xmx минимум на 1–1.5 ГБ.


Вычислительные ресурсы: криптография, CFS Throttling и метрика %st

Нагрузка Keycloak носит выраженный CPU-интенсивный характер. Каждая процедура входа пользователя влечет: * Хеширование паролей алгоритмами PBKDF2 (по умолчанию 600 000 итераций по рекомендациям OWASP) или Argon2id; * Вычисление пар асимметричных ключей и подписание токенов JWT (JSON Web Signature) алгоритмами RSA (RS256, 2048/4096 бит) либо эллиптическими кривыми (ES256, EdDSA); * Обмен ключами по протоколу Diffie-Hellman при инициализации TLS-сессий.

При запуске нескольких параллельных запросов на аутентификацию нагрузка на vCPU мгновенно подскакивает до 100%. Если на хосте включено жесткое ограничение через CFS (Completely Fair Scheduler), например, директивой cpus: '2.0', ядро накладывает квоты через параметры cpu.cfs_quota_us и cpu.cfs_period_us. Исчерпание квоты внутри стомиллисекундного окна приводит к принудительному троттлингу (CPU throttling): поток замораживается на уровне планировщика ядра, в результате чего p99 latency генерации токена деградирует с 45 мс до 1500–3000 мс.

# Мониторинг троттлинга процессора внутри cgroup контейнера
cat /sys/fs/cgroup/system.slice/docker-<CONTAINER_ID>.scope/cpu.stat
# Ключевые показатели: nr_throttled (число периодов задержки) и throttled_usec (суммарное время троттлинга)

Вторым критическим фактором является оверселлинг физических процессоров на стороне хостинг-провайдера. Если на гипервизоре запущено избыточное число виртуальных машин, планировщик KVM вынужден ставить vCPU в очередь ожидания физического ядра. Эта задержка выражается метрикой CPU Steal Time (%st):

# Проверка процента украденных процессорных тактов в реальном времени
vmstat 1 5
# или через системный монитор sar из пакета sysstat:
sar -u 1 5

Если показатель %st поднимается выше 0.0–0.2%, фаза Stop-the-World сборщика мусора JVM (G1GC) длительностью 15 мс превращается в реальную физическую паузу в 150–400 мс, вызывая каскадные разрывы соединений в пуле HikariCP и тайм-ауты обратного прокси-сервера (504 Gateway Timeout).

Именно поэтому инфраструктурным стандартом для развертывания Keycloak выступает платформа tropic.host. Честная виртуализация KVM без оверподписки ресурсов гарантирует нулевой CPU Steal Time (%st = 0.0%) на производительных процессорах AMD EPYC и Ryzen 9 с высокой базовой частотой ядер. Это исключает деградацию планировщика JVM и обеспечивает предсказуемое время отклика криптографических примитивов даже в моменты пиковых логин-штормов (login storms).


Спецификация дисковой подсистемы и сайзинг

Хотя Keycloak хранит постоянные данные в PostgreSQL, локальная дисковая подсистема хоста задействуется для: * Записи WAL-журналов и транзакций встроенной распределенной кэш-памяти Infinispan; * Ротации структурированных JSON-логов доступа и аудита безопасности; * Чтения слоев контейнера и библиотек при перезапуске стека.

Задержки произвольной записи (I/O wait) вызывают блокировки потоков логирования, что при синхронном выводе приводит к ступору сетевых воркеров. Требуются накопители NVMe корпоративного класса (PCIe 4.0) со случайным чтением блоками 4K QD1 не менее 40 000–50 000 IOPS, доступные по умолчанию на инстансах tropic.host.

Сводная матрица сайзинга инстансов KVM VPS под Keycloak

Профиль нагрузки Активные сессии / RPS Спецификация KVM VPS Выделение RAM (Хост) JVM Heap (-Xms / -Xmx) Metaspace & Native Дисковая подсистема Конфигурация tropic.host
Dev / Staging < 500 сессий / 5–10 RPS 2 vCPU (KVM) 4 ГБ 1024M / 1536M 256M / 512M 40 ГБ NVMe (PCIe 4.0) KVM Starter
Small Production 5 000 сессий / 30–60 RPS 4 vCPU (KVM, %st=0) 8 ГБ 2048M / 3072M 384M / 1024M 80 ГБ Enterprise NVMe KVM Production
High-Load Enterprise 50 000+ сессий / 200+ RPS 8 vCPU (AMD EPYC/Ryzen) 16 ГБ 6144M / 8192M 512M / 2048M 160 ГБ NVMe RAID-10 KVM Enterprise

Системный тюнинг ядра (sysctl) и лимиты в Docker Compose

Перед развертыванием контейнера хостовая ОС Linux (Ubuntu 24.04 LTS / Debian 12) настраивается на уровне ядра для предотвращения вытеснения страниц JVM в swap и устранения ограничений сетевых сокетов:

# /etc/sysctl.d/99-keycloak.conf

# Минимизация сброса анонимных страниц в пространство подкачки
vm.swappiness = 1

# Разрешение выделения виртуальной памяти без жестких проверок эвристики ядра
vm.overcommit_memory = 1

# Увеличение лимита областей виртуальной памяти процесса для предотвращения падений JVM по mmap
vm.max_map_count = 262144

# Расширение очереди неполных TCP-соединений (SYN backlog) под сценарии SSO-штормов
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096

Применение параметров выполняется без перезагрузки:

sudo sysctl --system

В файле манифеста Docker Compose параметры изоляции ресурсов cgroups v2 и переменные HotSpot JVM жестко синхронизируются. Ниже приведен эталонный фрагмент спецификации сервиса для промышленного стенда Small/Medium Production:

services:
  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    command: start --optimized
    environment:
      KC_DB: postgres
      KC_HTTP_ENABLED: "true"
      KC_PROXY_HEADERS: "xforwarded"
      # Оптимизация сборщика мусора и структуры памяти HotSpot
      JAVA_OPTS_APPEND: >-
        -XX:+UseG1GC
        -XX:MaxGCPauseMillis=20
        -XX:InitiatingHeapOccupancyPercent=45
        -XX:G1ReservePercent=15
        -XX:+ExitOnOutOfMemoryError
        -Xms3072m
        -Xmx3072m
        -XX:MaxMetaspaceSize=384m
        -XX:MaxDirectMemorySize=512m
    deploy:
      resources:
        limits:
          # Лимит cgroups v2 memory.max: 3072M (Heap) + 384M (Meta) + 512M (Direct) + ~1 ГБ (Native/OS) = 5 ГБ
          memory: 5120M
          cpus: '3.50'
        reservations:
          # Гарантированное резервирование памяти в ядре без overcommit
          memory: 4096M
          cpus: '2.00'
    restart: unless-stopped
    ulimits:
      nofile:
        soft: 65535
        hard: 65535

Флаг -XX:+ExitOnOutOfMemoryError предотвращает зависание JVM в состоянии деградировавшего зомби-процесса при исчерпании лимитов кучи: контейнер мгновенно завершается, позволяя Docker Compose перезапустить сервис и восстановить обработку аутентификационных потоков. Указанная конфигурация ресурсов гарантирует, что приложение не будет уничтожено демоном ядра oomd, а сетевой стек сохранит стабильную задержку p99 в рамках соглашения об уровне сервиса (SLA).

Подготовка Linux и тюнинг параметров ядра для базы данных

При эксплуатации стека Keycloak на VPS в Docker Compose узким местом инфраструктуры становится не столько сам Quarkus-рантайм, сколько системные вызовы ядра Linux, обслуживающие дисковый ввод-вывод PostgreSQL и сетевые сокеты пограничного прокси. Базовая конфигурация дистрибутивов Ubuntu 24.04 LTS и Debian 12 оптимизирована под универсальные десктопные или слабонагруженные серверные сценарии. При пиковых нагрузках (SSO-штормы в начале рабочего дня, массовая валидация токенов микросервисами) стандартные параметры ядра приводят к лавинообразному росту задержек p99, зависанию тредов сброса буферов на диск и аварийному завершению процессов через OOM Killer.

Для обеспечения стабильного времени отклика на уровне SLA системные параметры хоста настраиваются на детерминированную работу с виртуальной памятью, дисковыми очередями и сетевыми буферами.


1. Подсистема виртуальной памяти (VM Subsystem) и подавление задержек I/O

Служба базы данных PostgreSQL активно использует буферный пул (shared_buffers) и системный Page Cache ядра. Некорректное соотношение грязных страниц приводит к тому, что фоновый демон ядра kswapd или процессы вытеснения сбрасывают мегабайты данных на диск синхронно, блокируя системный вызов fsync для PostgreSQL WAL (Write-Ahead Logging).

Ограничение кэширования грязных страниц (Dirty Memory)

По умолчанию параметры vm.dirty_background_ratio (10%) и vm.dirty_ratio (20%) исчисляются в процентах от общего объема RAM. На сервере с 32–64 ГБ оперативной памяти это означает, что ядро может накопить до 6–12 ГБ несинхронизированных данных, прежде чем начнет принудительно тормозить процессы записи через вызов sync_file_range. Это вызывает микрофризы базы данных от 500 мс до нескольких секунд.

Вместо процентов задаются жесткие лимиты в байтах:

# Старт асинхронного сброса страниц демоном ядра при накоплении 64 МБ
vm.dirty_background_bytes = 67108864

# Принудительная блокировка пишущих процессов и синхронная запись при достижении 256 МБ
vm.dirty_bytes = 268435456

При таких значениях на платформе tropic.host, оснащенной серверными NVMe-накопителями корпоративного уровня с шиной PCIe 4.0, операции сброса страниц выполняются непрерывным потоком без накопления гигантских очередей, сохраняя латентность случайной записи 4K QD1 в пределах сотен микросекунд.

Управление подкачкой (Swappiness) и аллокацией (Overcommit)

Полное отключение swap (swapoff -a) в продакшене не рекомендуется: ядро теряет возможность вытеснять неиспользуемые анонимные страницы демонов инициализации, что сокращает пространство под дисковый кэш. Однако для базы данных сброс активных страниц в swap фатален.

# Агрессивное удержание страниц процессов в физической памяти (минимальный swappiness)
vm.swappiness = 10

# Предсказуемое поведение аллокатора страниц ядра
vm.vfs_cache_pressure = 50

Параметр vm.vfs_cache_pressure = 50 заставляет ядро отдавать приоритет сохранению кэша индексных дескрипторов (dentry/inode), предотвращая повторные обращения к файловой системе при чтении таблиц базы данных.

Расширение таблицы mmap (vm.max_map_count)

Quarkus-рантайм Keycloak, работающий на OpenJDK 21, и распределенный кэш Infinispan создают сотни тредов, активно использующих системный вызов mmap. Стандартный лимит ядра 65530 приводит к краху виртуальной машины с ошибкой java.lang.OutOfMemoryError: Map failed при росте числа сессий.

# Расширение максимального количества областей отображения памяти для процесса
vm.max_map_count = 262144

2. Отключение Transparent Huge Pages (THP)

Функция Transparent Huge Pages (THP) пытается объединять стандартные страницы памяти размером 4 КБ в блоки по 2 МБ. Для приложений с паттерном доступа к данным, характерным для реляционных СУБД (PostgreSQL выполняет произвольное чтение блоков по 8 КБ), THP приводит к двум деградациям: 1. Memory Bloat: Чтение одной строки размером 100 байт приводит к загрузке в память всех 2 МБ Huge Page. 2. Агрессивная дефрагментация (Compaction Stalls): При нехватке непрерывных блоков памяти демон kcompactd замораживает аллокации приложения на десятки миллисекунд, порождая всплески p99 latency.

Для отключения THP создается systemd-юнит, применяющий параметры на этапе инициализации ядра:

sudo tee /etc/systemd/system/disable-thp.service << 'EOF'
[Unit]
Description=Disable Transparent Huge Pages (THP) for Database Performance
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=mongod.service postgresql.service docker.service

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled && echo never > /sys/kernel/mm/transparent_hugepage/defrag'

[Install]
WantedBy=basic.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now disable-thp.service

Проверка применения:

cat /sys/kernel/mm/transparent_hugepage/enabled
# Ожидаемый вывод: always madvise [never]

3. Тюнинг сетевого стека ядра и активация алгоритма TCP BBR

В сценариях развертывания Keycloak на VPS через Docker Compose трафик проходит цепочку: клиент $\rightarrow$ Reverse Proxy (Nginx/Traefik) $\rightarrow$ интерфейс моста docker0 $\rightarrow$ контейнер Keycloak $\rightarrow$ сетевой стек PostgreSQL. Стандартный алгоритм контроля перегрузок TCP CUBIC интерпретирует единичные потери пакетов как признак перегрузки канала и режет окно передачи данных вдвое, что резко увеличивает Round-Trip Time (RTT) при пиковых запросах аутентификации.

Активация TCP BBR и Fair Queuing

Алгоритм TCP BBR (Bottleneck Bandwidth and RTT), разработанный Google, моделирует физическую емкость канала и предотвращает раздувание очередей на сетевых маршрутизаторах (bufferbloat). На серверах tropic.host с аплинками от 1 до 10 Гбит/с и прямыми стыками на точках обмена трафиком (Frankfurt, Amsterdam) BBR стабилизирует время доставки TLS-сессий и токенов независимо от удаленности клиента.

# Активация планировщика очередей FQ (Fair Queueing), необходимого для BBR
net.core.default_qdisc = fq

# Установка BBR в качестве основного алгоритма TCP Congestion Control
net.ipv4.tcp_congestion_control = bbr

Проверка загрузки модуля ядра:

sysctl net.ipv4.tcp_congestion_control
# Ожидаемый вывод: net.ipv4.tcp_congestion_control = bbr
lsmod | grep bbr
# Ожидаемый вывод: tcp_bbr                20480  1

Расширение очередей сокетов и буферов TCP

В моменты пиковых всплесков трафика стандартные очереди входящих соединений переполняются, и ядро отбрасывает SYN-пакеты (drop/reset), вынуждая клиентов повторять запросы через TCP retransmission.

# Максимальный размер очереди сокета на прослушивание (backlog для listen())
net.core.somaxconn = 65535

# Максимальное число не подтвержденных трехсторонних рукопожатий (SYN backlog)
net.ipv4.tcp_max_syn_backlog = 16384

# Длина очереди входных пакетов на сетевом интерфейсе до их обработки ядром
net.core.netdev_max_backlog = 10000

# Повторное использование сокетов в состоянии TIME_WAIT для исходящих TCP-соединений
net.ipv4.tcp_tw_reuse = 1

# Время удержания сокета в состоянии FIN-WAIT-2 до принудительного закрытия
net.ipv4.tcp_fin_timeout = 15

# Расширение диапазона эфемерных портов для соединений с базой данных и внешними IdP
net.ipv4.ip_local_port_range = 10240 65535

# Буферы приема и отправки TCP (min, default, max) в байтах
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

Параметр net.ipv4.tcp_tw_reuse = 1 критически важен для связки Keycloak с PostgreSQL: без него пул соединений пула HikariCP при частых сбросах контекста исчерпает свободные порты сетевого интерфейса моста.


4. Планировщик I/O и оптимизация дискового интерфейса

Для блочных устройств на базе шины NVMe традиционные планировщики очередей ядра (CFQ, BFQ) неэффективны, так как создают избыточный оверхед синхронизации процессора. Контроллеры NVMe поддерживают аппаратную многопоточную обработку очередей (Multi-Queue Block Layer, blk-mq).

Планировщик проверяется командой:

cat /sys/block/nvme0n1/queue/scheduler

Для аппаратных NVMe-дисков планировщик переводится в режим none (аппаратная обработка контроллером без программных очередей ядра) либо mq-deadline:

echo none | sudo tee /sys/block/nvme0n1/queue/scheduler

Параметры монтирования файловой системы, где размещаются данные каталога /var/lib/docker/volumes/ и WAL-логи PostgreSQL, должны исключать запись временных меток доступа (atime). В /etc/fstab добавляются опции:

UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx / ext4 noatime,nodiratime,data=ordered,commit=60 0 1

Флаг commit=60 увеличивает интервал автоматической групповой фиксации метаданных ext4 с 5 до 60 секунд, снимая паразитную нагрузку с журнала файловой системы.


5. Лимиты файловых дескрипторов и ресурсов (Limits & systemd)

Каждое подключение клиента к Keycloak, внутренний тред JVM, пул HikariCP и соединение PostgreSQL транслируются в файловый дескриптор Linux (epoll сокеты, файлы сопоставления, открытые таблицы). По умолчанию лимит непривилегированного пользователя в системе равен 1024, что моментально приведет к ошибке java.io.IOException: Too many open files.

Конфигурация /etc/security/limits.d/99-keycloak.conf:

*               soft    nofile          1048576
*               hard    nofile          1048576
*               soft    nproc           65535
*               hard    nproc           65535
root            soft    nofile          1048576
root            hard    nofile          1048576
root            soft    nproc           65535
root            hard    nproc           65535

Поскольку Docker-демон запускается через systemd, глобальные лимиты служб определяются в /etc/systemd/system.conf и /etc/systemd/user.conf:

[Manager]
DefaultLimitNOFILE=1048576:1048576
DefaultLimitNPROC=65535:65535
DefaultLimitMEMLOCK=infinity

6. Сводный конфигурационный профиль /etc/sysctl.d/99-keycloak-vps.conf

Все параметры ядра консолидируются в единый файл конфигурации для сохранения между перезагрузками:

sudo tee /etc/sysctl.d/99-keycloak-vps.conf << 'EOF'
# ====================================================================
# Тюнинг ядра Linux для Keycloak + PostgreSQL + Docker Compose
# ====================================================================

# Подсистема виртуальной памяти
vm.max_map_count = 262144
vm.swappiness = 10
vm.vfs_cache_pressure = 50
vm.dirty_background_bytes = 67108864
vm.dirty_bytes = 268435456
vm.overcommit_memory = 0

# Увеличение глобального лимита дескрипторов ядра
fs.file-max = 2097152

# Сетевой стек: базовая производительность и очереди
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 16384
net.core.netdev_max_backlog = 10000

# Оптимизация жизненного цикла TCP-соединений
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 10240 65535

# Буферы приема и передачи сокетов
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# Защита от SYN-флуда и безопасность
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_rfc1337 = 1
EOF

Применение конфигурации в рабочей системе выполняется без перезагрузки:

sudo sysctl --system
sudo systemctl daemon-reexec
sudo systemctl restart docker

7. Диагностика и верификация метрик в реальном времени

После применения тюнинга состояние инфраструктуры валидируется утилитами системного анализа под нагрузкой.

  1. Контроль CPU Steal Time (%st): bash mpstat 1 5 Метрика %st в выводе обязана строго равняться 0.00%. Ненулевое значение указывает на оверселлинг процессорных мощностей со стороны гипервизора. На облачных KVM-инстансах tropic.host за счет жесткого закрепления ядер (CPU pinning) и отсутствия переподписки vCPU значение %st гарантированно держится на отметке 0.0%, исключая случайные задержки при исполнении транзакций базы данных.
  2. Мониторинг дисковой задержки и утилизации очередей: bash iostat -xz 1 Критическими метриками являются r_await и w_await (среднее время ожидания чтения/записи блочным устройством). Для NVMe-накопителей показатель w_await при непрерывных коммитах WAL PostgreSQL не должен превышать 0.5–1.0 мс при значении %util < 40%.
  3. Аудит переполнения очередей сокетов: bash ss -ltnp В колонках Send-Q и Recv-Q отображается состояние очередей. Если для сокетов порта 8080 (Keycloak) или 5432 (PostgreSQL) значение Recv-Q регулярно приближается к Send-Q, это сигнализирует о задержках тредов приложения при обработке входящих accept() вызовов, требуя масштабирования пула соединений HikariCP или добавления контейнеров Keycloak в кластер.

Развертывание Keycloak Quarkus в Docker Compose с PostgreSQL

Переход Keycloak с legacy-сервера WildFly на рантайм Quarkus кардинально изменил модель потребления аппаратных ресурсов: время холодного старта сократилось с десятков секунд до 3–5 секунд, а базовый оверхед памяти снизился на 40–50%. Однако эффективная эксплуатация Keycloak на VPS в Docker Compose требует детерминированного разделения фаз сборки (build-time augmentation) и исполнения (run-time execution), жесткой изоляции ресурсов через cgroups v2 и тонкой синхронизации пула соединений HikariCP с параметрами PostgreSQL.


Архитектура каталогов и разграничение прав доступа

Перед запуском контейнеров формируется изолированная структура каталогов в /opt/keycloak. Эксплуатация сервисов от имени суперпользователя (root) запрещена: контейнер PostgreSQL работает под фиксированным системным UID 999 (postgres), а образ Keycloak Quarkus на базе Red Hat UBI9 использует непривилегированного пользователя keycloak с UID 1000.

Создание директорий и применение прав доступа:

sudo mkdir -p /opt/keycloak/{postgres_data,kc_certs,scripts}
sudo chown -R 999:999 /opt/keycloak/postgres_data
sudo chown -R 1000:1000 /opt/keycloak/kc_certs
sudo chmod 700 /opt/keycloak/postgres_data
sudo chmod 750 /opt/keycloak/kc_certs

Если права на postgres_data не соответствуют UID 999, процесс инициализации initdb завершится аварийным сбоем PANIC: could not open file "global/pg_control": Permission denied.


Оптимизация сборки контейнера (Build-time vs Run-time)

По умолчанию вызов kc.sh start в стандартном контейнере запускает процедуру динамической компиляции и сборки графа метаданных Quarkus (augmentation) при каждом перезапуске инстанса. Это приводит к всплескам утилизации CPU до 100% и задержке старта сервиса на 25–40 секунд.

Для production-окружения применяется двухэтапная сборка через локальный Dockerfile. Build-time параметры (драйвер БД, провайдеры метрик и трассировки) компилируются в бинарный образ один раз.

Создайте файл /opt/keycloak/Dockerfile:

FROM quay.io/keycloak/keycloak:24.0.5 AS builder

# Установка build-time конфигурации для Quarkus
ENV KC_DB=postgres \
    KC_HEALTH_ENABLED=true \
    KC_METRICS_ENABLED=true \
    KC_FEATURES=token-exchange,admin-fine-grained-authz

WORKDIR /opt/keycloak
# Выполнение статической компиляции графа компонентов
RUN /opt/keycloak/bin/kc.sh build

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

ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

Конфигурация переменных окружения (.env)

Переменные выносятся в закрытый файл /opt/keycloak/.env с правами доступа chmod 600. Случайные пароли с энтропией не менее 256 бит генерируются системным вызовом getrandom через OpenSSL:

openssl rand -base64 32

Содержимое /opt/keycloak/.env:

# Параметры СУБД PostgreSQL
POSTGRES_DB=keycloak_prod
POSTGRES_USER=kc_pg_user
POSTGRES_PASSWORD=SECURE_AUTO_GENERATED_POSTGRES_PASSWORD_STRING
POSTGRES_PORT=5432

# Сетевые параметры Keycloak Quarkus
KEYCLOAK_ADMIN=sysadmin
KEYCLOAK_ADMIN_PASSWORD=SECURE_AUTO_GENERATED_ADMIN_PASSWORD_STRING
KEYCLOAK_HOSTNAME=auth.example.com
KEYCLOAK_HTTP_PORT=8080

# Тюнинг JVM под cgroups v2
JAVA_OPTS_APPEND=-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=45 -XX:G1ReservePercent=15 -XX:+ExitOnOutOfMemoryError

Параметр -XX:MaxRAMPercentage=75.0 заставляет JVM рассчитывать максимальный размер кучи (Xmx) динамически, отталкиваясь от лимита, назначенного через cgroups (memory.max), оставляя 25% оперативной памяти контейнера под Metaspace, треды операционной системы и стек native-библиотек. Флаг -XX:+ExitOnOutOfMemoryError предотвращает зависание процесса в деградировавшем состоянии при исчерпании кучи, форсируя немедленный перезапуск контейнера демоном Docker.


Production-манифест docker-compose.yml

Манифест настраивает изолированную внутреннюю сеть bridge без публикации наружу сервисного порта СУБД, определяет healthcheck-проверки на основе сигналов готовности сервисов, задает жесткие ограничения cgroups v2 и монтирует блочное хранилище.

Создайте файл /opt/keycloak/docker-compose.yml:

services:
  postgres:
    image: postgres:16-alpine
    container_name: keycloak-db
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - /opt/keycloak/postgres_data:/var/lib/postgresql/data
    networks:
      backend:
        ipv4_address: 172.28.10.2
    shm_size: 256mb
    command: >
      postgres
      -c shared_buffers=512MB
      -c effective_cache_size=1536MB
      -c work_mem=16MB
      -c maintenance_work_mem=128MB
      -c min_wal_size=1GB
      -c max_wal_size=4GB
      -c checkpoint_completion_target=0.9
      -c checkpoint_timeout=15min
      -c wal_buffers=16MB
      -c default_statistics_target=100
      -c random_page_cost=1.1
      -c effective_io_concurrency=200
      -c max_connections=150
      -c synchronous_commit=on
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2048M
        reservations:
          cpus: '0.5'
          memory: 1024M
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"

  keycloak:
    build:
      context: /opt/keycloak
      dockerfile: Dockerfile
    container_name: keycloak-app
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://172.28.10.2:5432/${POSTGRES_DB}?sslmode=disable
      KC_DB_USERNAME: ${POSTGRES_USER}
      KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
      KC_DB_POOL_INITIAL_SIZE: 10
      KC_DB_POOL_MIN_SIZE: 10
      KC_DB_POOL_MAX_SIZE: 50
      KC_HOSTNAME: ${KEYCLOAK_HOSTNAME}
      KC_HOSTNAME_STRICT_BACKCHANNEL: "false"
      KC_PROXY_HEADERS: xforwarded
      KC_HTTP_ENABLED: "true"
      KC_HTTP_PORT: ${KEYCLOAK_HTTP_PORT}
      KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN}
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      JAVA_OPTS_APPEND: ${JAVA_OPTS_APPEND}
    command: ["start", "--optimized"]
    ports:
      - "127.0.0.1:8080:8080"
    networks:
      backend:
        ipv4_address: 172.28.10.3
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2560M
        reservations:
          cpus: '1.0'
          memory: 1536M
    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 && cat <&3 | grep -q 'HTTP/1.1 200 OK'"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"

networks:
  backend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.28.10.0/24

Анализ критических параметров манифеста

  1. Детерминированный старт --optimized:
    Поскольку контейнер собран через предварительную стадию kc.sh build, команда исполнения принимает аргумент start --optimized. В этом режиме Keycloak пропускает стадию валидации плагинов и схемы БД, сразу открывая порты подсистемы ввода-вывода Netty. Время инициализации сокращается до минимума.
  2. Проксирование заголовков (KC_PROXY_HEADERS: xforwarded):
    В Keycloak на стеке Quarkus устаревшая директива KC_PROXY: edge заменена спецификацией KC_PROXY_HEADERS: xforwarded. При входящем трафике от Nginx или Traefik сервер валидирует заголовки X-Forwarded-For, X-Forwarded-Proto и X-Forwarded-Host. Это исключает ошибку генерации OAuth2 Redirect URI с некорректным портом или протоколом http:// вместо https://.
  3. Связывание с локальным сокетом 127.0.0.1:8080:
    Порт сервиса не публикуется на внешнем интерфейсе 0.0.0.0. Это блокирует неавторизованные сетевые запросы к Keycloak в обход реверс-прокси и правил межсетевого экрана хоста.
  4. Параметры PostgreSQL под NVMe-накопители:
    Значение random_page_cost = 1.1 принуждает оптимизатор запросов PostgreSQL использовать сканирование индексов вместо полного перебора таблиц (Seq Scan), поскольку время произвольного доступа на NVMe-накопителях сопоставимо с последовательным чтением.
    На виртуальной инфраструктуре tropic.host применение KVM-инстансов с аппаратными серверными NVMe-дисками (PCIe 4.0) обеспечивает показатель случайного чтения 4K QD1 на уровне более 50 000 IOPS. В сочетании с параметром effective_io_concurrency = 200 это удерживает дисковую задержку записи WAL-логов (w_await) на уровне менее 0.5 мс даже при пиковых операциях обновления токенов (refresh token rotation).
  5. Безопасная проверка состояния (Zero-dependency Healthcheck):
    Образы Red Hat UBI-minimal не содержат бинарного файла curl. Проверка жизнеспособности Keycloak реализована через встроенный системный псевдо-девайс Bash /dev/tcp. Контейнер отправляет сырой HTTP-запрос к эндпоинту /health/ready и анализирует ответ без установки дополнительного инструментария, раздувающего вектор атак.

Сборка, запуск и проверка распределения ресурсов cgroups v2

Сборка локального образа и перевод стека в фоновый режим:

cd /opt/keycloak
docker compose build --no-cache
docker compose up -d

Мониторинг перехода статусов контейнеров в состояние healthy:

docker compose ps

Вывод команды отображает готовность инфраструктуры:

NAME           IMAGE                  COMMAND                  SERVICE    STATUS                    PORTS
keycloak-db    postgres:16-alpine     "docker-entrypoint.s…"   postgres   Up 45 seconds (healthy)   5432/tcp
keycloak-app   keycloak-keycloak:latest   "/opt/keycloak/bin/k…"   keycloak   Up 30 seconds (healthy)   127.0.0.1:8080->8080/tcp

Фактическое применение лимитов проверяется чтением управляющих файлов контроллера cgroups v2 напрямую из псевдофайловой системы ядра Linux:

# Получение идентификатора контейнера Keycloak
CONTAINER_ID=$(docker inspect --format '{{.Id}}' keycloak-app)

# Проверка аппаратного ограничения оперативной памяти (2560 MB = 2684354560 bytes)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max

# Проверка квоты CPU (2 vCPU = 200000 микросекунд на период 100000 микросекунд)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/cpu.max

Вывод команды cpu.max должен содержать 200000 100000, подтверждая, что ядро Linux не позволит процессу Keycloak монополизировать вычислительные мощности хоста, предотвращая деградацию соседних служб. Отсутствие аппаратного оверселлинга на KVM-нодах tropic.host гарантирует процессу чистый процессорный бюджет без конкуренции за циклы ядра (%st = 0.0%), что удерживает задержку обработки запросов генерации JWT-токенов в рамках жесткого SLA ($p99 < 15\text{ ms}$).

Настройка Nginx Reverse Proxy с TLS 1.3 и передачей заголовков X-Forwarded

После изоляции контейнеров в пространстве пользователя и фиксации ресурсов через cgroups v2 сервис Keycloak принимает входящие TCP-соединения исключительно на локальном интерфейсе хоста (127.0.0.1:8080). Прямой проброс трафика к Quarkus runtime без обратного прокси недопустим: встроенный в Keycloak HTTP-сервер не предназначен для эффективной обработки TLS-хэндшейков под пиковыми нагрузками, не обеспечивает гранулярной фильтрации L7-трафика и уязвим к атакам медленного чтения (Slowloris).

При эксплуатации стека Keycloak на VPS в Docker Compose обратный прокси Nginx выполняет роль шлюза терминации TLS, буферизации клиентских запросов и проброса сетевых контекстов. В архитектуре OpenID Connect и OAuth 2.0 малейшая ошибка в трансляции заголовков приводит к невалидным значениям в iss (Issuer URL), повреждению валидации PKCE-челленджей и циклическим редиректам (HTTP 302 Loop) между шлюзом и бекендом.


Архитектура передачи заголовков и решение проблемы OIDC-буферизации

Keycloak генерирует ссылки перенаправления (Redirect URI) и метаданные эндпоинтов (.well-known/openid-configuration), основываясь на входных заголовках X-Forwarded-*. Если прокси-сервер скрывает реальный протокол или хост клиента, Keycloak отправляет пользователя на внутренний адрес http://127.0.0.1:8080, нарушая спецификацию безопасности OIDC.

Второй критический аспект взаимодействия Nginx с Quarkus — размер HTTP-заголовков. В процессе аутентификации корпоративные SAML-ассерты, JWT-токены и связки сессионных кук (AUTH_SESSION_ID, KEYCLOAK_IDENTITY, KEYCLOAK_SESSION) легко превышают стандартный размер системной страницы памяти в 4 КБ. Если буферы Nginx оставлены в значениях по умолчанию, шлюз разрывает соединение с кодом HTTP 502 Bad Gateway, регистрируя в /var/log/nginx/error.log ошибку:

upstream sent too big header while reading response header from upstream

Для устранения этой проблемы директивы proxy_buffer_size и proxy_busy_buffers_size расширяются до 128–256 КБ.


Генерация параметров Диффи-Хеллмана и получение сертификата Let's Encrypt

Перед сборкой конфигурационного файла генерируются надежные параметры Диффи-Хеллмана для обеспечения прямой секретности (Forward Secrecy) в клиентах, использующих протокол TLS 1.2:

openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096

Первичный выпуск SSL/TLS-сертификата выполняется через ACME-клиент Certbot в режиме certonly с использованием механизма подтверждения домена через веб-корень (webroot), что исключает простой сервиса при последующих автоматических перевыпусках:

# Подготовка директории для проверки ACME-челленджа
mkdir -p /var/www/certbot

# Выпуск сертификата через webroot
certbot certonly --webroot \
  -w /var/www/certbot \
  -d auth.example.com \
  --rsa-key-size 4096 \
  --agree-tos \
  --no-eff-email \
  --register-unsafely-without-email

Конфигурация виртуального хоста Nginx

Конфигурационный файл виртуального хоста /etc/nginx/sites-available/keycloak.conf обеспечивает жесткое принуждение к HTTPS, оптимизацию TLS 1.3, изоляцию буферов и корректный маппинг клиентских заголовков.

Создайте и запишите конфигурацию:

# /etc/nginx/sites-available/keycloak.conf

# Определение пула апстримов
upstream keycloak_upstream {
    server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

# Редирект незащищенного HTTP-трафика на HTTPS с сохранением ACME-челленджей
server {
    listen 80;
    listen [::]:80;
    server_name auth.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
        try_files $uri =404;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# Основной терминатор TLS-соединений
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name auth.example.com;

    # Сертификаты Let's Encrypt
    ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;

    # Криптографические параметры TLS
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers off;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    # Кэширование TLS-сессий и оптимизация рукопожатий
    ssl_session_cache shared:SSL:32m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling для ускорения проверки статуса отзыва сертификата
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/auth.example.com/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # Заголовки безопасности HTTP
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Отключение gzip для предотвращения атак BREACH/CRIME на токены сессий
    gzip off;

    # Ограничение размера тела запроса (загрузка тем оформления и импорт realm)
    client_max_body_size 20M;

    location / {
        proxy_pass http://keycloak_upstream;

        # Использование протокола HTTP 1.1 для поддержки keepalive-соединений с бекендом
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Критически важные заголовки маршрутизации и валидации OIDC
        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_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;

        # Поддержка WebSocket для административной консоли Keycloak
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Расширение буферов для исключения ошибок HTTP 502 при передаче длинных JWT
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
        proxy_temp_file_write_size 256k;

        # Таймауты ожидания ответа от контейнера Quarkus
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Активация виртуального хоста и проверка синтаксиса:

ln -s /etc/nginx/sites-available/keycloak.conf /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

Настройка ядра Linux для ускорения TLS Handshake и TCP BBR

Интенсивный обмен JWT-токенами и микросервисная валидация сессий создают тысячи короткоживущих TCP-соединений. Для предотвращения переполнения очередей сокетов на уровне ядра Linux и минимизации сетевых задержек в /etc/sysctl.d/99-network-tuning.conf вносятся параметры сетевой подсистемы:

# /etc/sysctl.d/99-network-tuning.conf

# Расширение максимального размера очереди входящих соединений
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# Быстрое переиспользование сокетов в состоянии TIME_WAIT
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# Включение TCP Fast Open (RFC 7413) для ускорения повторных TLS-хэндшейков
net.ipv4.tcp_fastopen = 3

# Активация алгоритма управления перегрузкой TCP BBR
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

Применение изменений в ядре без перезагрузки ноды:

sysctl --system

На виртуальных серверах tropic.host премиальные сетевые аплинки 1–10 Гбит/с функционируют с поддержкой прямой BGP-маршрутизации к крупнейшим европейским точкам обмена трафиком (DE-CIX Frankfurt, AMS-IX). Алгоритм TCP BBR в комбинации с аппаратной платформой без оверселлинга гарантирует стабильное удержание задержки на сетевом уровне ($p99 < 3\text{ ms}$ в пределах региона), исключая деградацию времени отклика при выполнении криптографических операций TLS 1.3.


Автоматизация ротации сертификатов и валидация конфигурации

Для исключения сбоев при плановой замене сертификатов Certbot настраивается системный таймер systemd с директивой автоматической перезагрузки Nginx. Проверка конфигурации автообновления:

# Тестовый запуск ротации сертификатов в режиме dry-run
certbot renew --dry-run --post-hook "systemctl reload nginx"

Комплексная верификация развернутого шлюза выполняется с удаленного хоста для проверки корректности согласования шифров TLS 1.3 и проброса метаданных OIDC:

# Проверка согласования протокола TLS 1.3 и шифра
openssl s_client -connect auth.example.com:443 -tls1_3 -servername auth.example.com < /dev/null 2>&1 | grep -E "Protocol|Cipher"

Вывод команды подтверждает применение современных стандартов шифрования:

    New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384

Финальная проверка эндпоинта обнаружения OpenID Connect подтверждает корректную генерацию внешних URL самим Keycloak без искажения протокола и портов:

curl -s https://auth.example.com/realms/master/.well-known/openid-configuration | grep -o '"issuer":[^,]*'

Ответ должен содержать точно заданный домен и схему: "issuer":"https://auth.example.com/realms/master". Это гарантирует, что заголовки X-Forwarded-Proto и X-Forwarded-Host успешно достигли JVM-процесса Keycloak, сформировав доверенную среду для последующей интеграции клиентов авторизации.

Настройка первого Realm, OpenID Connect клиента и защиты от брутфорса

После подтверждения доступности эндпоинта OpenID Connect Discovery управление инстансом переводится в плоскость безопасной изоляции доменов аутентификации. Развертывание Keycloak на VPS в Docker Compose требует разграничения служебной инфраструктуры и прикладных сервисов: использование базового пространства master для хранения пользователей или регистрации корпоративных клиентов категорически недопустимо. Реалм master предназначен исключительно для супер-администраторов кластера; любая компрометация учетных записей или уязвимость в клиентских редиректах внутри него дает злоумышленнику вектор эскалации привилегий до уровня контроля над всем JVM-процессом и базой данных.

Автоматизация конфигурирования реализуется через утилиту командной строки kcadm.sh, встроенную в образ Keycloak. Это исключает ручные ошибки в веб-интерфейсе и позволяет хранить конфигурации в виде декларативных скриптов инициализации.


Создание изолированного Realm и сайзинг жизненного цикла токенов

Инициализация сессии kcadm.sh внутри контейнера выполняется через вызов локального REST API с авторизацией администратора:

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh config credentials \
  --server http://localhost:8080 \
  --realm master \
  --user "${KEYCLOAK_ADMIN}" \
  --password "${KEYCLOAK_ADMIN_PASSWORD}"

Для рабочих сервисов создается изолированный реалм production. Одновременно переопределяются временные интервалы жизни токенов и пользовательских сессий:

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh create realms \
  -s realm=production \
  -s enabled=true \
  -s displayName="Corporate Production Realm" \
  -s sslRequired=external \
  -s registrationAllowed=false \
  -s resetPasswordAllowed=true \
  -s editUsernameAllowed=false \
  -s accessTokenLifespan=300 \
  -s ssoSessionIdleTimeout=1800 \
  -s ssoSessionMaxLifespan=36000 \
  -s offlineSessionIdleTimeout=2592000 \
  -s offlineSessionMaxLifespan=5184000 \
  -s accessCodeLifespan=60 \
  -s accessCodeLifespanUserAction=300 \
  -s revokeRefreshToken=true \
  -s refreshTokenMaxReuse=0

Техническое обоснование выбранных параметров: * accessTokenLifespan=300: Время жизни Access Token (JWT) ограничено 5 минутами. Поскольку stateless-токены валидируются микросервисами по открытому ключу (JWKS) без постоянного обращения к базе Keycloak, короткий TTL минимизирует окно атаки при перехвате токена без необходимости поддерживать массивные распределенные черные списки (revocation lists). * revokeRefreshToken=true и refreshTokenMaxReuse=0: Включение жесткой ротации Refresh Token (RTR — Refresh Token Rotation). При каждом обновлении пары токенов старый Refresh Token инвалидируется, а клиенту выдается новый. Если фиксируется повторная попытка использования уже отозванного refresh-токена, Keycloak немедленно аннулирует всю сессию пользователя, нейтрализуя последствия кражи токена из локального хранилища браузера. * sslRequired=external: Требование HTTPS для внешних клиентов при сохранении возможности обмена данными между Nginx и Keycloak внутри изолированной Docker-сети internal_net по чистому HTTP.


Регистрация и харденинг OpenID Connect (OIDC) клиента

Для интеграции веб-приложений и backend-сервисов создается OIDC-клиент с поддержкой протокола авторизации Authorization Code Flow с обязательным расширением PKCE (Proof Key for Code Exchange, RFC 7636).

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh create clients -r production \
  -s clientId=enterprise-portal \
  -s name="Corporate Web Portal" \
  -s enabled=true \
  -s protocol=openid-connect \
  -s publicClient=false \
  -s clientAuthenticatorType=client-secret \
  -s standardFlowEnabled=true \
  -s implicitFlowEnabled=false \
  -s directAccessGrantsEnabled=false
## Настройка первого Realm, OpenID Connect клиента и защиты от брутфорса

После верификации TLS-терминации и валидации discovery-эндпоинта управление инстансом переводится на уровень прикладной безопасности. Развертывание связки Keycloak на VPS в Docker Compose требует изоляции системных сущностей от пользовательских учетных записей: эксплуатация встроенного пространства `master` для хранения рабочих учетных записей или подключения внешних сервисов запрещена регламентами безопасности. Дефолтный `master` предназначен исключительно для супер-администраторов кластера; регистрация прикладных клиентов внутри него создает вектор вертикальной эскалации привилегий в случае выявления уязвимостей в redirect URI или цепочках токенов.

---

### Создание изолированного Realm и конфигурирование TTL токенов

Инициализация сущностей автоматизируется через встроенную утилиту командной строки `kcadm.sh`. Это исключает дрейф конфигураций (configuration drift) и обеспечивает повторяемость развертывания без обращения к административной веб-консоли.

Авторизация сессии `kcadm.sh` в запущенном контейнере:

```bash
docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh config credentials \
  --server http://127.0.0.1:8080 \
  --realm master \
  --user "${KEYCLOAK_ADMIN}" \
  --password "${KEYCLOAK_ADMIN_PASSWORD}"

Создание продуктивного пространства production-apps с ужесточенными параметрами жизненного цикла токенов и ротации:

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh create realms \
  -s realm=production-apps \
  -s enabled=true \
  -s displayName="Production Security Realm" \
  -s sslRequired=external \
  -s registrationAllowed=false \
  -s resetPasswordAllowed=true \
  -s editUsernameAllowed=false \
  -s accessTokenLifespan=300 \
  -s ssoSessionIdleTimeout=1800 \
  -s ssoSessionMaxLifespan=28800 \
  -s offlineSessionIdleTimeout=2592000 \
  -s accessCodeLifespan=60 \
  -s revokeRefreshToken=true \
  -s refreshTokenMaxReuse=0

Технический разбор ключевых параметров жизненного цикла сессий: * accessTokenLifespan=300: Время жизни Access Token (JWT) зафиксировано на уровне 5 минут (300 секунд). Stateless-токены проверяются микросервисами локально по открытому ключу (JWKS) без постоянных синхронных сетевых запросов к базе данных Keycloak. Короткий TTL минимизирует окно атаки при компрометации токена на стороне клиента. * revokeRefreshToken=true и refreshTokenMaxReuse=0: Реализация стандарта Refresh Token Rotation (RTR). При каждом обращении клиента за новой парой токенов старый refresh-токен немедленно инвалидируется. Повторное предъявление уже отозванного токена расценивается системой как инцидент безопасности (token theft), что вызывает каскадную аннуляцию всей родительской SSO-сессии пользователя. * ssoSessionIdleTimeout=1800: Завершение неактивной сессии через 30 минут простоя.


Регистрация клиента OpenID Connect по стандартам OAuth 2.1 и PKCE

Для веб-приложений и защищаемых сервисов регистрируется закрытый (confidential) OIDC-клиент с принудительной защитой кода авторизации через механизм PKCE (RFC 7636).

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh create clients -r production-apps \
  -s clientId=core-workload-client \
  -s name="Production Workload Service" \
  -s enabled=true \
  -s protocol=openid-connect \
  -s publicClient=false \
  -s clientAuthenticatorType=client-secret \
  -s standardFlowEnabled=true \
  -s implicitFlowEnabled=false \
  -
## Настройка первого Realm, OpenID Connect клиента и защиты от брутфорса

После верификации TLS-терминации и валидации discovery-эндпоинта управление инстансом переводится на уровень прикладной безопасности. Архитектурный регламент развертывания Keycloak на VPS в Docker Compose требует строгой изоляции системного контура от прикладных сервисов: использование базового пространства `master` для хранения пользовательских учетных записей или подключения внешних клиентов недопустимо. Реалм `master` зарезервирован исключительно под задачи супер-администрирования самого кластера. Любая компрометация учетных записей или некорректная настройка клиентских редиректов внутри `master` создает прямой вектор эскалации привилегий до полного контроля над JVM-процессом и реляционной базой данных.

---

### Архитектурное разделение и декларативная инициализация Realm

Для прикладных сервисов формируется отдельный домен аутентификации — `production-apps`. Чтобы исключить расхождения в конфигурациях (configuration drift), параметры реалма задаются в виде структурированного JSON-манифеста, передаваемого через встроенный CLI-клиент `kcadm.sh`.

Создадим декларативный файл конфигурации реалма `realm-security-config.json`:

```json
{
  "realm": "production-apps",
  "displayName": "Production Services Realm",
  "enabled": true,
  "sslRequired": "external",
  "registrationAllowed": false,
  "resetPasswordAllowed": true,
  "editUsernameAllowed": false,
  "rememberMe": false,
  "accessTokenLifespan": 300,
  "ssoSessionIdleTimeout": 1800,
  "ssoSessionMaxLifespan": 28800,
  "offlineSessionIdleTimeout": 2592000,
  "offlineSessionMaxLifespan": 5184000,
  "accessCodeLifespan": 60,
  "accessCodeLifespanUserAction": 300,
  "revokeRefreshToken": true,
  "refreshTokenMaxReuse": 0
}

Анализ параметров жизненного цикла токенов и сессий: * accessTokenLifespan: 300: Время жизни Access Token (JWT) зафиксировано на уровне 5 минут. Поскольку микросервисы валидируют stateless-токены локально по открытым ключам (JWKS) без постоянных синхронных запросов в Keycloak, короткий TTL минимизирует окно эксплуатации в случае перехвата токена на стороне клиента. * revokeRefreshToken: true и refreshTokenMaxReuse: 0: Включение механизма ротации Refresh Token (RTR — Refresh Token Rotation). При каждом обращении за новым Access Token выданный ранее Refresh Token уничтожается. Если атакующий попытается применить перехваченный старый токен повторно, Keycloak фиксирует факт компрометации и каскадно аннулирует всю сессию пользователя, делая недействительными все активные токены цепочки. * ssoSessionIdleTimeout: 1800 и ssoSessionMaxLifespan: 28800: Завершение неактивной сессии через 30 минут простоя с принудительным закрытием сессии через 8 часов независимо от активности.

Применение конфигурации выполняется авторизованным вызовом kcadm.sh внутри работающего контейнера:

# Аутентификация администратора кластера через локальный сокет
docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh config credentials \
  --server http://127.0.0.1:8080 \
  --realm master \
  --user "${KEYCLOAK_ADMIN}" \
  --password "${KEYCLOAK_ADMIN_PASSWORD}"

# Импорт конфигурации нового Realm
docker compose exec -i keycloak /opt/keycloak/bin/kcadm.sh create realms \
  -f - < realm-security-config.json

Регистрация клиента OIDC по спецификации OAuth 2.1 и PKCE

Для взаимодействия backend-приложений с Keycloak создается конфиденциальный клиент (Confidential Client), защищенный криптографическим секретом и принудительным обменом ключами через протокол PKCE (Proof Key for Code Exchange, RFC 7636).

Подготовим спецификацию клиента client-app-spec.json:

{
  "clientId": "workload-portal-backend",
  "name": "Production Workload Portal",
  "enabled": true,
  "protocol": "openid-connect",
  "publicClient": false,
  "bearerOnly": false,
  "standardFlowEnabled": true,
  "implicitFlowEnabled": false,
  "directAccessGrantsEnabled": false,
  "serviceAccountsEnabled": true,
  "rootUrl": "https://app.example.com",
  "baseUrl": "/",
  "redirectUris": [
    "https://app.example.com/oauth2/callback"
  ],
  "webOrigins": [
    "https://app.example.com"
  ],
  "attributes": {
    "pkce.code.challenge.method": "S256",
    "post.logout.redirect.uris": "https://app.example.com/",
    "backchannel.logout.session.required": "true",
    "access.token.lifespan": "300"
  }
}

Критические параметры безопасности: 1. implicitFlowEnabled: false и directAccessGrantsEnabled: false: Полное отключение устаревшего Implicit Flow (где токены передавались в URL-фрагменте браузера) и Resource Owner Password Credentials (ROPC). Передача логина и пароля напрямую в backend-сервис исключена: пользователь аутентифицируется исключительно на защищенной форме Keycloak. 2. pkce.code.challenge.method: "S256": Активация PKCE с алгоритмом SHA-256. Клиент генерирует криптографический верификатор (code_verifier) и передает его хэш (code_challenge) при переходе на авторизацию. Перехват временного authorization_code в канале передачи данных становится бесполезным, так как обмен кода на токен требует оригинального секрета code_verifier. 3. redirectUris: Запрет масок подстановки (*). Указание только точных абсолютных URI исключает атаки класса Open Redirect и кражу кодов авторизации сторонними доменами.

Регистрация клиента и извлечение сгенерированного секрета для приложения:

# Создание клиента в пространстве production-apps
docker compose exec -i keycloak /opt/keycloak/bin/kcadm.sh create clients \
  -r production-apps \
  -f - < client-app-spec.json

# Получение сгенерированного client_secret для интеграции в микросервис
CLIENT_UUID=$(docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh get clients \
  -r production-apps -q clientId=workload-portal-backend --fields id | grep -o '"id" : "[^"]*' | cut -d'"' -f4)

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh get clients/$CLIENT_UUID/client-secret \
  -r production-apps

Активация и калибровка защиты от брутфорса (Brute Force Detection)

Keycloak содержит модуль защиты от атак методом подбора паролей и распределенного credential stuffing. Модуль отслеживает неудачные попытки входа, динамически увеличивая время блокировки учетной записи без раскрытия злоумышленнику статуса блокировки (защита от перечисления пользователей).

Конфигурирование параметров защиты для реалма production-apps:

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/production-apps \
  -s bruteForceProtected=true \
  -s permanentLockout=false \
  -s maxFailureWaitSeconds=900 \
  -s minimumQuickLoginWaitSeconds=60 \
  -s waitIncrementSeconds=60 \
  -s quickLoginCheckMilliSeconds=1
## Настройка первого Realm, OpenID Connect клиента и защиты от брутфорса

После верификации TLS-терминации и доступности discovery-эндпоинта управление инстансом переводится на уровень прикладной безопасности. Эксплуатация Keycloak на VPS в Docker Compose требует жесткого разграничения контуров: системный реалм `master` используется исключительно для супер-администрирования кластера. Хранение пользователей, интеграция корпоративных приложений или связывание с внешними identity-провайдерами внутри `master` запрещены корпоративными регламентами, поскольку любая компрометация учетных записей или ошибка в клиентских редиректах создает вектор эскалации привилегий до уровня доступа к системным вызовам и всей базе данных.

Все прикладные сервисы изолируются в выделенном пространстве, управление которым автоматизируется через встроенный интерфейс командной строки `kcadm.sh`.

---

### Архитектурная изоляция: декларативное создание прикладного Realm

Для управления конфигурациями без риска рассинхронизации параметров (configuration drift) используется декларативный манифест. На хосте формируется файл спецификации реалма `realm-isolated-prod.json`, описывающий базовые политики безопасности, аудит и параметры жизни токенов:

```json
{
  "realm": "corporate-prod",
  "displayName": "Corporate Production IAM",
  "enabled": true,
  "sslRequired": "external",
  "registrationAllowed": false,
  "resetPasswordAllowed": true,
  "editUsernameAllowed": false,
  "rememberMe": false,
  "verifyEmail": true,
  "loginWithEmailAllowed": true,
  "duplicateEmailsAllowed": false,
  "accessTokenLifespan": 300,
  "ssoSessionIdleTimeout": 1800,
  "ssoSessionMaxLifespan": 28800,
  "offlineSessionIdleTimeout": 2592000,
  "offlineSessionMaxLifespan": 5184000,
  "accessCodeLifespan": 60,
  "accessCodeLifespanUserAction": 300,
  "revokeRefreshToken": true,
  "refreshTokenMaxReuse": 0
}

Критические параметры жизненного цикла токенов и сессий: * accessTokenLifespan: 300: Время жизни JWT Access Token ограничено 5 минутами. Поскольку микросервисы валидируют подпись токена локально через JWKS-эндпоинт без отправки запросов в базу Keycloak, ультракороткий TTL минимизирует окно атаки в случае утечки токена из оперативной памяти или браузерного хранилища. * revokeRefreshToken: true и refreshTokenMaxReuse: 0: Принудительная активация Refresh Token Rotation (RTR). Каждая операция обновления пары токенов уничтожает предыдущий refresh-токен. Повторное использование уже отозванного токена интерпретируется алгоритмом как инцидент безопасности (token theft) и влечет немедленное аннулирование всей цепочки токенов и родительской сессии пользователя. * ssoSessionIdleTimeout: 1800 / ssoSessionMaxLifespan: 28800: Таймаут неактивности составляет 30 минут, а абсолютная продолжительность рабочей сессии ограничена 8 часами, после чего требуется повторная аутентификация.

Импорт спецификации выполняется пакетно через локальный сокет контейнера:

# Получение административного токена авторизации для CLI
docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh config credentials \
  --server http://127.0.0.1:8080 \
  --realm master \
  --user "${KEYCLOAK_ADMIN}" \
  --password "${KEYCLOAK_ADMIN_PASSWORD}"

# Создание изолированного пространства corporate-prod
docker compose exec -i keycloak /opt/keycloak/bin/kcadm.sh create realms \
  -f - < realm-isolated-prod.json

Регистрация клиента OIDC по спецификации OAuth 2.1 и интеграция PKCE

Подключение бэкенд-сервисов и Single Page Applications (SPA) выполняется по профилю OAuth 2.1 с отказом от небезопасных потоков авторизации (Implicit Flow, Resource Owner Password Credentials). Код авторизации защищается криптографическим механизмом PKCE (RFC 7636).

Спецификация клиента workload-client.json:

{
  "clientId": "workload-portal-backend",
  "name": "Production Workload Portal",
  "enabled": true,
  "protocol": "openid-connect",
  "publicClient": false,
  "bearerOnly": false,
  "standardFlowEnabled": true,
  "implicitFlowEnabled": false,
  "directAccessGrantsEnabled": false,
  "serviceAccountsEnabled": true,
  "rootUrl": "https://app.example.com",
  "baseUrl": "/",
  "redirectUris": [
    "https://app.example.com/api/v1/auth/callback"
  ],
  "webOrigins": [
    "https://app.example.com"
  ],
  "attributes": {
    "pkce.code.challenge.method": "S256",
    "post.logout.redirect.uris": "https://app.example.com/",
    "backchannel.logout.session.required": "true",
    "access.token.lifespan": "300"
  }
}

Инженерные ограничения конфигурации: 1. Исключение wildcard-масок в redirectUris: Применение конструкций вроде https://app.example.com/* запрещено. Допускается только точный URI коллбэка приложения. Это исключает атаки класса Open Redirector и компрометацию временных кодов авторизации. 2. Принудительный PKCE (pkce.code.challenge.method: "S256"): Клиент обязан формировать code_verifier (энтропия $\ge 256$ бит) и передавать его SHA-256 хэш (code_challenge). Даже в случае перехвата кода авторизации на уровне сетевых прокси или системных логов атакующий не сможет обменять его на токены без исходного секрета. 3. serviceAccountsEnabled: true: Включение OAuth 2.0 Client Credentials Grant для безопасного взаимодействия микросервисов между собой (M2M) с аутентификацией по клиентскому секрету или mTLS-сертификату.

Регистрация клиента и извлечение приватного ключа:

# Импорт клиента в целевой реалм
docker compose exec -i keycloak /opt/keycloak/bin/kcadm.sh create clients \
  -r corporate-prod \
  -f - < workload-client.json

# Получение сгенерированного UUID клиента
CLIENT_UUID=$(docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh get clients \
  -r corporate-prod -q clientId=workload-portal-backend --fields id | grep -o '"id" : "[^"]*' | cut -d'"' -f4)

# Вывод сгенерированного client_secret для конфигурации сервиса
docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh get clients/"${CLIENT_UUID}"/client-secret \
  -r corporate-prod

Многофакторная аутентификация (MFA) и криптографическая нагрузка

Для обеспечения нулевого доверия (Zero Trust) в реалме настраивается принудительное прохождение второго фактора (Time-based One-Time Password, RFC 6238, либо аппаратные ключи WebAuthn/FIDO2).

Настройка политик OTP через CLI

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/corporate-prod \
  -s otpPolicyType=totp \
  -s otpPolicyAlgorithm=HmacSHA256 \
  -s otpPolicyPeriod=30 \
  -s otpPolicyDigits=6 \
  -s otpPolicyLookAheadWindow=1

Переход с алгоритма SHA-1 на HmacSHA256 увеличивает криптостойкость генерируемых кодов, а окно упреждения otpPolicyLookAheadWindow=1 компенсирует дрейф системных часов на клиентских смартфонах без расширения вектора атаки по времени.

Нагрузочные характеристики алгоритмов хэширования паролей

Хранение паролей в Keycloak по умолчанию опирается на алгоритм pbkdf2-sha512 с числом итераций не менее 210 000 (согласно рекомендациям OWASP). Каждая попытка аутентификации вызывает интенсивный расход циклов процессора для вычисления хэша HMAC.

При одновременном входе 200–500 пользователей (типичный утренний пик в корпоративных сетях) пиковая нагрузка на CPU возрастает многократно:

$$\text{T}_c = \text{Requests} \times \text{Iterations} \times \text{Cost per round}$$

Если виртуальный сервер страдает от переподписки ресурсов (CPU Overselling), процент украденных тактов процессора (%st в top или vmstat 1) может подскочить до 15–40%. В результате время отклика точки входа деградирует с 60 мс до катастрофических 3–5 секунд ($p99 > 4000\text{ ms}$), вызывая таймауты upstream-шлюзов Nginx и каскадный отказ аутентификации.

Аппаратная база tropic.host полностью исключает эту проблему: гарантированное выделение физических ядер AMD EPYC и Ryzen 9 с нулевым троттлингом (%st = 0.0%) обеспечивает предсказуемое выполнение криптографических примитивов PBKDF2/Argon2id с удержанием сетевой задержки на уровне $p99 < 85\text{ ms}$ даже при экстремальных штормах авторизации.


Двухуровневая защита от распределенного Brute Force и Credential Stuffing

Защита учетных записей от перебора строится на синергии внутреннего счетчика Keycloak и фильтрации трафика на уровне ядра Linux и обратного прокси-сервера.

                  Входящий трафик (L7 HTTPS)
                              │
                              ▼
┌───────────────────────────────────────────────────────────┐
│ Nginx: limit_req_zone (Rate Limiting по IP)               │
│ Защита сетевого сокета от истощения соединений           │
└─────────────────────────────┬─────────────────────────────┘
                              │ Проксирование (X-Forwarded-For)
                              ▼
┌───────────────────────────────────────────────────────────┐
│ Keycloak: Brute Force Detection Engine                   │
│ Блокировка учетной записи по хешу имени пользователя     │
└─────────────────────────────┬─────────────────────────────┘
                              │
                ┌─────────────┴─────────────┐
                ▼                           ▼
    Валидные реквизиты              Превышение счетчика
    Генерация JWT/MFA               Временный Lockout аккаунта

Уровень 1: Активация Brute Force Detection в Keycloak

Встроенный модуль блокирует перебор учетных данных, даже если атака распределена по обширному пулу прокси-серверов и обращается к одной конкретной учетной записи с разных IP-адресов.

docker compose exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/corporate-prod \
  -s bruteForceProtected=true \
  -s permanentLockout=false \
  -s maxFailureWaitSeconds=900 \
  -s minimumQuickLoginWaitSeconds=60 \
  -s waitIncrementSeconds=60 \
  -s quickLoginCheckMilliSeconds=1
## Настройка первого Realm, OpenID Connect клиента и защиты от брутфорса

После завершения отладки reverse proxy и валидации эндпоинта Discovery базовый инстанс готов к разграничению контуров безопасности. Эксплуатация Keycloak на VPS в Docker Compose требует строгого следования принципу наименьших привилегий: системная область `master` используется исключительно для сопровождения самого сервиса и глобального аудита. Размещение прикладных учетных записей или подключение клиентских приложений внутри `master` нарушает базовую модель изоляции: любая ошибка в конфигурации redirect URI или перехват токена администратора скомпрометирует весь кластер и реляционную СУБД.

Для прикладных сервисов создается изолированный домен безопасности (Realm). Это обеспечивает автономную базу пользователей, независимые криптографические ключи подписи JWT, раздельные политики паролей и сессионные тайм-ауты.

---

### Автоматизация провижининга Realm через Admin REST API

Вместо ручных операций в интерфейсе применяется прямой вызов Keycloak Admin REST API через локальный сокет. Это гарантирует версионирование конфигураций в репозитории и предотвращает расхождение параметров между staging- и production-средами.

Скрипт генерации рабочего реалма `corporate-workload` с жесткими политиками сессий:

```bash
#!/usr/bin/env bash
set -euo pipefail

# 1. Получение сервисного токена администратора
ADMIN_TOKEN=$(curl -s -X POST "http://127.0.0.1:8080/realms/master/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=${KEYCLOAK_ADMIN}" \
  -d "password=${KEYCLOAK_ADMIN_PASSWORD}" \
  -d "grant_type=password" \
  -d "client_id=admin-cli" | jq -r '.access_token')

# 2. Создание изолированного Realm с ограничением TTL токенов
curl -s -X POST "http://127.0.0.1:8080/admin/realms" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "realm": "corporate-workload",
    "displayName": "Corporate Production Services",
    "enabled": true,
    "sslRequired": "external",
    "registrationAllowed": false,
    "loginWithEmailAllowed": true,
    "duplicateEmailsAllowed": false,
    "resetPasswordAllowed": true,
    "editUsernameAllowed": false,
    "accessTokenLifespan": 300,
    "ssoSessionIdleTimeout": 1800,
    "ssoSessionMaxLifespan": 28800,
    "offlineSessionIdleTimeout": 2592000,
    "offlineSessionMaxLifespan": 5184000,
    "accessCodeLifespan": 60,
    "accessCodeLifespanUserAction": 300,
    "revokeRefreshToken": true,
    "refreshTokenMaxReuse": 0
  }'

Инженерное назначение параметров: * accessTokenLifespan: 300: Время жизни JWT Access Token установлено на 5 минут. Микросервисы валидируют подпись токена локально через JWKS без постоянных сетевых вызовов к Keycloak. Короткий интервал делает украденный stateless-токен недействительным до того, как атакующий успеет развернуть эксплуатацию. * revokeRefreshToken: true и refreshTokenMaxReuse: 0: Реализация Refresh Token Rotation (RTR). При каждом обновлении сессии выданный ранее Refresh Token уничтожается, а клиенту отправляется новая криптографическая пара. При повторной попытке передачи уже использованного refresh-токена сервер классифицирует действие как атаку перехвата (token theft) и моментально инвалидирует всю сессию пользователя. * ssoSessionIdleTimeout: 1800 / ssoSessionMaxLifespan: 28800: При отсутствии активности сессия сбрасывается через 30 минут. Максимальная длительность сессии ограничена 8 часами, после чего повторная аутентификация становится обязательной независимо от текущих действий.


Регистрация клиента OIDC по стандарту OAuth 2.1 (PKCE)

В корпоративном секторе устаревшие профили Implicit Flow и Resource Owner Password Credentials (ROPC) запрещены из-за рисков перехвата учетных данных в URL-фрагментах или на промежуточных proxy-серверах. Интеграция приложений выполняется строго через Authorization Code Flow с расширением PKCE (RFC 7636).

Регистрация конфиденциального клиента через REST API:

curl -s -X POST "http://127.0.0.1:8080/admin/realms/corporate-workload/clients" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "frontend-portal-client",
    "name": "Production Workload Portal",
    "enabled": true,
    "protocol": "openid-connect",
    "publicClient": false,
    "bearerOnly": false,
    "standardFlowEnabled": true,
    "implicitFlowEnabled": false,
    "directAccessGrantsEnabled": false,
    "serviceAccountsEnabled": true,
    "rootUrl": "https://app.example.com",
    "baseUrl": "/",
    "redirectUris": [
      "https://app.example.com/api/auth/callback"
    ],
    "webOrigins": [
      "https://app.example.com"
    ],
    "attributes": {
      "pkce.code.challenge.method": "S256",
      "post.logout.redirect.uris": "https://app.example.com/",
      "backchannel.logout.session.required": "true",
      "access.token.lifespan": "300"
    }
  }'

Обязательные требования к безопасности клиента: 1. Точные redirectUris: Применение символа подстановки * исключено. Допускаются исключительно фиксированные пути обратного вызова (callback). Любое отклонение в URI блокирует выдачу кода авторизации, нейтрализуя атаки класса Open Redirect. 2. Алгоритм S256 для PKCE: Клиент создает криптографический code_verifier и отправляет его дайджест SHA-256 (code_challenge) в запросе на авторизацию. Обмен кода на токен требует раскрытия исходного code_verifier, что исключает атаку Man-in-the-Middle при перехвате authorization_code. 3. serviceAccountsEnabled: true: Активация Machine-to-Machine (M2M) взаимодействия для фоновых демонов и планировщиков через Client Credentials Grant без привязки к конкретным учетным записям сотрудников.

Для извлечения сгенерированного секрета клиента (client_secret):

# Получение внутреннего UUID зарегистрированного клиента
INTERNAL_CLIENT_ID=$(curl -s "http://127.0.0.1:8080/admin/realms/corporate-workload/clients?clientId=frontend-portal-client" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" | jq -r '.[0].id')

# Получение секретного ключа
CLIENT_SECRET=$(curl -s "http://127.0.0.1:8080/admin/realms/corporate-workload/clients/${INTERNAL_CLIENT_ID}/client-secret" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" | jq -r '.value')

echo "Generated Client Secret: ${CLIENT_SECRET}"

Многофакторная аутентификация (MFA) и расчет аппаратной нагрузки

Внедрение второго фактора (Time-based One-Time Password, RFC 6238) выполняется на уровне политик аутентификации реалма.

Обновление параметров OTP для перехода на защищенный алгоритм хэширования:

curl -s -X PUT "http://127.0.0.1:8080/admin/realms/corporate-workload" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "otpPolicyType": "totp",
    "otpPolicyAlgorithm": "HmacSHA256",
    "otpPolicyPeriod": 30,
    "otpPolicyDigits": 6,
    "otpPolicyLookAheadWindow": 1
  }'

Использование HmacSHA256 вместо стандартного SHA-1 повышает устойчивость против коллизий. Окно упреждения otpPolicyLookAheadWindow: 1 компенсирует расхождение системного времени на мобильных устройствах клиентов в пределах $\pm 30$ секунд.

Аппаратный сайзинг и профиль вычислений парольных хэшей

Keycloak применяет алгоритм хеширования паролей PBKDF2 с HMAC-SHA512 и дефолтным числом раундов 210 000. Каждая операция валидации пароля генерирует интенсивный расчет в одном потоке CPU:

$$T_{\text{auth}} \approx N_{\text{iterations}} \times \tau_{\text{HMAC-SHA512}} + \tau_{\text{DB-lookup}}$$

При утреннем пике авторизации сотни пользователей одновременно обращаются к эндпоинту /token. Если хост функционирует в среде с агрессивной переподпиской процессорных ресурсов (vCPU Overselling), гипервизор принудительно приостанавливает выполнение виртуальной машины. В утилите top или vmstat 1 значение CPU Steal Time (%st) подскакивает до $15-35\%$. В таких условиях длительность расчета PBKDF2 возрастает с нормальных $45\text{ ms}$ до $1200-3000\text{ ms}$ ($p99 > 3.5\text{ s}$), что провоцирует таймауты в Nginx (504 Gateway Time-out) и лавинообразные повторные запросы клиентов.

Инфраструктура tropic.host ликвидирует фактор деградации задержек: выделенные вычислительные ядра AMD EPYC и Ryzen 9 с чистой виртуализацией KVM функционируют с показателем %st = 0.0%. Отсутствие теплового троттлинга и задержки доступа к памяти удерживают общее время криптографической обработки под нагрузкой в границах $p99 < 70\text{ ms}$.


Многоуровневое отражение брутфорса и Credential Stuffing

Защита периметра аутентификации требует разделения задач: Keycloak блокирует перебор учетных записей логически, в то время как сеть и reverse proxy пресекают исчерпание системных сокетов.

1. Логическая блокировка в Keycloak (Account Lockout)

Модуль защиты отслеживает аномальные попытки входа и накладывает прогрессивный штрафной тайм-аут:

curl -s -X PUT "http://127.0.0.1:8080/admin/realms/corporate-workload" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "bruteForceProtected": true,
    "permanentLockout": false,
    "maxFailureWaitSeconds": 900,
    "minimumQuickLoginWaitSeconds": 60,
    "waitIncrementSeconds": 60,
    "quickLoginCheckMilliSeconds": 1000,
    "maxDeltaTimeSeconds": 43200,
    "failureFactor": 5
  }'
  • failureFactor: 5: Учетная запись блокируется после 5 последовательных ошибок ввода пароля.
  • permanentLockout: false: Аккаунт не выключается навсегда (что исключает атаки типа «отказ в обслуживании» против легитимных пользователей). Время блокировки динамически растет с каждым новым инцидентом до максимума в 15 минут (maxFailureWaitSeconds: 900).

2. Защита сетевого сокета на уровне ядра Linux и Nginx

Атака распределенного брутфорса способна переполнить стек TCP-соединений хоста. В конфигурацию ядра /etc/sysctl.d/99-keycloak-security.conf вносятся директивы управления очередями:

# Защита от SYN-флуда и переполнения очереди соединений
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192

# Быстрая утилизация соединений в состоянии TIME_WAIT
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# Ограничение выделения памяти под TCP-буферы
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

Применение параметров:

sysctl --system

В блоке http конфигурации Nginx ограничивается частота обращений к формам входа через модуль ngx_http_limit_req_module:

# Выделение зоны памяти под хранение состояний IP-адресов (10 МБ = ~160 000 адресов)
limit_req_zone $binary_remote_addr zone=keycloak_auth_zone:10m rate=5r/s;

server {
    server_name auth.example.com;

    # Изолированный лимит для эндпоинтов генерации токенов и аутентификации
    location ~* /realms/.+/protocol/openid-connect/(auth|token) {
        limit_req zone=keycloak_auth_zone burst=10 nodelay;
        limit_req_status 429;

        proxy_pass http://keycloak_upstream;
        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 https;
    }

    location / {
        proxy_pass http://keycloak_upstream;
        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 https;
    }
}

Директива limit_req zone=keycloak_auth_zone burst=10 nodelay срезает всплески запросов: клиент может совершить до 5 обращений в секунду с кратковременным буфером в 10 пакетов. При превышении порога Nginx мгновенно возвращает код 429 Too Many Requests, не нагружая контейнер Keycloak и освобождая процессорные ресурсы под легитимные сессии.

Регламент Disaster Recovery: бэкап базы данных и экспорт конфигураций realm

Эксплуатация Keycloak на VPS в Docker Compose в рамках корпоративного контура аутентификации требует разграничения двух типов резервных копий: 1. Транзакционный снимок состояния (Stateful Database Backup): бинарный или SQL-дамп PostgreSQL, включающий активные пользовательские сессии, хэши паролей (PBKDF2/Argon2), ключи федерации (LDAP/Active Directory), счетчики неудачных попыток входа и аудит событий (event_entity). Необходим для соблюдения RPO (Recovery Point Objective) $\le 1$ часа. 2. Декларативный экспорт Realm (Metadata Infrastructure-as-Code): версионируемый JSON-манифест, описывающий клиентов OIDC/SAML, mappers, scope-параметры, цепочки аутентификации (Authentication Flows) и политики доступа. Необходим для воспроизведения конфигурации с нуля без переноса устаревших сессий и при миграциях между мажорными версиями.

Выполнение бэкапов на инфраструктуре с высокой плотностью виртуализации часто вызывает деградацию p99-латентности аутентификации из-за очередей ввода-вывода (IO wait %wa) и кражи процессорного времени соседями по ноде. На чистых KVM-серверах tropic.host со стабильным %st = 0.0% и накопителями Enterprise NVMe PCIe 4.0 (случайное чтение 4K QD1 > 50 000 IOPS) дисковый сброс данных проходит на полной скорости физического интерфейса без троттлинга контейнера Keycloak.


1. Автоматизированное горячее резервирование PostgreSQL

Резервное копирование реляционной базы выполняется через утилиту pg_dump в кастомном бинарном формате (-Fc). Формат сжатия custom гарантирует консистентность на уровне транзакций благодаря изоляции снимка (--single-transaction), сжимает объем данных средствами zlib и позволяет запускать многопоточное параллельное восстановление через pg_restore -j.

Создайте директорию для скрипта и локального хранилища дампов с разграничением прав доступа на хосте:

mkdir -p /opt/keycloak/backups/db /opt/keycloak/scripts
chmod 700 /opt/keycloak/backups

Скрипт создания зашифрованного дампа с ротацией /opt/keycloak/scripts/backup-postgres.sh:

#!/usr/bin/env bash
set -euo pipefail

# Конфигурационные переменные
PROJECT_DIR="/opt/keycloak"
BACKUP_DIR="${PROJECT_DIR}/backups/db"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/keycloak_db_${TIMESTAMP}.dump"
GPG_RECIPIENT="[email protected]"
RETENTION_DAYS=7

# Загрузка переменных окружения проекта
if [ -f "${PROJECT_DIR}/.env" ]; then
    # shellcheck disable=SC1091
    source "${PROJECT_DIR}/.env"
else
    echo "ERROR: Файл .env не найден в ${PROJECT_DIR}" >&2
    exit 1
fi

echo "[$(date +'%Y-%m-%d %H:%M:%S')] Инициализация снятия дампа базы данных Keycloak..."

# Вызов pg_dump внутри изолированного контейнера базы данных
# Флаги:
# -Fc: формат custom (сжатие, поддержка pg_restore)
# -b: выгрузка BLOB-объектов
# -v: подробный вывод процесса в stderr
docker compose -f "${PROJECT_DIR}/docker-compose.yml" exec -T postgres \
    pg_dump \
    -U "${POSTGRES_USER}" \
    -d "${POSTGRES_DB}" \
    -Fc \
    -b > "${BACKUP_FILE}"

# Проверка целостности созданного файла
if [ ! -s "${BACKUP_FILE}" ]; then
    echo "ERROR: Дамп пуст или завершился сбоем." >&2
    rm -f "${BACKUP_FILE}"
    exit 1
fi

# Вычисление контрольной суммы SHA-256
sha256sum "${BACKUP_FILE}" > "${BACKUP_FILE}.sha256"

# Асимметричное шифрование дампа (GPG) перед передачей в сетевое хранилище
# Для использования симметричного пароля замените на: gpg --batch --yes --passphrase-file /path/to/key -c
if command -v gpg &> /dev/null && [ -n "${GPG_RECIPIENT}" ]; then
    gpg --batch --yes --encrypt --recipient "${GPG_RECIPIENT}" "${BACKUP_FILE}"
    rm -f "${BACKUP_FILE}"
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] Дамп успешно зашифрован: ${BACKUP_FILE}.gpg"
fi

# Очистка локальных копий старше RETENTION_DAYS
find "${BACKUP_DIR}" -type f \( -name "*.dump" -o -name "*.gpg" -o -name "*.sha256" \) -mtime "+${RETENTION_DAYS}" -delete

echo "[$(date +'%Y-%m-%d %H:%M:%S')] Резервное копирование завершено штатно."

Сделайте файл исполняемым:

chmod 700 /opt/keycloak/scripts/backup-postgres.sh

2. Декларативный экспорт Realm (JSON) через CLI Quarkus

Начиная с дистрибутива Keycloak на базе Quarkus, экспорт конфигураций realm в файл JSON осуществляется через встроенную подкоманду kc.sh export. Поскольку одновременная работа экспорта и боевого инстанса с доступом к одной базе на запись может вызывать блокировки таблиц транзакционных блокировок Liquibase, экспорт запускается как отдельный одноразовый эфемерный контейнер в режиме read-only к базе.

Скрипт экспорта схемы /opt/keycloak/scripts/export-realm.sh:

#!/usr/bin/env bash
set -euo pipefail

PROJECT_DIR="/opt/keycloak"
EXPORT_DIR="${PROJECT_DIR}/backups/realms"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
TARGET_DIR="${EXPORT_DIR}/export_${TIMESTAMP}"

mkdir -p "${TARGET_DIR}"
chmod 700 "${TARGET_DIR}"

echo "[$(date +'%Y-%m-%d %H:%M:%S')] Запуск экспорта конфигураций Realm..."

# Запуск одноразового сервиса Keycloak с переопределенной точкой входа
# Флаги экспорта:
# --dir: директория сохранения файлов
# --users realm_file: экспорт пользователей вместе с realm в единый файл
# --optimized: пропуск предварительного build-шага Quarkus (быстрый старт)
docker compose -f "${PROJECT_DIR}/docker-compose.yml" run --rm --no-deps \
    -v "${TARGET_DIR}:/opt/keycloak/data/export" \
    keycloak \
    export \
    --dir /opt/keycloak/data/export \
    --users realm_file \
    --optimized

# Коррекция прав доступа (контейнер пишет от UID 1000)
chown -R root:root "${TARGET_DIR}"
chmod 600 "${TARGET_DIR}"/*

echo "[$(date +'%Y-%m-%d %H:%M:%S')] Конфигурация успешно экспортирована в ${TARGET_DIR}"
chmod 700 /opt/keycloak/scripts/export-realm.sh

3. Автоматизация через systemd Timer

Использование таймеров systemd вместо классического cron гарантирует корректный сбор логов в journald, строгую изоляцию ресурсов через cgroups v2 и защиту от наложения параллельно выполняющихся задач.

Создайте юнит сервиса /etc/systemd/system/keycloak-backup.service:

[Unit]
Description=Keycloak PostgreSQL and Realm Disaster Recovery Backup
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
WorkingDirectory=/opt/keycloak
ExecStart=/opt/keycloak/scripts/backup-postgres.sh
ExecStartPost=/opt/keycloak/scripts/export-realm.sh
StandardOutput=journal
StandardError=journal
# Ограничение ресурсов процесса бэкапа через cgroups v2
CPUAccounting=true
CPUQuota=80%
MemoryAccounting=true
MemoryMax=1G
IOAccounting=true
IOWeight=100

Создайте юнит таймера /etc/systemd/system/keycloak-backup.timer:

[Unit]
Description=Daily Keycloak Backup Timer at 03:00 UTC

[Timer]
OnCalendar=*-*-* 03:00:00 UTC
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

Активация расписания:

systemctl daemon-reload
systemctl enable --now keycloak-backup.timer
systemctl list-timers --all | grep keycloak

4. Репликация бэкапов в удаленное S3-хранилище

Локальные бэкапы не защищают сервис от физического уничтожения гипервизора, аварии в дата-центре или отзыва сетевой зоны. Для выгрузки архивов во внешнее S3-совместимое хранилище используется rclone.

Конфигурация /root/.config/rclone/rclone.conf:

[s3-cold-storage]
type = s3
provider = Other
env_auth = false
access_key_id = YOUR_S3_ACCESS_KEY
secret_access_key = YOUR_S3_SECRET_KEY
endpoint = s3.eu-central-1.storage-provider.com
acl = private

Команда синхронизации архивов (добавляется в качестве шага ExecStartPost в keycloak-backup.service):

rclone sync /opt/keycloak/backups/ s3-cold-storage:company-infra-backups/keycloak/ \
    --fast-list \
    --transfers 4 \
    --checkers 8 \
    --bwlimit 50M \
    --log-file=/var/log/rclone-keycloak.log \
    --log-level INFO

За счет сетевых аплинков до 10 Гбит/с и прямого BGP-пиринга на узлах tropic.host передача нескольких гигабайт сжатых дампов в ключевые европейские хабы (Франкфурт, Амстердам) завершается за секунды, не забивая транзитный канал рабочих сервисов.


5. Пошаговый регламент полного восстановления (Bare-Metal Restore)

Сценарий полного уничтожения исходного хоста: развертывание на чистом инстансе tropic.host (Ubuntu 24.04 LTS / Debian 12).

Шаг 1. Подготовка базовой ОС и Docker-стека

Установите Docker Engine и сопутствующие сетевые утилиты:

apt-get update && apt-get install -y ca-certificates curl gnupg lsb-release zstd
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Шаг 2. Развертывание директорий и манифестов

Восстановите исходную рабочую структуру:

mkdir -p /opt/keycloak/backups/db
cd /opt/keycloak

Скопируйте с аварийного носителя или Git-репозитория файлы docker-compose.yml и .env. Убедитесь, что параметры POSTGRES_DB, POSTGRES_USER и секрет POSTGRES_PASSWORD строго совпадают со старыми значениями, иначе токены шифрования конфигурации потеряют валидность.

Шаг 3. Запуск чистой базы данных в режиме обслуживания

Запустите контейнер PostgreSQL отдельно от Keycloak, чтобы предотвратить выполнение миграций Liquibase поверх пустой схемы:

docker compose up -d postgres

Ожидание готовности сокета базы данных (loop healthcheck):

until docker compose exec -T postgres pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}"; do
    echo "Ожидание инициализации сокета PostgreSQL..."
    sleep 2
done

Шаг 4. Восстановление дампа базы данных

Получите свежий файл бэкапа из внешнего хранилища S3:

rclone copy s3-cold-storage:company-infra-backups/keycloak/db/ /opt/keycloak/backups/db/ --include "*_latest*.dump.gpg"

Расшифруйте дамп:

gpg --decrypt /opt/keycloak/backups/db/keycloak_db_latest.dump.gpg > /opt/keycloak/backups/db/restore_target.dump

Сверьте контрольную сумму:

sha256sum -c /opt/keycloak/backups/db/restore_target.dump.sha256

Выполните восстановление через pg_restore:

# Флаги:
# --clean: очистка (DROP) существующих объектов перед созданием
# --if-exists: подавление ошибок, если объектов еще не существует
# --no-owner: пропуск команд назначения владельцев объектов (исключает конфликты UID)
# --no-privileges: пропуск восстановления прав доступа (GRANT/REVOKE)
docker compose exec -T postgres \
    pg_restore \
    -U "${POSTGRES_USER}" \
    -d "${POSTGRES_DB}" \
    --clean \
    --if-exists \
    --no-owner \
    --no-privileges \
    -v < /opt/keycloak/backups/db/restore_target.dump

Примечание: Возврат кода 1 или предупреждений вида already exists на этапе создания системных расширений (например, plpgsql) является штатным поведением pg_restore и не свидетельствует об ошибке.

Шаг 5. Верификация целостности схемы и данных

Проверьте наличие основных сущностей в восстановленной структуре:

docker compose exec -T postgres psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "
SELECT 
    (SELECT COUNT(*) FROM realm) AS realms_count,
    (SELECT COUNT(*) FROM client) AS clients_count,
    (SELECT COUNT(*) FROM user_entity) AS users_count,
    (SELECT COUNT(*) FROM user_role_mapping) AS role_mappings_count;
"

Если счетчики realms_count и users_count возвращают значения, отличные от нуля и сопоставимые с метриками до аварии, слой данных консистентен.

Шаг 6. Запуск Keycloak и инвалидация распределенного кэша

Запустите основной стек Keycloak и балансировщик Nginx:

docker compose up -d keycloak nginx

Мониторинг журнала инициализации Quarkus:

docker compose logs -f keycloak

Ожидайте системного маркера успешного старта: Keycloak [version] (Quarkus) started in Xms. Listening on: http://0.0.0.0:8080.

Шаг 7. Проверка жизнеспособности (Readiness Probe) и Smoke-тест

Запрос проверки статуса готовности (readiness):

curl -sf -o /dev/null -w "%{http_code}\n" http://127.0.0.1:9000/health/ready

Ответ должен возвращать HTTP-код 200.

Выполните тестовый запрос на генерацию мастер-токена OIDC через curl для валидации криптографических операций и генерации ключей:

curl -s -X POST "http://127.0.0.1:8080/realms/master/protocol/openid-connect/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "username=${KEYCLOAK_ADMIN}" \
    -d "password=${KEYCLOAK_ADMIN_PASSWORD}" \
    -d "grant_type=password" \
    -d "client_id=admin-cli" | grep -q "access_token" && echo "SMOKE TEST PASSED: Токен успешно сгенерирован."

После завершения тестов удалите расшифрованный локальный файл дампа:

shred -u /opt/keycloak/backups/db/restore_target.dump

Часто задаваемые вопросы (FAQ)

Сколько оперативной памяти требуется для Keycloak на VPS?

Для продакшена Keycloak на базе Quarkus минимально требуется 4 GB RAM (выделяя 2 GB под JVM Heap) и 2 vCPU. При нагрузке свыше 10 000 активных пользователей рекомендуется от 8 GB RAM.

Почему для Keycloak критичен KVM без оверселлинга?

JVM крайне чувствительна к паузам сборщика мусора (GC pauses). При наличии CPU Steal Time (> 2%) потоки GC зависают, вызывая таймауты SSO-авторизации во всех корпоративных сервисах.