390 lines
16 KiB
Bash
390 lines
16 KiB
Bash
#!/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 <user>
|
|
# smbpasswd -a <user>
|
|
# 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://<host>:smb-policystore:<password>
|
|
# 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" <<CONF
|
|
[global]
|
|
workgroup = WORKGROUP
|
|
server string = Ansipa Security Server
|
|
security = user
|
|
map to guest = never
|
|
# Store passdb on the persistent volume so passwords survive container restarts.
|
|
passdb backend = tdbsam:/data/samba/passdb.tdb
|
|
log file = /var/log/samba/log.%m
|
|
max log size = 50
|
|
# Disable printing subsystem entirely.
|
|
load printers = no
|
|
printing = bsd
|
|
printcap name = /dev/null
|
|
disable spoolss = yes
|
|
|
|
[ansipa-scans]
|
|
comment = Ansipa scan results — managed by ansipa-enforce-policies
|
|
path = $SCAN_BASE
|
|
valid users = $SMB_USER
|
|
read only = no
|
|
browseable = no
|
|
create mask = 0644
|
|
directory mask = 0755
|
|
force user = $SMB_USER
|
|
|
|
[ansipa-luks-keys]
|
|
comment = Ansipa LUKS backup keys — KeyAdmin read, luks-upload write only
|
|
path = $LUKS_BASE
|
|
valid users = @$KEYADMIN_GROUP
|
|
read only = yes
|
|
write list = $LUKS_UPLOAD_USER
|
|
browseable = no
|
|
create mask = 0640
|
|
directory mask = 0750
|
|
|
|
[ansipa-policystore]
|
|
comment = Ansipa policy store — admin rw; enforcer syncs policy files from here
|
|
path = $POLICY_STORE_BASE
|
|
valid users = $POLICY_STORE_USER
|
|
read only = no
|
|
browseable = no
|
|
force user = $POLICY_STORE_USER
|
|
force group = $POLICY_STORE_USER
|
|
create mask = 0664
|
|
directory mask = 02775
|
|
CONF
|
|
}
|
|
|
|
# ── Samba passwords (idempotent — smbpasswd -a adds or updates) ───────────────
|
|
_smb_set_pass() {
|
|
local user="$1" pass="$2"
|
|
log "Setting Samba password for $user"
|
|
printf '%s\n%s\n' "$pass" "$pass" | smbpasswd -a -s "$user" 2>/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_<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
|
|
}
|
|
|
|
# ── 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://<host>:smb-policystore:<password>
|
|
# 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)"
|