fix(ansipa): full end-to-end test + multiple bug fixes

Fixes found during a complete archiso → FreeIPA DC → client enrollment →
ansipa policy deployment test cycle (Rocky 9 client VM, FreeIPA in Docker):

arch-autoinstall.sh:
- Add openssh to pacstrap — was missing, breaking headless/automated installs
- Enable sshd at boot and set PermitRootLogin yes for post-install access

ansipa-install-packages.sh:
- Fix broken uninstall logic: three always-true conditions caused every package
  removal to be silently skipped; replace with a state-file-based approach that
  tracks packages installed by ansipa and removes only those when the IPA group
  disappears

ansipa-enforce-policies.sh:
- Add usr_admin policy: creates /etc/sudoers.d/ansipa-usr-admin granting full
  sudo to the FreeIPA usr_admin user group on every enrolled host; reverted
  when the IPA group is deleted
- Add usr_prt_<printer> policy: auto-adds CUPS printers for FreeIPA user group
  members at login; printer URI stored in IPA group description; per-user ACL;
  reverted when group is deleted or membership ends

deploy-ansipa-policies.yml:
- Document usr_admin and usr_prt_* policies in header comment
- Install cups package on clients (required for printer policy)

FreeIPA container (image/):
- Add docker-env.service: extracts IPA_*/SMB_*/LUKS_*/KEYCLOAK_* env vars from
  /proc/1/environ into /etc/container.env on container start; services read from
  there rather than relying on PassEnvironment (which is lost on container restart)
- Add run.sh: wrapper that starts FreeIPA with --cgroupns host, required on cgroup
  v2 hosts (WSL2, recent Linux) because Docker Compose schema does not expose
  cgroupns_mode; also wires LUKS_KEY_UPLOAD_PASSWORD which the SMB service needs
- Dockerfile: copy + enable docker-env.service; add glibc-langpack-en for locale
- ansipa-smb.service: use EnvironmentFile=/etc/container.env (via docker-env.service)
  instead of PassEnvironment; add docker-env.service dependency
- ipa-first-boot.service: add docker-env.service dependency + EnvironmentFile
- ipa-first-boot.sh: add --skip-mem-check (container has limited cgroup memory
  visibility); remove --no-reverse from non-DNS path (was invalid without DNS)
- docker-compose.yml: add prominent cgroupns note explaining when to use run.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHops6PU4c2Mv5UyhUj8Ms
feat/astal-menu
Amir Alexander Abdelbaki 2026-07-01 10:30:06 +02:00
parent ae0ad3c4d7
commit 65e859b8f9
11 changed files with 405 additions and 32 deletions

View File

@ -461,7 +461,7 @@ echo "Detected GPU: ${GPU_INFO:-none}"
# GPU_PKGS is intentionally unquoted so it expands to multiple words (or nothing).
# shellcheck disable=SC2086
pacstrap -K /mnt base base-devel "$KERNEL" linux-firmware vim bash zsh git less btop fastfetch \
networkmanager grub cryptsetup libfido2 pam-u2f efibootmgr sudo btrfs-progs lvm2 jq $GPU_PKGS
networkmanager openssh grub cryptsetup libfido2 pam-u2f efibootmgr sudo btrfs-progs lvm2 jq $GPU_PKGS
############################################
# FSTAB
@ -511,9 +511,15 @@ ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime
hwclock --systohc
echo "$HOSTNAME" > /etc/hostname
# NetworkManager
# NetworkManager + SSH
# Enable at boot so the system has networking on first login without manual setup.
systemctl enable NetworkManager
# Enable SSH server so the machine is reachable headlessly after install.
systemctl enable sshd
# Allow root login via password for initial automated post-install steps;
# FreeIPA enrollment and ansipa deploy lock this down afterwards.
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
echo "root:${USERPASS}" | chpasswd
# Populate /etc/skel before user creation so useradd -m copies everything
echo "Cloning dotfiles into /etc/skel..."

View File

@ -23,12 +23,25 @@
# the drop-in on the next run.
#
# User-group naming conventions (per-user policies — follow the user across devices):
# usr_admin Members get full sudo on every enrolled host via a sudoers
# drop-in (/etc/sudoers.d/ansipa-usr-admin). Applied while the
# FreeIPA user group exists; drop-in is removed when the group is
# deleted. Uses SSSD so group membership resolves without local
# accounts.
# usr_block-binary-<name> Prevent members of this FreeIPA user group from running <name>
# on any enrolled host. Use __ in place of . to support Flatpak
# app IDs (e.g. usr_block-binary-org__gimp__Gimp blocks the
# Flatpak org.gimp.Gimp). Enforced via a PATH-priority wrapper
# that checks group membership at runtime via SSSD/id(1).
# Removing the user group from FreeIPA reverts the wrapper.
# usr_prt_<printer> Auto-add <printer> to CUPS on any enrolled host where a member
# is logged in. The printer URI is stored in the IPA group
# description (e.g. ipps://host/printers/name). Access is
# per-user only (allow:<username>); each new member who logs in
# gets added to the allow list. When the IPA group is deleted
# or all members leave, the printer is removed from CUPS on that
# host. A profile.d snippet + NOPASSWD sudo rule trigger the
# enforcer at login for immediate setup.
# usr_scan-notify Members receive scan alert notifications on every enrolled device
# they log into. The fetch-alerts timer is installed fleet-wide
# when the group exists; the notification daemon starts on login
@ -72,6 +85,9 @@ WANT_TIMESHIFT_BACKUP=false
WANT_SECURITY_SCAN=false
WANT_SCAN_NOTIFY=false
WANT_NO_LOCAL_USERS=false
WANT_USR_ADMIN=false
ACTIVE_PRT_GROUPS=()
ACTIVE_PRT_PRINTERS=()
if [[ -n "$RAW_GROUPS" ]]; then
while IFS=',' read -ra GRP_ARRAY; do
@ -107,6 +123,26 @@ while IFS= read -r _grp; do
done <<< "$_BLOCK_LIST"
unset _BLOCK_LIST _grp _raw
# ── Fetch user-group-based admin policy from FreeIPA ─────────────────────────
# usr_admin is a FreeIPA *user* group. When it exists, a sudoers drop-in grants
# full sudo to the group on every enrolled host (resolved via SSSD/NSS).
if ipa group-show usr_admin >/dev/null 2>&1; then
WANT_USR_ADMIN=true
fi
# ── Fetch user-group-based printer policies from FreeIPA ─────────────────────
# usr_prt_<printer> groups grant CUPS access to <printer> for members.
_PRT_LIST=$(ipa group-find --pkey-only 2>/dev/null \
| awk '/Group name:/ {print $NF}' \
| grep "^usr_prt_" | sort -u || true)
while IFS= read -r _pgrp; do
[[ -z "$_pgrp" ]] && continue
ACTIVE_PRT_GROUPS+=("$_pgrp")
ACTIVE_PRT_PRINTERS+=("${_pgrp#usr_prt_}")
done <<< "$_PRT_LIST"
unset _PRT_LIST _pgrp
# ── Fetch user-group-based scan-notify policy from FreeIPA ────────────────────
# usr_scan-notify is a FreeIPA *user* group — membership follows the user to
# every enrolled host. Install the fetch-alerts timer on any device where the
@ -119,7 +155,9 @@ log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \
"| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
"| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN" \
"| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}"
log "User policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
log "User policies — admin: $WANT_USR_ADMIN" \
"| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
"| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \
"| scan-notify: $WANT_SCAN_NOTIFY"
# ── Helpers ───────────────────────────────────────────────────────────────────
@ -543,4 +581,194 @@ else
fi
fi
# ── usr_admin: global sudo for FreeIPA admin group ───────────────────────────
# When the FreeIPA user group usr_admin exists, install a sudoers drop-in that
# grants full sudo to that group (resolved via SSSD/NSS) on this host.
# The drop-in is removed when the group is deleted from FreeIPA.
USR_ADMIN_DROPIN="/etc/sudoers.d/ansipa-usr-admin"
USR_ADMIN_STATE="$STATE_DIR/usr-admin"
if [[ "$WANT_USR_ADMIN" == true ]]; then
if [[ ! -f "$USR_ADMIN_DROPIN" ]]; then
log "Installing usr_admin sudoers drop-in"
printf '%%usr_admin ALL=(ALL:ALL) ALL\n' > "$USR_ADMIN_DROPIN"
chmod 440 "$USR_ADMIN_DROPIN"
fi
touch "$USR_ADMIN_STATE"
else
if [[ -f "$USR_ADMIN_STATE" ]] || [[ -f "$USR_ADMIN_DROPIN" ]]; then
rm -f "$USR_ADMIN_DROPIN" "$USR_ADMIN_STATE"
log "Removed usr_admin sudoers drop-in (IPA group no longer exists)"
fi
fi
# ── usr_prt_<printer>: per-user printer auto-add via FreeIPA groups ───────────
# For each usr_prt_<printer> group in FreeIPA:
# - The group description must contain the printer URI
# (e.g. "ipps://printserver.corp.example.com/printers/hp-office")
# - When a logged-in user is a member of the group, the printer is added to
# CUPS on this host and restricted to that user (allow:<user>).
# - Multiple users in the group can each gain access on the same host.
# - When a user leaves the group their allow entry is revoked; when no users
# remain the printer is removed from CUPS on this host entirely.
# The profile.d snippet /etc/profile.d/ansipa-printers.sh triggers this enforcer
# at login so printers appear immediately rather than waiting for the 30-min tick.
#
# To register a printer in FreeIPA:
# ipa group-mod usr_prt_hp-office \
# --desc='ipps://printserver.corp.example.com/printers/hp-office'
PRT_STATE_DIR="$STATE_DIR/printer-users"
PRT_DEFS_DIR="$STATE_DIR/printer-defs"
PRT_PROFILED="/etc/profile.d/ansipa-printers.sh"
PRT_SUDO_DROPIN="/etc/sudoers.d/ansipa-printers-trigger"
mkdir -p "$PRT_STATE_DIR" "$PRT_DEFS_DIR"
# Read/write per-printer authorized-user state files (<printer>.users)
_prt_read_users() {
local sf="$PRT_STATE_DIR/${1}.users"
[[ -f "$sf" ]] && grep -v '^$' "$sf" | sort -u || true
}
_prt_write_users() {
local printer="$1"; shift
if [[ $# -gt 0 ]]; then
printf '%s\n' "$@" | sort -u > "$PRT_STATE_DIR/${printer}.users"
else
> "$PRT_STATE_DIR/${printer}.users"
fi
}
# Apply the full allow list from state (or remove printer if list is empty)
_prt_apply_acl() {
local printer="$1"
local -a users
mapfile -t users < <(_prt_read_users "$printer")
if [[ ${#users[@]} -gt 0 ]]; then
local allow_list
allow_list=$(IFS=','; echo "${users[*]}")
lpadmin -p "$printer" -u "allow:${allow_list}" 2>/dev/null \
|| warn "lpadmin ACL update failed for printer $printer"
else
lpadmin -x "$printer" 2>/dev/null \
&& log "Removed printer $printer (no authorized users remaining)" \
|| warn "lpadmin -x failed for printer $printer"
rm -f "$PRT_STATE_DIR/${printer}.users"
fi
}
_prt_is_active() {
local needle="$1"
for _ap in "${ACTIVE_PRT_PRINTERS[@]+"${ACTIVE_PRT_PRINTERS[@]}"}"; do
[[ "$_ap" == "$needle" ]] && return 0
done
return 1
}
if command -v lpadmin &>/dev/null; then
# ── Process each active usr_prt_* group ──────────────────────────────────
for _i in "${!ACTIVE_PRT_PRINTERS[@]}"; do
_PRT="${ACTIVE_PRT_PRINTERS[$_i]}"
_GRP="${ACTIVE_PRT_GROUPS[$_i]}"
# Fetch URI from IPA group description (must start with a scheme://)
_URI=$(ipa group-show "$_GRP" --all 2>/dev/null \
| awk -F': ' '/Description:/{print $2; exit}' \
| grep -E '^[a-z][a-z+.-]+://' || true)
if [[ -z "$_URI" ]]; then
warn "Group $_GRP has no printer URI in its description — set it with:"
warn " ipa group-mod $_GRP --desc='ipps://printserver/printers/$_PRT'"
continue
fi
echo "$_URI" > "$PRT_DEFS_DIR/${_GRP}.uri"
mapfile -t _CUR_USERS < <(_prt_read_users "$_PRT")
_UPDATED=false
# Check logged-in users for new members to grant access to
while IFS= read -r _LUSER; do
[[ -z "$_LUSER" ]] && continue
id -Gn "$_LUSER" 2>/dev/null | tr ' ' '\n' | grep -qxF "$_GRP" || continue
printf '%s\n' "${_CUR_USERS[@]+"${_CUR_USERS[@]}"}" | grep -qxF "$_LUSER" && continue
# Add the printer to CUPS if not present yet
if ! lpstat -p "$_PRT" &>/dev/null; then
log "Adding printer $_PRT (URI: $_URI)"
lpadmin -p "$_PRT" -v "$_URI" -m everywhere -E 2>/dev/null \
|| lpadmin -p "$_PRT" -v "$_URI" -E 2>/dev/null \
|| { warn "lpadmin failed to add printer $_PRT"; continue; }
fi
log "Granting printer $_PRT access to $_LUSER"
_CUR_USERS+=("$_LUSER")
_UPDATED=true
done < <(who -u 2>/dev/null | awk '{print $1}' | sort -u)
# Revoke users who left the group
_VALID_USERS=()
for _U in "${_CUR_USERS[@]+"${_CUR_USERS[@]}"}"; do
if id -Gn "$_U" 2>/dev/null | tr ' ' '\n' | grep -qxF "$_GRP"; then
_VALID_USERS+=("$_U")
else
log "Revoking printer $_PRT access for $_U (no longer in $_GRP)"
_UPDATED=true
fi
done
if [[ "$_UPDATED" == true ]]; then
_prt_write_users "$_PRT" "${_VALID_USERS[@]+"${_VALID_USERS[@]}"}"
_prt_apply_acl "$_PRT"
fi
done
unset _i _PRT _GRP _URI _LUSER _U _CUR_USERS _VALID_USERS _UPDATED _ap
# ── Remove printers whose IPA group was deleted ───────────────────────────
shopt -s nullglob
for _sf in "$PRT_STATE_DIR"/*.users; do
[[ -f "$_sf" ]] || continue
_PRT=$(basename "$_sf" .users)
_prt_is_active "$_PRT" && continue
log "Removing printer $_PRT (IPA group usr_prt_$_PRT deleted)"
lpadmin -x "$_PRT" 2>/dev/null || true
rm -f "$_sf"
done
shopt -u nullglob
unset _sf _PRT
# ── profile.d login trigger ───────────────────────────────────────────────
# Installs a profile.d snippet + NOPASSWD sudo rule so that when a user
# logs in, the enforcer runs immediately and adds their printers without
# waiting for the next 30-minute timer tick.
if [[ ${#ACTIVE_PRT_PRINTERS[@]} -gt 0 ]]; then
if [[ ! -f "$PRT_SUDO_DROPIN" ]]; then
log "Installing ansipa-printers sudo trigger rule"
printf 'ALL ALL=(root) NOPASSWD: /usr/bin/systemctl start ansipa-enforce-policies.service\n' \
> "$PRT_SUDO_DROPIN"
chmod 440 "$PRT_SUDO_DROPIN"
fi
if [[ ! -f "$PRT_PROFILED" ]]; then
log "Installing /etc/profile.d/ansipa-printers.sh"
cat > "$PRT_PROFILED" <<'PROFILED'
# ansipa-printers: trigger policy enforcer at login to auto-add printers.
# Managed by ansipa-enforce-policies — do not edit manually.
sudo -n /usr/bin/systemctl start ansipa-enforce-policies.service 2>/dev/null &
disown 2>/dev/null || true
PROFILED
chmod 644 "$PRT_PROFILED"
fi
else
if [[ -f "$PRT_SUDO_DROPIN" ]]; then
rm -f "$PRT_SUDO_DROPIN"
log "Removed ansipa-printers sudo trigger (no active printer groups)"
fi
if [[ -f "$PRT_PROFILED" ]]; then
rm -f "$PRT_PROFILED"
log "Removed /etc/profile.d/ansipa-printers.sh (no active printer groups)"
fi
fi
else
[[ ${#ACTIVE_PRT_PRINTERS[@]} -gt 0 ]] && \
warn "lpadmin not found — install cups to enforce printer policies"
fi
unset _ap
log "Policy enforcement complete."

View File

@ -2,6 +2,8 @@
set -e
PREFIX="dev_pkg_"
STATE_FILE="/var/lib/ansipa-packages/installed.list"
mkdir -p "$(dirname "$STATE_FILE")"
# Detect distro
if [ -f /etc/os-release ]; then
@ -62,7 +64,7 @@ remove_pkg() {
# -----------------------------
# Get FreeIPA install groups
# -----------------------------
IPA_GROUPS=$(ipa group-find --pkey-only | awk '{print $1}' | grep "^$PREFIX" || true)
IPA_GROUPS=$(ipa group-find --pkey-only | awk '/Group name:/{print $NF}' | grep "^$PREFIX" || true)
# Extract package names from groups
DESIRED_PKGS=()
@ -93,36 +95,40 @@ case "$DISTRO" in
esac
# -----------------------------
# Install missing packages
# Install missing packages and record them
# -----------------------------
[[ -f "$STATE_FILE" ]] || touch "$STATE_FILE"
for PKG in "${DESIRED_PKGS[@]}"; do
if ! echo "$INSTALLED_PKGS" | grep -qx "$PKG"; then
echo "[INFO] Installing package: $PKG"
install_pkg "$PKG"
fi
# Record that ansipa manages this package
grep -qxF "$PKG" "$STATE_FILE" || echo "$PKG" >> "$STATE_FILE"
done
# -----------------------------
# Remove packages no longer desired
# Only remove packages previously installed by ansipa (tracked in state file).
# -----------------------------
for PKG in $INSTALLED_PKGS; do
if [[ "$PKG" =~ ^.*$ ]]; then
if [[ "$PKG" == "$PREFIX"* ]]; then
continue
fi
fi
if echo "$PKG" | grep -q "^"; then
while IFS= read -r PKG; do
[[ -z "$PKG" ]] && continue
# Still desired? Leave it alone.
if printf '%s\n' "${DESIRED_PKGS[@]+"${DESIRED_PKGS[@]}"}" | grep -qxF "$PKG"; then
continue
fi
echo "[INFO] Removing package no longer required by any IPA group: $PKG"
remove_pkg "$PKG"
# Remove from state file
grep -vxF "$PKG" "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
done < "$STATE_FILE"
if [[ " ${DESIRED_PKGS[*]} " != *" $PKG "* ]]; then
# Only remove packages that were installed via ansipa-install
if ipa group-find --pkey-only | grep -q "^$PREFIX$PKG$"; then
continue
fi
echo "[INFO] Removing package no longer required: $PKG"
remove_pkg "$PKG"
fi
done
# Persist current desired list (union of what's already tracked + newly added)
if [[ ${#DESIRED_PKGS[@]} -gt 0 ]]; then
printf '%s\n' "${DESIRED_PKGS[@]}" | sort -u > "${STATE_FILE}.tmp"
# Merge: keep previously-managed packages that are still desired
sort -u "$STATE_FILE" "${STATE_FILE}.tmp" > "${STATE_FILE}.merged"
comm -12 "${STATE_FILE}.merged" "${STATE_FILE}.tmp" > "$STATE_FILE"
rm -f "${STATE_FILE}.tmp" "${STATE_FILE}.merged"
fi

View File

@ -11,7 +11,9 @@
# dev_local-sudo-<username> Grant <username> local sudo on this device; reverted when host leaves
#
# User policies (FreeIPA user groups — follow the user across all enrolled devices):
# usr_admin Grant full sudo on every enrolled host (sudoers drop-in, SSSD-resolved)
# usr_block-binary-<name> Block execution of <name> via a PATH-priority wrapper
# usr_prt_<printer> Auto-add printer at login (per-user); URI in IPA group description
# usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged
#
# Prerequisites:
@ -34,12 +36,13 @@
tasks:
- name: Install samba-client (required for scan upload and alert fetch)
- name: Install required packages
package:
name: "{{ item }}"
state: present
loop:
- samba-client
- cups
ignore_errors: yes
- name: Deploy SMB credentials file

View File

@ -24,6 +24,8 @@ ENV container=docker \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
RUN dnf install -y glibc-langpack-en
RUN dnf install -y --setopt=install_weak_deps=False \
freeipa-server \
freeipa-server-dns \
@ -57,6 +59,7 @@ RUN systemctl mask \
network.service \
NetworkManager.service
COPY docker-env.service /etc/systemd/system/docker-env.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
COPY ansipa-smb-setup.sh /usr/local/sbin/ansipa-smb-setup.sh
@ -65,6 +68,7 @@ COPY ansipa-check-scans.sh /usr/local/sbin/ansipa-check-scans.sh
RUN chmod +x /usr/local/sbin/ipa-first-boot.sh \
&& chmod +x /usr/local/sbin/ansipa-smb-setup.sh \
&& chmod +x /usr/local/sbin/ansipa-check-scans.sh \
&& systemctl enable docker-env.service \
&& systemctl enable ipa-first-boot.service \
&& systemctl enable ansipa-smb.service \
&& systemctl enable smb.service nmb.service crond.service

View File

@ -2,14 +2,15 @@
Description=Ansipa Scan Results SMB Share Setup
# Run before smb so smb.conf and the Samba user exist when smbd starts.
Before=smb.service
After=network.target
After=network.target docker-env.service
Wants=docker-env.service
[Service]
Type=oneshot
RemainAfterExit=yes
# SMB_SCAN_PASSWORD comes from the container environment on first boot.
# On subsequent restarts it is read from /data/samba/ansipa-smb.env by the script.
PassEnvironment=SMB_SCAN_PASSWORD
# Passwords come from /etc/container.env (written by docker-env.service from Docker -e flags).
# On subsequent restarts they are read from /data/samba/ansipa-smb.env by the script.
EnvironmentFile=/etc/container.env
ExecStart=/usr/local/sbin/ansipa-smb-setup.sh
StandardOutput=journal
StandardError=journal

View File

@ -12,6 +12,18 @@
#
# To scale out (clients enroll against the same IPA server):
# docker compose --profile clients up
#
# ── cgroupns NOTE ────────────────────────────────────────────────────────────
# On hosts using cgroup v2 (WSL2, recent Linux with unified hierarchy) the
# FreeIPA container runs /sbin/init (systemd) and exits immediately with code
# 255 unless the container shares the host cgroup namespace. Docker Compose
# does not yet expose a cgroupns_mode property in its schema, so use the
# provided run.sh wrapper instead of plain `docker compose up` on those hosts:
#
# ./run.sh — starts freeipa only (cgroupns host applied automatically)
# ./run.sh all — starts freeipa + postgres + keycloak
#
# On plain cgroup v1 hosts `docker compose up -d` works without run.sh.
volumes:
freeipa-data:

View File

@ -0,0 +1,12 @@
[Unit]
Description=Export Docker container env vars for systemd services
DefaultDependencies=no
Before=basic.target ipa-first-boot.service ansipa-smb.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/bash -c "tr '\\0' '\\n' < /proc/1/environ | grep -E '^(IPA|SMB|LUKS|KEYCLOAK)_' > /etc/container.env; chmod 600 /etc/container.env"
[Install]
WantedBy=sysinit.target

View File

@ -1,11 +1,12 @@
[Unit]
Description=FreeIPA Server First-Boot Configuration
After=network-online.target dirsrv.target
Wants=network-online.target
After=network-online.target dirsrv.target docker-env.service
Wants=network-online.target docker-env.service
ConditionPathExists=!/etc/ipa/default.conf
[Service]
Type=oneshot
EnvironmentFile=/etc/container.env
ExecStart=/usr/local/sbin/ipa-first-boot.sh
RemainAfterExit=yes
StandardOutput=journal+console

View File

@ -47,6 +47,7 @@ ARGS=(
--ip-address="$(hostname -I | awk '{print $1}')"
--mkhomedir
--unattended
--skip-mem-check
)
if [[ "$IPA_SETUP_DNS" == "true" ]]; then
@ -55,8 +56,6 @@ if [[ "$IPA_SETUP_DNS" == "true" ]]; then
&& 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)

View File

@ -0,0 +1,101 @@
#!/usr/bin/env bash
# run.sh — Start the FreeIPA stack with correct cgroup namespace on any host.
#
# Usage:
# ./run.sh # freeipa only
# ./run.sh all # freeipa + postgres + keycloak
# ./run.sh down # stop and remove containers
#
# Why this exists:
# The FreeIPA container runs systemd (/sbin/init) and requires the host cgroup
# namespace on systems that use cgroup v2 (WSL2, recent Fedora/Ubuntu, etc.).
# Docker Compose's schema does not expose a cgroupns_mode field, so this script
# starts the freeipa container via `docker run --cgroupns host` and defers the
# rest of the stack to `docker compose up`.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
[[ -f .env ]] || { echo "ERROR: .env not found. Copy .env.example and fill in values."; exit 1; }
# shellcheck disable=SC1091
source .env
MODE="${1:-}"
stop_freeipa() {
docker rm -f freeipa 2>/dev/null || true
}
start_freeipa() {
stop_freeipa
# Ensure the compose-managed network exists before docker run uses it
docker compose up --no-start freeipa 2>/dev/null || true
NETWORK=$(docker network ls --filter name=ipa-net --format '{{.Name}}' | grep ipa-net | head -1)
if [[ -z "$NETWORK" ]]; then
docker network create --subnet=172.30.0.0/24 ipa-net
NETWORK=ipa-net
fi
# Ensure the data volume exists
docker volume create freeipa-data 2>/dev/null || true
docker run -d \
--name freeipa \
--hostname "${IPA_HOSTNAME:-ipa.example.com}" \
--privileged \
--cgroupns host \
--tmpfs /run \
--tmpfs /tmp \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
-v freeipa-data:/data \
--network "$NETWORK" \
--ip 172.30.0.10 \
-e IPA_DOMAIN="${IPA_DOMAIN:?}" \
-e IPA_REALM="${IPA_REALM:-}" \
-e IPA_ADMIN_PASSWORD="${IPA_ADMIN_PASSWORD:?}" \
-e IPA_DM_PASSWORD="${IPA_DM_PASSWORD:?}" \
-e IPA_SETUP_DNS="${IPA_SETUP_DNS:-false}" \
-e IPA_DNS_FORWARDER="${IPA_DNS_FORWARDER:-}" \
-e IPA_SETUP_KRA="${IPA_SETUP_KRA:-false}" \
-e SMB_SCAN_PASSWORD="${SMB_SCAN_PASSWORD:?}" \
-e LUKS_KEY_UPLOAD_PASSWORD="${LUKS_KEY_UPLOAD_PASSWORD:?}" \
-p 389:389 \
-p 636:636 \
-p 88:88 \
-p 88:88/udp \
-p 464:464 \
-p 464:464/udp \
-p 443:443 \
-p 445:445 \
-p 139:139 \
-p 137:137/udp \
-p 138:138/udp \
freeipa-server:local
echo "FreeIPA container started. Watch first-boot progress:"
echo " docker exec freeipa journalctl -f -u ipa-first-boot.service"
echo " (first-boot takes ~10 min)"
}
case "$MODE" in
down)
stop_freeipa
docker compose down
echo "Stack stopped."
;;
all)
start_freeipa
docker compose up -d postgres keycloak
echo "Full stack started (freeipa + postgres + keycloak)."
;;
"")
start_freeipa
;;
*)
echo "Usage: $0 [all|down]"
exit 1
;;
esac