Dotfiles/setup/modules/FreeipaAnsible/image/docker-compose.yml

287 lines
13 KiB
YAML

# FreeIPA + Keycloak + PostgreSQL constellation
#
# Setup:
# cp .env.example .env && $EDITOR .env
# docker compose up -d
# docker compose logs -f freeipa # watch first-boot install (~10 min)
# # Once freeipa is healthy:
# ./keycloak-configure.sh # wire Keycloak → FreeIPA LDAP
#
# To run without Keycloak:
# docker compose up -d freeipa
#
# To scale out (clients enroll against the same IPA server):
# docker compose --profile clients up
#
# ── cgroupns NOTE ────────────────────────────────────────────────────────────
# On hosts using cgroup v2 (WSL2, recent Linux with unified hierarchy) the
# FreeIPA container runs /sbin/init (systemd) and exits immediately with code
# 255 unless the container shares the host cgroup namespace. Docker Compose
# does not yet expose a cgroupns_mode property in its schema, so use the
# provided run.sh wrapper instead of plain `docker compose up` on those hosts:
#
# ./run.sh — starts freeipa only (cgroupns host applied automatically)
# ./run.sh all — starts freeipa + postgres + keycloak
#
# On plain cgroup v1 hosts `docker compose up -d` works without run.sh.
# Volume names are pinned (name:) so they match what run.sh creates with plain
# `docker volume create` on cgroup-v2 hosts. Without this, compose prefixes the
# project name (image_cmk-creds, ...) and the freeipa container started by
# run.sh mounts a DIFFERENT cmk-creds volume than the compose-managed checkmk
# container writes to — the automation secret then never reaches IPA and the
# CheckMK integration retries forever.
volumes:
freeipa-data:
name: freeipa-data
keycloak-db:
name: keycloak-db
cmk-data: # CheckMK site data (persistent across restarts)
name: cmk-data
cmk-creds: # shared: CheckMK writes automation.secret here; FreeIPA reads it
name: cmk-creds
networks:
ipa-net:
# Pin the real network name: run.sh (cgroup-v2 hosts) creates/uses a bare
# "ipa-net", while compose would otherwise create a project-prefixed
# "image_ipa-net" on the same subnet — the second one to run then fails
# with "Pool overlaps with other one on this address space".
name: ipa-net
ipam:
config:
- subnet: 172.30.0.0/24
services:
# ── FreeIPA ─────────────────────────────────────────────────────────────────
freeipa:
build:
context: .
dockerfile: Dockerfile
image: freeipa-server:local
container_name: freeipa
hostname: ${IPA_HOSTNAME:-ipa.example.com}
privileged: true
tmpfs:
- /run
- /tmp
volumes:
- freeipa-data:/data
- /sys/fs/cgroup:/sys/fs/cgroup:rw
- cmk-creds:/cmk-creds # read CMK secret + write push-mode requests for hosts
environment:
IPA_DOMAIN: ${IPA_DOMAIN:?set IPA_DOMAIN in .env}
IPA_REALM: ${IPA_REALM:-}
IPA_ADMIN_PASSWORD: ${IPA_ADMIN_PASSWORD:?set IPA_ADMIN_PASSWORD in .env}
IPA_DM_PASSWORD: ${IPA_DM_PASSWORD:?set IPA_DM_PASSWORD in .env}
IPA_SETUP_DNS: ${IPA_SETUP_DNS:-false}
IPA_DNS_FORWARDER: ${IPA_DNS_FORWARDER:-}
IPA_SETUP_KRA: ${IPA_SETUP_KRA:-false}
CMK_URL: http://172.30.0.12:5000
# Client-facing CheckMK URL stored in the dev_mon_base description —
# must be routable from enrolled clients (e.g. http://<docker-host>:8090).
CMK_ADVERTISED_URL: ${CMK_ADVERTISED_URL:-}
CMK_SITE_ID: ${CMK_SITE_ID:-cmk}
ports:
- "389:389"
- "636:636"
- "88:88"
- "88:88/udp"
- "464:464"
- "464:464/udp"
- "443:443"
- "445:445"
- "139:139"
- "137:137/udp"
- "138:138/udp"
networks:
ipa-net:
ipv4_address: 172.30.0.10
healthcheck:
test: ["CMD-SHELL", "ipactl status 2>/dev/null | grep -q 'running'"]
interval: 30s
timeout: 15s
retries: 20
start_period: 600s
# ── PostgreSQL (Keycloak backend) ────────────────────────────────────────────
postgres:
image: postgres:16-alpine
container_name: keycloak-db
restart: unless-stopped
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${KC_DB_PASSWORD:?set KC_DB_PASSWORD in .env}
volumes:
- keycloak-db:/var/lib/postgresql/data
networks:
ipa-net:
healthcheck:
test: ["CMD", "pg_isready", "-U", "keycloak"]
interval: 10s
retries: 5
# ── CheckMK CE ───────────────────────────────────────────────────────────────
# Monitoring server. Clients register themselves via ansipa-enforce-policies.sh
# when added to dev_mon_base. The automation user's secret is written to the
# cmk-creds shared volume so ansipa-checkmk-setup.sh (in the freeipa container)
# can store it in the IPA dev_mon_base hostgroup description for client discovery.
#
# Web UI: http://localhost:8090/cmk/ (user: cmkadmin, pass: CMK_ADMIN_PASSWORD)
# Agent dl: http://localhost:8090/cmk/check_mk/agents/
# Note: CheckMK 2.3 uses port 5000 (OMD apache); host port 8090 maps to it.
checkmk:
image: checkmk/check-mk-raw:2.3.0-latest
container_name: checkmk
hostname: checkmk.test.local
restart: unless-stopped
# Override entrypoint to copy the auto-generated automation secret to the
# shared volume so the freeipa container can read it without docker exec.
# $$ → $ after Compose expansion (i.e., $$! → $! in bash = background PID).
command:
- bash
- -c
- >-
/docker-entrypoint.sh & CMK_PID=$$!;
until [ -f /omd/sites/${CMK_SITE_ID:-cmk}/var/check_mk/web/automation/automation.secret ];
do sleep 3; done; sleep 5;
mkdir -p /cmk-creds;
cp /omd/sites/${CMK_SITE_ID:-cmk}/var/check_mk/web/automation/automation.secret /cmk-creds/automation.secret;
chmod 644 /cmk-creds/automation.secret;
printf '<LocationMatch ^/cmk/(omd|wiki|nagvis|check_mk|pnp4nagios|api)>\n Order allow,deny\n Allow from all\n Satisfy any\n</LocationMatch>\n'
> /omd/sites/${CMK_SITE_ID:-cmk}/etc/apache/conf.d/cookie_auth.conf;
omd reload ${CMK_SITE_ID:-cmk} apache 2>/dev/null || true;
while true; do
for _req in /cmk-creds/push-mode-*.req; do
[ -f "$$_req" ] || continue;
_hn=$$(cat "$$_req");
_hk=$$(echo "$$_hn" | tr . _);
_mk=/omd/sites/${CMK_SITE_ID:-cmk}/etc/check_mk/conf.d/wato/ansipa/hosts.mk;
[ -f "$$_mk" ] || continue;
grep -q "explicit_host_conf.*$$_hk\|'$$_hn'.*push-agent" "$$_mk" 2>/dev/null && { rm -f "$$_req"; continue; };
printf '\nexplicit_host_conf.setdefault('"'"'cmk_agent_connection'"'"', {})\nexplicit_host_conf['"'"'cmk_agent_connection'"'"'].update({'"'"'%s'"'"': '"'"'push-agent'"'"'})\n' "$$_hn" >> "$$_mk";
su - ${CMK_SITE_ID:-cmk} -c 'cmk -O' 2>/dev/null || true;
rm -f "$$_req";
done;
sleep 30;
done &
wait $$CMK_PID
environment:
CMK_SITE_ID: ${CMK_SITE_ID:-cmk}
CMK_PASSWORD: ${CMK_ADMIN_PASSWORD:?set CMK_ADMIN_PASSWORD in .env}
ports:
- "8090:5000" # web UI (CheckMK 2.3 uses port 5000 for OMD apache)
- "8000:8000" # agent receiver (push mode / cmk-agent-ctl)
- "6557:6557" # livestatus (optional, for external tools)
volumes:
- cmk-data:/omd/sites
- cmk-creds:/cmk-creds
networks:
ipa-net:
ipv4_address: 172.30.0.12
healthcheck:
# Probe the login page, not /api/1.0/version: the REST API version
# endpoint requires authentication in CheckMK 2.3, so an unauthenticated
# curl -f gets 401 and the container stays "unhealthy" forever even
# though the site is fully up.
test: ["CMD-SHELL", "curl -sf http://localhost:5000/${CMK_SITE_ID:-cmk}/check_mk/login.py -o /dev/null"]
interval: 30s
timeout: 15s
retries: 20
start_period: 180s
# ── Keycloak ─────────────────────────────────────────────────────────────────
# After first start, run ./keycloak-configure.sh to wire FreeIPA LDAP federation.
keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: keycloak
restart: unless-stopped
# Use 'start' (not start-dev) for production; requires a TLS certificate.
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KC_DB_PASSWORD:?set KC_DB_PASSWORD in .env}
KC_HOSTNAME: ${KC_HOSTNAME:-localhost}
KC_HTTP_PORT: 8080
KC_HTTPS_PORT: 8443
KC_HTTP_ENABLED: "true"
KC_HEALTH_ENABLED: "true" # exposes /health/* on management port 9000 (KC 25+)
# Serve Keycloak under /auth so the ansipa nginx gateway can proxy it at a
# native subpath (no rewriting). KC_PROXY_HEADERS=xforwarded makes Keycloak
# trust X-Forwarded-* so it builds correct absolute URLs behind the gateway
# (and behind a further upstream proxy). KC_HOSTNAME_STRICT=false keeps it
# from pinning to a single hostname during testing.
KC_HTTP_RELATIVE_PATH: /auth
KC_PROXY_HEADERS: xforwarded
KC_HOSTNAME_STRICT: "false"
KC_FEATURES: preview
KEYCLOAK_ADMIN: ${KC_ADMIN:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:?set KC_ADMIN_PASSWORD in .env}
extra_hosts:
- "${IPA_HOSTNAME:-ipa.example.com}:172.30.0.10"
ports:
- "8080:8080"
- "8443:8443"
depends_on:
postgres:
condition: service_healthy
networks:
ipa-net:
healthcheck:
# The official Keycloak image ships neither curl nor wget (but has bash),
# so probe with bash's /dev/tcp. Check the actual serving port under the
# configured relative path (/auth) rather than the 9000 management
# /health endpoint — with KC_HTTP_RELATIVE_PATH set, /health/ready on 9000
# returns 404, whereas /auth/realms/master reliably returns 200 once up.
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET /auth/realms/master HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && head -c 15 <&3 | grep -q '200 OK'"]
interval: 20s
timeout: 10s
retries: 20
start_period: 90s
# ── nginx gateway: reverse proxy + portal start-page ─────────────────────────
# Single HTTP entry point that fronts all three web UIs on one hostname:
# / → portal start page (links to each service)
# /ipa/ → FreeIPA web UI (proxied to the freeipa container, 443)
# /cmk/ → CheckMK (proxied to the checkmk container, 5000)
# /auth/ → Keycloak (KC served under /auth relative path)
#
# It speaks plain HTTP and trusts X-Forwarded-* so it is meant to sit BEHIND
# the operator's own TLS-terminating reverse proxy (see .env ANSIPA_* vars and
# the README snippet for the one-line upstream config). Published on
# ANSIPA_HTTP_PORT for direct access/testing when no upstream proxy is used.
nginx:
image: nginx:alpine
container_name: ansipa-nginx
restart: unless-stopped
environment:
# Upstream container addresses on ipa-net (envsubst'd into the template).
ANSIPA_IPA_UPSTREAM: 172.30.0.10
ANSIPA_CMK_UPSTREAM: 172.30.0.12:5000
ANSIPA_KC_UPSTREAM: keycloak:8080
IPA_HOSTNAME: ${IPA_HOSTNAME:-ipa.example.com}
volumes:
- ./nginx/templates:/etc/nginx/templates:ro # *.template → envsubst → conf.d
- ./nginx/portal:/usr/share/nginx/portal:ro
ports:
- "${ANSIPA_HTTP_PORT:-8088}:80"
depends_on:
- freeipa
- checkmk
- keycloak
networks:
ipa-net:
healthcheck:
# Use 127.0.0.1, not localhost: inside the container localhost resolves to
# IPv6 ::1, but nginx listens on IPv4 0.0.0.0:80 only, so a localhost probe
# is refused even though the gateway is serving fine.
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 20s