diff --git a/docs/md/ansipa-setup.md b/docs/md/ansipa-setup.md index 97ea5df..d4ab2a0 100644 --- a/docs/md/ansipa-setup.md +++ b/docs/md/ansipa-setup.md @@ -37,10 +37,6 @@ catalogue and playbook reference see [FreeIPA & Ansible](freeipa-ansible.md). └─────────────────────────────┘ └────────────────────────────┘ ``` -Every path inside the gateway box requires an extra shared sign-in -(`auth_basic`) before reaching any backend — see -[Sign-in gate in front of the whole gateway](#sign-in-gate-in-front-of-the-whole-gateway). - The design is **pull-based**: the server never initiates connections to clients. Every enrolled host runs `ansipa-enforce-policies.sh` on a 30-minute timer, does `kinit -k host/$FQDN`, reads its own group memberships from FreeIPA, and applies @@ -187,30 +183,6 @@ The gateway speaks plain HTTP on `ANSIPA_HTTP_PORT` (default **8088**) and trust `X-Forwarded-*`, so it is designed to sit **behind your own TLS-terminating reverse proxy**. -### Sign-in gate in front of the whole gateway - -Every path above — the portal *and* every proxied UI — sits behind one shared -HTTP Basic Auth credential first (`auth_basic` at the `server {}` level in -`nginx/templates/default.conf.template`, inherited by every `location`). -FreeIPA, CheckMK and Keycloak still each ask for their own login afterwards — -that's deliberate, not a bug: an attacker now has to beat two locks instead -of one, and each backend's own auth stays fully intact as a second, -independent layer. - -- Credential: `ANSIPA_PORTAL_USER` / `ANSIPA_PORTAL_PASSWORD` in `.env`. - Leave the password blank to have one generated on first boot — retrieve it - with `docker exec freeipa journalctl -u ansipa-portal-auth.service`. -- The htpasswd file is generated on the FreeIPA container - (`ansipa-portal-auth-setup.sh`, `apr1` hash — natively understood by nginx, - no dependency on the base image's libc `crypt()`) and published to nginx - via the `portal-auth` shared docker volume (same pattern as `cmk-creds`). -- To add more accounts, re-run the setup script inside the container with a - different `ANSIPA_PORTAL_USER`/`ANSIPA_PORTAL_PASSWORD` pair — it appends - rather than overwrites the htpasswd file. -- An unauthenticated request to `/` now correctly gets **401**, not 200 — - the gateway healthcheck in `docker-compose.yml` treats 401 as healthy for - exactly this reason. - ### Exposing ansipa behind your main reverse proxy Point one hostname at the gateway. TLS is terminated by your proxy; ansipa @@ -347,9 +319,7 @@ manual intervention is needed — just expect the first enrollment to take a whi ```bash # server docker compose ps # all healthy -curl -s -o /dev/null -w '%{http_code}\n' http://DOCKER_HOST_IP:8088/ # expect 401 (gateway sign-in required) -curl -fsS -u "$ANSIPA_PORTAL_USER:$ANSIPA_PORTAL_PASSWORD" \ - http://DOCKER_HOST_IP:8088/ | grep ansipa # portal, authenticated +curl -fsS http://DOCKER_HOST_IP:8088/ | grep ansipa # portal # client (after enrollment + one enforcer run) id admin # SSSD resolves IPA users kinit admin && klist # Kerberos works diff --git a/setup/modules/FreeipaAnsible/image/.env.example b/setup/modules/FreeipaAnsible/image/.env.example index b2ccde5..3cc7f6a 100644 --- a/setup/modules/FreeipaAnsible/image/.env.example +++ b/setup/modules/FreeipaAnsible/image/.env.example @@ -52,16 +52,6 @@ ANSIPA_GIT_ADMIN_PUBKEY= ANSIPA_GIT_SIGNING_PUBKEY= # ── nginx gateway (reverse proxy + portal) ──────────────────────────────────── -# ANSIPA_PORTAL_USER / ANSIPA_PORTAL_PASSWORD — deliberate extra sign-in gate -# in front of the ENTIRE gateway (portal page + /ipa/ + /cmk/ + /auth/), on -# top of whatever login each backend already asks for on its own. Two -# prompts beats one attacker who only had to beat one lock. -# Leave ANSIPA_PORTAL_PASSWORD blank to auto-generate one on first boot — -# check `docker exec freeipa journalctl -u ansipa-portal-auth.service` to -# retrieve it, or just set your own here and restart. -ANSIPA_PORTAL_USER=ansipa -ANSIPA_PORTAL_PASSWORD= - # 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 diff --git a/setup/modules/FreeipaAnsible/image/Dockerfile b/setup/modules/FreeipaAnsible/image/Dockerfile index 350b471..022c753 100644 --- a/setup/modules/FreeipaAnsible/image/Dockerfile +++ b/setup/modules/FreeipaAnsible/image/Dockerfile @@ -45,7 +45,6 @@ RUN dnf install -y --setopt=install_weak_deps=False \ openssh-server \ openssh-clients \ gnupg2 \ - openssl \ && dnf clean all \ && rm -rf /var/cache/dnf @@ -74,21 +73,17 @@ COPY ansipa-git-server-setup.sh /usr/local/sbin/ansipa-git-server-setup.sh COPY ansipa-git-add-deploy-key.sh /usr/local/sbin/ansipa-git-add-deploy-key.sh COPY ansipa-git-server-setup.service /etc/systemd/system/ansipa-git-server-setup.service COPY ansipa-git-sshd.service /etc/systemd/system/ansipa-git-sshd.service -COPY ansipa-portal-auth-setup.sh /usr/local/sbin/ansipa-portal-auth-setup.sh -COPY ansipa-portal-auth.service /etc/systemd/system/ansipa-portal-auth.service RUN chmod +x /usr/local/sbin/ipa-first-boot.sh \ && chmod +x /usr/local/sbin/ansipa-smb-setup.sh \ && chmod +x /usr/local/sbin/ansipa-checkmk-setup.sh \ && chmod +x /usr/local/sbin/ansipa-git-server-setup.sh \ && chmod +x /usr/local/sbin/ansipa-git-add-deploy-key.sh \ - && chmod +x /usr/local/sbin/ansipa-portal-auth-setup.sh \ && systemctl enable docker-env.service \ && systemctl enable ipa-first-boot.service \ && systemctl enable ansipa-smb.service \ && systemctl enable ansipa-checkmk.service \ && systemctl enable ansipa-git-server-setup.service \ && systemctl enable ansipa-git-sshd.service \ - && systemctl enable ansipa-portal-auth.service \ && systemctl enable smb.service nmb.service crond.service VOLUME ["/data"] diff --git a/setup/modules/FreeipaAnsible/image/ansipa-portal-auth-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-portal-auth-setup.sh deleted file mode 100755 index c2b3b3f..0000000 --- a/setup/modules/FreeipaAnsible/image/ansipa-portal-auth-setup.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/bash -# ansipa-portal-auth-setup.sh — generate the HTTP Basic Auth credential file -# that gates the ansipa nginx gateway (portal + every proxied web UI). -# -# Runs on every container start via ansipa-portal-auth.service so the -# htpasswd file is always in place after container recreation (rootfs is -# ephemeral; only /data survives). The file itself is written to the shared -# 'portal-auth' volume so the nginx container (which has no access to /data) -# can read it — same pattern as the cmk-creds volume shared with checkmk. -# -# This is a DELIBERATE extra sign-in layer in front of the whole gateway — -# FreeIPA, CheckMK and Keycloak all still enforce their own logins behind it. -# Two prompts beats one attacker who only had to beat one lock. -# -# Password sources (first match wins): -# 1. ANSIPA_PORTAL_PASSWORD env var (first boot / explicit override) -# 2. /data/portal-auth.env (persisted from first boot) -# 3. auto-generated, logged once (so nothing is silently left with a -# predictable/blank credential) -# -# To add more accounts later, hand-edit the persisted file directly: -# printf 'alice:%s\n' "$(openssl passwd -apr1 'their password')" \ -# >> /data/portal-auth/htpasswd (then re-copy to the shared volume, or -# just re-run this script with ANSIPA_PORTAL_USER=alice ANSIPA_PORTAL_PASSWORD=... -# — it appends/updates rather than truncating). - -set -euo pipefail - -LOG_TAG="ansipa-portal-auth-setup" -ENV_FILE="/data/portal-auth.env" -PERSIST_HTPASSWD="/data/portal-auth/htpasswd" -SHARED_HTPASSWD="/portal-auth/htpasswd" # mounted from the portal-auth docker volume - -log() { echo "[$LOG_TAG] $*"; } -warn() { echo "[$LOG_TAG][WARN] $*" >&2; } - -mkdir -p "$(dirname "$PERSIST_HTPASSWD")" /portal-auth - -PORTAL_USER="${ANSIPA_PORTAL_USER:-}" -PORTAL_PASS="${ANSIPA_PORTAL_PASSWORD:-}" - -if [[ -f "$ENV_FILE" ]]; then - # shellcheck source=/dev/null - source "$ENV_FILE" - PORTAL_USER="${ANSIPA_PORTAL_USER:-$PORTAL_USER}" - PORTAL_PASS="${ANSIPA_PORTAL_PASSWORD:-$PORTAL_PASS}" -fi - -PORTAL_USER="${PORTAL_USER:-ansipa}" - -if [[ -z "$PORTAL_PASS" ]]; then - PORTAL_PASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 32 \ - || openssl rand -base64 24 | tr -d '/+=\n') - warn "ANSIPA_PORTAL_PASSWORD not set — generated one for user '$PORTAL_USER':" - warn " $PORTAL_PASS" - warn "Set ANSIPA_PORTAL_PASSWORD in .env to pin it; otherwise it is reused as-is on every restart." -fi - -# ── Persist for subsequent restarts ────────────────────────────────────────── -{ - printf 'ANSIPA_PORTAL_USER=%q\n' "$PORTAL_USER" - printf 'ANSIPA_PORTAL_PASSWORD=%q\n' "$PORTAL_PASS" -} > "$ENV_FILE" -chmod 600 "$ENV_FILE" - -# ── Write the htpasswd entry (apr1 — natively understood by nginx's -# auth_basic_module, no dependency on the base image's libc crypt()) ───────── -HASH=$(openssl passwd -apr1 "$PORTAL_PASS") - -touch "$PERSIST_HTPASSWD" -# Replace this user's line if present, else append. -grep -vE "^${PORTAL_USER}:" "$PERSIST_HTPASSWD" > "${PERSIST_HTPASSWD}.tmp" 2>/dev/null || true -printf '%s:%s\n' "$PORTAL_USER" "$HASH" >> "${PERSIST_HTPASSWD}.tmp" -mv "${PERSIST_HTPASSWD}.tmp" "$PERSIST_HTPASSWD" -chmod 644 "$PERSIST_HTPASSWD" - -# ── Publish to the volume nginx reads from ─────────────────────────────────── -cp "$PERSIST_HTPASSWD" "$SHARED_HTPASSWD" -chmod 644 "$SHARED_HTPASSWD" - -log "Gateway credential ready for user '$PORTAL_USER' ($SHARED_HTPASSWD)" diff --git a/setup/modules/FreeipaAnsible/image/ansipa-portal-auth.service b/setup/modules/FreeipaAnsible/image/ansipa-portal-auth.service deleted file mode 100644 index a71c461..0000000 --- a/setup/modules/FreeipaAnsible/image/ansipa-portal-auth.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Ansipa portal gateway Basic Auth credential setup -After=network.target docker-env.service -Wants=docker-env.service - -[Service] -Type=oneshot -RemainAfterExit=yes -# Passwords come from /etc/container.env (written by docker-env.service from -# Docker -e flags) on first boot; persisted thereafter under /data. -EnvironmentFile=/etc/container.env -ExecStart=/usr/local/sbin/ansipa-portal-auth-setup.sh -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target diff --git a/setup/modules/FreeipaAnsible/image/docker-compose.yml b/setup/modules/FreeipaAnsible/image/docker-compose.yml index 5924dee..5bb32b8 100644 --- a/setup/modules/FreeipaAnsible/image/docker-compose.yml +++ b/setup/modules/FreeipaAnsible/image/docker-compose.yml @@ -40,8 +40,6 @@ volumes: 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: @@ -72,7 +70,6 @@ services: - 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:-} @@ -86,8 +83,6 @@ services: # 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:-} @@ -276,7 +271,6 @@ services: 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: @@ -289,10 +283,7 @@ services: # 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)'"] + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"] interval: 30s timeout: 10s retries: 10 diff --git a/setup/modules/FreeipaAnsible/image/docker-env.service b/setup/modules/FreeipaAnsible/image/docker-env.service index e38626a..4e98b7e 100644 --- a/setup/modules/FreeipaAnsible/image/docker-env.service +++ b/setup/modules/FreeipaAnsible/image/docker-env.service @@ -1,12 +1,12 @@ [Unit] Description=Export Docker container env vars for systemd services DefaultDependencies=no -Before=basic.target ipa-first-boot.service ansipa-smb.service ansipa-git-server-setup.service ansipa-portal-auth.service +Before=basic.target ipa-first-boot.service ansipa-smb.service ansipa-git-server-setup.service [Service] Type=oneshot RemainAfterExit=yes -ExecStart=/bin/bash -c "tr '\\0' '\\n' < /proc/1/environ | grep -E '^(IPA|SMB|LUKS|KEYCLOAK|ANSIPA_GIT|ANSIPA_PORTAL)_' > /etc/container.env; chmod 600 /etc/container.env" +ExecStart=/bin/bash -c "tr '\\0' '\\n' < /proc/1/environ | grep -E '^(IPA|SMB|LUKS|KEYCLOAK|ANSIPA_GIT)_' > /etc/container.env; chmod 600 /etc/container.env" [Install] WantedBy=sysinit.target diff --git a/setup/modules/FreeipaAnsible/image/nginx/portal/index.html b/setup/modules/FreeipaAnsible/image/nginx/portal/index.html index c7b9093..46d4b0c 100644 --- a/setup/modules/FreeipaAnsible/image/nginx/portal/index.html +++ b/setup/modules/FreeipaAnsible/image/nginx/portal/index.html @@ -33,7 +33,6 @@ diff --git a/setup/modules/FreeipaAnsible/image/nginx/templates/default.conf.template b/setup/modules/FreeipaAnsible/image/nginx/templates/default.conf.template index 2a2eb1e..45ca735 100644 --- a/setup/modules/FreeipaAnsible/image/nginx/templates/default.conf.template +++ b/setup/modules/FreeipaAnsible/image/nginx/templates/default.conf.template @@ -28,17 +28,6 @@ server { proxy_buffers 8 32k; proxy_buffer_size 32k; - # ── Gateway-wide sign-in (deliberate, in addition to each backend's own - # login) ──────────────────────────────────────────────────────────────── - # Inherited by every location below — the portal AND every proxied web UI - # all sit behind this one shared credential first. FreeIPA / CheckMK / - # Keycloak still each ask for their own login afterwards: two prompts is - # the intended tradeoff, not a bug — see ansipa-portal-auth-setup.sh. - # File is populated by the freeipa container and shared via the - # 'portal-auth' volume; managed with ANSIPA_PORTAL_USER/_PASSWORD in .env. - auth_basic "ansipa gateway — sign in"; - auth_basic_user_file /etc/nginx/auth/htpasswd; - # ── Portal start page ──────────────────────────────────────────────────── location = / { root /usr/share/nginx/portal; diff --git a/setup/modules/FreeipaAnsible/image/run.sh b/setup/modules/FreeipaAnsible/image/run.sh index dcbd7b6..d3c1586 100755 --- a/setup/modules/FreeipaAnsible/image/run.sh +++ b/setup/modules/FreeipaAnsible/image/run.sh @@ -52,7 +52,6 @@ start_freeipa() { # dev_mon_* CheckMK integration silently never activates) docker volume create freeipa-data 2>/dev/null || true docker volume create cmk-creds 2>/dev/null || true - docker volume create portal-auth 2>/dev/null || true docker run -d \ --name freeipa \ @@ -64,7 +63,6 @@ start_freeipa() { -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ -v freeipa-data:/data \ -v cmk-creds:/cmk-creds \ - -v portal-auth:/portal-auth \ --network "$NETWORK" \ --ip 172.30.0.10 \ -e IPA_DOMAIN="${IPA_DOMAIN:?}" \ @@ -81,8 +79,6 @@ start_freeipa() { -e ANSIPA_GIT_SSH_PORT="${ANSIPA_GIT_SSH_PORT:-2222}" \ -e ANSIPA_GIT_ADMIN_PUBKEY="${ANSIPA_GIT_ADMIN_PUBKEY:-}" \ -e ANSIPA_GIT_SIGNING_PUBKEY="${ANSIPA_GIT_SIGNING_PUBKEY:-}" \ - -e ANSIPA_PORTAL_USER="${ANSIPA_PORTAL_USER:-ansipa}" \ - -e ANSIPA_PORTAL_PASSWORD="${ANSIPA_PORTAL_PASSWORD:-}" \ -p 389:389 \ -p 636:636 \ -p 88:88 \