setup: add FreeIPA image builder and Keycloak integration

freeipa-image-builder.sh: TUI chooser that builds a FreeIPA server image
and exports it to four target formats:
  docker      — builds via podman/docker, optional registry push
  lxc         — exports container rootfs as .tar.zst Proxmox CT template,
                 generates pct import instructions
  proxmox-vm  — downloads Rocky/Fedora cloud image, customizes with
                 virt-customize, outputs QCOW2 + cloud-init user-data.yml
  oci-archive — skopeo OCI tarball for air-gapped import

Keycloak TUI option generates the full constellation:
  docker-compose.yml   FreeIPA + Keycloak + PostgreSQL stack
  .env                 pre-filled env template (passwords placeholder)
  keycloak-configure.sh  post-start Keycloak REST API config script

image/Dockerfile: Fedora 41 + freeipa-server-dns + ansible-core,
systemd-enabled container (CMD /sbin/init).

image/ipa-first-boot.{sh,service}: systemd oneshot that runs
ipa-server-install on first container/VM boot from env vars
(IPA_DOMAIN, IPA_ADMIN_PASSWORD, IPA_DM_PASSWORD, and optionals).
ConditionPathExists=!/etc/ipa/default.conf makes it idempotent.

image/keycloak-configure.sh: Keycloak REST API automation that:
  - waits for Keycloak readiness
  - creates a realm
  - wires FreeIPA LDAP user federation (READ_ONLY, vendor=rhds)
  - adds attribute mappers: email, firstName, lastName, uidNumber
  - adds group mapper (IPA groups → Keycloak groups, cn=groups,cn=accounts)
  - triggers an initial full user sync

image/docker-compose.yml: freeipa + postgres + keycloak services on
a private 172.30.0.0/24 bridge; FreeIPA has a fixed IP so Keycloak
can resolve it via extra_hosts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
The_miro 2026-05-18 11:22:48 +02:00
parent 7279a781b0
commit f66775ce54
9 changed files with 969 additions and 1 deletions

View File

@ -120,6 +120,7 @@ count_steps() {
[[ "$sel" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"python"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"zfs"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"zfs"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"wprs"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"wprs"* ]] && TOTAL=$(( TOTAL + 1 ))
@ -185,6 +186,7 @@ SELECTED=$(dialog --backtitle "$BACKTITLE" \
"cockpit" "Cockpit web UI · machines · podman" off \ "cockpit" "Cockpit web UI · machines · podman" off \
"ssh-server" "SSH server openssh · key-auth · enabled" off \ "ssh-server" "SSH server openssh · key-auth · enabled" off \
"freeipa-server" "FreeIPA Server interactive server setup + client gen" off \ "freeipa-server" "FreeIPA Server interactive server setup + client gen" off \
"freeipa-image" "FreeIPA Image OCI/LXC/Proxmox/VM builder + Keycloak" off \
"python" "Python tools pyright · pipx · pynvim" off \ "python" "Python tools pyright · pipx · pynvim" off \
"zfs" "ZFS zfs-dkms kernel module" off \ "zfs" "ZFS zfs-dkms kernel module" off \
"wprs" "WPRS wprs-git (AUR)" off \ "wprs" "WPRS wprs-git (AUR)" off \
@ -229,6 +231,7 @@ SUMMARY=""
[[ "$SELECTED" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit\n" [[ "$SELECTED" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit\n"
[[ "$SELECTED" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server\n" [[ "$SELECTED" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server\n"
[[ "$SELECTED" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n" [[ "$SELECTED" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n"
[[ "$SELECTED" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n"
[[ "$SELECTED" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n" [[ "$SELECTED" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n"
[[ "$SELECTED" == *"zfs"* ]] && SUMMARY+=" ✦ ZFS\n" [[ "$SELECTED" == *"zfs"* ]] && SUMMARY+=" ✦ ZFS\n"
[[ "$SELECTED" == *"wprs"* ]] && SUMMARY+=" ✦ WPRS\n" [[ "$SELECTED" == *"wprs"* ]] && SUMMARY+=" ✦ WPRS\n"
@ -276,6 +279,7 @@ count_steps "$SELECTED"
[[ "$SELECTED" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh" [[ "$SELECTED" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh"
[[ "$SELECTED" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.sh" [[ "$SELECTED" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.sh"
[[ "$SELECTED" == *"freeipa-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh" [[ "$SELECTED" == *"freeipa-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh"
[[ "$SELECTED" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.sh"
[[ "$SELECTED" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh" [[ "$SELECTED" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh"
[[ "$SELECTED" == *"zfs"* ]] && run_module "ZFS" "$MODULES/optional-Modules/zfs.sh" [[ "$SELECTED" == *"zfs"* ]] && run_module "ZFS" "$MODULES/optional-Modules/zfs.sh"
[[ "$SELECTED" == *"wprs"* ]] && run_module "WPRS" "$MODULES/optional-Modules/wprs.sh" [[ "$SELECTED" == *"wprs"* ]] && run_module "WPRS" "$MODULES/optional-Modules/wprs.sh"

View File

@ -0,0 +1,23 @@
# ── FreeIPA ───────────────────────────────────────────────────────────────────
IPA_HOSTNAME=ipa.corp.example.com
IPA_DOMAIN=corp.example.com
IPA_REALM=CORP.EXAMPLE.COM
IPA_ADMIN_PASSWORD=ChangeMe123!
IPA_DM_PASSWORD=ChangeMe456!
IPA_SETUP_DNS=false
IPA_DNS_FORWARDER=
IPA_SETUP_KRA=false
# ── Keycloak ──────────────────────────────────────────────────────────────────
KC_HOSTNAME=keycloak.corp.example.com
KC_REALM=corp
KC_ADMIN=admin
KC_ADMIN_PASSWORD=ChangeMe789!
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
IPA_BIND_PASSWORD=
IPA_USE_LDAPS=false

View File

@ -0,0 +1,68 @@
# FreeIPA server container image (Fedora / systemd-based)
#
# Build:
# docker build -t freeipa-server .
#
# Run (quick test):
# docker run --privileged --name freeipa \
# --tmpfs /run --tmpfs /tmp \
# -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
# -v freeipa-data:/data \
# -h ipa.example.com \
# -e IPA_DOMAIN=example.com \
# -e IPA_ADMIN_PASSWORD=Secret123 \
# -e IPA_DM_PASSWORD=Secret456 \
# -p 443:443 -p 389:389 -p 636:636 -p 88:88 \
# freeipa-server
#
# For production use docker-compose.yml instead.
FROM fedora:41
ENV container=docker \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
RUN dnf install -y --setopt=install_weak_deps=False \
freeipa-server \
freeipa-server-dns \
freeipa-server-trust-ad \
freeipa-admintools \
ansible-core \
python3-netaddr \
openldap-clients \
krb5-workstation \
bind-utils \
procps-ng \
net-tools \
rsync \
hostname \
&& dnf clean all \
&& rm -rf /var/cache/dnf
# Mask units that either require host-level access or are irrelevant in containers
RUN systemctl mask \
systemd-remount-fs.service \
dev-hugepages.mount \
sys-fs-fuse-connections.mount \
systemd-logind.service \
getty.target \
console-getty.service \
dnf-makecache.timer \
plymouth-quit-wait.service \
plymouth-start.service \
network.service \
NetworkManager.service
COPY ipa-first-boot.sh /usr/local/sbin/ipa-first-boot.sh
COPY ipa-first-boot.service /etc/systemd/system/ipa-first-boot.service
RUN chmod +x /usr/local/sbin/ipa-first-boot.sh \
&& systemctl enable ipa-first-boot.service
VOLUME ["/data"]
# LDAP, LDAPS, Kerberos, kpasswd, HTTPS, DNS, NTP
EXPOSE 389 636 88/tcp 88/udp 464/tcp 464/udp 443 80 53/tcp 53/udp 123/udp
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

View File

@ -0,0 +1,122 @@
# FreeIPA + Keycloak + PostgreSQL constellation
#
# Setup:
# cp .env.example .env && $EDITOR .env
# docker compose up -d
# docker compose logs -f freeipa # watch first-boot install (~10 min)
# # Once freeipa is healthy:
# ./keycloak-configure.sh # wire Keycloak → FreeIPA LDAP
#
# To run without Keycloak:
# docker compose up -d freeipa
#
# To scale out (clients enroll against the same IPA server):
# docker compose --profile clients up
volumes:
freeipa-data:
keycloak-db:
networks:
ipa-net:
ipam:
config:
- subnet: 172.30.0.0/24
services:
# ── FreeIPA ─────────────────────────────────────────────────────────────────
freeipa:
build:
context: .
dockerfile: Dockerfile
image: freeipa-server:local
container_name: freeipa
hostname: ${IPA_HOSTNAME:-ipa.example.com}
privileged: true
tmpfs:
- /run
- /tmp
volumes:
- freeipa-data:/data
- /sys/fs/cgroup:/sys/fs/cgroup:rw
environment:
IPA_DOMAIN: ${IPA_DOMAIN:?set IPA_DOMAIN in .env}
IPA_REALM: ${IPA_REALM:-}
IPA_ADMIN_PASSWORD: ${IPA_ADMIN_PASSWORD:?set IPA_ADMIN_PASSWORD in .env}
IPA_DM_PASSWORD: ${IPA_DM_PASSWORD:?set IPA_DM_PASSWORD in .env}
IPA_SETUP_DNS: ${IPA_SETUP_DNS:-false}
IPA_DNS_FORWARDER: ${IPA_DNS_FORWARDER:-}
IPA_SETUP_KRA: ${IPA_SETUP_KRA:-false}
ports:
- "389:389"
- "636:636"
- "88:88"
- "88:88/udp"
- "464:464"
- "464:464/udp"
- "443:443"
networks:
ipa-net:
ipv4_address: 172.30.0.10
healthcheck:
test: ["CMD-SHELL", "ipactl status 2>/dev/null | grep -q 'running'"]
interval: 30s
timeout: 15s
retries: 20
start_period: 600s
# ── PostgreSQL (Keycloak backend) ────────────────────────────────────────────
postgres:
image: postgres:16-alpine
container_name: keycloak-db
restart: unless-stopped
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${KC_DB_PASSWORD:?set KC_DB_PASSWORD in .env}
volumes:
- keycloak-db:/var/lib/postgresql/data
networks:
ipa-net:
healthcheck:
test: ["CMD", "pg_isready", "-U", "keycloak"]
interval: 10s
retries: 5
# ── Keycloak ─────────────────────────────────────────────────────────────────
# After first start, run ./keycloak-configure.sh to wire FreeIPA LDAP federation.
keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: keycloak
restart: unless-stopped
# Use 'start' (not start-dev) for production; requires a TLS certificate.
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KC_DB_PASSWORD:?set KC_DB_PASSWORD in .env}
KC_HOSTNAME: ${KC_HOSTNAME:-localhost}
KC_HTTP_PORT: 8080
KC_HTTPS_PORT: 8443
KC_HTTP_ENABLED: "true"
KC_FEATURES: preview
KEYCLOAK_ADMIN: ${KC_ADMIN:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:?set KC_ADMIN_PASSWORD in .env}
extra_hosts:
- "${IPA_HOSTNAME:-ipa.example.com}:172.30.0.10"
ports:
- "8080:8080"
- "8443:8443"
depends_on:
postgres:
condition: service_healthy
networks:
ipa-net:
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:8080/health/ready || exit 1"]
interval: 20s
timeout: 10s
retries: 20
start_period: 90s

View File

@ -0,0 +1,16 @@
[Unit]
Description=FreeIPA Server First-Boot Configuration
After=network-online.target dirsrv.target
Wants=network-online.target
ConditionPathExists=!/etc/ipa/default.conf
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ipa-first-boot.sh
RemainAfterExit=yes
StandardOutput=journal+console
StandardError=journal+console
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,78 @@
#!/bin/bash
# ipa-first-boot.sh — runs once on first container start via ipa-first-boot.service
#
# Required environment variables:
# IPA_DOMAIN IPA domain (e.g. corp.example.com)
# IPA_ADMIN_PASSWORD Admin UI / API password
# IPA_DM_PASSWORD Directory Manager (LDAP root) password
#
# Optional environment variables:
# IPA_REALM Kerberos realm (default: DOMAIN uppercased)
# IPA_HOSTNAME Server FQDN (default: container hostname)
# IPA_SETUP_DNS Enable integrated DNS (default: false)
# IPA_DNS_FORWARDER DNS forwarder IP
# IPA_AUTO_REVERSE Auto reverse DNS zone (default: false)
# IPA_SETUP_KRA Install KRA (default: false)
# IPA_NO_NTP Disable NTP setup (default: true)
# IPA_INSTALL_OPTS Extra verbatim flags for ipa-server-install
set -euo pipefail
LOG=/var/log/ipa-first-boot.log
exec > >(tee -a "$LOG") 2>&1
echo "=== ipa-first-boot: $(date) ==="
if [[ -f /etc/ipa/default.conf ]]; then
echo "FreeIPA already configured — skipping."
exit 0
fi
: "${IPA_DOMAIN:?IPA_DOMAIN is required}"
: "${IPA_ADMIN_PASSWORD:?IPA_ADMIN_PASSWORD is required}"
: "${IPA_DM_PASSWORD:?IPA_DM_PASSWORD is required}"
IPA_REALM="${IPA_REALM:-${IPA_DOMAIN^^}}"
IPA_HOSTNAME="${IPA_HOSTNAME:-$(hostname -f)}"
IPA_SETUP_DNS="${IPA_SETUP_DNS:-false}"
IPA_AUTO_REVERSE="${IPA_AUTO_REVERSE:-false}"
IPA_SETUP_KRA="${IPA_SETUP_KRA:-false}"
IPA_NO_NTP="${IPA_NO_NTP:-true}"
ARGS=(
--realm="$IPA_REALM"
--domain="$IPA_DOMAIN"
--admin-password="$IPA_ADMIN_PASSWORD"
--ds-password="$IPA_DM_PASSWORD"
--hostname="$IPA_HOSTNAME"
--ip-address="$(hostname -I | awk '{print $1}')"
--mkhomedir
--unattended
)
if [[ "$IPA_SETUP_DNS" == "true" ]]; then
ARGS+=(--setup-dns)
[[ -n "${IPA_DNS_FORWARDER:-}" ]] \
&& ARGS+=(--forwarder="$IPA_DNS_FORWARDER") \
|| ARGS+=(--no-forwarders)
[[ "$IPA_AUTO_REVERSE" == "true" ]] && ARGS+=(--auto-reverse) || ARGS+=(--no-reverse)
else
ARGS+=(--no-reverse)
fi
[[ "$IPA_NO_NTP" == "true" ]] && ARGS+=(--no-ntp)
[[ "$IPA_SETUP_KRA" == "true" ]] && ARGS+=(--setup-kra)
[[ -n "${IPA_INSTALL_OPTS:-}" ]] && read -ra EXTRA <<< "$IPA_INSTALL_OPTS" && ARGS+=("${EXTRA[@]}")
echo "Running ipa-server-install..."
ipa-server-install "${ARGS[@]}"
# Persist key directories to /data volume so they survive container restarts
if mountpoint -q /data 2>/dev/null; then
echo "Persisting data to /data..."
for d in /var/lib/dirsrv /var/lib/ipa /etc/ipa /etc/dirsrv \
/etc/named.conf /var/lib/named /var/lib/krb5kdc; do
[[ -e "$d" ]] && rsync -a --relative "$d" /data/ 2>/dev/null || true
done
fi
echo "=== ipa-first-boot complete: $(date) ==="

View File

@ -0,0 +1,272 @@
#!/bin/bash
# keycloak-configure.sh — wire Keycloak to FreeIPA via LDAP user federation
#
# Run this AFTER both FreeIPA and Keycloak are fully up.
# Reads settings from environment variables or a .env file in the same directory.
#
# Required env vars:
# IPA_SERVER FreeIPA server FQDN
# IPA_DOMAIN FreeIPA domain
# IPA_DM_PASSWORD Directory Manager password (used as LDAP bind credential
# unless IPA_BIND_DN / IPA_BIND_PASSWORD override it)
# KC_ADMIN_PASSWORD Keycloak admin password
#
# Optional env vars (defaults shown):
# KC_URL http://localhost:8080
# KC_ADMIN admin
# KC_REALM freeipa (realm to create)
# KC_REALM_DISPLAY <IPA_DOMAIN>
# IPA_REALM <IPA_DOMAIN uppercased>
# IPA_BIND_DN cn=Directory Manager
# IPA_BIND_PASSWORD <IPA_DM_PASSWORD>
# IPA_USE_LDAPS false
# IPA_LDAP_PORT 389 (or 636 if LDAPS)
# SYNC_FULL_PERIOD 604800 (1 week)
# SYNC_CHANGED_PERIOD 86400 (1 day)
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'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
info() { echo -e "${CYAN}[i]${NC} $*"; }
: "${IPA_SERVER:?IPA_SERVER is required}"
: "${IPA_DOMAIN:?IPA_DOMAIN is required}"
: "${IPA_DM_PASSWORD:?IPA_DM_PASSWORD is required}"
: "${KC_ADMIN_PASSWORD:?KC_ADMIN_PASSWORD is required}"
KC_URL="${KC_URL:-http://localhost:8080}"
KC_ADMIN="${KC_ADMIN:-admin}"
KC_REALM="${KC_REALM:-freeipa}"
KC_REALM_DISPLAY="${KC_REALM_DISPLAY:-$IPA_DOMAIN}"
IPA_REALM="${IPA_REALM:-${IPA_DOMAIN^^}}"
IPA_BIND_DN="${IPA_BIND_DN:-cn=Directory Manager}"
IPA_BIND_PASSWORD="${IPA_BIND_PASSWORD:-$IPA_DM_PASSWORD}"
IPA_USE_LDAPS="${IPA_USE_LDAPS:-false}"
IPA_LDAP_SCHEME="ldap"
IPA_LDAP_PORT=389
[[ "$IPA_USE_LDAPS" == "true" ]] && IPA_LDAP_SCHEME="ldaps" && IPA_LDAP_PORT=636
IPA_LDAP_URL="${IPA_LDAP_URL:-${IPA_LDAP_SCHEME}://${IPA_SERVER}:${IPA_LDAP_PORT}}"
IPA_BASEDN="dc=${IPA_DOMAIN/./,dc=}"
SYNC_FULL_PERIOD="${SYNC_FULL_PERIOD:-604800}"
SYNC_CHANGED_PERIOD="${SYNC_CHANGED_PERIOD:-86400}"
# ─── Helpers ──────────────────────────────────────────────────────────────────
kc_token() {
curl -sf -X POST \
"$KC_URL/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=admin-cli&grant_type=password" \
-d "username=$KC_ADMIN" \
--data-urlencode "password=$KC_ADMIN_PASSWORD" \
| jq -r '.access_token'
}
kc_get() { curl -sf -H "Authorization: Bearer $TOKEN" "$KC_URL$1"; }
kc_post() { curl -sf -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d "$2" "$KC_URL$1"; }
kc_put() { curl -sf -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d "$2" "$KC_URL$1"; }
kc_status() { curl -sf -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" "$KC_URL$1"; }
# ─── Wait for Keycloak ────────────────────────────────────────────────────────
info "Waiting for Keycloak at $KC_URL..."
for i in $(seq 1 60); do
curl -sf "$KC_URL/health/ready" &>/dev/null && break
[[ $i -eq 60 ]] && { error "Keycloak not ready after 120s."; exit 1; }
sleep 2
done
log "Keycloak is ready."
# ─── Authenticate ─────────────────────────────────────────────────────────────
TOKEN=$(kc_token)
[[ -z "$TOKEN" || "$TOKEN" == "null" ]] && { error "Failed to obtain Keycloak token."; exit 1; }
log "Admin token obtained."
# ─── Create realm ─────────────────────────────────────────────────────────────
REALM_STATUS=$(kc_status "/admin/realms/$KC_REALM")
if [[ "$REALM_STATUS" == "200" ]]; then
warn "Realm '$KC_REALM' already exists — updating."
kc_put "/admin/realms/$KC_REALM" \
"{\"realm\":\"$KC_REALM\",\"displayName\":\"$KC_REALM_DISPLAY\",\"enabled\":true,
\"ssoSessionMaxLifespan\":36000,\"accessTokenLifespan\":300}" >/dev/null
else
kc_post "/admin/realms" \
"{\"realm\":\"$KC_REALM\",\"displayName\":\"$KC_REALM_DISPLAY\",\"enabled\":true,
\"ssoSessionMaxLifespan\":36000,\"accessTokenLifespan\":300}" >/dev/null
log "Realm '$KC_REALM' created."
fi
TOKEN=$(kc_token)
# ─── LDAP user federation ─────────────────────────────────────────────────────
log "Configuring FreeIPA LDAP user federation..."
LDAP_COMPONENT=$(cat <<JSON
{
"name": "freeipa-ldap",
"providerId": "ldap",
"providerType": "org.keycloak.storage.UserStorageProvider",
"config": {
"enabled": ["true"],
"priority": ["1"],
"editMode": ["READ_ONLY"],
"syncRegistrations": ["false"],
"vendor": ["rhds"],
"usernameLDAPAttribute": ["uid"],
"rdnLDAPAttribute": ["uid"],
"uuidLDAPAttribute": ["ipaUniqueID"],
"userObjectClasses": ["inetOrgPerson, organizationalPerson"],
"connectionUrl": ["$IPA_LDAP_URL"],
"usersDn": ["cn=users,cn=accounts,$IPA_BASEDN"],
"authType": ["simple"],
"bindDn": ["$IPA_BIND_DN"],
"bindCredential": ["$IPA_BIND_PASSWORD"],
"searchScope": ["1"],
"validatePasswordPolicy": ["false"],
"trustEmail": ["false"],
"useTruststoreSpi": ["ldapsOnly"],
"connectionPooling": ["true"],
"pagination": ["true"],
"batchSizeForSync": ["1000"],
"fullSyncPeriod": ["$SYNC_FULL_PERIOD"],
"changedSyncPeriod": ["$SYNC_CHANGED_PERIOD"],
"importEnabled": ["true"],
"cachePolicy": ["DEFAULT"],
"kerberosRealm": ["$IPA_REALM"],
"serverPrincipal": ["HTTP/$IPA_SERVER@$IPA_REALM"],
"useKerberosForPasswordAuthentication": ["false"],
"allowKerberosAuthentication": ["false"],
"debug": ["false"]
}
}
JSON
)
EXISTING_ID=$(kc_get "/admin/realms/$KC_REALM/components?type=org.keycloak.storage.UserStorageProvider&name=freeipa-ldap" \
| jq -r '.[0].id // empty')
if [[ -n "$EXISTING_ID" ]]; then
warn "LDAP provider already exists (id=$EXISTING_ID) — updating."
kc_put "/admin/realms/$KC_REALM/components/$EXISTING_ID" "$LDAP_COMPONENT" >/dev/null
LDAP_ID="$EXISTING_ID"
else
LDAP_ID=$(kc_post "/admin/realms/$KC_REALM/components" "$LDAP_COMPONENT" \
| jq -r '.id // empty')
# Keycloak returns 201 with Location header, not a body with id — extract from header or re-query
if [[ -z "$LDAP_ID" ]]; then
LDAP_ID=$(kc_get "/admin/realms/$KC_REALM/components?type=org.keycloak.storage.UserStorageProvider&name=freeipa-ldap" \
| jq -r '.[0].id')
fi
log "LDAP provider created (id=$LDAP_ID)."
fi
# ─── Attribute mappers ────────────────────────────────────────────────────────
log "Adding LDAP attribute mappers..."
add_mapper() {
local name="$1" type="$2" ldap_attr="$3" user_attr="$4"
local payload
payload=$(cat <<JSON
{
"name": "$name",
"providerId": "$type",
"providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
"parentId": "$LDAP_ID",
"config": {
"ldap.attribute": ["$ldap_attr"],
"user.model.attribute": ["$user_attr"],
"read.only": ["true"],
"always.read.value.from.ldap": ["false"],
"is.mandatory.in.ldap": ["false"]
}
}
JSON
)
local exists
exists=$(kc_get "/admin/realms/$KC_REALM/components?parent=$LDAP_ID&name=$name" \
| jq -r '.[0].id // empty')
if [[ -z "$exists" ]]; then
kc_post "/admin/realms/$KC_REALM/components" "$payload" >/dev/null
log " mapper: $name"
else
warn " mapper '$name' already exists — skipping."
fi
}
add_mapper "email" "user-attribute-ldap-mapper" "mail" "email"
add_mapper "first-name" "user-attribute-ldap-mapper" "givenName" "firstName"
add_mapper "last-name" "user-attribute-ldap-mapper" "sn" "lastName"
add_mapper "uid-number" "user-attribute-ldap-mapper" "uidNumber" "uidNumber"
# Group mapper (maps IPA groups to Keycloak groups)
GROUP_MAPPER=$(cat <<JSON
{
"name": "freeipa-groups",
"providerId": "group-ldap-mapper",
"providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
"parentId": "$LDAP_ID",
"config": {
"groups.dn": ["cn=groups,cn=accounts,$IPA_BASEDN"],
"group.name.ldap.attribute": ["cn"],
"group.object.classes": ["groupOfNames"],
"preserve.group.inheritance": ["false"],
"membership.ldap.attribute": ["member"],
"membership.attribute.type": ["DN"],
"mode": ["READ_ONLY"],
"user.roles.retrieve.strategy": ["LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"],
"mapped.group.attributes": [""],
"drop.non.existing.groups.during.sync": ["false"]
}
}
JSON
)
exists=$(kc_get "/admin/realms/$KC_REALM/components?parent=$LDAP_ID&name=freeipa-groups" \
| jq -r '.[0].id // empty')
if [[ -z "$exists" ]]; then
kc_post "/admin/realms/$KC_REALM/components" "$GROUP_MAPPER" >/dev/null
log " mapper: freeipa-groups"
fi
# ─── Trigger initial sync ──────────────────────────────────────────────────────
log "Triggering initial user sync..."
SYNC_RESULT=$(kc_post "/admin/realms/$KC_REALM/user-storage/$LDAP_ID/sync?action=triggerFullSync" "" 2>/dev/null || echo "{}")
ADDED=$(echo "$SYNC_RESULT" | jq -r '.added // 0')
UPDATED=$(echo "$SYNC_RESULT" | jq -r '.updated // 0')
log "Sync complete: $ADDED added, $UPDATED updated."
# ─── Enable email login ────────────────────────────────────────────────────────
kc_put "/admin/realms/$KC_REALM" \
'{"loginWithEmailAllowed":true,"duplicateEmailsAllowed":false}' >/dev/null
log "Email login enabled on realm '$KC_REALM'."
# ─── Summary ─────────────────────────────────────────────────────────────────
cat <<EOF
${GREEN}Keycloak ↔ FreeIPA configuration complete.${NC}
Keycloak URL: $KC_URL
Realm: $KC_REALM (display: $KC_REALM_DISPLAY)
LDAP provider: $IPA_LDAP_URL
Users DN: cn=users,cn=accounts,$IPA_BASEDN
Groups DN: cn=groups,cn=accounts,$IPA_BASEDN
Sync schedule: full=${SYNC_FULL_PERIOD}s / changed=${SYNC_CHANGED_PERIOD}s
Admin console: $KC_URL/admin/$KC_REALM/console
User login: $KC_URL/realms/$KC_REALM/account
Next steps:
• Verify users are visible: Admin console → Users
• Set up client applications (OIDC/SAML) in this realm
• 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
EOF

View File

@ -0,0 +1,381 @@
#!/bin/bash
# freeipa-image-builder.sh — build a FreeIPA server image for multiple targets
#
# Targets (TUI chooser):
# docker Build Docker/Podman image and optionally push to a registry
# lxc Export rootfs as a Proxmox/LXC .tar.zst CT template
# proxmox-vm Customize a Rocky/Fedora cloud image → QCOW2 (requires virt-customize)
# oci-archive Export OCI tarball via skopeo (for air-gapped import)
#
# Keycloak:
# Optionally generates a docker-compose.yml that includes the full
# FreeIPA + Keycloak + PostgreSQL constellation and a pre-configured
# keycloak-configure.sh for post-start LDAP federation setup.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGE_SRC="$SCRIPT_DIR/../../FreeipaAnsible/image"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; 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} $*"; }
section() { echo -e "\n${BLUE}━━━ $* ━━━${NC}"; }
ask() { printf "${MAGENTA}[?]${NC} %s " "$*"; }
[[ $EUID -eq 0 ]] && { error "Run as your normal user (not root)."; exit 1; }
# ─── Detect container engine ──────────────────────────────────────────────────
if command -v podman &>/dev/null; then
ENGINE="podman"
elif command -v docker &>/dev/null; then
ENGINE="docker"
else
error "Neither podman nor docker found. Install one first."
exit 1
fi
info "Container engine: $ENGINE"
# ─── dialog theme ─────────────────────────────────────────────────────────────
TMP_D="$(mktemp -d)"; trap 'rm -rf "$TMP_D"' EXIT
BACKTITLE="FreeIPA Image Builder"
export DIALOGRC="$TMP_D/dialogrc"
cat > "$DIALOGRC" <<'EOF'
use_shadow = ON
use_colors = ON
screen_color = (BLACK,BLACK,ON)
title_color = (MAGENTA,BLACK,ON)
border_color = (MAGENTA,BLACK,ON)
button_active_color = (BLACK,MAGENTA,ON)
button_inactive_color = (WHITE,BLACK,OFF)
menubox_color = (WHITE,BLACK,OFF)
menubox_border_color = (MAGENTA,BLACK,ON)
item_color = (WHITE,BLACK,OFF)
item_selected_color = (BLACK,MAGENTA,ON)
tag_color = (CYAN,BLACK,ON)
tag_selected_color = (BLACK,CYAN,ON)
check_color = (WHITE,BLACK,OFF)
check_selected_color = (BLACK,MAGENTA,ON)
uarrow_color = (MAGENTA,BLACK,ON)
darrow_color = (MAGENTA,BLACK,ON)
EOF
command -v dialog &>/dev/null || { sudo pacman -S --noconfirm dialog; }
# ─── Target chooser ───────────────────────────────────────────────────────────
TARGET=$(dialog --backtitle "$BACKTITLE" \
--title " Select Target Format " \
--menu "Choose the output format:" 16 70 4 \
"docker" "Docker / Podman image (local + optional registry push)" \
"lxc" "LXC / Proxmox CT template (.tar.zst rootfs archive)" \
"proxmox-vm" "Proxmox VM (cloud-init QCOW2, requires virt-customize)" \
"oci-archive" "OCI archive (skopeo tarball for air-gapped import)" \
3>&1 1>&2 2>&3) || { clear; echo "Aborted."; exit 0; }
# ─── Keycloak? ───────────────────────────────────────────────────────────────
WITH_KEYCLOAK=false
dialog --backtitle "$BACKTITLE" \
--title " Keycloak Integration " \
--yesno "\nInclude Keycloak in the output?\n\n\
Adds PostgreSQL + Keycloak to the compose stack and generates\n\
keycloak-configure.sh for LDAP federation post-start setup." 11 64 \
&& WITH_KEYCLOAK=true || true
# ─── FreeIPA config ───────────────────────────────────────────────────────────
section "FreeIPA configuration"
info "These values are embedded in the .env template and compose file."
info "Override them at runtime via environment variables."
echo
ask "IPA Domain (e.g. corp.example.com):"; read -r IPA_DOMAIN
[[ -z "$IPA_DOMAIN" ]] && { error "IPA_DOMAIN is required."; exit 1; }
GUESSED_HOSTNAME="ipa.$IPA_DOMAIN"
ask "Server FQDN [$GUESSED_HOSTNAME]:"; read -r I; IPA_HOSTNAME="${I:-$GUESSED_HOSTNAME}"
GUESSED_REALM="${IPA_DOMAIN^^}"
ask "Kerberos Realm [$GUESSED_REALM]:"; read -r I; IPA_REALM="${I:-$GUESSED_REALM}"
ask "Setup integrated DNS? [y/N]:"; read -r I
IPA_SETUP_DNS=false; [[ "${I,,}" == "y"* ]] && IPA_SETUP_DNS=true
IMAGE_TAG="freeipa-server:${IPA_DOMAIN//./-}"
ask "Image tag [$IMAGE_TAG]:"; read -r I; IMAGE_TAG="${I:-$IMAGE_TAG}"
# Output directory
DEFAULT_OUT="$HOME/freeipa-image-output"
ask "Output directory [$DEFAULT_OUT]:"; read -r I; OUTPUT_DIR="${I:-$DEFAULT_OUT}"
# Keycloak config (if selected)
KC_HOSTNAME="keycloak.$IPA_DOMAIN"
KC_REALM="${IPA_DOMAIN%%.*}"
if [[ "$WITH_KEYCLOAK" == true ]]; then
echo
ask "Keycloak hostname [$KC_HOSTNAME]:"; read -r I; KC_HOSTNAME="${I:-$KC_HOSTNAME}"
ask "Keycloak realm name [$KC_REALM]:"; read -r I; KC_REALM="${I:-$KC_REALM}"
fi
# Proxmox VM cloud image (only if proxmox-vm selected)
CLOUD_IMAGE_URL=""
if [[ "$TARGET" == "proxmox-vm" ]]; then
echo
ROCKY_URL="https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2"
ask "Cloud image URL [$ROCKY_URL]:"; read -r I; CLOUD_IMAGE_URL="${I:-$ROCKY_URL}"
fi
# ─── Confirm ──────────────────────────────────────────────────────────────────
echo
info "──────────────────────────────────────"
printf " Target: %s\n" "$TARGET"
printf " Domain: %s\n" "$IPA_DOMAIN"
printf " FQDN: %s\n" "$IPA_HOSTNAME"
printf " Realm: %s\n" "$IPA_REALM"
printf " Image tag: %s\n" "$IMAGE_TAG"
printf " Keycloak: %s\n" "$WITH_KEYCLOAK"
printf " Output: %s\n" "$OUTPUT_DIR"
info "──────────────────────────────────────"
echo
ask "Proceed? [y/N]:"; read -r CONFIRM
[[ "${CONFIRM,,}" != "y"* ]] && { echo "Aborted."; exit 0; }
mkdir -p "$OUTPUT_DIR"
# ─── Step 1: Always build the container image ─────────────────────────────────
section "Building container image ($ENGINE)"
$ENGINE build \
--tag "$IMAGE_TAG" \
--label "ipa.domain=$IPA_DOMAIN" \
--label "ipa.realm=$IPA_REALM" \
"$IMAGE_SRC"
log "Image built: $IMAGE_TAG"
# ─── Step 2: Target-specific export ──────────────────────────────────────────
case "$TARGET" in
docker)
section "Docker/Podman — local image ready"
log "Image '$IMAGE_TAG' is in the local $ENGINE store."
ask "Push to a registry? [y/N]:"; read -r I
if [[ "${I,,}" == "y"* ]]; then
ask "Registry image name (e.g. registry.example.com/freeipa-server:latest):"; read -r REG_TAG
if [[ -n "$REG_TAG" ]]; then
$ENGINE tag "$IMAGE_TAG" "$REG_TAG"
$ENGINE push "$REG_TAG"
log "Pushed: $REG_TAG"
fi
fi
;;
lxc)
section "LXC / Proxmox CT template"
LXC_ARCHIVE="$OUTPUT_DIR/freeipa-server-lxc.tar.zst"
info "Creating temporary container to export rootfs..."
TMP_CTR="freeipa-lxc-export-$$"
$ENGINE create --name "$TMP_CTR" "$IMAGE_TAG" /bin/true
info "Exporting rootfs (this may take a minute)..."
if command -v zstd &>/dev/null; then
$ENGINE export "$TMP_CTR" | zstd -T0 -o "$LXC_ARCHIVE"
else
warn "zstd not found — falling back to gzip (.tar.gz)"
LXC_ARCHIVE="${LXC_ARCHIVE%.tar.zst}.tar.gz"
$ENGINE export "$TMP_CTR" | gzip -9 > "$LXC_ARCHIVE"
fi
$ENGINE rm "$TMP_CTR" &>/dev/null
log "LXC template: $LXC_ARCHIVE"
# Proxmox import instructions
cat > "$OUTPUT_DIR/lxc-import-instructions.txt" <<LXCTXT
Proxmox LXC import instructions
================================
Upload template to Proxmox:
scp $LXC_ARCHIVE root@proxmox:/var/lib/vz/template/cache/
Create container (via Proxmox web UI or CLI):
pct create <VMID> local:vztmpl/$(basename "$LXC_ARCHIVE") \\
--hostname $IPA_HOSTNAME \\
--memory 2048 --cores 2 \\
--net0 name=eth0,bridge=vmbr0,ip=dhcp \\
--ostype fedora --unprivileged 0
NOTE: FreeIPA requires a privileged LXC container (unprivileged=0).
Add to /etc/pve/lxc/<VMID>.conf:
lxc.apparmor.profile: unconfined
lxc.cap.drop:
Set FreeIPA env vars before first start:
pct exec <VMID> -- bash -c 'cat >> /etc/environment <<EOF
IPA_DOMAIN=$IPA_DOMAIN
IPA_REALM=$IPA_REALM
IPA_ADMIN_PASSWORD=YourAdminPassword
IPA_DM_PASSWORD=YourDMPassword
EOF'
Start and watch first-boot:
pct start <VMID>
pct exec <VMID> -- journalctl -f -u ipa-first-boot
LXCTXT
log "Instructions: $OUTPUT_DIR/lxc-import-instructions.txt"
;;
proxmox-vm)
section "Proxmox VM — cloud-init QCOW2"
if ! command -v virt-customize &>/dev/null; then
error "virt-customize not found. Install libguestfs-tools:"
error " sudo pacman -S libguestfs # Arch"
error " sudo dnf install libguestfs-tools # Fedora/RHEL"
exit 1
fi
if ! command -v qemu-img &>/dev/null; then
error "qemu-img not found. Install qemu-img / qemu-tools."
exit 1
fi
CLOUD_IMG="$TMP_D/cloud-base.qcow2"
QCOW2_OUT="$OUTPUT_DIR/freeipa-server.qcow2"
info "Downloading cloud image..."
curl -L --progress-bar -o "$CLOUD_IMG" "$CLOUD_IMAGE_URL"
info "Customizing image (installing FreeIPA packages)..."
virt-customize -a "$CLOUD_IMG" \
--install freeipa-server,freeipa-server-dns,ansible-core,python3-netaddr \
--copy-in "$IMAGE_SRC/ipa-first-boot.sh":/usr/local/sbin/ \
--copy-in "$IMAGE_SRC/ipa-first-boot.service":/etc/systemd/system/ \
--run-command "chmod +x /usr/local/sbin/ipa-first-boot.sh" \
--run-command "systemctl enable ipa-first-boot.service" \
--selinux-relabel
qemu-img convert -O qcow2 "$CLOUD_IMG" "$QCOW2_OUT"
log "QCOW2: $QCOW2_OUT"
# Cloud-init snippet
cat > "$OUTPUT_DIR/cloud-init-user-data.yml" <<CIEOF
#cloud-config
# Attach this as cloud-init user-data when creating the Proxmox VM.
hostname: ${IPA_HOSTNAME%%.*}
fqdn: $IPA_HOSTNAME
manage_etc_hosts: true
write_files:
- path: /etc/environment
append: true
content: |
IPA_DOMAIN=$IPA_DOMAIN
IPA_REALM=$IPA_REALM
IPA_ADMIN_PASSWORD=ChangeMe123!
IPA_DM_PASSWORD=ChangeMe456!
runcmd:
- systemctl enable --now ipa-first-boot.service
CIEOF
log "Cloud-init snippet: $OUTPUT_DIR/cloud-init-user-data.yml"
info "Proxmox VM import:"
info " qm create <VMID> --name freeipa --memory 4096 --cores 4 --net0 virtio,bridge=vmbr0"
info " qm importdisk <VMID> $QCOW2_OUT local-lvm"
info " qm set <VMID> --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-<VMID>-disk-0"
info " qm set <VMID> --ide2 local-lvm:cloudinit --boot c --bootdisk scsi0"
;;
oci-archive)
section "OCI archive (skopeo)"
if ! command -v skopeo &>/dev/null; then
error "skopeo not found. Install: sudo pacman -S skopeo"
exit 1
fi
OCI_ARCHIVE="$OUTPUT_DIR/freeipa-server-oci.tar"
info "Exporting OCI archive..."
skopeo copy \
"${ENGINE}-daemon:${IMAGE_TAG}" \
"oci-archive:${OCI_ARCHIVE}:latest"
log "OCI archive: $OCI_ARCHIVE"
info "Import on air-gapped host:"
info " skopeo copy oci-archive:freeipa-server-oci.tar docker-daemon:freeipa-server:latest"
info " # or for podman:"
info " skopeo copy oci-archive:freeipa-server-oci.tar containers-storage:freeipa-server:latest"
;;
esac
# ─── Generate compose + .env for all targets ─────────────────────────────────
section "Generating deployment files → $OUTPUT_DIR"
cp "$IMAGE_SRC/docker-compose.yml" "$OUTPUT_DIR/docker-compose.yml"
cp "$IMAGE_SRC/keycloak-configure.sh" "$OUTPUT_DIR/keycloak-configure.sh"
chmod +x "$OUTPUT_DIR/keycloak-configure.sh"
# Patch compose to reference the built image tag instead of building locally
sed -i "s|image: freeipa-server:local|image: $IMAGE_TAG|" "$OUTPUT_DIR/docker-compose.yml"
if [[ "$WITH_KEYCLOAK" == false ]]; then
# Comment out Keycloak and postgres services
python3 - <<PYEOF "$OUTPUT_DIR/docker-compose.yml"
import sys, re
path = sys.argv[1]
with open(path) as f: content = f.read()
# Add a note but keep the file intact so it's easy to re-enable
header = "# Keycloak disabled at build time. Remove the '# ' prefixes below to enable.\n"
content = re.sub(r'(\n (postgres|keycloak):\n)', lambda m: '\n # ' + m.group(0).lstrip('\n'), content)
with open(path, 'w') as f: f.write(content)
PYEOF
fi
# Generate .env with current values as defaults
cat > "$OUTPUT_DIR/.env" <<ENVEOF
# Generated by freeipa-image-builder.sh — edit before use
IPA_HOSTNAME=$IPA_HOSTNAME
IPA_DOMAIN=$IPA_DOMAIN
IPA_REALM=$IPA_REALM
IPA_ADMIN_PASSWORD=ChangeMe123!
IPA_DM_PASSWORD=ChangeMe456!
IPA_SETUP_DNS=$IPA_SETUP_DNS
IPA_DNS_FORWARDER=
IPA_SETUP_KRA=false
KC_HOSTNAME=$KC_HOSTNAME
KC_REALM=$KC_REALM
KC_ADMIN=admin
KC_ADMIN_PASSWORD=ChangeMe789!
KC_DB_PASSWORD=ChangeMe000!
IPA_BIND_DN=cn=Directory Manager
IPA_BIND_PASSWORD=
IPA_USE_LDAPS=false
ENVEOF
log ".env (edit passwords before use)"
log "docker-compose.yml"
log "keycloak-configure.sh"
# ─── Final summary ────────────────────────────────────────────────────────────
section "Done"
cat <<EOF
Image: $IMAGE_TAG
Target: $TARGET
Output: $OUTPUT_DIR/
Quick start:
cd $OUTPUT_DIR
\$EDITOR .env # set real passwords
$ENGINE compose up -d$([ "$TARGET" == "docker" ] && echo "" || echo " freeipa")
$ENGINE compose logs -f freeipa # watch first-boot (~10 min)
$([ "$WITH_KEYCLOAK" == "true" ] && cat <<KC
# After FreeIPA is healthy:
KC_ADMIN_PASSWORD=<pw> IPA_SERVER=$IPA_HOSTNAME \\
IPA_DOMAIN=$IPA_DOMAIN IPA_DM_PASSWORD=<pw> \\
./keycloak-configure.sh
KC
)
EOF

View File

@ -130,6 +130,7 @@ count_steps() {
[[ "$a" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"python"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"zfs"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"zfs"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"wprs"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"wprs"* ]] && TOTAL=$(( TOTAL + 1 ))
@ -220,6 +221,7 @@ SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \
"cockpit" "Cockpit web UI · machines · podman" off \ "cockpit" "Cockpit web UI · machines · podman" off \
"ssh-server" "SSH server openssh · key-auth · enabled" off \ "ssh-server" "SSH server openssh · key-auth · enabled" off \
"freeipa-server" "FreeIPA Server interactive server setup + client gen" off \ "freeipa-server" "FreeIPA Server interactive server setup + client gen" off \
"freeipa-image" "FreeIPA Image OCI/LXC/Proxmox/VM builder + Keycloak" off \
"python" "Python tools pyright · pipx · pynvim" off \ "python" "Python tools pyright · pipx · pynvim" off \
"zfs" "ZFS zfs-dkms kernel module" off \ "zfs" "ZFS zfs-dkms kernel module" off \
"wprs" "WPRS wprs-git (AUR)" off \ "wprs" "WPRS wprs-git (AUR)" off \
@ -271,6 +273,7 @@ if [[ -n "$SELECTED_APPS" ]]; then
[[ "$SELECTED_APPS" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit web UI\n" [[ "$SELECTED_APPS" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit web UI\n"
[[ "$SELECTED_APPS" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server (openssh, key auth)\n" [[ "$SELECTED_APPS" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server (openssh, key auth)\n"
[[ "$SELECTED_APPS" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n" [[ "$SELECTED_APPS" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n"
[[ "$SELECTED_APPS" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n"
[[ "$SELECTED_APPS" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n" [[ "$SELECTED_APPS" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n"
[[ "$SELECTED_APPS" == *"zfs"* ]] && SUMMARY+=" ✦ ZFS\n" [[ "$SELECTED_APPS" == *"zfs"* ]] && SUMMARY+=" ✦ ZFS\n"
[[ "$SELECTED_APPS" == *"wprs"* ]] && SUMMARY+=" ✦ WPRS\n" [[ "$SELECTED_APPS" == *"wprs"* ]] && SUMMARY+=" ✦ WPRS\n"
@ -333,6 +336,7 @@ fi
[[ "$SELECTED_APPS" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh" [[ "$SELECTED_APPS" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh"
[[ "$SELECTED_APPS" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.sh" [[ "$SELECTED_APPS" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.sh"
[[ "$SELECTED_APPS" == *"freeipa-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh" [[ "$SELECTED_APPS" == *"freeipa-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh"
[[ "$SELECTED_APPS" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.sh"
[[ "$SELECTED_APPS" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh" [[ "$SELECTED_APPS" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh"
[[ "$SELECTED_APPS" == *"zfs"* ]] && run_module "ZFS" "$MODULES/optional-Modules/zfs.sh" [[ "$SELECTED_APPS" == *"zfs"* ]] && run_module "ZFS" "$MODULES/optional-Modules/zfs.sh"
[[ "$SELECTED_APPS" == *"wprs"* ]] && run_module "WPRS" "$MODULES/optional-Modules/wprs.sh" [[ "$SELECTED_APPS" == *"wprs"* ]] && run_module "WPRS" "$MODULES/optional-Modules/wprs.sh"