# 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 portal-auth: # shared: FreeIPA writes the gateway htpasswd here; nginx reads it name: portal-auth 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 - portal-auth:/portal-auth # write the gateway htpasswd for nginx to read 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://:8090). CMK_ADVERTISED_URL: ${CMK_ADVERTISED_URL:-} CMK_SITE_ID: ${CMK_SITE_ID:-cmk} ANSIPA_PORTAL_USER: ${ANSIPA_PORTAL_USER:-ansipa} ANSIPA_PORTAL_PASSWORD: ${ANSIPA_PORTAL_PASSWORD:-} ANSIPA_GIT_SSH_PORT: ${ANSIPA_GIT_SSH_PORT:-2222} ANSIPA_GIT_ADMIN_PUBKEY: ${ANSIPA_GIT_ADMIN_PUBKEY:-} ANSIPA_GIT_SIGNING_PUBKEY: ${ANSIPA_GIT_SIGNING_PUBKEY:-} 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" - "${ANSIPA_GIT_SSH_PORT:-2222}:${ANSIPA_GIT_SSH_PORT:-2222}" 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 '\n Order allow,deny\n Allow from all\n Satisfy any\n\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 - portal-auth:/etc/nginx/auth:ro # htpasswd written by the freeipa container 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. # 401 counts as healthy: the gateway now requires Basic Auth on every # location (see ansipa-portal-auth-setup.sh) — an unauthenticated probe # is SUPPOSED to get 401, that's proof the gate is up, not a fault. test: ["CMD-SHELL", "wget -S -O /dev/null http://127.0.0.1/ 2>&1 | grep -qE 'HTTP/[0-9.]+ (200|401)'"] interval: 30s timeout: 10s retries: 10 start_period: 20s