From 76c3c2c1c5f69d9023100774aebfee466d066f57 Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 1 Jul 2026 13:52:26 +0200 Subject: [PATCH] feat(ansipa): add usr_smb_* and dev_ssh_* policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit usr_smb_r_ / usr_smb_rw_ (user groups): - Server creates /data/smb-shares// with smb- system user, setgid directory, and Samba share stanzas [usr--r/rw] on startup - Auto-generates credentials and stores them in IPA group descriptions (cifs://::) so no out-of-band secret distribution - Client mounts //ipa/ at ~/‹name› for logged-in members; rw membership wins over r when user is in both groups - Revert: unmounts and rmdir the mount point on group leave dev_ssh_ (host group): - Reads IPA user's SSH public keys (ipasshpubkey / --all output) - Writes them into /home//.ssh/authorized_keys in an ansipa-managed section (preserves manually-added keys) - Home dir is created if it doesn't exist (user may not have logged in yet) - Revert: removes only the ansipa-managed section on group leave - Enables same-key SSH access from any registered device to enrolled hosts Also: add cifs-utils + openssh-server to Ansible package list Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DHops6PU4c2Mv5UyhUj8Ms --- .../ansible/ansipa-enforce-policies.sh | 292 +++++++++++++++++- .../ansible/deploy-ansipa-policies.yml | 8 + .../FreeipaAnsible/image/ansipa-smb-setup.sh | 169 +++++++++- 3 files changed, 465 insertions(+), 4 deletions(-) diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 77ca638..7aaa2a1 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -81,6 +81,7 @@ RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \ ACTIVE_DAEMON_ENABLE=() ACTIVE_DAEMON_DISABLE=() ACTIVE_LOCAL_SUDO_USERS=() +ACTIVE_SSH_USERS=() WANT_TIMESHIFT_BACKUP=false WANT_SECURITY_SCAN=false WANT_SCAN_NOTIFY=false @@ -100,6 +101,7 @@ if [[ -n "$RAW_GROUPS" ]]; then dev_security-scan) WANT_SECURITY_SCAN=true ;; dev_no-local-users) WANT_NO_LOCAL_USERS=true ;; dev_local-sudo-*) ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;; + dev_ssh_*) ACTIVE_SSH_USERS+=("${g#dev_ssh_}") ;; esac done done <<< "$RAW_GROUPS" @@ -154,7 +156,8 @@ fi 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}" + "| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}" \ + "| ssh-keys: ${ACTIVE_SSH_USERS[*]:-none}" log "User policies — admin: $WANT_USR_ADMIN" \ "| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \ "| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \ @@ -771,4 +774,291 @@ else fi unset _ap +# ── usr_smb__: auto-mount network shares in user home dirs ──────── +# For each usr_smb_r_ or usr_smb_rw_ group in FreeIPA: +# - The FreeIPA container hosts a Samba share [usr--r] or [usr--rw] +# under /data/smb-shares// with UNIX group permissions. +# - When a logged-in user is a member of the group the share is mounted at +# ~/‹name› (a new folder named after the share, no r/rw suffix). +# - rw membership takes precedence over r if the user is in both groups for +# the same share name. +# - The mount is tracked in the state file; leaving the group unmounts it. +# +# Credential string (server, Samba user, password) is stored in the IPA group +# description by ansipa-smb-setup.sh: cifs://:: +# +# Requires: cifs-utils installed on the client. + +SMB_MOUNT_STATE="$STATE_DIR/smb-mounts" +[[ -f "$SMB_MOUNT_STATE" ]] || touch "$SMB_MOUNT_STATE" +SMB_CREDS_DIR="$STATE_DIR/smb-creds" +mkdir -p "$SMB_CREDS_DIR" +chmod 700 "$SMB_CREDS_DIR" + +if command -v mount.cifs &>/dev/null; then + + # ── Discover usr_smb_* groups from IPA ─────────────────────────────────── + declare -A _SMB_R_GRP _SMB_RW_GRP # share-name → IPA group name + + while IFS= read -r _g; do + [[ -z "$_g" ]] && continue + if [[ "$_g" =~ ^usr_smb_r_(.+)$ ]]; then _SMB_R_GRP["${BASH_REMATCH[1]}"]="$_g" + elif [[ "$_g" =~ ^usr_smb_rw_(.+)$ ]]; then _SMB_RW_GRP["${BASH_REMATCH[1]}"]="$_g" + fi + done < <(ipa group-find --pkey-only 2>/dev/null \ + | awk '/Group name:/ {print $NF}' \ + | grep "^usr_smb_" | sort -u || true) + unset _g + + # Union of r + rw share names. + declare -A _SMB_NAMES + for _n in "${!_SMB_R_GRP[@]}" "${!_SMB_RW_GRP[@]}"; do _SMB_NAMES["$_n"]=1; done + unset _n + + # ── Helpers ─────────────────────────────────────────────────────────────── + # Outputs "host samba_user password" when description contains a cifs:// credential. + _smb_parse_cred() { + local _desc + _desc=$(ipa group-show "$1" --all 2>/dev/null \ + | awk -F': ' '/Description:/{print $2; exit}' || true) + [[ "$_desc" =~ ^cifs://([^:]+):([^:]+):(.+)$ ]] && \ + echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}" + } + _smb_member() { id -nG "$1" 2>/dev/null | tr ' ' '\n' | grep -qxF "$2"; } + + # ── Collect active logged-in users (loginctl → who fallback) ───────────── + declare -A _SMB_SESS # user → home + _smb_add_sess() { + local _u + [[ "$2" == "loginctl" ]] && _u=$(awk '{print $3}' <<< "$1") \ + || _u=$(awk '{print $1}' <<< "$1") + [[ -z "$_u" || "$_u" == "root" || "$_u" == "USER" ]] && return 0 + local _h; _h=$(getent passwd "$_u" | cut -d: -f6 2>/dev/null) || return 0 + [[ -d "$_h" ]] && _SMB_SESS["$_u"]="$_h" + } + if loginctl list-sessions --no-legend &>/dev/null; then + while IFS= read -r _L; do _smb_add_sess "$_L" "loginctl" + done < <(loginctl list-sessions --no-legend 2>/dev/null) + else + while IFS= read -r _L; do _smb_add_sess "$_L" "who" + done < <(who 2>/dev/null) + fi + unset _L + + # ── Apply: mount shares for logged-in members ───────────────────────────── + for _SMB_NAME in "${!_SMB_NAMES[@]}"; do + _R_GRP="${_SMB_R_GRP[$_SMB_NAME]:-}" + _RW_GRP="${_SMB_RW_GRP[$_SMB_NAME]:-}" + + # Both access levels use the same credential; read from whichever has it. + _CRED_LINE="" + [[ -n "$_RW_GRP" ]] && _CRED_LINE=$(_smb_parse_cred "$_RW_GRP") + [[ -z "$_CRED_LINE" && -n "$_R_GRP" ]] && _CRED_LINE=$(_smb_parse_cred "$_R_GRP") + + if [[ -z "$_CRED_LINE" ]]; then + warn "No credential in IPA description for share $_SMB_NAME — skipped (run ansipa-smb.service on FreeIPA container)" + continue + fi + read -r _SMB_HOST _SMB_SUSER _SMB_SPASS <<< "$_CRED_LINE" + + _CREDS_FILE="$SMB_CREDS_DIR/${_SMB_NAME}.creds" + printf 'username=%s\npassword=%s\ndomain=WORKGROUP\n' "$_SMB_SUSER" "$_SMB_SPASS" \ + > "$_CREDS_FILE" + chmod 600 "$_CREDS_FILE" + + for _SMB_U in "${!_SMB_SESS[@]}"; do + # Determine per-user access level (rw beats r). + _SMB_LVL="" + [[ -n "$_RW_GRP" ]] && _smb_member "$_SMB_U" "$_RW_GRP" && _SMB_LVL="rw" + [[ -z "$_SMB_LVL" && -n "$_R_GRP" ]] && _smb_member "$_SMB_U" "$_R_GRP" && _SMB_LVL="r" + [[ -z "$_SMB_LVL" ]] && continue + + _SMB_H="${_SMB_SESS[$_SMB_U]}" + _SMB_MP="$_SMB_H/$_SMB_NAME" + _SMB_SHARE="usr-${_SMB_NAME}-${_SMB_LVL}" + + if mountpoint -q "$_SMB_MP" 2>/dev/null; then + grep -qxF "${_SMB_U}:${_SMB_NAME}" "$SMB_MOUNT_STATE" 2>/dev/null || \ + echo "${_SMB_U}:${_SMB_NAME}" >> "$SMB_MOUNT_STATE" + continue + fi + + mkdir -p "$_SMB_MP" + chown "$_SMB_U" "$_SMB_MP" 2>/dev/null || true + + if mount -t cifs "//${_SMB_HOST}/${_SMB_SHARE}" "$_SMB_MP" \ + -o "credentials=${_CREDS_FILE},uid=${_SMB_U},gid=${_SMB_U},file_mode=0644,dir_mode=0755,nounix,noserverino" \ + 2>/dev/null; then + log "Mounted //${_SMB_HOST}/${_SMB_SHARE} → $_SMB_MP for $_SMB_U" + grep -qxF "${_SMB_U}:${_SMB_NAME}" "$SMB_MOUNT_STATE" 2>/dev/null || \ + echo "${_SMB_U}:${_SMB_NAME}" >> "$SMB_MOUNT_STATE" + else + warn "Failed to mount //${_SMB_HOST}/${_SMB_SHARE} for $_SMB_U" + rmdir "$_SMB_MP" 2>/dev/null || true + fi + done + done + unset _SMB_NAME _R_GRP _RW_GRP _CRED_LINE _SMB_HOST _SMB_SUSER _SMB_SPASS \ + _CREDS_FILE _SMB_U _SMB_H _SMB_MP _SMB_SHARE _SMB_LVL + + # ── Revert: unmount shares whose group was removed or user left ─────────── + _NEW_SMB_MOUNTS=() + while IFS=: read -r _OLD_USER _OLD_NAME; do + [[ -z "$_OLD_USER" || -z "$_OLD_NAME" ]] && continue + _OLD_HOME=$(getent passwd "$_OLD_USER" | cut -d: -f6 2>/dev/null) || continue + _OLD_MPT="$_OLD_HOME/$_OLD_NAME" + + _STILL_MEMBER=false + if [[ -n "${_SMB_R_GRP[$_OLD_NAME]+x}" ]] && \ + _smb_member "$_OLD_USER" "${_SMB_R_GRP[$_OLD_NAME]}"; then + _STILL_MEMBER=true + elif [[ -n "${_SMB_RW_GRP[$_OLD_NAME]+x}" ]] && \ + _smb_member "$_OLD_USER" "${_SMB_RW_GRP[$_OLD_NAME]}"; then + _STILL_MEMBER=true + fi + + if [[ "$_STILL_MEMBER" == true ]]; then + _NEW_SMB_MOUNTS+=("${_OLD_USER}:${_OLD_NAME}") + else + umount -l "$_OLD_MPT" 2>/dev/null || true + rmdir "$_OLD_MPT" 2>/dev/null || true + log "Unmounted ${_OLD_MPT} for ${_OLD_USER} (left usr_smb_*_${_OLD_NAME})" + fi + done < "$SMB_MOUNT_STATE" + unset _OLD_USER _OLD_NAME _OLD_HOME _OLD_MPT _STILL_MEMBER + + if [[ ${#_NEW_SMB_MOUNTS[@]} -gt 0 ]]; then + printf '%s\n' "${_NEW_SMB_MOUNTS[@]}" | sort -u > "$SMB_MOUNT_STATE" + else + > "$SMB_MOUNT_STATE" + fi + + # Credential files are written fresh on each run; remove stale ones. + rm -f "$SMB_CREDS_DIR"/*.creds 2>/dev/null || true + + unset _SMB_R_GRP _SMB_RW_GRP _SMB_NAMES _SMB_SESS _NEW_SMB_MOUNTS + +else + warn "mount.cifs not found — install cifs-utils to enable usr_smb_* policies" + [[ -s "$SMB_MOUNT_STATE" ]] && \ + warn "smb-mounts state is non-empty — cannot revert existing mounts without cifs-utils" || true +fi + +# ── dev_ssh_: distribute SSH public keys to user home dirs ──────────── +# When this host is in dev_ssh_, the IPA user 's public keys are +# written to /home//.ssh/authorized_keys in an ansipa-managed section. +# This enables Amir (or any named IPA user) to SSH into this device using the key +# registered in their IPA profile — the same key reaches every enrolled device. +# +# Keys are stored in the IPA user object (not on each device); add via: +# ipa user-mod --sshpubkey="ssh-ed25519 AAAA..." +# Multiple keys (e.g. workstation + laptop) are each added with --sshpubkey. +# +# The ansipa-managed section in authorized_keys is marked with: +# # BEGIN ansipa-ssh-managed … # END ansipa-ssh-managed +# This preserves any manually-added keys and is cleanly removed on revert. +# +# Leaving the host group removes only the ansipa-managed section on the next run. + +SSH_KEY_STATE="$STATE_DIR/ssh-keys" +[[ -f "$SSH_KEY_STATE" ]] || touch "$SSH_KEY_STATE" + +# Write or clear the ansipa-managed section in an authorized_keys file. +# Usage: _ssh_write_keys [key1 key2 ...] +# Passing no keys removes the section (revert). +_ssh_write_keys() { + local _u="$1" _h="$2"; shift 2 + local _auth="$_h/.ssh/authorized_keys" + mkdir -p "$_h/.ssh" + chmod 700 "$_h/.ssh" + [[ -f "$_auth" ]] || touch "$_auth" + # Strip any existing ansipa section. + local _rest + _rest=$(awk ' + /^# BEGIN ansipa-ssh-managed$/ { skip=1; next } + skip && /^# END ansipa-ssh-managed$/ { skip=0; next } + !skip + ' "$_auth" 2>/dev/null || true) + if [[ $# -gt 0 ]]; then + { [[ -n "$_rest" ]] && printf '%s\n' "$_rest"; \ + echo "# BEGIN ansipa-ssh-managed"; \ + printf '%s\n' "$@"; \ + echo "# END ansipa-ssh-managed"; } > "$_auth" + else + [[ -n "$_rest" ]] && printf '%s\n' "$_rest" > "$_auth" || > "$_auth" + fi + chmod 600 "$_auth" + chown "$_u:$_u" "$_h/.ssh" "$_auth" 2>/dev/null || true +} + +declare -A _SSH_APPLIED # userid → 1 (applied this run) + +for _SSH_UID in "${ACTIVE_SSH_USERS[@]+"${ACTIVE_SSH_USERS[@]}"}"; do + # Verify the IPA user exists. + ipa user-show "$_SSH_UID" &>/dev/null || { + warn "dev_ssh_${_SSH_UID}: IPA user '$_SSH_UID' not found — skipping" + continue + } + + # Fetch SSH public keys from IPA user profile. + # `ipa user-show --all` shows: " SSH public key: " + # " SSH public key fingerprint: ..." is a separate field — exclude it. + _SSH_KEYS=() + mapfile -t _SSH_KEYS < <( + ipa user-show "$_SSH_UID" --all 2>/dev/null \ + | grep "^ SSH public key:" | grep -v "fingerprint" \ + | sed 's/^ SSH public key: //' || true + ) + + if [[ ${#_SSH_KEYS[@]} -eq 0 ]]; then + warn "dev_ssh_${_SSH_UID}: '$_SSH_UID' has no SSH keys in IPA — add via:" + warn " ipa user-mod $_SSH_UID --sshpubkey='ssh-ed25519 AAAA...'" + continue + fi + + # Resolve home directory via SSSD (works for IPA users once SSSD is running). + _SSH_HOME=$(getent passwd "$_SSH_UID" 2>/dev/null | cut -d: -f6 || true) + if [[ -z "$_SSH_HOME" ]]; then + warn "dev_ssh_${_SSH_UID}: '$_SSH_UID' not resolvable via getent — is SSSD running?" + continue + fi + + # Create home dir now if it doesn't yet exist (IPA user may not have logged in). + if [[ ! -d "$_SSH_HOME" ]]; then + log "Creating home dir for $_SSH_UID: $_SSH_HOME" + mkdir -p "$_SSH_HOME" + chown "$_SSH_UID:$_SSH_UID" "$_SSH_HOME" 2>/dev/null || true + chmod 700 "$_SSH_HOME" + fi + + _ssh_write_keys "$_SSH_UID" "$_SSH_HOME" "${_SSH_KEYS[@]}" + log "SSH keys for $_SSH_UID: ${#_SSH_KEYS[@]} key(s) written to $_SSH_HOME/.ssh/authorized_keys" + _SSH_APPLIED["$_SSH_UID"]=1 + grep -qxF "$_SSH_UID" "$SSH_KEY_STATE" 2>/dev/null || echo "$_SSH_UID" >> "$SSH_KEY_STATE" +done +unset _SSH_UID _SSH_KEYS _SSH_HOME + +# Revert: remove ansipa keys for users whose dev_ssh_* group was removed from this host. +_NEW_SSH_STATE=() +while IFS= read -r _OLD_SSH_UID; do + [[ -z "$_OLD_SSH_UID" ]] && continue + if [[ -n "${_SSH_APPLIED[$_OLD_SSH_UID]+x}" ]]; then + _NEW_SSH_STATE+=("$_OLD_SSH_UID") + else + _OLD_SSH_HOME=$(getent passwd "$_OLD_SSH_UID" 2>/dev/null | cut -d: -f6 || true) + if [[ -n "$_OLD_SSH_HOME" ]]; then + _ssh_write_keys "$_OLD_SSH_UID" "$_OLD_SSH_HOME" # no keys = remove section + log "Removed ansipa SSH keys for $_OLD_SSH_UID (host left dev_ssh_${_OLD_SSH_UID})" + fi + fi +done < "$SSH_KEY_STATE" +unset _OLD_SSH_UID _OLD_SSH_HOME + +if [[ ${#_NEW_SSH_STATE[@]} -gt 0 ]]; then + printf '%s\n' "${_NEW_SSH_STATE[@]}" | sort -u > "$SSH_KEY_STATE" +else + > "$SSH_KEY_STATE" +fi +unset _SSH_APPLIED _NEW_SSH_STATE + log "Policy enforcement complete." diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml index efaf488..8970409 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml @@ -9,12 +9,18 @@ # dev_security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans + SMB upload (02:00) # dev_no-local-users Lock local account passwords; only FreeIPA accounts can auth # dev_local-sudo- Grant local sudo on this device; reverted when host leaves +# dev_ssh_ Distribute 's SSH public keys from IPA to authorized_keys +# on this device; allows that IPA user to SSH in from any of their +# registered keys without a password. Keys stored in IPA via: +# ipa user-mod --sshpubkey='ssh-ed25519 AAAA...' # # 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- Block execution of via a PATH-priority wrapper # usr_prt_ 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 +# usr_smb_r_ Mount read-only Samba share ~/‹name› for members; credentials in IPA group description +# usr_smb_rw_ Mount read-write Samba share ~/‹name› for members (rw beats r if both) # # Prerequisites: # - Host enrolled in FreeIPA (sssd + ipa CLI available) @@ -43,6 +49,8 @@ loop: - samba-client - cups + - cifs-utils # needed for usr_smb_* CIFS mounts + - openssh-server # needed for dev_ssh_* (sshd must be running to accept keys) ignore_errors: yes - name: Deploy SMB credentials file diff --git a/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh index bc80f15..9c1a51b 100644 --- a/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh +++ b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh @@ -21,6 +21,7 @@ set -euo pipefail LOG_TAG="ansipa-smb-setup" SCAN_BASE="/data/scan-results" LUKS_BASE="/data/luks-keys" +SHARES_BASE="/data/smb-shares" SMB_CONF="/etc/samba/smb.conf" SMB_USER="scanupload" LUKS_UPLOAD_USER="luks-upload" @@ -28,17 +29,20 @@ 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." @@ -49,12 +53,14 @@ 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" # ── KeyAdmin group ──────────────────────────────────────────────────────────── if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then @@ -80,9 +86,10 @@ 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 -# ── smb.conf ────────────────────────────────────────────────────────────────── -log "Writing $SMB_CONF" -cat > "$SMB_CONF" < "$SMB_CONF" < "$SMB_CONF" < / 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 +} + +# ── Write smb.conf, set passwords, configure user shares ───────────────────── _smb_set_pass "$SMB_USER" "$SMB_PASS" _smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_PASS" +# Write base smb.conf first, then have _setup_user_shares append to it. +_write_smb_conf +_setup_user_shares + +# 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.