#!/bin/bash # ansipa-smb-setup.sh — configure Samba shares on the IPA container. # # Runs on every container start via ansipa-smb.service so that smb.conf and # Samba users are always in place after container restarts (ephemeral rootfs). # # Password sources (first match wins per variable): # 1. Environment variable (first boot / explicit override) # 2. /data/samba/ansipa-smb.env (persisted from first boot) # # Shares: # ansipa-scans — write-only for 'scanupload'; clients push scan results here. # ansipa-luks-keys — write-only for 'luks-upload' (Ansible controller); # read for members of the 'KeyAdmin' Linux group. # Add a Samba user to KeyAdmin to grant key-read access: # useradd -r -G KeyAdmin # smbpasswd -a # ansipa-policystore — read-write for 'smb-policystore'; admins in the IPA group # usr_smb_adm_policystore mount this to edit policy files live. # Password auto-generated and stored in the IPA group description: # cifs://:smb-policystore: # The enforcer on clients reads this credential to sync policy files. set -euo pipefail LOG_TAG="ansipa-smb-setup" SCAN_BASE="/data/scan-results" LUKS_BASE="/data/luks-keys" SHARES_BASE="/data/smb-shares" POLICY_STORE_BASE="/data/policy-store" SMB_CONF="/etc/samba/smb.conf" SMB_USER="scanupload" LUKS_UPLOAD_USER="luks-upload" POLICY_STORE_USER="smb-policystore" KEYADMIN_GROUP="KeyAdmin" ENV_FILE="/data/samba/ansipa-smb.env" log() { echo "[$LOG_TAG] $*"; } warn() { echo "[$LOG_TAG][WARN] $*" >&2; } die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; } # ── Resolve passwords ───────────────────────────────────────────────────────── SMB_PASS="${SMB_SCAN_PASSWORD:-}" LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-}" IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-}" if [[ -f "$ENV_FILE" ]]; then # shellcheck source=/dev/null source "$ENV_FILE" SMB_PASS="${SMB_SCAN_PASSWORD:-$SMB_PASS}" LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}" IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-$IPA_ADMIN_PASS}" fi [[ -z "$SMB_PASS" ]] && die "SMB_SCAN_PASSWORD not set and $ENV_FILE not present. Set it in .env." [[ -z "$LUKS_PASS" ]] && die "LUKS_KEY_UPLOAD_PASSWORD not set and $ENV_FILE not present. Set it in .env." # ── Persist for subsequent restarts ────────────────────────────────────────── mkdir -p "$(dirname "$ENV_FILE")" { printf 'SMB_SCAN_PASSWORD=%q\n' "$SMB_PASS" printf 'LUKS_KEY_UPLOAD_PASSWORD=%q\n' "$LUKS_PASS" [[ -n "$IPA_ADMIN_PASS" ]] && printf 'IPA_ADMIN_PASSWORD=%q\n' "$IPA_ADMIN_PASS" || true } > "$ENV_FILE" chmod 600 "$ENV_FILE" # ── Directory structure (idempotent) ────────────────────────────────────────── mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts" mkdir -p "$LUKS_BASE" mkdir -p "$SHARES_BASE" mkdir -p "$POLICY_STORE_BASE/policies.d" # ── KeyAdmin group ──────────────────────────────────────────────────────────── if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then groupadd -r "$KEYADMIN_GROUP" log "Created group: $KEYADMIN_GROUP" fi # ── System users ────────────────────────────────────────────────────────────── if ! id "$SMB_USER" &>/dev/null; then useradd -r -s /sbin/nologin -d "$SCAN_BASE" -M "$SMB_USER" log "Created system user: $SMB_USER" fi if ! id "$POLICY_STORE_USER" &>/dev/null; then useradd -r -s /sbin/nologin -d "$POLICY_STORE_BASE" -M "$POLICY_STORE_USER" log "Created system user: $POLICY_STORE_USER" fi if ! id "$LUKS_UPLOAD_USER" &>/dev/null; then useradd -r -s /sbin/nologin -d "$LUKS_BASE" -M -G "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER" log "Created system user: $LUKS_UPLOAD_USER (member of $KEYADMIN_GROUP)" else # Ensure group membership is correct after container recreations usermod -aG "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER" fi chown -R "$SMB_USER:$SMB_USER" "$SCAN_BASE" chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE" chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group chown -R "$POLICY_STORE_USER:$POLICY_STORE_USER" "$POLICY_STORE_BASE" chmod 2770 "$POLICY_STORE_BASE" # ── smb.conf (base only; _setup_user_shares appends stanzas afterwards) ─────── _write_smb_conf() { log "Writing $SMB_CONF" cat > "$SMB_CONF" </dev/null || \ printf '%s\n%s\n' "$pass" "$pass" | smbpasswd -s "$user" 2>/dev/null || \ log "WARN: smbpasswd returned non-zero for $user (may already be set correctly)" } # ── User-share management (usr_smb_r_ / usr_smb_rw_ IPA groups) ── # Each group maps to a Samba share under $SHARES_BASE//: # usr_smb_r_ → read-only share [usr--r] # usr_smb_rw_ → read-write share [usr--rw] # # Both access levels for the same share use one Linux user (smb-) and one # password; the difference is read-only vs read-write enforced by Samba. # Credentials are auto-generated on first setup and stored in the IPA group # description so clients can retrieve them: # cifs://:: # # The server kinits as admin to query/update IPA. If IPA is not yet configured # (first-boot race), this function exits gracefully; the next smb.service restart # will pick up the groups once IPA is ready. # Appends user share stanzas directly to SMB_CONF; logs go to stdout→journal. # Split from _write_smb_conf to avoid stdout capture swallowing log lines. _setup_user_shares() { # Require IPA to be configured on this host already. if [[ ! -f /etc/ipa/default.conf ]]; then warn "IPA not yet configured — skipping user share setup (restart after ipa-first-boot)" return 0 fi if [[ -z "$IPA_ADMIN_PASS" ]]; then warn "IPA_ADMIN_PASSWORD not set — skipping user share setup" return 0 fi local realm realm=$(awk -F'[[:space:]]*=[[:space:]]*' '/^realm[[:space:]]*=/{print $2; exit}' \ /etc/ipa/default.conf 2>/dev/null || echo "") # On the IPA server itself the 'server' field points to itself; prefer hostname -f. local ipa_host ipa_host=$(hostname -f 2>/dev/null || echo "ipa.test.local") echo "$IPA_ADMIN_PASS" | kinit "admin@${realm}" &>/dev/null || { warn "kinit admin@${realm} failed — skipping user share setup" return 0 } # Discover usr_smb_* groups. local -a smb_groups mapfile -t smb_groups < <( ipa group-find --pkey-only 2>/dev/null \ | awk '/Group name:/ {print $NF}' \ | grep "^usr_smb_" | sort -u || true ) if [[ ${#smb_groups[@]} -eq 0 ]]; then log "No usr_smb_* groups found — no user shares to configure" kdestroy &>/dev/null || true return 0 fi # Collect unique share names and track which access levels exist. declare -A _HAS_R _HAS_RW _DONE for _g in "${smb_groups[@]}"; do if [[ "$_g" =~ ^usr_smb_r_(.+)$ ]]; then _HAS_R["${BASH_REMATCH[1]}"]=1 elif [[ "$_g" =~ ^usr_smb_rw_(.+)$ ]]; then _HAS_RW["${BASH_REMATCH[1]}"]=1 fi done for NAME in "${!_HAS_R[@]}" "${!_HAS_RW[@]}"; do [[ -n "${_DONE[$NAME]+x}" ]] && continue _DONE["$NAME"]=1 local SUSER="smb-${NAME}" local SDIR="$SHARES_BASE/$NAME" log "User share: $NAME path=$SDIR user=$SUSER" # Create directory and Linux user. mkdir -p "$SDIR" if ! id "$SUSER" &>/dev/null; then useradd -r -s /sbin/nologin -d "$SDIR" -M "$SUSER" log " Created system user: $SUSER" fi chown "$SUSER:$SUSER" "$SDIR" chmod 2770 "$SDIR" # setgid — new files inherit group; no world access # Retrieve existing password from IPA group description or generate one. local PASS="" local _desc _grp for _grp in "usr_smb_r_${NAME}" "usr_smb_rw_${NAME}"; do ipa group-show "$_grp" &>/dev/null || continue _desc=$(ipa group-show "$_grp" --all 2>/dev/null \ | awk -F': ' '/Description:/{print $2; exit}' || true) # Description format: cifs://:: if [[ "$_desc" =~ ^cifs://[^:]*:smb-[^:]+:(.+)$ ]]; then PASS="${BASH_REMATCH[1]}" break fi done if [[ -z "$PASS" ]]; then PASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 32 \ || openssl rand -base64 24 | tr -d '/+=\n') log " Generated new password for $SUSER" fi # Set Samba password. _smb_set_pass "$SUSER" "$PASS" # Store credential in both group descriptions so clients can read it. local CRED="cifs://${ipa_host}:${SUSER}:${PASS}" if [[ -n "${_HAS_R[$NAME]+x}" ]]; then ipa group-mod "usr_smb_r_${NAME}" --desc="$CRED" &>/dev/null && \ log " Stored credential in usr_smb_r_${NAME}" || true fi if [[ -n "${_HAS_RW[$NAME]+x}" ]]; then ipa group-mod "usr_smb_rw_${NAME}" --desc="$CRED" &>/dev/null && \ log " Stored credential in usr_smb_rw_${NAME}" || true fi # Append share stanzas directly to smb.conf. # Clients mount [usr--r] or [usr--rw]; folder in ~ is just . if [[ -n "${_HAS_R[$NAME]+x}" ]]; then cat >> "$SMB_CONF" <> "$SMB_CONF" </dev/null || true } # ── Policystore share management (usr_smb_adm_policystore IPA user group) ──── # Creates the ansipa-policystore Samba share with auto-generated credentials. # Credential is stored in the IPA group description so clients can discover it: # cifs://:smb-policystore: # Uses the same kinit/kdestroy pattern as _setup_user_shares. _setup_policystore() { if [[ ! -f /etc/ipa/default.conf ]]; then warn "IPA not yet configured — skipping policystore setup (restart after ipa-first-boot)" return 0 fi if [[ -z "$IPA_ADMIN_PASS" ]]; then warn "IPA_ADMIN_PASSWORD not set — skipping policystore setup" return 0 fi local realm ipa_host realm=$(awk -F'[[:space:]]*=[[:space:]]*' '/^realm[[:space:]]*=/{print $2; exit}' \ /etc/ipa/default.conf 2>/dev/null || echo "") ipa_host=$(hostname -f 2>/dev/null || echo "ipa.test.local") echo "$IPA_ADMIN_PASS" | kinit "admin@${realm}" &>/dev/null || { warn "kinit admin@${realm} failed — skipping policystore setup" return 0 } # Retrieve existing password from the IPA group description or generate one. local PASS="" local _desc if ipa group-show usr_smb_adm_policystore &>/dev/null; then _desc=$(ipa group-show usr_smb_adm_policystore --all 2>/dev/null \ | awk -F': ' '/Description:/{print $2; exit}' || true) if [[ "$_desc" =~ ^cifs://[^:]*:smb-policystore:(.+)$ ]]; then PASS="${BASH_REMATCH[1]}" fi fi if [[ -z "$PASS" ]]; then PASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 32 \ || openssl rand -base64 24 | tr -d '/+=\n') log "Generated new password for $POLICY_STORE_USER" fi _smb_set_pass "$POLICY_STORE_USER" "$PASS" # Create the IPA group if absent, then store/update the credential in its description. ipa group-add usr_smb_adm_policystore \ --desc="cifs://${ipa_host}:${POLICY_STORE_USER}:${PASS}" 2>/dev/null \ || ipa group-mod usr_smb_adm_policystore \ --desc="cifs://${ipa_host}:${POLICY_STORE_USER}:${PASS}" 2>/dev/null \ || true log "Stored policystore credential in IPA group usr_smb_adm_policystore" unset PASS _desc kdestroy &>/dev/null || true } # ── Write smb.conf, set passwords, configure shares ────────────────────────── _smb_set_pass "$SMB_USER" "$SMB_PASS" _smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_PASS" # Write base smb.conf first, then append dynamic share stanzas. _write_smb_conf _setup_user_shares _setup_policystore # Reload smbd if it is already running (picks up new user shares without restart). smbcontrol smbd reload-config 2>/dev/null || true # ── Server-side scan checker cron (hourly, analysed on the IPA server itself) ─ # Always (re-)write: /etc/cron.d is on the ephemeral container layer and is # lost on container recreation, so we must restore it on every start. cat > /etc/cron.d/ansipa-check-scans <<'CRON' # ansipa: analyze client scan logs and write alerts — managed, do not edit. 0 * * * * root /usr/local/sbin/ansipa-check-scans.sh 2>&1 | logger -t ansipa-check-scans CRON chmod 644 /etc/cron.d/ansipa-check-scans log "Installed hourly scan-checker cron" log "Samba setup complete. Shares: ansipa-scans ($SMB_USER), ansipa-policystore ($POLICY_STORE_USER)"