diff --git a/setup/modules/FreeipaAnsible/image/.env.example b/setup/modules/FreeipaAnsible/image/.env.example index 7b2b608..6827c3d 100644 --- a/setup/modules/FreeipaAnsible/image/.env.example +++ b/setup/modules/FreeipaAnsible/image/.env.example @@ -35,6 +35,18 @@ CMK_ADMIN_PASSWORD=ChangeMe_CMK! CMK_SITE_ID=cmk CMK_ADVERTISED_URL= +# ── nginx gateway (reverse proxy + portal) ──────────────────────────────────── +# ANSIPA_HTTP_PORT — host port the ansipa nginx gateway listens on (plain HTTP). +# Put your own TLS-terminating reverse proxy in front of it and forward to +# http://:${ANSIPA_HTTP_PORT}. The gateway fronts all UIs on one +# host: /ipa/ (FreeIPA), /cmk/ (CheckMK), /auth/ (Keycloak), / (portal). +# Example upstream nginx (in your main reverse proxy), to expose ansipa over HTTPS: +# location / { proxy_pass http://:8088; proxy_set_header Host $host; +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } +# Or Caddy one-liner: ansipa.example.com { reverse_proxy :8088 } +ANSIPA_HTTP_PORT=8088 + # ── Keycloak ────────────────────────────────────────────────────────────────── KC_HOSTNAME=keycloak.corp.example.com KC_REALM=corp diff --git a/setup/modules/FreeipaAnsible/image/docker-compose.yml b/setup/modules/FreeipaAnsible/image/docker-compose.yml index b57d568..1964440 100644 --- a/setup/modules/FreeipaAnsible/image/docker-compose.yml +++ b/setup/modules/FreeipaAnsible/image/docker-compose.yml @@ -211,6 +211,14 @@ services: 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} @@ -234,3 +242,42 @@ services: 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: + test: ["CMD-SHELL", "wget -qO- http://localhost/ >/dev/null 2>&1 || exit 1"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 20s diff --git a/setup/modules/FreeipaAnsible/image/nginx/portal/index.html b/setup/modules/FreeipaAnsible/image/nginx/portal/index.html new file mode 100644 index 0000000..46d4b0c --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/nginx/portal/index.html @@ -0,0 +1,39 @@ + + + + + + ansipa · service portal + + + +
+
+

ansipa

+

FreeIPA identity · Ansible policy · CheckMK monitoring · Keycloak SSO

+
+ +
+ +

FreeIPA

+

Identity, hosts, groups, sudo & HBAC rules — the policy source of truth.

+ /ipa/ui/ +
+ +

CheckMK

+

Fleet monitoring: agents, host status, ansipa dev_mon_* checks.

+ /cmk/ +
+ +

Keycloak

+

Single sign-on and identity federation against FreeIPA LDAP.

+ /auth/ +
+
+ + +
+ + diff --git a/setup/modules/FreeipaAnsible/image/nginx/portal/portal.css b/setup/modules/FreeipaAnsible/image/nginx/portal/portal.css new file mode 100644 index 0000000..8f4161f --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/nginx/portal/portal.css @@ -0,0 +1,43 @@ +:root { + --bg: #0b0e14; + --fg: #d7dce5; + --muted: #8a93a6; + --card: #141925; + --accent: #c678dd; + --accent2: #56b6c2; + --border: #232a3a; +} +* { box-sizing: border-box; } +body { + margin: 0; + font-family: "JetBrains Mono", ui-monospace, "Cascadia Code", monospace; + background: radial-gradient(1200px 600px at 70% -10%, #17203a 0%, var(--bg) 60%); + color: var(--fg); + min-height: 100vh; +} +main { max-width: 960px; margin: 0 auto; padding: 4rem 1.5rem; } +header { text-align: center; margin-bottom: 3rem; } +h1 { + font-size: 3rem; margin: 0; letter-spacing: 0.15em; + background: linear-gradient(90deg, var(--accent), var(--accent2)); + -webkit-background-clip: text; background-clip: text; color: transparent; +} +.sub { color: var(--muted); margin-top: 0.5rem; } +.grid { + display: grid; gap: 1.25rem; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} +.card { + display: block; text-decoration: none; color: inherit; + background: var(--card); border: 1px solid var(--border); + border-radius: 12px; padding: 1.5rem; + transition: transform 0.12s ease, border-color 0.12s ease; +} +.card:hover { transform: translateY(-3px); border-color: var(--accent); } +.card h2 { margin: 0 0 0.5rem; color: var(--accent2); } +.card p { margin: 0 0 1rem; color: var(--muted); font-size: 0.9rem; line-height: 1.5; } +.path { + font-size: 0.8rem; color: var(--accent); + background: #0d1119; padding: 0.2rem 0.5rem; border-radius: 6px; +} +footer { margin-top: 3rem; text-align: center; color: var(--muted); font-size: 0.8rem; } diff --git a/setup/modules/FreeipaAnsible/image/nginx/templates/default.conf.template b/setup/modules/FreeipaAnsible/image/nginx/templates/default.conf.template new file mode 100644 index 0000000..45ca735 --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/nginx/templates/default.conf.template @@ -0,0 +1,78 @@ +# ansipa reverse-proxy + portal — rendered by nginx's envsubst entrypoint. +# Placeholders (${VAR}) are filled from the container environment at start. +# +# Design: each backend is proxied at the path it ALREADY serves natively +# (FreeIPA /ipa, CheckMK /cmk, Keycloak under a configured /auth relative path), +# so no fragile path rewriting is needed and a single hostname/entry works — +# which is what lets the whole stack sit behind one upstream reverse proxy. + +# Honor an upstream proxy's X-Forwarded-Proto if present (double-proxy case), +# otherwise fall back to this server's own scheme. +map $http_x_forwarded_proto $ansipa_fwd_proto { + default $scheme; + "~.+" $http_x_forwarded_proto; +} +# Preserve the original client IP chain when we sit behind another proxy. +map $http_x_forwarded_for $ansipa_fwd_for { + default $remote_addr; + "~.+" "$http_x_forwarded_for, $remote_addr"; +} + +server { + listen 80 default_server; + server_name _; + + # Big cookies/headers: CheckMK and Keycloak set sizeable auth cookies. + large_client_header_buffers 4 32k; + proxy_busy_buffers_size 32k; + proxy_buffers 8 32k; + proxy_buffer_size 32k; + + # ── Portal start page ──────────────────────────────────────────────────── + location = / { + root /usr/share/nginx/portal; + try_files /index.html =404; + } + location = /index.html { root /usr/share/nginx/portal; } + location = /portal.css { root /usr/share/nginx/portal; } + + # ── FreeIPA web UI (native path /ipa) ────────────────────────────────────── + # IPA's Apache is strict about Host and Referer (anti-CSRF): both must name + # the IPA server itself, not the proxy. Pin Host to the IPA hostname and + # rewrite Referer so the referer-check passes through the proxy. + location /ipa/ { + proxy_pass https://${ANSIPA_IPA_UPSTREAM}/ipa/; + proxy_ssl_server_name on; + proxy_ssl_verify off; # IPA ships a self-signed CA + proxy_set_header Host ${IPA_HOSTNAME}; + proxy_set_header Referer https://${IPA_HOSTNAME}/ipa/ui/; + proxy_set_header X-Forwarded-Proto $ansipa_fwd_proto; + proxy_set_header X-Forwarded-For $ansipa_fwd_for; + proxy_set_header X-Forwarded-Host $host; + # IPA issues 301s to its own hostname; rewrite them back onto this proxy. + proxy_redirect https://${IPA_HOSTNAME}/ /ipa/../; + proxy_redirect http://${IPA_HOSTNAME}/ /ipa/../; + } + + # ── CheckMK (native path /cmk) ───────────────────────────────────────────── + location /cmk/ { + proxy_pass http://${ANSIPA_CMK_UPSTREAM}/cmk/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $ansipa_fwd_proto; + proxy_set_header X-Forwarded-For $ansipa_fwd_for; + proxy_set_header X-Forwarded-Host $host; + } + + # ── Keycloak (relative path /auth, set via KC_HTTP_RELATIVE_PATH) ────────── + location /auth/ { + proxy_pass http://${ANSIPA_KC_UPSTREAM}/auth/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $ansipa_fwd_proto; + proxy_set_header X-Forwarded-For $ansipa_fwd_for; + proxy_set_header X-Forwarded-Host $host; + # Deliberately NOT forwarding X-Forwarded-Port: this gateway listens on + # :80 internally, but the public port is whatever the upstream TLS proxy + # uses (typically 443). Sending :80 makes Keycloak emit redirects like + # https://host:80/... — let it infer the standard port from the proto. + } +}