Adds a built-in Nextcloud to the ansipa stack and wires every service's
logins back to FreeIPA, either through Keycloak or directly.
Nextcloud:
- nextcloud + redis services, sharing the existing postgres server via a
second database. The initdb hook only fires on an empty data directory,
so run.sh also creates the role/DB idempotently for stacks that predate
Nextcloud.
- served at the /nextcloud subpath of the gateway (OVERWRITEWEBROOT), with
the /.well-known/* DAV redirects at the server root that sync clients and
Nextcloud's own setup checks expect.
Identity:
- keycloak-configure.sh now provisions confidential OIDC clients per relying
party (nextcloud, proxmox-ve, proxmox-bs, opnsense) plus a realm-wide
"groups" claim, writing secrets to .oidc-secrets (0600, gitignored).
- nextcloud-configure.sh binds the IPA LDAP backend as the account source
and user_oidc for login. The internal username is pinned to the IPA uid
and unique-uid is disabled, otherwise every SSO login creates a second,
empty account beside the LDAP one.
- checkmk-ldap-configure.sh binds CheckMK straight to FreeIPA. CheckMK
cannot go through Keycloak: SAML is commercial-edition only and no
edition supports OIDC. The LDAP REST endpoints landed in 2.4 (werk
#16527) but the stack pins 2.3, so it writes user_connections.mk —
generated via Python repr() so quotes in a password cannot produce a
SyntaxError that takes the GUI down, backed up and rolled back if the
site fails to load it. Directory type is 389directoryserver, which is
what gives FreeIPA the correct uid/member attribute defaults.
Monitoring:
- OPNsense local check (gateways, pf state table vs its hard limit,
firmware, CARP) plus an installer. POSIX sh, since OPNsense is FreeBSD
with no bash or GNU grep. Transport is agent-over-SSH rather than inetd
on 6556, because OPNsense regenerates inetd.conf/services/hosts.allow
from config.xml and silently reverts those edits on reboot.
Fixes found along the way:
- keycloak-configure.sh used KC_URL without the /auth relative path and
probed /health/ready, which lives on management port 9000 and is not
under that path — so it 404'd and timed out on this deployment.
- .env.example had an unquoted `cn=Directory Manager`, which the shell
scripts sourcing it parsed as a command named `Manager`.
- summary heredocs printed raw escape codes; colours now use ANSI-C quoting.
docs/md/ansipa-sso.md covers the identity chain, the per-service support
matrix, Proxmox VE/PBS OpenID realm setup, and why packet captures should
not be tunnelled through the CheckMK API.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWLuyyrydC1u2FjM7naPcz
Adds a deliberate extra sign-in layer covering the entire nginx gateway
(portal page + every proxied web UI) via a shared htpasswd credential
generated on the FreeIPA container and published to nginx through a new
portal-auth volume. FreeIPA, CheckMK and Keycloak still each enforce their
own login behind it — two prompts beats one attacker who only had to beat
one lock. Healthcheck updated to treat 401 as healthy accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the unsigned SMB-policystore auto-sync (enforcer blindly sourced
whatever .sh files sat on the share, no integrity check) with a dedicated
git-over-SSH server plus GPG commit signing: nodes pull on a timer, verify
every commit's signature and fast-forward history before an unprivileged
puller account hands off to root via one exact scoped sudo command, and a
fetch failure is a safe no-op that keeps the last-known-good policy running.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename the security-scan policy to dev_mon_clamscan (fits the XXX_mon_* naming
for monitoring groups) and make it hand results to CheckMK natively instead of
uploading logs to an SMB share.
- dev_mon_clamscan.sh: runs the daily ClamAV/rkhunter/chkrootkit scan and writes
its own CheckMK local check (ansipa_clamscan → service "Ansipa_ClamScan"); the
agent from dev_mon_base collects it. Removes the SMB upload entirely. Discovery
matches dev_mon_clamscan BEFORE the generic dev_mon_* catch-all so it sets
WANT_CLAMSCAN rather than being treated as a dev_mon.sh check. mkdir -p the
cron dir (minimal hosts lack /etc/cron.d until cron is installed).
- Remove the now-redundant dev_mon_malware check from dev_mon.sh (subsumed).
- Remove usr_scan-notify and the whole SMB scan-alert pipeline it fed:
ansipa-fetch-alerts.sh, ansipa-scan-notify.sh, server ansipa-check-scans.sh,
the ansipa-scans Samba share + scanupload account, /etc/ansipa-smb.creds, and
the SMB_SCAN_PASSWORD/smb_scan_password variables (playbook, compose, run.sh,
.env, Dockerfile). The ansipa-luks-keys and ansipa-policystore shares stay.
Verified live on the Arch client: enrolling in dev_mon_clamscan writes the scan
script, cron, and local check (which reports "Ansipa_ClamScan"); leaving the
group removes all three. Docs updated (policy tables, architecture, SMB shares,
scan-flow → CheckMK).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Add an nginx service that fronts all three web UIs on a single hostname/port
and serves a portal landing page, designed to sit behind the operator's own
TLS-terminating reverse proxy.
- 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 one upstream proxy line exposes
everything. FreeIPA's strict Host/Referer anti-CSRF checks are satisfied by
pinning Host to the IPA hostname and rewriting Referer.
- Honors incoming X-Forwarded-Proto/For so it works behind a second reverse
proxy (double-proxy verified: portal, FreeIPA, CheckMK and Keycloak all
reachable through two layers, and a CheckMK login POST completes to an
authenticated session through the gateway).
- Keycloak: KC_HTTP_RELATIVE_PATH=/auth + KC_PROXY_HEADERS=xforwarded so it
builds correct URLs behind the gateway; deliberately not forwarding
X-Forwarded-Port (this gateway is :80 internally; the public port is the
upstream proxy's) to avoid https://host:80 redirects.
- .env.example documents ANSIPA_HTTP_PORT and gives the exact upstream
reverse-proxy snippet (nginx location block + Caddy one-liner).
Portal is a static cyberqueer-themed page linking to /ipa/ui/, /cmk/, /auth/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Found while bringing up the FreeIPA+CheckMK+Keycloak stack in a VM:
- Pin volume and network names (name:) so compose and the cgroup-v2 run.sh
wrapper share the same objects. Compose otherwise prefixes the project name
(image_cmk-creds, image_ipa-net), so the run.sh-started freeipa container
mounted a different cmk-creds volume than the compose-started checkmk wrote
to — the automation secret never reached IPA and the dev_mon_* integration
retried forever. run.sh also now labels the network it creates so compose
adopts it instead of erroring "exists but not created by compose".
- run.sh: mount cmk-creds into freeipa and include checkmk in the "all" target;
both were missing, so the CheckMK path was never wired up via run.sh.
- Fix both healthchecks: Keycloak has no curl/wget and serves /health on the
management port 9000 only when KC_HEALTH_ENABLED=true, so the old curl probe
left it permanently unhealthy — use a bash /dev/tcp probe against 9000.
CheckMK's /api/1.0/version needs auth (401), so probe the login page instead.
- Advertise a client-routable CheckMK URL (CMK_ADVERTISED_URL) in the
dev_mon_base description instead of the container-internal 172.30.0.12, which
enrolled clients cannot reach; falls back to the internal URL when unset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
- ansipa-enforce-policies.sh header: add missing dev_ssh_*, usr_smb_r_*,
usr_smb_rw_*, dev_mon_* (base/malware/timeshift/power), usr_mon_logins
with concise descriptions matching actual behaviour. Notes section
updated with CheckMK setup prerequisite.
- ansipa_packages local check: replace broken rpm||dpkg fallback (wc -l
always exits 0 so dpkg/pacman branches were never reached) with an
if/elif/elif that probes for rpm, dpkg, or pacman in order. Includes
pacman.log-based recent-packages list for Arch hosts.
- deploy-ansipa-policies.yml prerequisites: add CheckMK and SMB mount notes.
- .env.example: add CheckMK section (CMK_ADMIN_PASSWORD, CMK_SITE_ID).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ansipa-smb-setup.sh:
- Adds KeyAdmin Linux group and luks-upload service account (member of
KeyAdmin) on the IPA container, both persisted across restarts.
- LUKS base dir /data/luks-keys owned root:KeyAdmin, mode 2750 (setgid
so new files inherit the group).
- New [ansipa-luks-keys] SMB share: valid users = @KeyAdmin, read only,
write list = luks-upload. Human admins gain read access by being added
to KeyAdmin: useradd -r -G KeyAdmin <user> && smbpasswd -a <user>.
- LUKS_KEY_UPLOAD_PASSWORD sourced from env / /data/samba/ansipa-smb.env
alongside the existing SMB_SCAN_PASSWORD.
collect-luks-keys.yml:
- After fetching /_LUKS_BACKUP_KEY from each client, uploads it to the
ansipa-luks-keys share via smbclient using a temp credentials file
(no_log, deleted in post_tasks).
- Local staging copy is removed after a successful upload.
- SMB credentials file uses an epoch-stamped path to avoid collisions.
.env.example: documents LUKS_KEY_UPLOAD_PASSWORD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Container (ansipa image):
- Add samba + cronie to Dockerfile; expose ports 445/139
- ansipa-smb-setup.sh: idempotent setup of smbd + scanupload user +
/data/scan-results/{archive,alerts}/ on every container start
- ansipa-smb.service: runs setup before smb.service on each boot
- ansipa-check-scans.sh: hourly cron on server; analyses archive logs for
ClamAV/rkhunter/chkrootkit findings and writes <host>/<date>.alert files
- docker-compose.yml: add SMB_SCAN_PASSWORD env var + port mappings
- .env.example: document SMB_SCAN_PASSWORD
Client (policy-security-scan):
- Scan script now uploads log to //ipa-server/ansipa-scans/archive/<host>/
via smbclient after each run
Client (policy-scan-notify — new policy group):
- ansipa-fetch-alerts.sh: root timer (10 min) downloads alerts from SMB into
~/administration/<hostname>/ for each active login session; deletes server
alert when user removes local file (acknowledgment)
- ansipa-scan-notify.sh: user daemon started via /etc/profile.d/ansipa-notify.sh;
sends notify-send every 10 min while *.alert files remain in ~/administration/
- deploy-ansipa-policies.yml: installs samba-client, deploys SMB creds file
(/etc/ansipa-smb.creds, 0600), and deploys both notification scripts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
freeipa-image-builder.sh: TUI chooser that builds a FreeIPA server image
and exports it to four target formats:
docker — builds via podman/docker, optional registry push
lxc — exports container rootfs as .tar.zst Proxmox CT template,
generates pct import instructions
proxmox-vm — downloads Rocky/Fedora cloud image, customizes with
virt-customize, outputs QCOW2 + cloud-init user-data.yml
oci-archive — skopeo OCI tarball for air-gapped import
Keycloak TUI option generates the full constellation:
docker-compose.yml FreeIPA + Keycloak + PostgreSQL stack
.env pre-filled env template (passwords placeholder)
keycloak-configure.sh post-start Keycloak REST API config script
image/Dockerfile: Fedora 41 + freeipa-server-dns + ansible-core,
systemd-enabled container (CMD /sbin/init).
image/ipa-first-boot.{sh,service}: systemd oneshot that runs
ipa-server-install on first container/VM boot from env vars
(IPA_DOMAIN, IPA_ADMIN_PASSWORD, IPA_DM_PASSWORD, and optionals).
ConditionPathExists=!/etc/ipa/default.conf makes it idempotent.
image/keycloak-configure.sh: Keycloak REST API automation that:
- waits for Keycloak readiness
- creates a realm
- wires FreeIPA LDAP user federation (READ_ONLY, vendor=rhds)
- adds attribute mappers: email, firstName, lastName, uidNumber
- adds group mapper (IPA groups → Keycloak groups, cn=groups,cn=accounts)
- triggers an initial full user sync
image/docker-compose.yml: freeipa + postgres + keycloak services on
a private 172.30.0.0/24 bridge; FreeIPA has a fixed IP so Keycloak
can resolve it via extra_hosts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>