feat(ansipa): add Nextcloud, and make FreeIPA the sole identity source
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
main
parent
68b3f2bdd0
commit
268feaa1f2
|
|
@ -3,13 +3,16 @@
|
|||
**ansipa** = **Ans**ible + Free**IPA**: a GPO-like management plane for Linux
|
||||
fleets. FreeIPA provides identity, Kerberos, sudo/HBAC and host/user groups;
|
||||
a lightweight client-side enforcer turns group membership into enforced policy;
|
||||
CheckMK monitors the fleet; Keycloak adds SSO; and an nginx gateway fronts all
|
||||
CheckMK monitors the fleet; Keycloak adds SSO; Nextcloud provides files,
|
||||
calendar and contacts on the same identities; and an nginx gateway fronts all
|
||||
the web UIs behind one hostname.
|
||||
|
||||
This guide covers a full deployment: architecture, every port, the reverse-proxy
|
||||
gateway, the exact snippet to expose it behind your own reverse proxy, the
|
||||
NAT / behind-firewall behaviour, and known distro caveats. For the policy
|
||||
catalogue and playbook reference see [FreeIPA & Ansible](freeipa-ansible.md).
|
||||
catalogue and playbook reference see [FreeIPA & Ansible](freeipa-ansible.md);
|
||||
for how logins flow from FreeIPA through Keycloak into each service — and why
|
||||
CheckMK is the one exception — see [ansipa Single Sign-On](ansipa-sso.md).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -24,8 +27,10 @@ catalogue and playbook reference see [FreeIPA & Ansible](freeipa-ansible.md).
|
|||
https://ansipa ──┼──►│ /ipa/ ─► freeipa :443 ───┼──► 172.30.0.10 FreeIPA │
|
||||
.example.com │ │ /cmk/ ─► checkmk :5000 ──┼──► 172.30.0.12 CheckMK │
|
||||
(:443) │ │ /auth/ ─► keycloak :8080 ──┼──► keycloak Keycloak │
|
||||
│ └────────────────────────────┘ │ │
|
||||
│ :8088 (http) └─ postgres (KC backend) │
|
||||
│ │ /nextcloud/ ─► nextcloud ──┼──► nextcloud Nextcloud │
|
||||
│ └────────────────────────────┘ │ │ │
|
||||
│ :8088 (http) │ └─ redis │
|
||||
│ └─ postgres (KC + NC) │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
▲ ▲
|
||||
client-initiated only │ (enroll, kinit, policy pull, │
|
||||
|
|
@ -61,7 +66,9 @@ cp .env.example .env && $EDITOR .env # set domain, realm, passwords, ports
|
|||
docker compose up -d
|
||||
|
||||
docker compose logs -f freeipa # watch first-boot (~10 min)
|
||||
./keycloak-configure.sh # wire Keycloak → FreeIPA LDAP (once healthy)
|
||||
./keycloak-configure.sh # Keycloak → FreeIPA LDAP + OIDC clients
|
||||
./nextcloud-configure.sh # Nextcloud → IPA LDAP + Keycloak OIDC
|
||||
./checkmk-ldap-configure.sh # CheckMK → FreeIPA LDAP (no OIDC in Raw)
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -168,16 +175,17 @@ udp dport { 88, 464, 137, 138 } accept
|
|||
|
||||
## The nginx gateway (portal + reverse proxy)
|
||||
|
||||
The `ansipa-nginx` service fronts all three web UIs on **one hostname/port** and
|
||||
The `ansipa-nginx` service fronts every web UI on **one hostname/port** and
|
||||
serves a portal landing page. Each backend is proxied at the path it already
|
||||
uses natively, so there is no path rewriting to fight:
|
||||
|
||||
| Path | Backend | Notes |
|
||||
|------|---------|-------|
|
||||
| `/` | portal start page | static, links to the three UIs |
|
||||
| `/` | portal start page | static, links to each UI |
|
||||
| `/ipa/` | FreeIPA `:443` | Host + Referer pinned to the IPA hostname (IPA anti-CSRF) |
|
||||
| `/cmk/` | CheckMK `:5000` | CheckMK already serves under `/cmk` |
|
||||
| `/auth/` | Keycloak `:8080` | Keycloak runs with `KC_HTTP_RELATIVE_PATH=/auth` |
|
||||
| `/nextcloud/` | Nextcloud `:80` | matches `OVERWRITEWEBROOT`; `/.well-known/*` DAV redirects sit at the root |
|
||||
|
||||
The gateway speaks plain HTTP on `ANSIPA_HTTP_PORT` (default **8088**) and trusts
|
||||
`X-Forwarded-*`, so it is designed to sit **behind your own TLS-terminating
|
||||
|
|
|
|||
|
|
@ -0,0 +1,281 @@
|
|||
# ansipa Single Sign-On
|
||||
|
||||
How identity flows through the ansipa stack, which services can actually use
|
||||
it, and how to wire the infrastructure appliances (Proxmox VE/BS, OPNsense)
|
||||
into it.
|
||||
|
||||
The rule for this stack: **no service keeps its own account list.** Every
|
||||
account lives in FreeIPA, and every login is decided either by Keycloak or by
|
||||
FreeIPA itself. The only exceptions are two deliberate break-glass local
|
||||
admins, documented at the bottom.
|
||||
|
||||
---
|
||||
|
||||
## The identity chain
|
||||
|
||||
```
|
||||
FreeIPA ── accounts, groups, sudo/HBAC rules. The source of truth.
|
||||
│
|
||||
├─ LDAP (READ_ONLY federation) ──▶ Keycloak ── issues OIDC/SAML
|
||||
│ │
|
||||
│ ├─▶ Nextcloud (user_oidc)
|
||||
│ ├─▶ Proxmox VE (OpenID realm)
|
||||
│ ├─▶ Proxmox Backup (OpenID realm)
|
||||
│ └─▶ OPNsense (see caveat)
|
||||
│
|
||||
└─ LDAP (direct bind) ───────────▶ CheckMK
|
||||
└ no SAML/OIDC in the Community
|
||||
edition, so it binds IPA itself
|
||||
```
|
||||
|
||||
Keycloak mirrors FreeIPA read-only. Creating a user in Keycloak's own database
|
||||
is always a mistake here — it produces an account IPA does not know about, and
|
||||
it will not survive a re-sync.
|
||||
|
||||
---
|
||||
|
||||
## Support matrix
|
||||
|
||||
| Service | Method | Edition/version caveat |
|
||||
|---|---|---|
|
||||
| Nextcloud | OIDC via Keycloak, accounts from IPA LDAP | `user_oidc` app must be installed |
|
||||
| Proxmox VE | OpenID Connect realm | PVE 7.0+ |
|
||||
| Proxmox Backup Server | OpenID Connect realm | PBS 2.2+ |
|
||||
| CheckMK | **Direct LDAP to FreeIPA** | SAML is commercial-only; OIDC unsupported in every edition |
|
||||
| OPNsense | LDAP to FreeIPA (recommended) | No built-in OIDC relying-party support |
|
||||
|
||||
### The two that cannot use Keycloak
|
||||
|
||||
**CheckMK.** The Community (Raw) edition has no SAML — the docs state plainly
|
||||
that "SAML is supported in the commercial editions" — and no edition of CheckMK
|
||||
supports OIDC at all. The old `mod_auth_mellon` workaround was dropped in 2.3.
|
||||
So CheckMK binds FreeIPA over LDAP directly. Identity still comes from one
|
||||
place, it just takes a shorter path. Run `./checkmk-ldap-configure.sh`.
|
||||
|
||||
**OPNsense.** It can consume LDAP and RADIUS, but it is not an OIDC relying
|
||||
party, so Keycloak cannot front the web UI. Point it at FreeIPA's LDAP
|
||||
directly (System → Access → Servers), which keeps accounts centralised even
|
||||
though the login is not federated. The `opnsense` Keycloak client that
|
||||
`keycloak-configure.sh` can provision is only useful if you put an
|
||||
OIDC-aware proxy in front of the UI, or for OPNsense plugins that speak OIDC.
|
||||
|
||||
---
|
||||
|
||||
## Setup order
|
||||
|
||||
Everything below assumes the stack is up (`./run.sh all`).
|
||||
|
||||
```bash
|
||||
cd setup/modules/FreeipaAnsible/image
|
||||
|
||||
./keycloak-configure.sh # IPA LDAP federation + OIDC clients + groups claim
|
||||
./nextcloud-configure.sh # Nextcloud → IPA LDAP + Keycloak OIDC
|
||||
./checkmk-ldap-configure.sh # CheckMK → IPA LDAP directly
|
||||
```
|
||||
|
||||
`keycloak-configure.sh` writes each client's secret to `image/.oidc-secrets`
|
||||
(mode 0600, gitignored). Clients are only created for relying parties whose
|
||||
base URL is set in `.env` — `NC_PUBLIC_URL`, `PVE_URL`, `PBS_URL`,
|
||||
`OPNSENSE_URL`. Set one later and re-run; existing clients keep their secrets.
|
||||
|
||||
The discovery URL every relying party needs is:
|
||||
|
||||
```
|
||||
https://<your-gateway>/auth/realms/<KC_REALM>/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nextcloud
|
||||
|
||||
Two backends do two different jobs, and both are required:
|
||||
|
||||
- **`user_ldap`** binds FreeIPA and owns the account list. Users, display
|
||||
names, mail and group membership come from IPA and are read-only.
|
||||
- **`user_oidc`** performs the login against Keycloak, so password policy, MFA
|
||||
and session control stay in one place.
|
||||
|
||||
The join between them is the username, and it is the part that breaks if you
|
||||
change it carelessly. Nextcloud's internal username is pinned to the IPA `uid`
|
||||
(`ldapExpertUsernameAttr`), and the OIDC provider maps `preferred_username`
|
||||
onto it with **unique-uid disabled**. Leave unique-uid on and `user_oidc`
|
||||
hashes the claim into an ID of its own, so every SSO login creates a second,
|
||||
empty account beside the LDAP one.
|
||||
|
||||
Verify after setup:
|
||||
|
||||
```bash
|
||||
docker compose exec -u www-data nextcloud php occ user:list
|
||||
```
|
||||
|
||||
That should list your FreeIPA users. Log in once through the Keycloak button,
|
||||
then run it again — if a second account appeared for the same person, the uid
|
||||
mapping is wrong.
|
||||
|
||||
Restrict who gets an account with `NC_LDAP_GROUP=usr_nextcloud` in `.env`; the
|
||||
LDAP filter also excludes IPA-disabled users (`nsAccountLock=TRUE`), so
|
||||
disabling someone in IPA locks them out of Nextcloud too.
|
||||
|
||||
---
|
||||
|
||||
## Proxmox VE
|
||||
|
||||
Set `PVE_URL=https://pve.example.com:8006` in `.env`, re-run
|
||||
`./keycloak-configure.sh`, then on the PVE node:
|
||||
|
||||
```bash
|
||||
pveum realm add keycloak \
|
||||
--type openid \
|
||||
--issuer-url https://<gateway>/auth/realms/<KC_REALM> \
|
||||
--client-id proxmox-ve \
|
||||
--client-key <secret from .oidc-secrets> \
|
||||
--username-claim username \
|
||||
--autocreate 1
|
||||
```
|
||||
|
||||
`--username-claim` accepts only `subject`, `username` or `email` — not
|
||||
`preferred_username`, despite that being the underlying claim name. `username`
|
||||
is the one you want; it is what makes the PVE user match the IPA uid.
|
||||
|
||||
`--autocreate 1` creates the PVE user entry on first login. It does **not**
|
||||
grant permissions: assign those to groups under Datacenter → Permissions.
|
||||
|
||||
Two things that reliably break this:
|
||||
|
||||
- Keycloak must be reachable from the PVE node over **HTTPS with a
|
||||
certificate the node trusts**. A self-signed gateway cert produces an
|
||||
opaque HTTP 500 at login; install the CA on the node.
|
||||
- Proxmox expects RS256-signed tokens, which is Keycloak's default. Do not
|
||||
switch the realm or client to ES256/HS256.
|
||||
|
||||
## Proxmox Backup Server
|
||||
|
||||
Same model, `PBS_URL` and the `proxmox-bs` client:
|
||||
|
||||
```bash
|
||||
proxmox-backup-manager openid create keycloak \
|
||||
--issuer-url https://<gateway>/auth/realms/<KC_REALM> \
|
||||
--client-id proxmox-bs \
|
||||
--client-key <secret from .oidc-secrets> \
|
||||
--username-claim username \
|
||||
--autocreate true
|
||||
```
|
||||
|
||||
PBS needs its own client — reusing the PVE one fails on the redirect URI.
|
||||
|
||||
---
|
||||
|
||||
## Monitoring the same appliances
|
||||
|
||||
Separate from SSO, and worth not conflating: these appliances are also
|
||||
monitored, and monitoring uses CheckMK's automation credentials, never a user
|
||||
identity.
|
||||
|
||||
### Proxmox VE / PBS — already wired
|
||||
|
||||
`ansipa-infra-checkmk-install.sh` installs the agent, drops the
|
||||
`ansipa_pve` / `ansipa_pbs` local check and registers the host under
|
||||
`/ansipa/infra`. Run it on each node:
|
||||
|
||||
```bash
|
||||
./ansipa-infra-checkmk-install.sh https://mon.example.com:8090 cmk <automation-secret>
|
||||
```
|
||||
|
||||
That covers cluster quorum, storage utilisation, guest counts and backup
|
||||
freshness (PVE); datastore usage, last-backup outcome and task failures (PBS).
|
||||
|
||||
**Also worth enabling:** CheckMK ships a built-in *Proxmox VE special agent*
|
||||
(Setup → Agents → VM, cloud, container → Proxmox VE) that talks to the PVE API
|
||||
on port 8006 and generates piggyback data for the guests. It complements the
|
||||
local checks rather than replacing them — the special agent sees the cluster
|
||||
from outside and gives you per-VM data, the local check sees the node from
|
||||
inside. Create a dedicated `checkmk@pve` user in the *Proxmox VE
|
||||
authentication server* realm for it, not an OIDC user: the special agent needs
|
||||
a password it can present non-interactively.
|
||||
|
||||
### OPNsense — new
|
||||
|
||||
```bash
|
||||
scp ansipa-opnsense-*.sh root@fw:/root/
|
||||
ssh root@fw /root/ansipa-opnsense-checkmk-install.sh \
|
||||
https://mon.example.com:8090 cmk <automation-secret>
|
||||
```
|
||||
|
||||
Adds `Ansipa_OPN_Gateways`, `Ansipa_OPN_States` (pf state table against its
|
||||
hard limit — the check that actually catches a firewall falling over),
|
||||
`Ansipa_OPN_Firmware`, and `Ansipa_OPN_CARP` on HA pairs.
|
||||
|
||||
The transport is deliberately **agent-over-SSH**, not the usual TCP 6556 via
|
||||
inetd. OPNsense generates `/etc/inetd.conf`, `/etc/services` and
|
||||
`/etc/hosts.allow` from `config.xml`, so it reverts those edits on the next
|
||||
reboot or config apply and monitoring dies quietly weeks later. With the SSH
|
||||
datasource nothing listens on the firewall and nothing gets overwritten. The
|
||||
installer prints the exact key-restriction and CheckMK rule to add.
|
||||
|
||||
If you would rather not give CheckMK SSH access at all, enable SNMP on the
|
||||
firewall and set the host to SNMP in CheckMK — you get interfaces, CPU, memory
|
||||
and uptime, but none of the `Ansipa_OPN_*` checks, which need local execution.
|
||||
|
||||
### Keycloak itself
|
||||
|
||||
Keycloak exposes Prometheus-format metrics on the management port 9000
|
||||
(`/metrics`, with `KC_HEALTH_ENABLED=true` already set in the compose file).
|
||||
CheckMK Raw can scrape those, and health endpoints `/health/ready` and
|
||||
`/health/live` are the cheap liveness signal. Note these live on port **9000**
|
||||
and are *not* under `KC_HTTP_RELATIVE_PATH` — probing
|
||||
`https://gateway/auth/health/ready` returns 404 and will look like an outage
|
||||
that is not there.
|
||||
|
||||
---
|
||||
|
||||
## Packet captures to the SmartestHome LLM
|
||||
|
||||
Short answer: **not through CheckMK.** Passing a CheckMK API key to the LLM is
|
||||
a good idea, but it buys you monitoring state, not packet data.
|
||||
|
||||
**Why not.** CheckMK is a state-and-metrics system. Its REST API exposes hosts,
|
||||
services, service states, metrics, events, downtimes and comments — there is no
|
||||
packet-capture object and no blob transport. You could technically smuggle
|
||||
base64 pcap through a custom agent section, but the agent payload is re-parsed
|
||||
in full on every check cycle (60s by default): captures are megabytes per
|
||||
second, agent output is expected to be kilobytes, and you would blow up the
|
||||
monitoring core to build a bad file transfer.
|
||||
|
||||
**What does work, with the same "one API key" ergonomics:**
|
||||
|
||||
1. **Give the LLM the CheckMK REST API for state.** One automation secret gets
|
||||
it every host, every service, current and historical metrics, and the event
|
||||
console. That answers "what is broken, since when, and what changed" —
|
||||
which is most of what you actually want an LLM triaging.
|
||||
|
||||
2. **Put the packet analysis where the packets are.** Run Suricata or Zeek on
|
||||
the OPNsense box (both are OPNsense plugins). They turn traffic into
|
||||
structured events — `eve.json`, Zeek logs — which is far better LLM input
|
||||
than raw pcap anyway, and orders of magnitude smaller.
|
||||
|
||||
3. **Bridge the two.** A local check on the firewall summarises IDS alert
|
||||
counts/severities into CheckMK, so alerts show up beside everything else and
|
||||
the LLM can see them through the same API key. When it needs detail, it
|
||||
fetches the full event from the Suricata/Zeek store — a second, narrow
|
||||
credential — using the timestamp and flow ID CheckMK gave it.
|
||||
|
||||
That keeps the "one key for situational awareness" property you wanted, without
|
||||
making the monitoring system carry traffic it was never designed to move. If
|
||||
you do want genuine on-demand pcap, the honest design is a small capture
|
||||
service on the firewall exposing an authenticated endpoint, referenced *from* a
|
||||
CheckMK service — not tunnelled through it.
|
||||
|
||||
---
|
||||
|
||||
## Break-glass accounts
|
||||
|
||||
Two local accounts deliberately do not live in FreeIPA, because they are how
|
||||
you get in when the directory or Keycloak is the thing that is broken:
|
||||
|
||||
| Account | Where | Reach it at |
|
||||
|---|---|---|
|
||||
| `cmkadmin` | CheckMK | `/cmk/` normal login form |
|
||||
| `ncadmin` (`NC_ADMIN_USER`) | Nextcloud | `/nextcloud/login?direct=1` |
|
||||
|
||||
Keep both in a password manager. Everything else — including your own
|
||||
day-to-day admin account — should be an IPA identity.
|
||||
|
|
@ -18,6 +18,7 @@ A production-grade Arch Linux configuration for network administration, developm
|
|||
| [Archiso](archiso.md) | Building the custom live installer ISO |
|
||||
| [ansipa Setup Guide](ansipa-setup.md) | Full deployment: ports, nginx portal + reverse proxy, NAT, enrollment |
|
||||
| [ansipa Administration](ansipa-administration.md) | Day-to-day operation: the full policy catalogue + recipes |
|
||||
| [ansipa Single Sign-On](ansipa-sso.md) | Identity chain: FreeIPA → Keycloak → Nextcloud/Proxmox/OPNsense; CheckMK's LDAP exception |
|
||||
| [FreeIPA & Ansible](freeipa-ansible.md) | Identity management and automated config deployment |
|
||||
| [Editors](editors.md) | Neovim, Micro, Yazi |
|
||||
| [Utilities](utilities.md) | Encryption helpers, ClamAV, credentials, update scripts |
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ To add modules to an existing system: `bash ~/Dotfiles/setup/install-modules.sh`
|
|||
- **Hostname uniqueness** — the MAC address of the primary NIC is appended automatically when an answerfile hostname is set (`myhost` → `myhost-aabbccddee11`).
|
||||
- **LUKS encryption** — backup key is auto-generated from `/dev/urandom`, enrolled in a second LUKS slot, written to `/_LUKS_BACKUP_KEY` (root-only, inside the encrypted container). Collected by Ansible and stored on the SMB `ansipa-luks-keys` share (KeyAdmin-only read access).
|
||||
- **Custom ISO** — `setup/archiso/` builds a live USB that can embed a pre-baked answerfile for zero-touch deployment. The live environment also includes a **System Reset** mode that reinstalls the root subvolume while preserving home data and FIDO2 auth keys.
|
||||
- **FreeIPA + Keycloak + Samba container** — `setup/modules/FreeipaAnsible/image/` ships a single `docker compose up` stack: FreeIPA for identity, Keycloak for OIDC, and Samba for scan-result and LUKS-key SMB shares. Host-group-driven policies (binary blocking, daemon enable/disable, daily scans, alert delivery) are enforced on enrolled clients every 30 minutes via Ansible-deployed timers.
|
||||
- **FreeIPA + Keycloak + CheckMK + Nextcloud container** — `setup/modules/FreeipaAnsible/image/` ships a single `docker compose up` stack: FreeIPA for identity, Keycloak for OIDC, CheckMK for monitoring, Nextcloud for files/calendar/contacts, and Samba for LUKS-key SMB shares. FreeIPA is the single source of truth for accounts; see [ansipa-sso.md](docs/md/ansipa-sso.md). Host-group-driven policies (binary blocking, daemon enable/disable, daily scans, alert delivery) are enforced on enrolled clients every 30 minutes via Ansible-deployed timers.
|
||||
- **Modular** — core, shell, services, and desktop are independent components; pick only what you need.
|
||||
|
||||
---
|
||||
|
|
@ -43,6 +43,7 @@ Full docs live in [`docs/md/`](docs/md/) (Markdown) and [`docs/html/`](docs/html
|
|||
| Optional modules & app catalogue | [modules.md](docs/md/modules.md) | [modules.html](docs/html/modules.html) |
|
||||
| Custom Archiso builder | [archiso.md](docs/md/archiso.md) | [archiso.html](docs/html/archiso.html) |
|
||||
| FreeIPA, Ansible, Keycloak & SMB | [freeipa-ansible.md](docs/md/freeipa-ansible.md) | [freeipa-ansible.html](docs/html/freeipa-ansible.html) |
|
||||
| ansipa Single Sign-On | [ansipa-sso.md](docs/md/ansipa-sso.md) | [ansipa-sso.html](docs/html/ansipa-sso.html) |
|
||||
| Editors (Neovim, Micro, Yazi) | [editors.md](docs/md/editors.md) | [editors.html](docs/html/editors.html) |
|
||||
| Utilities (encrypt, ClamAV, updates) | [utilities.md](docs/md/utilities.md) | [utilities.html](docs/html/utilities.html) |
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,66 @@ KC_DB_PASSWORD=ChangeMe000!
|
|||
# ── Keycloak → FreeIPA LDAP federation ───────────────────────────────────────
|
||||
# Leave IPA_BIND_PASSWORD blank to reuse IPA_DM_PASSWORD.
|
||||
# In production, create a dedicated read-only service account in FreeIPA.
|
||||
IPA_BIND_DN=cn=Directory Manager
|
||||
#
|
||||
# QUOTE any value containing spaces. docker compose parses this file itself and
|
||||
# does not need the quotes, but the *-configure.sh scripts `source` it as shell,
|
||||
# where an unquoted `cn=Directory Manager` is read as the assignment
|
||||
# `IPA_BIND_DN=cn=Directory` followed by a command named `Manager`.
|
||||
IPA_BIND_DN="cn=Directory Manager"
|
||||
IPA_BIND_PASSWORD=
|
||||
IPA_USE_LDAPS=false
|
||||
|
||||
# ── Nextcloud ─────────────────────────────────────────────────────────────────
|
||||
# Served at ${NC_PUBLIC_URL}, i.e. the /nextcloud subpath of the ansipa gateway.
|
||||
#
|
||||
# Accounts come from FreeIPA (LDAP backend) and logins go through Keycloak
|
||||
# (user_oidc) — run ./nextcloud-configure.sh after the stack is up to wire both.
|
||||
# NC_ADMIN_USER/NC_ADMIN_PASSWORD are a BREAK-GLASS LOCAL account: Nextcloud
|
||||
# cannot complete its installer without one. It is not an IPA identity; keep the
|
||||
# password in your password manager and use it only if SSO itself is broken.
|
||||
#
|
||||
# NC_PUBLIC_URL — the externally reachable Nextcloud base URL. This is what
|
||||
# goes in the Keycloak redirect URI and what sync clients
|
||||
# are handed, so it must be the address USERS type (your
|
||||
# TLS proxy's hostname), not the container address.
|
||||
# NC_TRUSTED_DOMAINS — space-separated hostnames Nextcloud will answer for.
|
||||
# A host that is not listed here gets a hard "untrusted
|
||||
# domain" error page instead of a login form.
|
||||
NC_DB_PASSWORD=ChangeMe_NcDb!
|
||||
NC_ADMIN_USER=ncadmin
|
||||
NC_ADMIN_PASSWORD=ChangeMe_NcAdmin!
|
||||
NC_PUBLIC_URL=http://localhost:8088/nextcloud
|
||||
NC_TRUSTED_DOMAINS=localhost
|
||||
|
||||
# ── Nextcloud → FreeIPA LDAP (account source of truth) ───────────────────────
|
||||
# Dedicated read-only bind account is strongly recommended over Directory
|
||||
# Manager. Leave blank to fall back to IPA_BIND_DN / IPA_BIND_PASSWORD.
|
||||
# NC_LDAP_GROUP — if set, ONLY members of this IPA group get a Nextcloud
|
||||
# account (e.g. usr_nextcloud). Blank means every IPA user.
|
||||
# Quote any DN containing spaces (see the note under IPA_BIND_DN below).
|
||||
NC_LDAP_BIND_DN=
|
||||
NC_LDAP_BIND_PASSWORD=
|
||||
NC_LDAP_GROUP=
|
||||
|
||||
# ── CheckMK → FreeIPA LDAP ────────────────────────────────────────────────────
|
||||
# CheckMK Community/Raw edition has NO SAML and NO OIDC (both are commercial-
|
||||
# edition features), so it cannot sit behind Keycloak. It binds FreeIPA over
|
||||
# LDAP directly instead — still one account store, one less hop.
|
||||
# Run ./checkmk-ldap-configure.sh to apply.
|
||||
# CMK_LDAP_ADMIN_GROUP — IPA group whose members get the CheckMK "admin" role.
|
||||
# CMK_LDAP_USER_GROUP — IPA group whose members get the read-only "user" role.
|
||||
CMK_LDAP_ADMIN_GROUP=usr_cmk_admins
|
||||
CMK_LDAP_USER_GROUP=usr_cmk_users
|
||||
|
||||
# ── External SSO relying parties (Proxmox VE / PBS / OPNsense) ────────────────
|
||||
# Base URLs of infrastructure appliances that should log in through Keycloak.
|
||||
# Set a URL here and keycloak-configure.sh provisions a matching OIDC client,
|
||||
# writing its secret to .oidc-secrets; leave one blank to skip that client.
|
||||
# Use the URL an ADMIN types in the browser, including the port.
|
||||
# PVE_URL e.g. https://pve.corp.example.com:8006
|
||||
# PBS_URL e.g. https://pbs.corp.example.com:8007
|
||||
# OPNSENSE_URL e.g. https://fw.corp.example.com
|
||||
# See docs/md/ansipa-sso.md for the matching config on each appliance.
|
||||
PVE_URL=
|
||||
PBS_URL=
|
||||
OPNSENSE_URL=
|
||||
|
|
|
|||
|
|
@ -1,2 +1,12 @@
|
|||
# Test credentials — never commit
|
||||
.env
|
||||
|
||||
# Keycloak OIDC client secrets, written by keycloak-configure.sh
|
||||
.oidc-secrets
|
||||
.oidc-secrets.tmp
|
||||
|
||||
# Which Nextcloud LDAP config nextcloud-configure.sh owns (deployment state)
|
||||
.nextcloud-ldap-config
|
||||
|
||||
# CheckMK's FreeIPA sysaccount bind credentials, written by checkmk-ldap-configure.sh
|
||||
.cmk-ldap-bind
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
#!/bin/sh
|
||||
# ansipa-opnsense-checkmk-install.sh — wire an OPNsense firewall into CheckMK.
|
||||
#
|
||||
# Run as root ON THE FIREWALL. POSIX sh only: OPNsense is FreeBSD and has no
|
||||
# bash in the base system.
|
||||
#
|
||||
# Usage:
|
||||
# ansipa-opnsense-checkmk-install.sh <CMK_URL> <CMK_SITE> <CMK_SECRET> [HOST_FQDN] [CMK_USER]
|
||||
#
|
||||
# Copy this script plus ansipa-opnsense-monitor.sh to the firewall first, e.g.
|
||||
# scp ansipa-opnsense-*.sh root@fw:/root/
|
||||
#
|
||||
# ── Transport: why SSH and not the usual agent port ──────────────────────────
|
||||
# The normal CheckMK agent listens on TCP 6556 via inetd. On OPNsense that is a
|
||||
# poor fit: /etc/inetd.conf, /etc/services and /etc/hosts.allow are GENERATED
|
||||
# from config.xml, so the firewall silently reverts those edits on the next
|
||||
# reboot or config apply, and monitoring dies quietly weeks later.
|
||||
#
|
||||
# So this installs the agent as a plain script and leaves the transport to
|
||||
# CheckMK's "individual program call" datasource — CheckMK SSHes in and runs
|
||||
# the agent on demand. Nothing listens, nothing to punch through the firewall
|
||||
# rules, and nothing for OPNsense to overwrite. The script prints the exact
|
||||
# rule to create at the end.
|
||||
#
|
||||
# It also registers the host in CheckMK via the REST API, in folder
|
||||
# /ansipa/infra alongside the PVE/PBS hosts.
|
||||
|
||||
set -eu
|
||||
|
||||
LOG_TAG="ansipa-opnsense-checkmk"
|
||||
log() { echo "[$LOG_TAG] $*"; }
|
||||
warn() { echo "[$LOG_TAG][WARN] $*" >&2; }
|
||||
die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run as root."
|
||||
[ $# -ge 3 ] || die "Usage: $0 <CMK_URL> <CMK_SITE> <CMK_SECRET> [HOST_FQDN] [CMK_USER]"
|
||||
|
||||
CMK_URL=$(echo "$1" | sed 's|/$||')
|
||||
CMK_SITE="$2"
|
||||
CMK_SECRET="$3"
|
||||
HOST_FQDN="${4:-$(hostname)}"
|
||||
CMK_USER="${5:-automation}"
|
||||
CMK_API="${CMK_URL}/${CMK_SITE}/check_mk/api/1.0"
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
AGENT_BIN=/usr/local/bin/check_mk_agent
|
||||
LOCAL_DIR=/usr/local/lib/check_mk_agent/local
|
||||
|
||||
command -v opnsense-version >/dev/null 2>&1 \
|
||||
|| die "opnsense-version not found — this does not look like an OPNsense host."
|
||||
log "OPNsense detected: $(opnsense-version 2>/dev/null | head -1)"
|
||||
|
||||
# ── Install the FreeBSD agent ────────────────────────────────────────────────
|
||||
# Pulled from the monitoring server so agent and server versions always match.
|
||||
if [ ! -x "$AGENT_BIN" ]; then
|
||||
log "Downloading the FreeBSD agent from ${CMK_URL}..."
|
||||
fetch -q -o "$AGENT_BIN" \
|
||||
--user-agent="ansipa" \
|
||||
"${CMK_URL}/${CMK_SITE}/check_mk/agents/check_mk_agent.freebsd" 2>/dev/null \
|
||||
|| curl -sf -u "${CMK_USER}:${CMK_SECRET}" \
|
||||
"${CMK_URL}/${CMK_SITE}/check_mk/agents/check_mk_agent.freebsd" \
|
||||
-o "$AGENT_BIN" 2>/dev/null \
|
||||
|| die "Could not download check_mk_agent.freebsd — check CMK_URL/site/secret."
|
||||
chmod 755 "$AGENT_BIN"
|
||||
log "Agent installed at $AGENT_BIN"
|
||||
else
|
||||
log "Agent already present at $AGENT_BIN"
|
||||
fi
|
||||
|
||||
# ── Install the ansipa local check ───────────────────────────────────────────
|
||||
mkdir -p "$LOCAL_DIR"
|
||||
if [ -f "$SCRIPT_DIR/ansipa-opnsense-monitor.sh" ]; then
|
||||
install -m 755 "$SCRIPT_DIR/ansipa-opnsense-monitor.sh" "$LOCAL_DIR/ansipa_opnsense"
|
||||
log "Installed local check: $LOCAL_DIR/ansipa_opnsense"
|
||||
else
|
||||
warn "ansipa-opnsense-monitor.sh not found next to this script — local check NOT installed."
|
||||
fi
|
||||
|
||||
# ── Keep the firmware status cache warm ──────────────────────────────────────
|
||||
# The local check reads a cached update count rather than hitting the mirrors on
|
||||
# every poll. /usr/local/etc/rc.syshook.d survives reboots and firmware updates,
|
||||
# unlike /etc/crontab which OPNsense regenerates.
|
||||
SYSHOOK_DIR=/usr/local/etc/rc.syshook.d/start
|
||||
mkdir -p "$SYSHOOK_DIR"
|
||||
cat > "$SYSHOOK_DIR/99-ansipa-cmk" <<'HOOK'
|
||||
#!/bin/sh
|
||||
# ansipa: refresh the firmware status cache once a day so the CheckMK local
|
||||
# check can report pending updates without going to the network itself.
|
||||
(
|
||||
while true; do
|
||||
/usr/local/sbin/configctl firmware check >/dev/null 2>&1 || true
|
||||
sleep 86400
|
||||
done
|
||||
) &
|
||||
HOOK
|
||||
chmod 755 "$SYSHOOK_DIR/99-ansipa-cmk"
|
||||
log "Installed firmware-cache refresher: $SYSHOOK_DIR/99-ansipa-cmk"
|
||||
"$SYSHOOK_DIR/99-ansipa-cmk" 2>/dev/null || true
|
||||
|
||||
# ── Register the host in CheckMK ─────────────────────────────────────────────
|
||||
# Same folder the PVE/PBS appliances land in. 422 means it already exists.
|
||||
_folder_http=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||
-X POST "${CMK_API}/domain-types/folder_config/collections/all" \
|
||||
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
|
||||
-H "Content-Type: application/json" -H "Accept: application/json" \
|
||||
-d '{"name":"infra","title":"Ansipa Infra (PVE/PBS)","parent":"/ansipa"}' \
|
||||
2>/dev/null || echo "000")
|
||||
case "$_folder_http" in
|
||||
2*|422) ;;
|
||||
*) warn "Unexpected response creating /ansipa/infra: HTTP $_folder_http" ;;
|
||||
esac
|
||||
|
||||
_HOST_IP=$(ifconfig 2>/dev/null | awk '/inet /{if($2!="127.0.0.1"){print $2; exit}}')
|
||||
|
||||
if curl -sf -o /dev/null \
|
||||
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
|
||||
"${CMK_API}/objects/host_config/${HOST_FQDN}" 2>/dev/null; then
|
||||
log "${HOST_FQDN} already registered in CheckMK"
|
||||
else
|
||||
_body="{\"host_name\":\"${HOST_FQDN}\",\"folder\":\"/ansipa/infra\""
|
||||
[ -n "$_HOST_IP" ] && _body="${_body},\"attributes\":{\"ipaddress\":\"${_HOST_IP}\",\"tag_role\":\"opnsense\"}"
|
||||
_body="${_body}}"
|
||||
_http=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||
-X POST "${CMK_API}/domain-types/host_config/collections/all" \
|
||||
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
|
||||
-H "Content-Type: application/json" -H "Accept: application/json" \
|
||||
-d "$_body" 2>/dev/null || echo "000")
|
||||
case "$_http" in
|
||||
2*) log "Registered ${HOST_FQDN} in CheckMK (/ansipa/infra)" ;;
|
||||
*) warn "Host registration failed (HTTP ${_http}) — add it in the UI." ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
[$LOG_TAG] Firewall side is done. Two manual steps remain on the CheckMK server,
|
||||
because both involve credentials that should not travel in this script:
|
||||
|
||||
1. Let the CheckMK site SSH to this firewall as a monitoring user.
|
||||
On the CheckMK server, as the site user:
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ansipa_opnsense -N ''
|
||||
ssh-copy-id -i ~/.ssh/id_ansipa_opnsense root@${HOST_FQDN}
|
||||
Restrict it on this firewall by prefixing the key in
|
||||
/root/.ssh/authorized_keys with:
|
||||
command="${AGENT_BIN}",no-port-forwarding,no-pty
|
||||
|
||||
2. Tell CheckMK to call the agent over SSH instead of TCP 6556:
|
||||
Setup → Hosts → ${HOST_FQDN} → "Individual program call instead of agent access"
|
||||
(rule: "Datasource programs → Individual program call"), command:
|
||||
ssh -i \$HOME/.ssh/id_ansipa_opnsense -o StrictHostKeyChecking=accept-new root@${HOST_FQDN}
|
||||
|
||||
Then run a service discovery on ${HOST_FQDN} and activate changes.
|
||||
|
||||
Services this adds beyond the stock FreeBSD checks:
|
||||
Ansipa_OPN_Gateways gateway up/down
|
||||
Ansipa_OPN_States pf state-table usage vs the hard limit
|
||||
Ansipa_OPN_Firmware pending updates
|
||||
Ansipa_OPN_CARP HA health (silent on standalone firewalls)
|
||||
|
||||
Prefer no SSH access at all? Enable SNMP in OPNsense
|
||||
(Services → SNMP) and set this host to "SNMP" in CheckMK instead — you get
|
||||
interfaces, CPU, memory and uptime, but none of the Ansipa_OPN_* checks
|
||||
above, since those need local command execution.
|
||||
|
||||
EOF
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
#!/bin/sh
|
||||
# ansipa_opnsense — CheckMK local check for an OPNsense firewall.
|
||||
#
|
||||
# Install: copy to /usr/local/lib/check_mk_agent/local/ansipa_opnsense, chmod 755
|
||||
# (ansipa-opnsense-checkmk-install.sh does this for you).
|
||||
#
|
||||
# ── Why this is /bin/sh and not bash ────────────────────────────────────────
|
||||
# OPNsense is FreeBSD. There is no bash in the base system, no GNU grep (so no
|
||||
# `grep -P`), and no `mapfile`. Everything here is POSIX sh + awk/sed, unlike
|
||||
# the sibling ansipa-pve/pbs-monitor.sh scripts which target Debian and may use
|
||||
# bashisms. Do not "modernise" this file with [[ ]] or grep -o -P; it will
|
||||
# silently stop producing output on the firewall.
|
||||
#
|
||||
# Emits (CheckMK local-check format: "<state> <service> <perfdata> <text>"):
|
||||
# Ansipa_OPN_Gateways per-gateway up/down from the gateway status API
|
||||
# Ansipa_OPN_States pf state-table usage against the configured hard limit
|
||||
# Ansipa_OPN_Firmware pending firmware/package updates
|
||||
# Ansipa_OPN_CARP CARP interface health (skipped cleanly if unused)
|
||||
#
|
||||
# CPU / RAM / disk / interface counters are already covered by the stock
|
||||
# FreeBSD agent checks — deliberately not duplicated here.
|
||||
|
||||
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
|
||||
export PATH
|
||||
|
||||
# ── Gateways ────────────────────────────────────────────────────────────────
|
||||
# `configctl interface gateways status` returns a JSON blob. There is no jq in
|
||||
# the OPNsense base system, so the objects are split onto separate lines and
|
||||
# read with sed. We count anything not explicitly Online/none as degraded.
|
||||
if command -v configctl >/dev/null 2>&1; then
|
||||
_gw=$(configctl interface gateways status 2>/dev/null)
|
||||
if [ -z "$_gw" ]; then
|
||||
echo "3 Ansipa_OPN_Gateways - could not query gateway status"
|
||||
else
|
||||
# One JSON object per line, then pull name + status_translated per line.
|
||||
_parsed=$(printf '%s' "$_gw" | sed 's/},[[:space:]]*{/}\
|
||||
{/g')
|
||||
_total=$(echo "$_parsed" | grep -c '"name"' 2>/dev/null || echo 0)
|
||||
_downlist=$(echo "$_parsed" \
|
||||
| sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*"status_translated"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1=\2/p' \
|
||||
| grep -v '=Online$' | tr '\n' ' ')
|
||||
_down=$(printf '%s' "$_downlist" | wc -w | tr -d ' ')
|
||||
|
||||
if [ "${_total:-0}" -eq 0 ]; then
|
||||
echo "3 Ansipa_OPN_Gateways - no gateways found in status output"
|
||||
elif [ "${_down:-0}" -gt 0 ]; then
|
||||
echo "2 Ansipa_OPN_Gateways gateways_total=${_total};; gateways_down=${_down};1;1 ${_down}/${_total} gateway(s) not online: ${_downlist}"
|
||||
else
|
||||
echo "0 Ansipa_OPN_Gateways gateways_total=${_total};; gateways_down=0;1;1 all ${_total} gateway(s) online"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "3 Ansipa_OPN_Gateways - configctl not found (not an OPNsense host?)"
|
||||
fi
|
||||
|
||||
# ── pf state table ──────────────────────────────────────────────────────────
|
||||
# A firewall that hits its state limit drops new connections while looking
|
||||
# perfectly healthy on CPU/RAM, so this is the check that actually catches it.
|
||||
if command -v pfctl >/dev/null 2>&1; then
|
||||
_cur=$(pfctl -si 2>/dev/null | awk '/current entries/ {print $3; exit}')
|
||||
_lim=$(pfctl -sm 2>/dev/null | awk '/^states/ {print $4; exit}')
|
||||
if [ -z "$_cur" ] || [ -z "$_lim" ] || [ "${_lim:-0}" -eq 0 ] 2>/dev/null; then
|
||||
echo "3 Ansipa_OPN_States - could not read pf state table counters"
|
||||
else
|
||||
_pct=$(( _cur * 100 / _lim ))
|
||||
_perf="states=${_cur};$(( _lim * 80 / 100 ));$(( _lim * 90 / 100 ));0;${_lim} states_pct=${_pct}%;80;90"
|
||||
if [ "$_pct" -ge 90 ]; then
|
||||
echo "2 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%) — near the hard limit"
|
||||
elif [ "$_pct" -ge 80 ]; then
|
||||
echo "1 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%)"
|
||||
else
|
||||
echo "0 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "3 Ansipa_OPN_States - pfctl not found"
|
||||
fi
|
||||
|
||||
# ── Firmware / package updates ──────────────────────────────────────────────
|
||||
# Read the cached firmware status rather than triggering a check: this runs
|
||||
# every agent poll, and hitting the mirrors that often is antisocial and slow.
|
||||
# `configctl firmware check` refreshes the cache; a cron job on the firewall
|
||||
# should do that once a day (the installer sets one up).
|
||||
_fw_status=/tmp/pkg_upgrade.progress
|
||||
if command -v opnsense-version >/dev/null 2>&1; then
|
||||
_pending=""
|
||||
if [ -r /var/cache/opnsense-update/changelog/firmware.json ]; then
|
||||
_pending=$(sed -n 's/.*"new_packages_count"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9]*\).*/\1/p' \
|
||||
/var/cache/opnsense-update/changelog/firmware.json 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$_pending" ] && command -v pkg >/dev/null 2>&1; then
|
||||
# Offline count from the local package DB — no network access.
|
||||
_pending=$(pkg version -vIL= 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
_ver=$(opnsense-version 2>/dev/null | head -1)
|
||||
if [ -z "$_pending" ]; then
|
||||
echo "3 Ansipa_OPN_Firmware - could not determine pending update count (${_ver})"
|
||||
elif [ "$_pending" -gt 0 ] 2>/dev/null; then
|
||||
echo "1 Ansipa_OPN_Firmware pending_updates=${_pending};1;;0; ${_pending} update(s) pending — ${_ver}"
|
||||
else
|
||||
echo "0 Ansipa_OPN_Firmware pending_updates=0;1;;0; up to date — ${_ver}"
|
||||
fi
|
||||
else
|
||||
echo "3 Ansipa_OPN_Firmware - opnsense-version not found"
|
||||
fi
|
||||
unset _fw_status
|
||||
|
||||
# ── CARP (high availability) ────────────────────────────────────────────────
|
||||
# Only meaningful on an HA pair. On a standalone firewall there are no carp
|
||||
# interfaces and we stay silent rather than emitting a permanent UNKNOWN.
|
||||
if command -v ifconfig >/dev/null 2>&1; then
|
||||
_carp=$(ifconfig 2>/dev/null | grep -c ' carp:' 2>/dev/null || echo 0)
|
||||
if [ "${_carp:-0}" -gt 0 ]; then
|
||||
_demote=$(sysctl -n net.inet.carp.demotion 2>/dev/null || echo 0)
|
||||
_init=$(ifconfig 2>/dev/null | grep ' carp:' | grep -c 'INIT' || echo 0)
|
||||
if [ "${_init:-0}" -gt 0 ]; then
|
||||
echo "2 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=${_demote};; ${_init} CARP interface(s) in INIT state"
|
||||
elif [ "${_demote:-0}" -ne 0 ]; then
|
||||
echo "1 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=${_demote};; CARP demotion factor is ${_demote} (not 0)"
|
||||
else
|
||||
echo "0 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=0;; ${_carp} CARP interface(s) healthy"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
#!/bin/bash
|
||||
# checkmk-ldap-configure.sh — bind CheckMK's user management to FreeIPA.
|
||||
#
|
||||
# ── Why LDAP and not Keycloak ────────────────────────────────────────────────
|
||||
# Every other app in this stack logs in through Keycloak. CheckMK cannot:
|
||||
# SAML is a commercial-edition feature (docs.checkmk.com/latest/en/saml.html
|
||||
# — "SAML is supported in the commercial editions") and there is no OIDC
|
||||
# support in any edition. The old mod_auth_mellon workaround was dropped in
|
||||
# 2.3. So CheckMK binds FreeIPA over LDAP directly. Identity still comes from
|
||||
# exactly one place; it just takes a shorter path.
|
||||
#
|
||||
# ── Why this writes a config file instead of calling the REST API ────────────
|
||||
# REST endpoints for LDAP connections exist (werk #16527) but only from
|
||||
# CheckMK 2.4.0b1. docker-compose.yml pins check-mk-raw:2.3.0-latest, so the
|
||||
# supported route here is the config file the GUI itself writes,
|
||||
# ~/etc/check_mk/multisite.d/wato/user_connections.mk.
|
||||
#
|
||||
# The structure written below was verified against the 2.3 sources: directory
|
||||
# type "389directoryserver" is the correct one for FreeIPA (FreeIPA IS a 389DS
|
||||
# derivative) and gives the right defaults — user_id "uid" and member attribute
|
||||
# "member". Picking "openldap" instead would default the member attribute to
|
||||
# "uniquemember", which IPA does not use, and group-to-role mapping would
|
||||
# silently never match.
|
||||
#
|
||||
# ON 2.4+: prefer the REST API. Keep this script for 2.3 or delete it once the
|
||||
# stack moves, but do not run both — they write the same file.
|
||||
#
|
||||
# Usage: ./checkmk-ldap-configure.sh
|
||||
# Env overrides: CMK_CONTAINER (default "checkmk"), IPA_CONTAINER ("freeipa")
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
[[ -f .env ]] && set -a && source .env && set +a
|
||||
|
||||
# ANSI-C quoting ($'...') so these hold real escape characters. With plain
|
||||
# single quotes they are the literal text \033[0;32m, which `echo -e` renders
|
||||
# but the summary heredocs below (plain `cat`) would print raw.
|
||||
RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'
|
||||
CYAN=$'\033[0;36m'; NC=$'\033[0m'
|
||||
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
|
||||
info() { echo -e "${CYAN}[i]${NC} $*"; }
|
||||
|
||||
: "${IPA_DOMAIN:?IPA_DOMAIN is required}"
|
||||
IPA_SERVER="${IPA_SERVER:-${IPA_HOSTNAME:?set IPA_HOSTNAME or IPA_SERVER}}"
|
||||
CMK_CONTAINER="${CMK_CONTAINER:-checkmk}"
|
||||
IPA_CONTAINER="${IPA_CONTAINER:-freeipa}"
|
||||
CMK_SITE="${CMK_SITE_ID:-cmk}"
|
||||
CMK_ADMIN_GROUP="${CMK_LDAP_ADMIN_GROUP:-usr_cmk_admins}"
|
||||
CMK_USER_GROUP="${CMK_LDAP_USER_GROUP:-usr_cmk_users}"
|
||||
|
||||
IPA_BASEDN="dc=${IPA_DOMAIN/./,dc=}"
|
||||
USERS_DN="cn=users,cn=accounts,$IPA_BASEDN"
|
||||
GROUPS_DN="cn=groups,cn=accounts,$IPA_BASEDN"
|
||||
SYSACCT_DN="uid=checkmk,cn=sysaccounts,cn=etc,$IPA_BASEDN"
|
||||
|
||||
docker inspect "$CMK_CONTAINER" &>/dev/null \
|
||||
|| { error "Container '$CMK_CONTAINER' not found. Start the stack first."; exit 1; }
|
||||
|
||||
# ─── Bind account ────────────────────────────────────────────────────────────
|
||||
# Prefer a dedicated read-only sysaccount over Directory Manager: this password
|
||||
# sits in a config file inside the CheckMK container, so it should be able to
|
||||
# read the directory and nothing else.
|
||||
CMK_LDAP_BIND_DN="${CMK_LDAP_BIND_DN:-$SYSACCT_DN}"
|
||||
CMK_LDAP_BIND_PASSWORD="${CMK_LDAP_BIND_PASSWORD:-}"
|
||||
|
||||
if [[ -z "$CMK_LDAP_BIND_PASSWORD" && "$CMK_LDAP_BIND_DN" == "$SYSACCT_DN" ]]; then
|
||||
if ! docker inspect "$IPA_CONTAINER" &>/dev/null; then
|
||||
error "No CMK_LDAP_BIND_PASSWORD set and the '$IPA_CONTAINER' container"
|
||||
error "is not running, so the sysaccount cannot be created automatically."
|
||||
error "Either start FreeIPA, or set CMK_LDAP_BIND_DN/CMK_LDAP_BIND_PASSWORD."
|
||||
exit 1
|
||||
fi
|
||||
CMK_LDAP_BIND_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24)
|
||||
log "Creating the read-only sysaccount ${SYSACCT_DN} in FreeIPA..."
|
||||
# sysaccounts live outside cn=accounts and are created with ldapadd, not the
|
||||
# ipa CLI (which only manages real user entries). Idempotent: an existing
|
||||
# entry returns 68 (Already exists), which we treat as success and then
|
||||
# reset the password so this script stays re-runnable.
|
||||
docker exec -i "$IPA_CONTAINER" env DM_PW="${IPA_DM_PASSWORD:?}" bash -s <<EOF || true
|
||||
set -e
|
||||
ldapadd -x -D "cn=Directory Manager" -w "\$DM_PW" -H ldap://localhost <<LDIF 2>&1 | grep -v "^\$" || true
|
||||
dn: $SYSACCT_DN
|
||||
changetype: add
|
||||
objectclass: account
|
||||
objectclass: simplesecurityobject
|
||||
uid: checkmk
|
||||
userPassword: $CMK_LDAP_BIND_PASSWORD
|
||||
passwordExpirationTime: 20380119031407Z
|
||||
nsIdleTimeout: 0
|
||||
LDIF
|
||||
ldapmodify -x -D "cn=Directory Manager" -w "\$DM_PW" -H ldap://localhost <<LDIF 2>&1 | grep -v "^\$" || true
|
||||
dn: $SYSACCT_DN
|
||||
changetype: modify
|
||||
replace: userPassword
|
||||
userPassword: $CMK_LDAP_BIND_PASSWORD
|
||||
LDIF
|
||||
EOF
|
||||
log " sysaccount ready."
|
||||
# Persist it so re-runs don't rotate the password behind CheckMK's back.
|
||||
touch .cmk-ldap-bind; chmod 600 .cmk-ldap-bind
|
||||
printf 'CMK_LDAP_BIND_DN=%s\nCMK_LDAP_BIND_PASSWORD=%s\n' \
|
||||
"$SYSACCT_DN" "$CMK_LDAP_BIND_PASSWORD" > .cmk-ldap-bind
|
||||
info " credentials saved to .cmk-ldap-bind (0600, gitignored)"
|
||||
elif [[ -z "$CMK_LDAP_BIND_PASSWORD" && -f .cmk-ldap-bind ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source .cmk-ldap-bind
|
||||
fi
|
||||
|
||||
[[ -z "$CMK_LDAP_BIND_PASSWORD" ]] && { error "No LDAP bind password available."; exit 1; }
|
||||
|
||||
# ─── IPA groups that drive CheckMK roles ─────────────────────────────────────
|
||||
if docker inspect "$IPA_CONTAINER" &>/dev/null; then
|
||||
log "Ensuring IPA groups ${CMK_ADMIN_GROUP} / ${CMK_USER_GROUP} exist..."
|
||||
docker exec -i "$IPA_CONTAINER" env \
|
||||
ADMIN_PW="${IPA_ADMIN_PASSWORD:?}" REALM="${IPA_REALM:-${IPA_DOMAIN^^}}" \
|
||||
AG="$CMK_ADMIN_GROUP" UG="$CMK_USER_GROUP" bash -s <<'EOF' || warn " group creation failed (create them by hand)"
|
||||
set -e
|
||||
echo "$ADMIN_PW" | kinit "admin@${REALM}" >/dev/null 2>&1
|
||||
ipa group-show "$AG" >/dev/null 2>&1 || \
|
||||
ipa group-add "$AG" --desc="Members: CheckMK administrators" >/dev/null
|
||||
ipa group-show "$UG" >/dev/null 2>&1 || \
|
||||
ipa group-add "$UG" --desc="Members: CheckMK read-only users" >/dev/null
|
||||
kdestroy >/dev/null 2>&1 || true
|
||||
EOF
|
||||
log " groups ready."
|
||||
else
|
||||
warn "FreeIPA container not running — create ${CMK_ADMIN_GROUP} / ${CMK_USER_GROUP} yourself."
|
||||
fi
|
||||
|
||||
# ─── Write the connection config ─────────────────────────────────────────────
|
||||
MK_PATH="/omd/sites/${CMK_SITE}/etc/check_mk/multisite.d/wato/user_connections.mk"
|
||||
log "Writing $MK_PATH in container '$CMK_CONTAINER'..."
|
||||
|
||||
# Back up first: a malformed .mk in multisite.d breaks the whole GUI, so we must
|
||||
# be able to put back exactly what was there.
|
||||
docker exec "$CMK_CONTAINER" sh -c \
|
||||
"[ -f '$MK_PATH' ] && cp -a '$MK_PATH' '${MK_PATH}.ansipa-bak' || true"
|
||||
|
||||
# The file is Python source that CheckMK exec()s, so every value must be a
|
||||
# valid Python literal. Do NOT build it by interpolating shell variables into
|
||||
# quoted strings: a password or DN containing a quote or backslash then
|
||||
# produces a SyntaxError, which takes the entire GUI down until the file is
|
||||
# removed. Instead the values are passed as environment variables and the
|
||||
# literal is emitted by Python's own repr() (via pprint), which escapes them
|
||||
# correctly by construction.
|
||||
docker exec -i \
|
||||
-e A_SERVER="$IPA_SERVER" \
|
||||
-e A_DOMAIN="$IPA_DOMAIN" \
|
||||
-e A_BIND_DN="$CMK_LDAP_BIND_DN" \
|
||||
-e A_BIND_PW="$CMK_LDAP_BIND_PASSWORD" \
|
||||
-e A_USERS_DN="$USERS_DN" \
|
||||
-e A_GROUPS_DN="$GROUPS_DN" \
|
||||
-e A_ADMIN_GROUP="$CMK_ADMIN_GROUP" \
|
||||
-e A_USER_GROUP="$CMK_USER_GROUP" \
|
||||
-e A_MK_PATH="$MK_PATH" \
|
||||
"$CMK_CONTAINER" "/omd/sites/${CMK_SITE}/bin/python3" - <<'PYEOF'
|
||||
import os, pprint
|
||||
|
||||
env = os.environ
|
||||
groups_dn = env["A_GROUPS_DN"]
|
||||
|
||||
connections = [
|
||||
{
|
||||
"id": "freeipa",
|
||||
"type": "ldap",
|
||||
"description": "FreeIPA (%s)" % env["A_DOMAIN"],
|
||||
"comment": "Managed by ansipa. Accounts and roles come from FreeIPA groups.",
|
||||
"docu_url": "",
|
||||
"disabled": False,
|
||||
# 389directoryserver, not openldap: FreeIPA is a 389DS derivative, and
|
||||
# this choice is what makes the member attribute default to "member"
|
||||
# (openldap would default it to "uniquemember", which IPA never sets,
|
||||
# so group-to-role mapping would silently match nothing).
|
||||
"directory_type": (
|
||||
"389directoryserver",
|
||||
{"connect_to": ("fixed_list", {"server": env["A_SERVER"]})},
|
||||
),
|
||||
"bind": (env["A_BIND_DN"], ("password", env["A_BIND_PW"])),
|
||||
"user_dn": env["A_USERS_DN"],
|
||||
"user_scope": "sub",
|
||||
"user_id_umlauts": "keep",
|
||||
"group_dn": groups_dn,
|
||||
"group_scope": "sub",
|
||||
# Roles follow IPA group membership. A user in neither group falls back
|
||||
# to the default user profile, i.e. gets no CheckMK access of substance.
|
||||
"active_plugins": {
|
||||
"email": {},
|
||||
"alias": {},
|
||||
"auth_expire": {},
|
||||
"groups_to_roles": {
|
||||
"admin": [("cn=%s,%s" % (env["A_ADMIN_GROUP"], groups_dn), None)],
|
||||
"user": [("cn=%s,%s" % (env["A_USER_GROUP"], groups_dn), None)],
|
||||
},
|
||||
},
|
||||
"cache_livetime": 300,
|
||||
},
|
||||
]
|
||||
|
||||
header = (
|
||||
"# Managed by ansipa checkmk-ldap-configure.sh — regenerate, do not hand-edit.\n"
|
||||
"#\n"
|
||||
"# FreeIPA is the source of truth for accounts. CheckMK's Community edition\n"
|
||||
"# has no SAML/OIDC, so it binds the directory directly instead of going\n"
|
||||
"# through Keycloak like the other services in this stack do.\n"
|
||||
)
|
||||
|
||||
with open(env["A_MK_PATH"], "w") as fh:
|
||||
fh.write(header)
|
||||
fh.write("user_connections = " + pprint.pformat(connections, width=88) + "\n")
|
||||
PYEOF
|
||||
|
||||
# The site user must own it — it was written by root via docker exec.
|
||||
docker exec "$CMK_CONTAINER" chown "${CMK_SITE}:${CMK_SITE}" "$MK_PATH"
|
||||
docker exec "$CMK_CONTAINER" chmod 660 "$MK_PATH"
|
||||
|
||||
# ─── Validate, and roll back if the site cannot load it ──────────────────────
|
||||
log "Validating the config with CheckMK's own loader..."
|
||||
if docker exec "$CMK_CONTAINER" su - "$CMK_SITE" -c 'python3 -c "
|
||||
from cmk.gui import main_modules
|
||||
main_modules.load_plugins()
|
||||
from cmk.gui.utils.script_helpers import gui_context
|
||||
from cmk.gui.userdb import _connections as c
|
||||
from cmk.gui.userdb.ldap_connector import LDAPUserConnector
|
||||
with gui_context():
|
||||
conns = [x for x in c.load_connection_config() if x[\"id\"] == \"freeipa\"]
|
||||
assert conns, \"freeipa connection not loaded\"
|
||||
conn = LDAPUserConnector(conns[0])
|
||||
assert conn._user_id_attr() == \"uid\", conn._user_id_attr()
|
||||
assert conn._member_attr() == \"member\", conn._member_attr()
|
||||
print(\"servers:\", conn.servers())
|
||||
print(\"user dn:\", conn._get_user_dn())
|
||||
"' 2>&1 | sed 's/^/ /'; then
|
||||
log "Config validated."
|
||||
else
|
||||
error "CheckMK could not load the config — rolling back."
|
||||
docker exec "$CMK_CONTAINER" sh -c \
|
||||
"[ -f '${MK_PATH}.ansipa-bak' ] && mv '${MK_PATH}.ansipa-bak' '$MK_PATH' || rm -f '$MK_PATH'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The GUI re-reads multisite.d per request, so no restart is needed; reload
|
||||
# apache anyway to drop any cached config in long-lived worker processes.
|
||||
docker exec "$CMK_CONTAINER" omd reload "$CMK_SITE" apache >/dev/null 2>&1 || true
|
||||
|
||||
# Confirm the UI still serves after the change.
|
||||
if docker exec "$CMK_CONTAINER" curl -sf -o /dev/null \
|
||||
"http://localhost:5000/${CMK_SITE}/check_mk/login.py"; then
|
||||
log "CheckMK GUI still responding."
|
||||
else
|
||||
warn "CheckMK login page did not respond — check 'docker logs $CMK_CONTAINER'."
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
${GREEN}CheckMK ↔ FreeIPA LDAP configuration complete.${NC}
|
||||
|
||||
Connection: freeipa (389directoryserver)
|
||||
Server: $IPA_SERVER
|
||||
Users DN: $USERS_DN
|
||||
Groups DN: $GROUPS_DN
|
||||
Bind DN: $CMK_LDAP_BIND_DN
|
||||
Roles: ${CMK_ADMIN_GROUP} → admin, ${CMK_USER_GROUP} → user
|
||||
|
||||
Grant someone access:
|
||||
docker exec $IPA_CONTAINER ipa group-add-member ${CMK_ADMIN_GROUP} --users=<uid>
|
||||
|
||||
Then in CheckMK: Setup → Users → "Synchronize users" (or wait for the
|
||||
300s cache to lapse) and log in with the IPA username and password.
|
||||
|
||||
cmkadmin remains a local break-glass account — it does not live in IPA, and
|
||||
that is deliberate: it is how you get in when the directory is unreachable.
|
||||
|
||||
EOF
|
||||
|
|
@ -1,11 +1,23 @@
|
|||
# FreeIPA + Keycloak + PostgreSQL constellation
|
||||
# FreeIPA + Keycloak + CheckMK + Nextcloud + PostgreSQL constellation
|
||||
#
|
||||
# Identity flows one way, and only one way:
|
||||
# FreeIPA (accounts, groups, sudo/HBAC — the source of truth)
|
||||
# └→ Keycloak federates IPA over LDAP, issues OIDC/SAML
|
||||
# ├→ Nextcloud (user_oidc, with the IPA LDAP backend owning accounts)
|
||||
# ├→ Proxmox VE / PBS (OpenID Connect realm)
|
||||
# └→ OPNsense (OIDC where supported, otherwise IPA LDAP directly)
|
||||
# └→ CheckMK binds IPA over LDAP DIRECTLY — the Community/Raw edition
|
||||
# has no SAML or OIDC, so Keycloak cannot front it.
|
||||
# No service keeps its own account list; see docs/md/ansipa-sso.md.
|
||||
#
|
||||
# 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
|
||||
# ./keycloak-configure.sh # wire Keycloak → FreeIPA LDAP + OIDC clients
|
||||
# ./nextcloud-configure.sh # wire Nextcloud → IPA LDAP + Keycloak OIDC
|
||||
# ./checkmk-ldap-configure.sh # wire CheckMK → FreeIPA LDAP
|
||||
#
|
||||
# To run without Keycloak:
|
||||
# docker compose up -d freeipa
|
||||
|
|
@ -40,6 +52,10 @@ volumes:
|
|||
name: cmk-data
|
||||
cmk-creds: # shared: CheckMK writes automation.secret here; FreeIPA reads it
|
||||
name: cmk-creds
|
||||
nextcloud-data: # Nextcloud /var/www/html (config, apps, and user data)
|
||||
name: nextcloud-data
|
||||
nextcloud-redis: # Redis persistence for Nextcloud file locking / cache
|
||||
name: nextcloud-redis
|
||||
|
||||
networks:
|
||||
ipa-net:
|
||||
|
|
@ -109,7 +125,12 @@ services:
|
|||
retries: 20
|
||||
start_period: 600s
|
||||
|
||||
# ── PostgreSQL (Keycloak backend) ────────────────────────────────────────────
|
||||
# ── PostgreSQL (Keycloak + Nextcloud backend) ────────────────────────────────
|
||||
# One server, two databases. The nextcloud role/DB is created by the initdb
|
||||
# hook below — but initdb hooks run ONLY when the data volume is empty, so on
|
||||
# a stack that already has a keycloak-db volume the hook never fires. run.sh
|
||||
# therefore also creates the role/DB idempotently before starting Nextcloud;
|
||||
# keep both paths in sync if you change the names here.
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: keycloak-db
|
||||
|
|
@ -118,8 +139,10 @@ services:
|
|||
POSTGRES_DB: keycloak
|
||||
POSTGRES_USER: keycloak
|
||||
POSTGRES_PASSWORD: ${KC_DB_PASSWORD:?set KC_DB_PASSWORD in .env}
|
||||
NC_DB_PASSWORD: ${NC_DB_PASSWORD:?set NC_DB_PASSWORD in .env}
|
||||
volumes:
|
||||
- keycloak-db:/var/lib/postgresql/data
|
||||
- ./postgres/initdb:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
ipa-net:
|
||||
healthcheck:
|
||||
|
|
@ -247,12 +270,83 @@ services:
|
|||
retries: 20
|
||||
start_period: 90s
|
||||
|
||||
# ── Redis (Nextcloud file locking + cache) ───────────────────────────────────
|
||||
# Nextcloud's transactional file locking needs a real lock backend; without it
|
||||
# concurrent clients corrupt each other's uploads. Not exposed to the host.
|
||||
nextcloud-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: nextcloud-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
|
||||
volumes:
|
||||
- nextcloud-redis:/data
|
||||
networks:
|
||||
ipa-net:
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ── Nextcloud ────────────────────────────────────────────────────────────────
|
||||
# Identity comes from FreeIPA, never from Nextcloud's own user table:
|
||||
# • the LDAP backend binds to FreeIPA and owns the account list, and
|
||||
# • user_oidc logs people in through Keycloak (which itself federates IPA).
|
||||
# ./nextcloud-configure.sh wires both up after first start. The admin account
|
||||
# created here (NC_ADMIN_USER) is a break-glass local account only — it exists
|
||||
# because Nextcloud refuses to install without one.
|
||||
#
|
||||
# Served at the /nextcloud subpath of the ansipa gateway. OVERWRITEWEBROOT
|
||||
# makes Nextcloud generate URLs under that prefix; OVERWRITECLIURL does the
|
||||
# same for cron/CLI-generated links. Protocol is deliberately NOT pinned —
|
||||
# with TRUSTED_PROXIES set, Nextcloud honours X-Forwarded-Proto, so the same
|
||||
# container works behind a TLS proxy and over plain HTTP on ANSIPA_HTTP_PORT.
|
||||
nextcloud:
|
||||
image: nextcloud:31-apache
|
||||
container_name: nextcloud
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_DB: nextcloud
|
||||
POSTGRES_USER: nextcloud
|
||||
POSTGRES_PASSWORD: ${NC_DB_PASSWORD:?set NC_DB_PASSWORD in .env}
|
||||
REDIS_HOST: nextcloud-redis
|
||||
NEXTCLOUD_ADMIN_USER: ${NC_ADMIN_USER:-ncadmin}
|
||||
NEXTCLOUD_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?set NC_ADMIN_PASSWORD in .env}
|
||||
# Every hostname the gateway may be reached under, space-separated.
|
||||
NEXTCLOUD_TRUSTED_DOMAINS: ${NC_TRUSTED_DOMAINS:-localhost}
|
||||
TRUSTED_PROXIES: 172.30.0.0/24
|
||||
OVERWRITEWEBROOT: /nextcloud
|
||||
OVERWRITECLIURL: ${NC_PUBLIC_URL:-http://localhost:8088/nextcloud}
|
||||
# Nextcloud's own cron + background jobs.
|
||||
NEXTCLOUD_UPDATE: "1"
|
||||
extra_hosts:
|
||||
- "${IPA_HOSTNAME:-ipa.example.com}:172.30.0.10"
|
||||
volumes:
|
||||
- nextcloud-data:/var/www/html
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
nextcloud-redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
ipa-net:
|
||||
healthcheck:
|
||||
# status.php answers before the installer finishes, which is what we want:
|
||||
# nextcloud-configure.sh waits on this and then drives occ.
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost/status.php -o /dev/null || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
start_period: 120s
|
||||
|
||||
# ── 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)
|
||||
# /nextcloud/ → Nextcloud (OVERWRITEWEBROOT matches this prefix)
|
||||
#
|
||||
# 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
|
||||
|
|
@ -267,6 +361,7 @@ services:
|
|||
ANSIPA_IPA_UPSTREAM: 172.30.0.10
|
||||
ANSIPA_CMK_UPSTREAM: 172.30.0.12:5000
|
||||
ANSIPA_KC_UPSTREAM: keycloak:8080
|
||||
ANSIPA_NC_UPSTREAM: nextcloud:80
|
||||
IPA_HOSTNAME: ${IPA_HOSTNAME:-ipa.example.com}
|
||||
volumes:
|
||||
- ./nginx/templates:/etc/nginx/templates:ro # *.template → envsubst → conf.d
|
||||
|
|
@ -277,6 +372,7 @@ services:
|
|||
- freeipa
|
||||
- checkmk
|
||||
- keycloak
|
||||
- nextcloud
|
||||
networks:
|
||||
ipa-net:
|
||||
healthcheck:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
#!/bin/bash
|
||||
# keycloak-configure.sh — wire Keycloak to FreeIPA via LDAP user federation
|
||||
# keycloak-configure.sh — wire Keycloak to FreeIPA, and provision OIDC clients
|
||||
#
|
||||
# Run this AFTER both FreeIPA and Keycloak are fully up.
|
||||
# Two jobs:
|
||||
# 1. LDAP user federation, so FreeIPA stays the single source of truth for
|
||||
# accounts and groups and Keycloak only ever mirrors it (READ_ONLY).
|
||||
# 2. A confidential OIDC client per relying party (Nextcloud, Proxmox VE/PBS,
|
||||
# OPNsense), with a "groups" claim so those apps can authorize on IPA
|
||||
# group membership. Secrets land in ./.oidc-secrets (0600, gitignored).
|
||||
#
|
||||
# CheckMK is deliberately NOT here: the Community/Raw edition supports neither
|
||||
# SAML nor OIDC, so it binds FreeIPA over LDAP directly — see
|
||||
# ./checkmk-ldap-configure.sh.
|
||||
#
|
||||
# Run this AFTER both FreeIPA and Keycloak are fully up. Safe to re-run: every
|
||||
# object is created-or-updated, and existing clients keep their secrets.
|
||||
# Reads settings from environment variables or a .env file in the same directory.
|
||||
#
|
||||
# Required env vars:
|
||||
|
|
@ -29,7 +41,11 @@ set -euo pipefail
|
|||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
[[ -f "$SCRIPT_DIR/.env" ]] && set -a && source "$SCRIPT_DIR/.env" && set +a
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
# ANSI-C quoting ($'...') so these hold real escape characters. With plain
|
||||
# single quotes they are the literal text \033[0;32m, which `echo -e` renders
|
||||
# but the summary heredocs below (plain `cat`) would print raw.
|
||||
RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'
|
||||
CYAN=$'\033[0;36m'; NC=$'\033[0m'
|
||||
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
|
||||
|
|
@ -40,7 +56,11 @@ info() { echo -e "${CYAN}[i]${NC} $*"; }
|
|||
: "${IPA_DM_PASSWORD:?IPA_DM_PASSWORD is required}"
|
||||
: "${KC_ADMIN_PASSWORD:?KC_ADMIN_PASSWORD is required}"
|
||||
|
||||
KC_URL="${KC_URL:-http://localhost:8080}"
|
||||
# Must include the relative path Keycloak is served under: docker-compose.yml
|
||||
# sets KC_HTTP_RELATIVE_PATH=/auth so the gateway can proxy it at a native
|
||||
# subpath, which moves EVERY endpoint (admin API, realms, token) under /auth.
|
||||
KC_URL="${KC_URL:-http://localhost:8080/auth}"
|
||||
KC_URL="${KC_URL%/}"
|
||||
KC_ADMIN="${KC_ADMIN:-admin}"
|
||||
KC_REALM="${KC_REALM:-freeipa}"
|
||||
KC_REALM_DISPLAY="${KC_REALM_DISPLAY:-$IPA_DOMAIN}"
|
||||
|
|
@ -77,9 +97,13 @@ kc_status() { curl -sf -o /dev/null -w "%{http_code}" \
|
|||
-H "Authorization: Bearer $TOKEN" "$KC_URL$1"; }
|
||||
|
||||
# ─── Wait for Keycloak ────────────────────────────────────────────────────────
|
||||
# Probe the master realm, not /health/ready: the health endpoints live on the
|
||||
# separate management port 9000 and are NOT under KC_HTTP_RELATIVE_PATH, so
|
||||
# "$KC_URL/health/ready" 404s forever on this deployment. /realms/master
|
||||
# answers 200 on the serving port as soon as Keycloak is actually usable.
|
||||
info "Waiting for Keycloak at $KC_URL..."
|
||||
for i in $(seq 1 60); do
|
||||
curl -sf "$KC_URL/health/ready" &>/dev/null && break
|
||||
curl -sf "$KC_URL/realms/master" &>/dev/null && break
|
||||
[[ $i -eq 60 ]] && { error "Keycloak not ready after 120s."; exit 1; }
|
||||
sleep 2
|
||||
done
|
||||
|
|
@ -247,6 +271,139 @@ kc_put "/admin/realms/$KC_REALM" \
|
|||
'{"loginWithEmailAllowed":true,"duplicateEmailsAllowed":false}' >/dev/null
|
||||
log "Email login enabled on realm '$KC_REALM'."
|
||||
|
||||
TOKEN=$(kc_token)
|
||||
|
||||
# ─── Group membership in tokens ───────────────────────────────────────────────
|
||||
# Relying parties authorize on FreeIPA group membership (Nextcloud quota groups,
|
||||
# Proxmox permissions, ...), so every token needs a "groups" claim. The LDAP
|
||||
# group mapper above only imports groups INTO Keycloak — a protocol mapper is
|
||||
# what actually puts them in the ID token / userinfo response.
|
||||
#
|
||||
# full.path=false emits bare names ("usr_nextcloud") rather than "/usr_nextcloud",
|
||||
# because that is what the consumers below match against.
|
||||
log "Adding realm-wide 'groups' claim mapper..."
|
||||
GROUPS_SCOPE_ID=$(kc_get "/admin/realms/$KC_REALM/client-scopes" \
|
||||
| jq -r '.[] | select(.name=="groups") | .id // empty' | head -1)
|
||||
|
||||
if [[ -z "$GROUPS_SCOPE_ID" ]]; then
|
||||
kc_post "/admin/realms/$KC_REALM/client-scopes" '{
|
||||
"name": "groups",
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {"include.in.token.scope":"true","display.on.consent.screen":"false"}
|
||||
}' >/dev/null 2>&1 || true
|
||||
GROUPS_SCOPE_ID=$(kc_get "/admin/realms/$KC_REALM/client-scopes" \
|
||||
| jq -r '.[] | select(.name=="groups") | .id // empty' | head -1)
|
||||
fi
|
||||
|
||||
if [[ -n "$GROUPS_SCOPE_ID" ]]; then
|
||||
HAS_GM=$(kc_get "/admin/realms/$KC_REALM/client-scopes/$GROUPS_SCOPE_ID/protocol-mappers/models" \
|
||||
| jq -r '.[] | select(.name=="groups") | .id // empty' | head -1)
|
||||
if [[ -z "$HAS_GM" ]]; then
|
||||
kc_post "/admin/realms/$KC_REALM/client-scopes/$GROUPS_SCOPE_ID/protocol-mappers/models" '{
|
||||
"name": "groups",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-group-membership-mapper",
|
||||
"config": {
|
||||
"full.path": "false",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"userinfo.token.claim": "true",
|
||||
"claim.name": "groups"
|
||||
}
|
||||
}' >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Hand the scope to every new client automatically.
|
||||
kc_put "/admin/realms/$KC_REALM/default-default-client-scopes/$GROUPS_SCOPE_ID" "" >/dev/null 2>&1 || true
|
||||
log " 'groups' client scope ready."
|
||||
else
|
||||
warn " could not create the 'groups' client scope — add it by hand if apps need group claims."
|
||||
fi
|
||||
|
||||
# ─── OIDC clients ─────────────────────────────────────────────────────────────
|
||||
# One confidential client per relying party. Secrets are written to
|
||||
# .oidc-secrets (mode 0600, gitignored) so nextcloud-configure.sh and the
|
||||
# Proxmox/OPNsense setup steps can pick them up without a trip through the UI.
|
||||
#
|
||||
# Clients whose base URL is not configured in .env are skipped — set PVE_URL,
|
||||
# PBS_URL or OPNSENSE_URL and re-run to add them later. Re-running is safe: an
|
||||
# existing client is updated in place and keeps its secret.
|
||||
SECRETS_FILE="$SCRIPT_DIR/.oidc-secrets"
|
||||
|
||||
kc_client() {
|
||||
local client_id="$1" name="$2" root_url="$3" redirect="$4"
|
||||
local payload existing cid secret
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg id "$client_id" --arg name "$name" \
|
||||
--arg root "$root_url" --arg redir "$redirect" \
|
||||
'{
|
||||
clientId: $id, name: $name, enabled: true, protocol: "openid-connect",
|
||||
publicClient: false, bearerOnly: false,
|
||||
standardFlowEnabled: true, directAccessGrantsEnabled: false,
|
||||
serviceAccountsEnabled: false, implicitFlowEnabled: false,
|
||||
rootUrl: $root, baseUrl: $root,
|
||||
redirectUris: [$redir], webOrigins: ["+"],
|
||||
attributes: {"post.logout.redirect.uris": $root + "/*"}
|
||||
}')
|
||||
|
||||
existing=$(kc_get "/admin/realms/$KC_REALM/clients?clientId=$client_id" \
|
||||
| jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "$existing" ]]; then
|
||||
kc_put "/admin/realms/$KC_REALM/clients/$existing" "$payload" >/dev/null
|
||||
cid="$existing"
|
||||
warn " client '$client_id' already existed — updated (secret unchanged)."
|
||||
else
|
||||
kc_post "/admin/realms/$KC_REALM/clients" "$payload" >/dev/null
|
||||
cid=$(kc_get "/admin/realms/$KC_REALM/clients?clientId=$client_id" \
|
||||
| jq -r '.[0].id // empty')
|
||||
log " client '$client_id' created."
|
||||
fi
|
||||
|
||||
[[ -z "$cid" ]] && { error " could not resolve client '$client_id'."; return 1; }
|
||||
|
||||
secret=$(kc_get "/admin/realms/$KC_REALM/clients/$cid/client-secret" \
|
||||
| jq -r '.value // empty')
|
||||
[[ -z "$secret" ]] && { error " could not read secret for '$client_id'."; return 1; }
|
||||
|
||||
# Rewrite this client's line in place, keeping the others.
|
||||
touch "$SECRETS_FILE"; chmod 600 "$SECRETS_FILE"
|
||||
grep -v "^${client_id}=" "$SECRETS_FILE" > "$SECRETS_FILE.tmp" 2>/dev/null || true
|
||||
mv "$SECRETS_FILE.tmp" "$SECRETS_FILE"
|
||||
echo "${client_id}=${secret}" >> "$SECRETS_FILE"
|
||||
chmod 600 "$SECRETS_FILE"
|
||||
}
|
||||
|
||||
log "Provisioning OIDC clients..."
|
||||
|
||||
# Nextcloud — user_oidc's callback is always <base>/apps/user_oidc/code
|
||||
NC_PUBLIC_URL="${NC_PUBLIC_URL:-}"
|
||||
if [[ -n "$NC_PUBLIC_URL" ]]; then
|
||||
NC_PUBLIC_URL="${NC_PUBLIC_URL%/}"
|
||||
kc_client "nextcloud" "Nextcloud" "$NC_PUBLIC_URL" "$NC_PUBLIC_URL/apps/user_oidc/code" || true
|
||||
else
|
||||
warn " NC_PUBLIC_URL unset — skipping the Nextcloud client."
|
||||
fi
|
||||
|
||||
# Proxmox VE / PBS — the OpenID realm posts back to the web UI root.
|
||||
if [[ -n "${PVE_URL:-}" ]]; then
|
||||
kc_client "proxmox-ve" "Proxmox VE" "${PVE_URL%/}" "${PVE_URL%/}/*" || true
|
||||
else
|
||||
warn " PVE_URL unset — skipping the Proxmox VE client."
|
||||
fi
|
||||
if [[ -n "${PBS_URL:-}" ]]; then
|
||||
kc_client "proxmox-bs" "Proxmox Backup Server" "${PBS_URL%/}" "${PBS_URL%/}/*" || true
|
||||
else
|
||||
warn " PBS_URL unset — skipping the Proxmox Backup Server client."
|
||||
fi
|
||||
if [[ -n "${OPNSENSE_URL:-}" ]]; then
|
||||
kc_client "opnsense" "OPNsense" "${OPNSENSE_URL%/}" "${OPNSENSE_URL%/}/*" || true
|
||||
else
|
||||
warn " OPNSENSE_URL unset — skipping the OPNsense client."
|
||||
fi
|
||||
|
||||
[[ -f "$SECRETS_FILE" ]] && log "Client secrets written to $SECRETS_FILE (mode 0600)."
|
||||
|
||||
# ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
cat <<EOF
|
||||
|
||||
|
|
@ -262,9 +419,19 @@ ${GREEN}Keycloak ↔ FreeIPA configuration complete.${NC}
|
|||
Admin console: $KC_URL/admin/$KC_REALM/console
|
||||
User login: $KC_URL/realms/$KC_REALM/account
|
||||
|
||||
OIDC clients: $(ls "$SECRETS_FILE" &>/dev/null && cut -d= -f1 "$SECRETS_FILE" | tr '\n' ' ' || echo "(none — set NC_PUBLIC_URL / PVE_URL / PBS_URL / OPNSENSE_URL)")
|
||||
Client secrets: $SECRETS_FILE
|
||||
|
||||
Discovery URL (give this to relying parties):
|
||||
$KC_URL/realms/$KC_REALM/.well-known/openid-configuration
|
||||
|
||||
Next steps:
|
||||
• Verify users are visible: Admin console → Users
|
||||
• Set up client applications (OIDC/SAML) in this realm
|
||||
• Run ./nextcloud-configure.sh to point Nextcloud at IPA LDAP + this realm
|
||||
• CheckMK Community edition has no OIDC/SAML — run ./checkmk-ldap-configure.sh
|
||||
to bind it straight to FreeIPA instead
|
||||
• Proxmox VE / PBS: add an OpenID Connect realm with the discovery URL above
|
||||
and the proxmox-ve / proxmox-bs client secret
|
||||
• For production: switch Keycloak to 'start' mode with a TLS cert
|
||||
• For Kerberos/SPNEGO: supply an HTTP service keytab and set
|
||||
allowKerberosAuthentication=true in the LDAP provider config
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
#!/bin/bash
|
||||
# nextcloud-configure.sh — point Nextcloud at FreeIPA for accounts and at
|
||||
# Keycloak for logins.
|
||||
#
|
||||
# Run AFTER the stack is up and ./keycloak-configure.sh has provisioned the
|
||||
# "nextcloud" OIDC client. Safe to re-run.
|
||||
#
|
||||
# The two backends do different jobs, and both are needed:
|
||||
#
|
||||
# user_ldap binds FreeIPA and OWNS the account list. Users, display names,
|
||||
# mail and group membership come from IPA and are read-only here,
|
||||
# which is what makes IPA the source of truth rather than a thing
|
||||
# Nextcloud happens to sync from once.
|
||||
# user_oidc performs the actual login against Keycloak, so password policy,
|
||||
# MFA and session control live in one place for every app.
|
||||
#
|
||||
# The join between them is the username. Nextcloud's internal username is
|
||||
# pinned to the IPA `uid` (ldapExpertUsernameAttr), and the OIDC provider maps
|
||||
# `preferred_username` onto it with unique-uid disabled — otherwise user_oidc
|
||||
# hashes the claim into an ID of its own and every SSO login creates a SECOND,
|
||||
# empty account beside the LDAP one. With them aligned, user_oidc's
|
||||
# soft_auto_provision updates the existing LDAP user instead.
|
||||
#
|
||||
# Required env (from .env):
|
||||
# IPA_SERVER / IPA_HOSTNAME, IPA_DOMAIN, NC_PUBLIC_URL, KC_REALM
|
||||
# NC_LDAP_BIND_DN + NC_LDAP_BIND_PASSWORD (falls back to IPA_BIND_* / DM)
|
||||
# Optional:
|
||||
# NC_LDAP_GROUP restrict Nextcloud accounts to members of this IPA group
|
||||
# KC_PUBLIC_URL externally reachable Keycloak base URL (default: derived)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
[[ -f .env ]] && set -a && source .env && set +a
|
||||
|
||||
# ANSI-C quoting ($'...') so these hold real escape characters. With plain
|
||||
# single quotes they are the literal text \033[0;32m, which `echo -e` renders
|
||||
# but the summary heredocs below (plain `cat`) would print raw.
|
||||
RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'
|
||||
CYAN=$'\033[0;36m'; NC=$'\033[0m'
|
||||
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
|
||||
info() { echo -e "${CYAN}[i]${NC} $*"; }
|
||||
|
||||
: "${IPA_DOMAIN:?IPA_DOMAIN is required}"
|
||||
IPA_SERVER="${IPA_SERVER:-${IPA_HOSTNAME:?set IPA_HOSTNAME or IPA_SERVER}}"
|
||||
NC_PUBLIC_URL="${NC_PUBLIC_URL:?NC_PUBLIC_URL is required (see .env.example)}"
|
||||
NC_PUBLIC_URL="${NC_PUBLIC_URL%/}"
|
||||
KC_REALM="${KC_REALM:-freeipa}"
|
||||
|
||||
IPA_BASEDN="dc=${IPA_DOMAIN/./,dc=}"
|
||||
LDAP_BIND_DN="${NC_LDAP_BIND_DN:-${IPA_BIND_DN:-cn=Directory Manager}}"
|
||||
LDAP_BIND_PW="${NC_LDAP_BIND_PASSWORD:-${IPA_BIND_PASSWORD:-${IPA_DM_PASSWORD:-}}}"
|
||||
[[ -z "$LDAP_BIND_PW" ]] && { error "No LDAP bind password (set NC_LDAP_BIND_PASSWORD)."; exit 1; }
|
||||
if [[ "$LDAP_BIND_DN" == "cn=Directory Manager" ]]; then
|
||||
warn "Binding LDAP as Directory Manager. For production create a read-only"
|
||||
warn " sysaccount in FreeIPA and set NC_LDAP_BIND_DN / NC_LDAP_BIND_PASSWORD."
|
||||
fi
|
||||
|
||||
LDAP_SCHEME="ldap"; LDAP_PORT=389
|
||||
[[ "${IPA_USE_LDAPS:-false}" == "true" ]] && LDAP_SCHEME="ldaps" && LDAP_PORT=636
|
||||
|
||||
# Keycloak's externally reachable base. Nextcloud redirects the BROWSER here, so
|
||||
# it must be an address the user's machine can resolve — not keycloak:8080.
|
||||
if [[ -z "${KC_PUBLIC_URL:-}" ]]; then
|
||||
# Default: same origin as Nextcloud, since both sit behind the ansipa gateway.
|
||||
KC_PUBLIC_URL="${NC_PUBLIC_URL%/nextcloud}/auth"
|
||||
fi
|
||||
KC_PUBLIC_URL="${KC_PUBLIC_URL%/}"
|
||||
DISCOVERY_URI="${KC_PUBLIC_URL}/realms/${KC_REALM}/.well-known/openid-configuration"
|
||||
|
||||
occ() { docker compose exec -T -u www-data nextcloud php occ "$@"; }
|
||||
|
||||
# ─── Wait for the installer to finish ────────────────────────────────────────
|
||||
info "Waiting for Nextcloud to finish installing..."
|
||||
for i in $(seq 1 60); do
|
||||
if occ status 2>/dev/null | grep -q 'installed: true'; then break; fi
|
||||
[[ $i -eq 60 ]] && { error "Nextcloud not installed after 5 minutes."; exit 1; }
|
||||
sleep 5
|
||||
done
|
||||
log "Nextcloud is installed."
|
||||
|
||||
# ─── Apps ────────────────────────────────────────────────────────────────────
|
||||
log "Enabling user_ldap and user_oidc..."
|
||||
occ app:enable user_ldap >/dev/null 2>&1 || warn " could not enable user_ldap"
|
||||
# user_oidc is not bundled; install pulls it from the app store on first run.
|
||||
if ! occ app:list 2>/dev/null | grep -q 'user_oidc'; then
|
||||
occ app:install user_oidc >/dev/null 2>&1 \
|
||||
|| { error " could not install user_oidc (no app store access?)."
|
||||
error " Install it from the Apps page, then re-run this script."; exit 1; }
|
||||
fi
|
||||
occ app:enable user_oidc >/dev/null 2>&1 || warn " could not enable user_oidc"
|
||||
log " apps ready."
|
||||
|
||||
# ─── LDAP backend (FreeIPA owns the accounts) ────────────────────────────────
|
||||
log "Configuring the FreeIPA LDAP backend..."
|
||||
|
||||
# Which config ID we own is recorded in a state file rather than recovered by
|
||||
# parsing `ldap:show-config`, whose table output is not a stable interface. If
|
||||
# the recorded ID has since been deleted in the UI, fall through and make a new
|
||||
# one — re-running must never silently configure a config that no longer exists.
|
||||
LDAP_STATE="$SCRIPT_DIR/.nextcloud-ldap-config"
|
||||
LDAP_CFG=""
|
||||
if [[ -f "$LDAP_STATE" ]]; then
|
||||
_saved=$(tr -d '[:space:]' < "$LDAP_STATE")
|
||||
if [[ -n "$_saved" ]] && occ ldap:show-config "$_saved" >/dev/null 2>&1; then
|
||||
LDAP_CFG="$_saved"
|
||||
log " reusing LDAP config $LDAP_CFG"
|
||||
else
|
||||
warn " recorded LDAP config '${_saved}' is gone — creating a fresh one."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$LDAP_CFG" ]]; then
|
||||
LDAP_CFG=$(occ ldap:create-empty-config 2>/dev/null | grep -oE 's[0-9]+' | head -1 || true)
|
||||
[[ -z "$LDAP_CFG" ]] && { error "Could not create an LDAP configuration."; exit 1; }
|
||||
echo "$LDAP_CFG" > "$LDAP_STATE"
|
||||
log " created LDAP config $LDAP_CFG"
|
||||
fi
|
||||
|
||||
set_ldap() { occ ldap:set-config "$LDAP_CFG" "$1" "$2" >/dev/null 2>&1 \
|
||||
|| warn " ldap:set-config $1 failed"; }
|
||||
|
||||
set_ldap ldapHost "${LDAP_SCHEME}://${IPA_SERVER}"
|
||||
set_ldap ldapPort "$LDAP_PORT"
|
||||
set_ldap ldapAgentName "$LDAP_BIND_DN"
|
||||
set_ldap ldapAgentPassword "$LDAP_BIND_PW"
|
||||
set_ldap ldapBase "$IPA_BASEDN"
|
||||
set_ldap ldapBaseUsers "cn=users,cn=accounts,$IPA_BASEDN"
|
||||
set_ldap ldapBaseGroups "cn=groups,cn=accounts,$IPA_BASEDN"
|
||||
|
||||
# Only enabled, non-expired posix accounts. IPA marks disabled users with
|
||||
# nsAccountLock=TRUE, so exclude them — otherwise a user disabled in IPA keeps
|
||||
# a working Nextcloud account, which defeats the whole point of central identity.
|
||||
_user_filter="(&(objectClass=posixAccount)(!(nsAccountLock=TRUE)))"
|
||||
if [[ -n "${NC_LDAP_GROUP:-}" ]]; then
|
||||
_user_filter="(&(objectClass=posixAccount)(!(nsAccountLock=TRUE))(memberOf=cn=${NC_LDAP_GROUP},cn=groups,cn=accounts,${IPA_BASEDN}))"
|
||||
log " restricting accounts to IPA group '${NC_LDAP_GROUP}'"
|
||||
fi
|
||||
set_ldap ldapUserFilter "$_user_filter"
|
||||
set_ldap ldapUserFilterMode "1" # 1 = raw filter, don't let the UI rewrite it
|
||||
set_ldap ldapLoginFilter "(&${_user_filter}(uid=%uid))"
|
||||
set_ldap ldapLoginFilterMode "1"
|
||||
set_ldap ldapUserDisplayName "displayName"
|
||||
set_ldap ldapEmailAttribute "mail"
|
||||
set_ldap ldapQuotaAttribute ""
|
||||
set_ldap ldapUserFilterObjectclass "posixAccount"
|
||||
|
||||
# THE critical setting: make Nextcloud's internal username the IPA uid.
|
||||
# Left at its default, Nextcloud invents a UUID-based internal name and the
|
||||
# OIDC preferred_username will never line up with it.
|
||||
set_ldap ldapExpertUsernameAttr "uid"
|
||||
|
||||
set_ldap ldapGroupFilter "(objectClass=groupOfNames)"
|
||||
set_ldap ldapGroupFilterMode "1"
|
||||
set_ldap ldapGroupDisplayName "cn"
|
||||
set_ldap ldapGroupMemberAssocAttr "member"
|
||||
set_ldap ldapNestedGroups "0"
|
||||
set_ldap turnOnPasswordChange "0" # passwords change in IPA, not here
|
||||
set_ldap hasMemberOfFilterSupport "1"
|
||||
set_ldap useMemberOfToDetectMembership "1"
|
||||
set_ldap ldapConfigurationActive "1"
|
||||
|
||||
if occ ldap:test-config "$LDAP_CFG" 2>&1 | grep -qi 'valid\|successful'; then
|
||||
log " LDAP bind OK."
|
||||
else
|
||||
warn " LDAP test-config did not report success — check the bind DN/password."
|
||||
warn " (run: docker compose exec -u www-data nextcloud php occ ldap:test-config $LDAP_CFG)"
|
||||
fi
|
||||
|
||||
# ─── OIDC login via Keycloak ─────────────────────────────────────────────────
|
||||
log "Configuring the Keycloak OIDC provider..."
|
||||
|
||||
CLIENT_SECRET=""
|
||||
if [[ -f .oidc-secrets ]]; then
|
||||
CLIENT_SECRET=$(grep '^nextcloud=' .oidc-secrets | cut -d= -f2- || true)
|
||||
fi
|
||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||
error "No 'nextcloud' client secret in .oidc-secrets."
|
||||
error "Run ./keycloak-configure.sh first (with NC_PUBLIC_URL set in .env)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# soft_auto_provision (default on, set explicitly here so it survives someone
|
||||
# turning auto_provision off later): when the OIDC subject already exists in
|
||||
# another backend — our LDAP one — update that account rather than making a
|
||||
# duplicate in the user_oidc backend.
|
||||
occ config:system:set user_oidc auto_provision --value=true --type=boolean >/dev/null
|
||||
occ config:system:set user_oidc soft_auto_provision --value=true --type=boolean >/dev/null
|
||||
|
||||
# --unique-uid=0 keeps the user ID as the raw claim instead of a hash of
|
||||
# (claim + provider name); with the hash on, SSO logins never match LDAP users.
|
||||
occ user_oidc:provider keycloak \
|
||||
--clientid="nextcloud" \
|
||||
--clientsecret="$CLIENT_SECRET" \
|
||||
--discoveryuri="$DISCOVERY_URI" \
|
||||
--scope="openid email profile groups" \
|
||||
--mapping-uid="preferred_username" \
|
||||
--mapping-display-name="name" \
|
||||
--mapping-email="email" \
|
||||
--unique-uid=0 \
|
||||
--group-provisioning=1 \
|
||||
>/dev/null 2>&1 \
|
||||
&& log " provider 'keycloak' registered." \
|
||||
|| warn " user_oidc:provider failed — check 'occ user_oidc:provider --help' for this app version."
|
||||
|
||||
# ─── Instance settings ───────────────────────────────────────────────────────
|
||||
occ config:system:set overwrite.cli.url --value="$NC_PUBLIC_URL" >/dev/null
|
||||
occ config:system:set default_phone_region --value="${NC_PHONE_REGION:-DE}" >/dev/null
|
||||
|
||||
# Nudge people to the SSO button but keep the password form reachable at
|
||||
# ?direct=1 — the break-glass local admin still needs a way in if Keycloak is
|
||||
# down, which is exactly when you cannot fix it through Keycloak.
|
||||
occ config:system:set hide_login_form --value=false --type=boolean >/dev/null
|
||||
|
||||
# Best-effort: this command only exists on newer Nextcloud releases, and the
|
||||
# LDAP backend populates itself on demand anyway, so a miss here is harmless.
|
||||
occ user:sync-account-data >/dev/null 2>&1 || true
|
||||
|
||||
cat <<EOF
|
||||
|
||||
${GREEN}Nextcloud identity configuration complete.${NC}
|
||||
|
||||
Nextcloud: $NC_PUBLIC_URL
|
||||
Accounts from: ${LDAP_SCHEME}://${IPA_SERVER} (config $LDAP_CFG)
|
||||
users: cn=users,cn=accounts,$IPA_BASEDN
|
||||
groups: cn=groups,cn=accounts,$IPA_BASEDN
|
||||
Login via: $DISCOVERY_URI
|
||||
Break-glass: ${NC_ADMIN_USER:-ncadmin} at $NC_PUBLIC_URL/login?direct=1
|
||||
|
||||
Verify:
|
||||
• docker compose exec -u www-data nextcloud php occ user:list
|
||||
→ should list your FreeIPA users
|
||||
• Log in through the "Keycloak" button and confirm with occ user:list that
|
||||
NO second account appeared for the same person. If one did, the uid
|
||||
mapping is off — check ldapExpertUsernameAttr and --unique-uid.
|
||||
|
||||
Note: background jobs default to AJAX. For a real deployment switch to cron
|
||||
(add a nextcloud-cron container running /cron.php, then
|
||||
occ background:cron).
|
||||
|
||||
EOF
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<main>
|
||||
<header>
|
||||
<h1>ansipa</h1>
|
||||
<p class="sub">FreeIPA identity · Ansible policy · CheckMK monitoring · Keycloak SSO</p>
|
||||
<p class="sub">FreeIPA identity · Ansible policy · CheckMK monitoring · Keycloak SSO · Nextcloud</p>
|
||||
</header>
|
||||
|
||||
<section class="grid">
|
||||
|
|
@ -29,6 +29,11 @@
|
|||
<p>Single sign-on and identity federation against FreeIPA LDAP.</p>
|
||||
<span class="path">/auth/</span>
|
||||
</a>
|
||||
<a class="card" href="/nextcloud/">
|
||||
<h2>Nextcloud</h2>
|
||||
<p>Files, calendar & contacts — accounts from FreeIPA, login via Keycloak.</p>
|
||||
<span class="path">/nextcloud/</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
# 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.
|
||||
# (FreeIPA /ipa, CheckMK /cmk, Keycloak under a configured /auth relative path,
|
||||
# Nextcloud under OVERWRITEWEBROOT=/nextcloud), 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.
|
||||
|
|
@ -63,6 +64,37 @@ server {
|
|||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
|
||||
# ── Nextcloud (subpath /nextcloud, matches OVERWRITEWEBROOT) ───────────────
|
||||
# Sync clients push multi-GB files and Nextcloud streams them, so buffering
|
||||
# is off and the body limit lifted; the default 1 MB cap would break every
|
||||
# upload larger than a photo.
|
||||
location /nextcloud/ {
|
||||
proxy_pass http://${ANSIPA_NC_UPSTREAM}/;
|
||||
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;
|
||||
proxy_set_header X-Forwarded-Prefix /nextcloud;
|
||||
|
||||
client_max_body_size 0;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
|
||||
# Nextcloud sets this itself, but only over HTTPS; harmless otherwise.
|
||||
proxy_hide_header X-Frame-Options;
|
||||
}
|
||||
|
||||
# Desktop/mobile clients and CalDAV/CardDAV consumers probe these at the
|
||||
# SERVER ROOT regardless of where Nextcloud is mounted. Without these
|
||||
# redirects Nextcloud's own setup checks flag the instance as misconfigured
|
||||
# and DAV auto-discovery fails.
|
||||
location = /.well-known/carddav { return 301 /nextcloud/remote.php/dav; }
|
||||
location = /.well-known/caldav { return 301 /nextcloud/remote.php/dav; }
|
||||
location = /.well-known/webdav { return 301 /nextcloud/remote.php/dav; }
|
||||
location = /.well-known/nodeinfo { return 301 /nextcloud/index.php$request_uri; }
|
||||
|
||||
# ── Keycloak (relative path /auth, set via KC_HTTP_RELATIVE_PATH) ──────────
|
||||
location /auth/ {
|
||||
proxy_pass http://${ANSIPA_KC_UPSTREAM}/auth/;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
# Create the Nextcloud role + database alongside Keycloak's.
|
||||
#
|
||||
# Runs from the postgres image's /docker-entrypoint-initdb.d hook, which fires
|
||||
# ONLY on a first-time init (empty data directory). Stacks that already have a
|
||||
# keycloak-db volume from before Nextcloud existed will never run this — run.sh
|
||||
# performs the same create idempotently before starting Nextcloud to cover that
|
||||
# upgrade path. Keep the two in sync.
|
||||
set -euo pipefail
|
||||
|
||||
# The password is passed as a psql variable and interpolated with :'pw' so psql
|
||||
# applies its own literal quoting — a password containing a quote would
|
||||
# otherwise break the statement (or worse).
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
|
||||
-v pw="$NC_DB_PASSWORD" <<-'SQL'
|
||||
CREATE ROLE nextcloud LOGIN PASSWORD :'pw';
|
||||
CREATE DATABASE nextcloud OWNER nextcloud ENCODING 'UTF8';
|
||||
SQL
|
||||
|
||||
echo "[initdb] nextcloud role and database created"
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
#
|
||||
# Usage:
|
||||
# ./run.sh # freeipa only
|
||||
# ./run.sh all # freeipa + postgres + keycloak
|
||||
# ./run.sh all # whole stack (freeipa + postgres + keycloak + checkmk
|
||||
# # + nextcloud + redis + nginx gateway)
|
||||
# ./run.sh down # stop and remove containers
|
||||
#
|
||||
# Why this exists:
|
||||
|
|
@ -28,6 +29,55 @@ stop_freeipa() {
|
|||
docker rm -f freeipa 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Create the nextcloud role/database if they are missing.
|
||||
#
|
||||
# postgres/initdb/10-nextcloud.sh already does this, but the image only runs
|
||||
# initdb hooks when the data directory is empty. Stacks created before Nextcloud
|
||||
# existed have a populated keycloak-db volume, so the hook never fires there and
|
||||
# Nextcloud's installer would fail with "database nextcloud does not exist".
|
||||
# This covers that upgrade path and is a no-op on fresh installs.
|
||||
ensure_nextcloud_db() {
|
||||
local _pw="${NC_DB_PASSWORD:?set NC_DB_PASSWORD in .env}"
|
||||
|
||||
echo "Waiting for PostgreSQL..."
|
||||
local _i
|
||||
for _i in $(seq 1 30); do
|
||||
docker compose exec -T postgres pg_isready -U keycloak &>/dev/null && break
|
||||
[[ $_i -eq 30 ]] && { echo "ERROR: PostgreSQL not ready after 60s."; exit 1; }
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# psql has no CREATE ROLE/DATABASE IF NOT EXISTS, and CREATE DATABASE cannot
|
||||
# run inside a transaction (so no DO block either) — hence query-then-create.
|
||||
#
|
||||
# The password is handed to psql as a variable and interpolated with :'pw',
|
||||
# which applies psql's own literal quoting, so a password containing quotes
|
||||
# or backslashes cannot break (or inject into) the statement.
|
||||
#
|
||||
# That interpolation only happens for SQL read from stdin — `psql -c` sends
|
||||
# its argument to the server verbatim and would fail with a syntax error on
|
||||
# the literal ":'pw'". Hence the inner heredoc rather than -c.
|
||||
#
|
||||
# The outer heredoc is quoted ('SH') so $NCPW expands in the container,
|
||||
# where `docker compose exec -e` set it, and never on this host.
|
||||
docker compose exec -T -e NCPW="$_pw" postgres sh -s <<'SH' \
|
||||
&& echo "Nextcloud database ready." \
|
||||
|| { echo "ERROR: could not create the nextcloud database."; exit 1; }
|
||||
set -e
|
||||
psql -U keycloak -d keycloak -tAc "SELECT 1 FROM pg_roles WHERE rolname='nextcloud'" \
|
||||
| grep -q 1 \
|
||||
|| psql -v ON_ERROR_STOP=1 -U keycloak -d keycloak -v pw="$NCPW" <<'Q'
|
||||
CREATE ROLE nextcloud LOGIN PASSWORD :'pw';
|
||||
Q
|
||||
|
||||
psql -U keycloak -d keycloak -tAc "SELECT 1 FROM pg_database WHERE datname='nextcloud'" \
|
||||
| grep -q 1 \
|
||||
|| psql -v ON_ERROR_STOP=1 -U keycloak -d keycloak <<'Q'
|
||||
CREATE DATABASE nextcloud OWNER nextcloud ENCODING 'UTF8';
|
||||
Q
|
||||
SH
|
||||
}
|
||||
|
||||
start_freeipa() {
|
||||
stop_freeipa
|
||||
|
||||
|
|
@ -106,8 +156,15 @@ case "$MODE" in
|
|||
;;
|
||||
all)
|
||||
start_freeipa
|
||||
docker compose up -d postgres keycloak checkmk
|
||||
echo "Full stack started (freeipa + postgres + keycloak + checkmk)."
|
||||
docker compose up -d postgres
|
||||
ensure_nextcloud_db
|
||||
docker compose up -d keycloak checkmk nextcloud-redis nextcloud nginx
|
||||
echo "Full stack started (freeipa + postgres + keycloak + checkmk + nextcloud + nginx)."
|
||||
echo
|
||||
echo "Next, once the containers report healthy:"
|
||||
echo " ./keycloak-configure.sh # Keycloak ← FreeIPA LDAP, and OIDC clients"
|
||||
echo " ./nextcloud-configure.sh # Nextcloud ← IPA LDAP + Keycloak OIDC"
|
||||
echo " ./checkmk-ldap-configure.sh # CheckMK ← FreeIPA LDAP"
|
||||
;;
|
||||
"")
|
||||
start_freeipa
|
||||
|
|
|
|||
|
|
@ -61,14 +61,23 @@ ask "Proxmox storage [local-lvm]:"; read -r I; PVE_STORAGE="${I:-local-lvm}"
|
|||
ask "Template storage [local]:"; read -r I; TPL_STORAGE="${I:-local}"
|
||||
ask "Debian CT template [debian-12-standard_12.7-1_amd64.tar.zst]:"; read -r I
|
||||
CT_TEMPLATE="${I:-debian-12-standard_12.7-1_amd64.tar.zst}"
|
||||
# FreeIPA alone wants ~2 GB; the full stack (CheckMK/Keycloak/Postgres) needs headroom.
|
||||
ask "RAM MB [6144]:"; read -r I; CT_RAM="${I:-6144}"
|
||||
# FreeIPA alone wants ~2 GB; the full stack (CheckMK/Keycloak/Postgres/Nextcloud
|
||||
# + Redis) needs headroom — 8 GB is comfortable, 6 GB is the practical floor.
|
||||
ask "RAM MB [8192]:"; read -r I; CT_RAM="${I:-8192}"
|
||||
ask "Cores [4]:"; read -r I; CT_CORES="${I:-4}"
|
||||
ask "Disk GB [20]:"; read -r I; CT_DISK="${I:-20}"
|
||||
ask "Bridge [vmbr0]:"; read -r I; CT_BRIDGE="${I:-vmbr0}"
|
||||
ask "Static IP CIDR or 'dhcp' [dhcp]:"; read -r I; CT_IP="${I:-dhcp}"
|
||||
ask "Advertised CheckMK URL for clients (e.g. http://ansipa.$IPA_DOMAIN:8090) [blank=set later]:"
|
||||
read -r CMK_ADVERTISED_URL
|
||||
# Nextcloud hands this URL to sync clients and registers it as the Keycloak
|
||||
# redirect URI, so it must be the address USERS type — i.e. your TLS reverse
|
||||
# proxy — not the container's own address.
|
||||
ask "Public ansipa gateway base URL (e.g. https://ansipa.$IPA_DOMAIN) [blank=set later]:"
|
||||
read -r ANSIPA_PUBLIC_URL
|
||||
ANSIPA_PUBLIC_URL="${ANSIPA_PUBLIC_URL%/}"
|
||||
# Bare hostname for NC_TRUSTED_DOMAINS (Nextcloud matches on host, not URL).
|
||||
ANSIPA_PUBLIC_HOST=$(printf '%s' "$ANSIPA_PUBLIC_URL" | sed 's|https\?://||; s|/.*||')
|
||||
|
||||
OUT_DIR="${1:-$HOME/ansipa-lxc-out}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
|
@ -78,6 +87,7 @@ section "Generating secrets"
|
|||
IPA_ADMIN_PASSWORD=$(gen_pw); IPA_DM_PASSWORD=$(gen_pw)
|
||||
CMK_ADMIN_PASSWORD=$(gen_pw); KC_ADMIN_PASSWORD=$(gen_pw); KC_DB_PASSWORD=$(gen_pw)
|
||||
LUKS_KEY_UPLOAD_PASSWORD=$(gen_pw)
|
||||
NC_DB_PASSWORD=$(gen_pw); NC_ADMIN_PASSWORD=$(gen_pw)
|
||||
log "Generated admin/service passwords (saved in $OUT_DIR/.env — keep it safe)."
|
||||
|
||||
# ─── .env for the ansipa stack ────────────────────────────────────────────────
|
||||
|
|
@ -103,6 +113,21 @@ KC_ADMIN_PASSWORD=$KC_ADMIN_PASSWORD
|
|||
KC_DB_PASSWORD=$KC_DB_PASSWORD
|
||||
|
||||
ANSIPA_HTTP_PORT=8088
|
||||
|
||||
# Nextcloud. NC_PUBLIC_URL/NC_TRUSTED_DOMAINS are derived from the gateway URL
|
||||
# you gave; if you left it blank, fill both in before the stack is started —
|
||||
# docker compose treats the NC_* passwords as required and Nextcloud rejects
|
||||
# any hostname missing from NC_TRUSTED_DOMAINS with an "untrusted domain" page.
|
||||
NC_DB_PASSWORD=$NC_DB_PASSWORD
|
||||
NC_ADMIN_USER=ncadmin
|
||||
NC_ADMIN_PASSWORD=$NC_ADMIN_PASSWORD
|
||||
NC_PUBLIC_URL=${ANSIPA_PUBLIC_URL:+$ANSIPA_PUBLIC_URL/nextcloud}
|
||||
NC_TRUSTED_DOMAINS=${ANSIPA_PUBLIC_HOST:-localhost}
|
||||
|
||||
# CheckMK cannot use Keycloak (no SAML/OIDC in the Community edition) — these
|
||||
# IPA groups drive its roles instead. See docs/md/ansipa-sso.md.
|
||||
CMK_LDAP_ADMIN_GROUP=usr_cmk_admins
|
||||
CMK_LDAP_USER_GROUP=usr_cmk_users
|
||||
EOF
|
||||
chmod 600 "$OUT_DIR/.env"
|
||||
|
||||
|
|
@ -186,7 +211,11 @@ fi
|
|||
|
||||
echo "[ansipa] Stack starting. Watch first-boot with:"
|
||||
echo " docker logs -f freeipa"
|
||||
echo "[ansipa] Once healthy, wire Keycloak↔FreeIPA LDAP: cd $STACK && ./keycloak-configure.sh"
|
||||
echo "[ansipa] Once healthy, wire identity up (in this order):"
|
||||
echo " cd $STACK"
|
||||
echo " ./keycloak-configure.sh # Keycloak <- FreeIPA LDAP, + OIDC clients"
|
||||
echo " ./nextcloud-configure.sh # Nextcloud <- IPA LDAP + Keycloak OIDC"
|
||||
echo " ./checkmk-ldap-configure.sh # CheckMK <- FreeIPA LDAP"
|
||||
PROVEOF
|
||||
chmod +x "$OUT_DIR/ansipa-provision.sh"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue