feat(ansipa): add usr_smb_* and dev_ssh_* policies
usr_smb_r_<name> / usr_smb_rw_<name> (user groups): - Server creates /data/smb-shares/<name>/ with smb-<name> system user, setgid directory, and Samba share stanzas [usr-<name>-r/rw] on startup - Auto-generates credentials and stores them in IPA group descriptions (cifs://<host>:<samba-user>:<password>) so no out-of-band secret distribution - Client mounts //ipa/<share> 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_<userid> (host group): - Reads IPA user's SSH public keys (ipasshpubkey / --all output) - Writes them into /home/<userid>/.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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHops6PU4c2Mv5UyhUj8Msfeat/astal-menu
parent
00ce4f5260
commit
76c3c2c1c5
|
|
@ -81,6 +81,7 @@ RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
|
||||||
ACTIVE_DAEMON_ENABLE=()
|
ACTIVE_DAEMON_ENABLE=()
|
||||||
ACTIVE_DAEMON_DISABLE=()
|
ACTIVE_DAEMON_DISABLE=()
|
||||||
ACTIVE_LOCAL_SUDO_USERS=()
|
ACTIVE_LOCAL_SUDO_USERS=()
|
||||||
|
ACTIVE_SSH_USERS=()
|
||||||
WANT_TIMESHIFT_BACKUP=false
|
WANT_TIMESHIFT_BACKUP=false
|
||||||
WANT_SECURITY_SCAN=false
|
WANT_SECURITY_SCAN=false
|
||||||
WANT_SCAN_NOTIFY=false
|
WANT_SCAN_NOTIFY=false
|
||||||
|
|
@ -100,6 +101,7 @@ if [[ -n "$RAW_GROUPS" ]]; then
|
||||||
dev_security-scan) WANT_SECURITY_SCAN=true ;;
|
dev_security-scan) WANT_SECURITY_SCAN=true ;;
|
||||||
dev_no-local-users) WANT_NO_LOCAL_USERS=true ;;
|
dev_no-local-users) WANT_NO_LOCAL_USERS=true ;;
|
||||||
dev_local-sudo-*) ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;;
|
dev_local-sudo-*) ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;;
|
||||||
|
dev_ssh_*) ACTIVE_SSH_USERS+=("${g#dev_ssh_}") ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
done <<< "$RAW_GROUPS"
|
done <<< "$RAW_GROUPS"
|
||||||
|
|
@ -154,7 +156,8 @@ fi
|
||||||
log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \
|
log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \
|
||||||
"| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
|
"| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
|
||||||
"| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN" \
|
"| 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" \
|
log "User policies — admin: $WANT_USR_ADMIN" \
|
||||||
"| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
|
"| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
|
||||||
"| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \
|
"| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \
|
||||||
|
|
@ -771,4 +774,291 @@ else
|
||||||
fi
|
fi
|
||||||
unset _ap
|
unset _ap
|
||||||
|
|
||||||
|
# ── usr_smb_<r|rw>_<name>: auto-mount network shares in user home dirs ────────
|
||||||
|
# For each usr_smb_r_<name> or usr_smb_rw_<name> group in FreeIPA:
|
||||||
|
# - The FreeIPA container hosts a Samba share [usr-<name>-r] or [usr-<name>-rw]
|
||||||
|
# under /data/smb-shares/<name>/ 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://<ipa-host>:<samba-user>:<password>
|
||||||
|
#
|
||||||
|
# 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_<userid>: distribute SSH public keys to user home dirs ────────────
|
||||||
|
# When this host is in dev_ssh_<userid>, the IPA user <userid>'s public keys are
|
||||||
|
# written to /home/<userid>/.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 <userid> --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 <user> <homedir> [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: <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."
|
log "Policy enforcement complete."
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,18 @@
|
||||||
# dev_security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans + SMB upload (02:00)
|
# 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_no-local-users Lock local account passwords; only FreeIPA accounts can auth
|
||||||
# dev_local-sudo-<username> Grant <username> local sudo on this device; reverted when host leaves
|
# dev_local-sudo-<username> Grant <username> local sudo on this device; reverted when host leaves
|
||||||
|
# dev_ssh_<userid> Distribute <userid>'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 <userid> --sshpubkey='ssh-ed25519 AAAA...'
|
||||||
#
|
#
|
||||||
# User policies (FreeIPA user groups — follow the user across all enrolled devices):
|
# 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_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_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_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
|
# usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged
|
||||||
|
# usr_smb_r_<name> Mount read-only Samba share ~/‹name› for members; credentials in IPA group description
|
||||||
|
# usr_smb_rw_<name> Mount read-write Samba share ~/‹name› for members (rw beats r if both)
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
|
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
|
||||||
|
|
@ -43,6 +49,8 @@
|
||||||
loop:
|
loop:
|
||||||
- samba-client
|
- samba-client
|
||||||
- cups
|
- 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
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: Deploy SMB credentials file
|
- name: Deploy SMB credentials file
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ set -euo pipefail
|
||||||
LOG_TAG="ansipa-smb-setup"
|
LOG_TAG="ansipa-smb-setup"
|
||||||
SCAN_BASE="/data/scan-results"
|
SCAN_BASE="/data/scan-results"
|
||||||
LUKS_BASE="/data/luks-keys"
|
LUKS_BASE="/data/luks-keys"
|
||||||
|
SHARES_BASE="/data/smb-shares"
|
||||||
SMB_CONF="/etc/samba/smb.conf"
|
SMB_CONF="/etc/samba/smb.conf"
|
||||||
SMB_USER="scanupload"
|
SMB_USER="scanupload"
|
||||||
LUKS_UPLOAD_USER="luks-upload"
|
LUKS_UPLOAD_USER="luks-upload"
|
||||||
|
|
@ -28,17 +29,20 @@ KEYADMIN_GROUP="KeyAdmin"
|
||||||
ENV_FILE="/data/samba/ansipa-smb.env"
|
ENV_FILE="/data/samba/ansipa-smb.env"
|
||||||
|
|
||||||
log() { echo "[$LOG_TAG] $*"; }
|
log() { echo "[$LOG_TAG] $*"; }
|
||||||
|
warn() { echo "[$LOG_TAG][WARN] $*" >&2; }
|
||||||
die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; }
|
die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; }
|
||||||
|
|
||||||
# ── Resolve passwords ─────────────────────────────────────────────────────────
|
# ── Resolve passwords ─────────────────────────────────────────────────────────
|
||||||
SMB_PASS="${SMB_SCAN_PASSWORD:-}"
|
SMB_PASS="${SMB_SCAN_PASSWORD:-}"
|
||||||
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-}"
|
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-}"
|
||||||
|
IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-}"
|
||||||
|
|
||||||
if [[ -f "$ENV_FILE" ]]; then
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
SMB_PASS="${SMB_SCAN_PASSWORD:-$SMB_PASS}"
|
SMB_PASS="${SMB_SCAN_PASSWORD:-$SMB_PASS}"
|
||||||
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}"
|
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}"
|
||||||
|
IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-$IPA_ADMIN_PASS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -z "$SMB_PASS" ]] && die "SMB_SCAN_PASSWORD not set and $ENV_FILE not present. Set it in .env."
|
[[ -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 'SMB_SCAN_PASSWORD=%q\n' "$SMB_PASS"
|
||||||
printf 'LUKS_KEY_UPLOAD_PASSWORD=%q\n' "$LUKS_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"
|
} > "$ENV_FILE"
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
# ── Directory structure (idempotent) ──────────────────────────────────────────
|
# ── Directory structure (idempotent) ──────────────────────────────────────────
|
||||||
mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts"
|
mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts"
|
||||||
mkdir -p "$LUKS_BASE"
|
mkdir -p "$LUKS_BASE"
|
||||||
|
mkdir -p "$SHARES_BASE"
|
||||||
|
|
||||||
# ── KeyAdmin group ────────────────────────────────────────────────────────────
|
# ── KeyAdmin group ────────────────────────────────────────────────────────────
|
||||||
if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then
|
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"
|
chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE"
|
||||||
chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group
|
chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group
|
||||||
|
|
||||||
# ── smb.conf ──────────────────────────────────────────────────────────────────
|
# ── smb.conf (base only; _setup_user_shares appends stanzas afterwards) ───────
|
||||||
log "Writing $SMB_CONF"
|
_write_smb_conf() {
|
||||||
cat > "$SMB_CONF" <<CONF
|
log "Writing $SMB_CONF"
|
||||||
|
cat > "$SMB_CONF" <<CONF
|
||||||
[global]
|
[global]
|
||||||
workgroup = WORKGROUP
|
workgroup = WORKGROUP
|
||||||
server string = Ansipa Security Server
|
server string = Ansipa Security Server
|
||||||
|
|
@ -118,6 +125,7 @@ cat > "$SMB_CONF" <<CONF
|
||||||
create mask = 0640
|
create mask = 0640
|
||||||
directory mask = 0750
|
directory mask = 0750
|
||||||
CONF
|
CONF
|
||||||
|
}
|
||||||
|
|
||||||
# ── Samba passwords (idempotent — smbpasswd -a adds or updates) ───────────────
|
# ── Samba passwords (idempotent — smbpasswd -a adds or updates) ───────────────
|
||||||
_smb_set_pass() {
|
_smb_set_pass() {
|
||||||
|
|
@ -128,9 +136,164 @@ _smb_set_pass() {
|
||||||
log "WARN: smbpasswd returned non-zero for $user (may already be set correctly)"
|
log "WARN: smbpasswd returned non-zero for $user (may already be set correctly)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── User-share management (usr_smb_r_<name> / usr_smb_rw_<name> IPA groups) ──
|
||||||
|
# Each group maps to a Samba share under $SHARES_BASE/<name>/:
|
||||||
|
# usr_smb_r_<name> → read-only share [usr-<name>-r]
|
||||||
|
# usr_smb_rw_<name> → read-write share [usr-<name>-rw]
|
||||||
|
#
|
||||||
|
# Both access levels for the same share use one Linux user (smb-<name>) 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://<ipa-host>:<samba-user>:<password>
|
||||||
|
#
|
||||||
|
# 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://<host>:<samba-user>:<password>
|
||||||
|
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-<name>-r] or [usr-<name>-rw]; folder in ~ is just <name>.
|
||||||
|
if [[ -n "${_HAS_R[$NAME]+x}" ]]; then
|
||||||
|
cat >> "$SMB_CONF" <<STANZA
|
||||||
|
|
||||||
|
[usr-${NAME}-r]
|
||||||
|
comment = Ansipa user share: ${NAME} (read-only)
|
||||||
|
path = ${SDIR}
|
||||||
|
valid users = ${SUSER}
|
||||||
|
read only = yes
|
||||||
|
browseable = no
|
||||||
|
force user = ${SUSER}
|
||||||
|
force group = ${SUSER}
|
||||||
|
STANZA
|
||||||
|
fi
|
||||||
|
if [[ -n "${_HAS_RW[$NAME]+x}" ]]; then
|
||||||
|
cat >> "$SMB_CONF" <<STANZA
|
||||||
|
|
||||||
|
[usr-${NAME}-rw]
|
||||||
|
comment = Ansipa user share: ${NAME} (read-write)
|
||||||
|
path = ${SDIR}
|
||||||
|
valid users = ${SUSER}
|
||||||
|
read only = no
|
||||||
|
write list = ${SUSER}
|
||||||
|
browseable = no
|
||||||
|
force user = ${SUSER}
|
||||||
|
force group = ${SUSER}
|
||||||
|
create mask = 0664
|
||||||
|
directory mask = 02775
|
||||||
|
STANZA
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
unset _HAS_R _HAS_RW _DONE _g NAME SUSER SDIR PASS _desc _grp CRED
|
||||||
|
kdestroy &>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Write smb.conf, set passwords, configure user shares ─────────────────────
|
||||||
_smb_set_pass "$SMB_USER" "$SMB_PASS"
|
_smb_set_pass "$SMB_USER" "$SMB_PASS"
|
||||||
_smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_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) ─
|
# ── 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
|
# 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.
|
# lost on container recreation, so we must restore it on every start.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue