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
parent
7279a781b0
commit
f66775ce54
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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) ==="
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue