#!/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 template (.tar.zst rootfs, generic LXC/LXD)" \ "proxmox-lxc" "Proxmox LXC CT template + conf + optional upload" \ "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 LXC extra config PVE_HOST=""; PVE_VMID="100"; PVE_STORAGE="local"; PVE_BRIDGE="vmbr0" PVE_MEMORY="4096"; PVE_CORES="4"; PVE_DISK_SIZE="20" if [[ "$TARGET" == "proxmox-lxc" ]]; then echo ask "Proxmox host (blank to skip upload):"; read -r PVE_HOST ask "Container ID [100]:"; read -r I; PVE_VMID="${I:-100}" ask "Storage for rootfs [local-lvm]:"; read -r I; PVE_STORAGE="${I:-local-lvm}" ask "Network bridge [vmbr0]:"; read -r I; PVE_BRIDGE="${I:-vmbr0}" ask "Memory MB [4096]:"; read -r I; PVE_MEMORY="${I:-4096}" ask "CPU cores [4]:"; read -r I; PVE_CORES="${I:-4}" ask "Disk size GB [20]:"; read -r I; PVE_DISK_SIZE="${I:-20}" 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" < 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/.conf: lxc.apparmor.profile: unconfined lxc.cap.drop: Set FreeIPA env vars before first start: pct exec -- bash -c 'cat >> /etc/environment < pct exec -- journalctl -f -u ipa-first-boot LXCTXT log "Instructions: $OUTPUT_DIR/lxc-import-instructions.txt" ;; proxmox-lxc) section "Proxmox LXC CT template" TEMPLATE_NAME="freeipa-server-proxmox-lxc.tar.zst" TEMPLATE_PATH="$OUTPUT_DIR/$TEMPLATE_NAME" info "Creating temporary container to export rootfs..." TMP_CTR="freeipa-pve-export-$$" $ENGINE create --name "$TMP_CTR" "$IMAGE_TAG" /bin/true info "Exporting rootfs..." if command -v zstd &>/dev/null; then $ENGINE export "$TMP_CTR" | zstd -T0 -o "$TEMPLATE_PATH" else warn "zstd not found — falling back to gzip (.tar.gz)" TEMPLATE_NAME="freeipa-server-proxmox-lxc.tar.gz" TEMPLATE_PATH="$OUTPUT_DIR/$TEMPLATE_NAME" $ENGINE export "$TMP_CTR" | gzip -9 > "$TEMPLATE_PATH" fi $ENGINE rm "$TMP_CTR" &>/dev/null log "Template: $TEMPLATE_PATH" # Generate Proxmox CT config file CT_CONF="$OUTPUT_DIR/pve-ct-${PVE_VMID}.conf" cat > "$CT_CONF" < "$OUTPUT_DIR/proxmox-lxc-setup.txt" <:/var/lib/vz/template/cache/ ── Step 2: Create the container ──────────────────────────── pct create $PVE_VMID local:vztmpl/$TEMPLATE_NAME \\ --hostname ${IPA_HOSTNAME%%.*} \\ --memory $PVE_MEMORY --cores $PVE_CORES \\ --rootfs $PVE_STORAGE:${PVE_DISK_SIZE} \\ --net0 name=eth0,bridge=$PVE_BRIDGE,ip=dhcp \\ --ostype fedora --unprivileged 0 \\ --features nesting=1 ── Step 3: Apply required LXC options ────────────────────── # FreeIPA needs unconfined AppArmor and full cgroup access. # Copy the generated config or append these lines: cat >> /etc/pve/lxc/$PVE_VMID.conf <> /etc/environment < \\ # IPA_DOMAIN=$IPA_DOMAIN IPA_DM_PASSWORD= \\ # ./keycloak-configure.sh GUIDEOF log "Setup guide: $OUTPUT_DIR/proxmox-lxc-setup.txt" # Optional: upload to Proxmox host if [[ -n "$PVE_HOST" ]]; then section "Uploading template to $PVE_HOST" scp "$TEMPLATE_PATH" "root@${PVE_HOST}:/var/lib/vz/template/cache/" scp "$CT_CONF" "root@${PVE_HOST}:/etc/pve/lxc/${PVE_VMID}.conf" log "Template uploaded to $PVE_HOST" info "Next: pct create $PVE_VMID local:vztmpl/$TEMPLATE_NAME ..." info " (see $OUTPUT_DIR/proxmox-lxc-setup.txt for full command)" fi ;; 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 - < "$OUTPUT_DIR/.env" < IPA_SERVER=$IPA_HOSTNAME \\ IPA_DOMAIN=$IPA_DOMAIN IPA_DM_PASSWORD= \\ ./keycloak-configure.sh KC ) EOF