#!/bin/bash # services/samba.sh — Samba (SMB/CIFS) file sharing: shares, users, passwords. # Part of the modular post-install system (sourced by setup.sh). # # Can also be run standalone on any machine: # sudo bash samba.sh # # Samba is a SYSTEM install (apt package + native smbd/nmbd services), NOT a # docker-compose service — same shape as services/crowdsec.sh. There is no # ~/docker/samba compose stack; we only create a docs-only folder there with # a README pointing at the real config under /etc/samba/smb.conf. This is # the SERVER side — for mounting an existing remote Samba share instead, see # services/vpn-data-mount.sh (deliberately the opposite: reads an existing # smb.conf over SSH, never installs Samba, never creates or resets a share # password). # ── Standalone bootstrap ────────────────────────────────────────────────────── # Detected when the script is executed directly rather than sourced by setup.sh. # Sets up helpers and globals, then defers execution until after the function # definition at the bottom of this file. if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then [[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; } _SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" _COMMON="$_SELF_DIR/../lib/common.sh" if [[ -f "$_COMMON" ]]; then # Full repo present — use the real helpers (picks up ~/docker/.config too) # shellcheck source=../lib/common.sh source "$_COMMON" else # One-off copy — inline minimal stubs so the script works without the repo log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; } log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; } log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; } log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; } ensure_docker_dir_ownership() { chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true } prompt_text() { local _q="$1" _def="$2" _var="$3" _r [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; } read -r -p " $_q " _r eval "$_var='${_r:-$_def}'" } prompt_yn() { local _q="$1" _def="$2" _var="$3" _r [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; } read -r -p " $_q " _r eval "$_var='${_r:-$_def}'" } generate_password() { local _len="${1:-32}" tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$_len" } ensure_ufw_enabled() { command -v ufw &>/dev/null || return 0 ufw status 2>/dev/null | grep -q "Status: active" && return 0 local _ssh_port _ssh_port="$(grep -iE '^[[:space:]]*Port[[:space:]]+[0-9]+' /etc/ssh/sshd_config 2>/dev/null \ | tail -1 | awk '{print $2}')" _ssh_port="${_ssh_port:-22}" ufw allow "${_ssh_port}/tcp" comment 'SSH' >/dev/null 2>&1 ufw --force enable >/dev/null 2>&1 log_success "UFW enabled (SSH on port ${_ssh_port} allowed first, so this won't lock you out)." } write_readme() { local _dir="$1"; shift mkdir -p "$_dir" cat > "$_dir/README.md" } backup_if_exists() { local _file="$1" [ -f "$_file" ] || return 0 cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR # ($HOME under sudo is /root, not the real user's home) ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")" DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}" DRY_RUN="${DRY_RUN:-false}" UNATTENDED="${UNATTENDED:-false}" register_service() { :; } # no-op — no wizard to register into _RUN_STANDALONE=1 fi # ───────────────────────────────────────────────────────────────────────────── register_service samba utilities "Samba file sharing (SMB/CIFS) — shares, users, passwords" install_samba() { local SMB_CONF="/etc/samba/smb.conf" local DOCS_DIR="$DOCKER_DIR/samba" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would install samba (smbd/nmbd) if not already present" echo "[DRY-RUN] Would show any shares this installer already manages" echo "[DRY-RUN] Would prompt to add one or more shares (path, guest-or-authenticated, users)" echo "[DRY-RUN] Would create a system Linux account + Samba password for any new user" echo "[DRY-RUN] Would append share stanzas to $SMB_CONF, validate with testparm, restart smbd/nmbd" echo "[DRY-RUN] Would open UFW for SMB (137/138 udp, 139/445 tcp) — scoped to the LAN by default" echo "[DRY-RUN] Would write $DOCS_DIR/README.md (docs only — Samba itself runs natively, not in Docker)" return 0 fi if ! command -v smbd &>/dev/null; then log_info "Installing Samba..." apt-get update -y apt-get install -y samba || { log_error "Samba install failed"; return 1; } log_success "Samba installed" else log_success "Samba already installed" fi backup_if_exists "$SMB_CONF" if grep -q '^# ubuntu-post-install:share:' "$SMB_CONF" 2>/dev/null; then echo "" log_info "Shares already managed by this installer:" grep '^# ubuntu-post-install:share:' "$SMB_CONF" | sed 's/^# ubuntu-post-install:share:/ - /' fi echo "" local _added_any=false while true; do local ADD_SHARE="" prompt_yn "Add a Samba share now? (y/n):" "y" ADD_SHARE [[ "$ADD_SHARE" =~ ^[Yy]$ ]] || break _samba_add_share "$SMB_CONF" && _added_any=true echo "" done if [ "$_added_any" = true ]; then log_info "Validating smb.conf..." if testparm -s "$SMB_CONF" &>/dev/null; then systemctl restart smbd 2>/dev/null systemctl restart nmbd 2>/dev/null # NetBIOS name resolution — some Samba packages split this out log_success "smbd/nmbd restarted with the new configuration" else log_error "testparm reports smb.conf is invalid — NOT restarting smbd/nmbd." log_error "Check manually: sudo testparm -s $SMB_CONF" return 1 fi else log_info "No shares added this run." fi _samba_configure_firewall mkdir -p "$DOCS_DIR" ensure_docker_dir_ownership "$DOCS_DIR" write_readme "$DOCS_DIR" << MD # Samba Samba runs natively on this box (not in Docker) — the real config is \`/etc/samba/smb.conf\`, managed by \`systemctl\`. This folder just holds this README; there's no compose stack here. ## Manage \`\`\`bash sudo testparm -s # validate smb.conf before restarting sudo systemctl restart smbd nmbd sudo systemctl status smbd \`\`\` ## Shares Re-run \`sudo ./setup.sh samba\` (or \`sudo bash services/samba.sh\` standalone) to add another share or another user — existing shares/users are left alone. Each share this installer wrote is marked in smb.conf with a \`# ubuntu-post-install:share:\` comment right above its \`[]\` stanza, so you can find (or hand-edit / remove) them later. ## Users Samba users need BOTH a Linux account and a separate Samba password (\`smbpasswd\`) — they are not the same credential. This installer creates a system account (\`useradd --system --no-create-home\`, no shell login) for any username that doesn't already exist as a Linux user, adds it to the \`sambashare\` group, and sets its Samba password with \`smbpasswd\`. \`\`\`bash sudo smbpasswd # change an existing user's Samba password sudo pdbedit -L # list all Samba users sudo smbpasswd -x # remove a user from Samba (leaves the Linux account alone) \`\`\` ## Connecting - Windows: \`\\\\\\\` - macOS Finder: Go -> Connect to Server -> \`smb:///\` - Linux: \`smbclient /// -U \` or mount with \`mount.cifs\` / \`cifs-utils\` (already installed by \`services/base.sh\`). ## Firewall SMB (137/138 UDP, 139/445 TCP) should almost never be exposed to the public internet — this installer scopes the UFW rule to your LAN subnet by default. Check what's currently allowed with \`sudo ufw status | grep -E '13[7-9]|445'\`. MD log_success "Samba configured. Re-run 'sudo ./setup.sh samba' any time to add another share or user." } # Appends one [share] stanza to smb.conf. Returns non-zero (and adds nothing) # on a blank/duplicate name so the caller's "did we actually add one" tracking # stays accurate. _samba_add_share() { local _conf="$1" local NAME="" SHARE_PATH="" GUEST="" prompt_text " Share name (letters/numbers/hyphens/underscores, e.g. media):" "" NAME NAME="$(echo "$NAME" | tr -cd 'A-Za-z0-9_-')" if [[ -z "$NAME" ]]; then log_warning "Share name required — skipping." return 1 fi if grep -q "^\[$NAME\]\$" "$_conf" 2>/dev/null; then log_warning "A share named [$NAME] already exists in smb.conf — skipping." log_warning "Edit $_conf by hand to change it, or pick a different name." return 1 fi local DEFAULT_PATH="/srv/samba/$NAME" prompt_text " Path to share [$DEFAULT_PATH]:" "$DEFAULT_PATH" SHARE_PATH SHARE_PATH="${SHARE_PATH:-$DEFAULT_PATH}" SHARE_PATH="${SHARE_PATH/#\~/$ACTUAL_HOME}" mkdir -p "$SHARE_PATH" prompt_yn " Allow guest (no password) access to '$NAME'? (y/n):" "n" GUEST local VALID_USERS="" if [[ ! "$GUEST" =~ ^[Yy]$ ]]; then echo " Enter Samba usernames to grant access to '$NAME' (blank to stop):" while true; do local SUSER="" prompt_text " Username:" "" SUSER [[ -z "$SUSER" ]] && break _samba_ensure_user "$SUSER" VALID_USERS="${VALID_USERS:+$VALID_USERS }$SUSER" done if [[ -z "$VALID_USERS" ]]; then log_warning "No users added and guest access declined — '$NAME' will be inaccessible until you add a user (re-run this installer, or edit smb.conf by hand)." fi fi getent group sambashare >/dev/null 2>&1 || groupadd sambashare if [[ "$GUEST" =~ ^[Yy]$ ]]; then chmod 0777 "$SHARE_PATH" else chgrp sambashare "$SHARE_PATH" 2>/dev/null || true chmod 0770 "$SHARE_PATH" fi { echo "" echo "# ubuntu-post-install:share:$NAME" echo "[$NAME]" echo " path = $SHARE_PATH" echo " browseable = yes" echo " read only = no" if [[ "$GUEST" =~ ^[Yy]$ ]]; then echo " guest ok = yes" else echo " guest ok = no" [[ -n "$VALID_USERS" ]] && echo " valid users = $VALID_USERS" fi } >> "$_conf" log_success "Share '$NAME' -> $SHARE_PATH added to smb.conf" } # Creates the Linux system account (if missing) and sets a Samba password for # it. Samba users need BOTH — a Linux account and a separate smbpasswd entry # — they are not the same credential, and smbpasswd -a fails outright against # a username with no matching Linux account at all. _samba_ensure_user() { local _user="$1" if ! id "$_user" &>/dev/null; then log_info "Linux account '$_user' doesn't exist — creating a system account (no shell login, no home dir)." useradd --system --no-create-home --shell /usr/sbin/nologin "$_user" fi getent group sambashare >/dev/null 2>&1 || groupadd sambashare usermod -aG sambashare "$_user" if pdbedit -L 2>/dev/null | cut -d: -f1 | grep -qx "$_user"; then log_info "Samba password already set for '$_user' — leaving as-is (change it later with: sudo smbpasswd $_user)." return 0 fi local _pass _entered="" _pass="$(generate_password 16)" prompt_text " Samba password for '$_user' [$_pass]:" "$_pass" _entered _pass="${_entered:-$_pass}" if printf '%s\n%s\n' "$_pass" "$_pass" | smbpasswd -s -a "$_user" >/dev/null 2>&1 \ && smbpasswd -e "$_user" >/dev/null 2>&1; then log_success "Samba user '$_user' set — password: $_pass (write this down, it isn't stored anywhere else)" else log_warning "Failed to set Samba password for '$_user' — set it manually: sudo smbpasswd $_user" fi } # SMB should almost never face the public internet — scope the UFW rule to # the LAN by default (LAN-subnet detection borrowed from the same pattern # services/asterisk.sh uses for its VLAN/local-network prompt). _samba_configure_firewall() { command -v ufw &>/dev/null || { log_warning "ufw not installed — if you use a firewall, open TCP 139/445 and UDP 137/138 for SMB (LAN only, never the internet)." return 0 } echo "" local DETECTED_NETS DEFAULT_SUBNET="" DETECTED_NETS="$(ip -o -f inet addr show scope global 2>/dev/null \ | awk '{print $2, $4}' \ | grep -Ev '^(docker|br-|veth|tun|tap|wg)' \ | awk '{ split($2,a,"/"); split(a[1],o,"."); print o[1]"."o[2]"."o[3]".0/"a[2] }' \ | sort -u)" DEFAULT_SUBNET="$(echo "$DETECTED_NETS" | head -1)" local RESTRICT_LAN="" prompt_yn "Restrict Samba access to your local network only (recommended — SMB should never face the internet)? (y/n):" "y" RESTRICT_LAN if [[ "$RESTRICT_LAN" =~ ^[Yy]$ ]]; then local SUBNET="" prompt_text " LAN subnet to allow (CIDR)${DEFAULT_SUBNET:+ [$DEFAULT_SUBNET]}:" "$DEFAULT_SUBNET" SUBNET SUBNET="${SUBNET:-$DEFAULT_SUBNET}" if [[ -z "$SUBNET" ]]; then log_warning "No subnet given — skipping UFW rules. Open them manually if needed." return 0 fi local p for p in 137 138; do ufw allow from "$SUBNET" to any port "$p" proto udp comment "Samba (LAN)" >/dev/null 2>&1 done for p in 139 445; do ufw allow from "$SUBNET" to any port "$p" proto tcp comment "Samba (LAN)" >/dev/null 2>&1 done log_success "UFW: Samba opened to $SUBNET only" else log_warning "Opening Samba to ALL sources — not recommended, SMB has a long history of remote exploits." ufw allow 137/udp comment "Samba" >/dev/null 2>&1 ufw allow 138/udp comment "Samba" >/dev/null 2>&1 ufw allow 139/tcp comment "Samba" >/dev/null 2>&1 ufw allow 445/tcp comment "Samba" >/dev/null 2>&1 log_success "UFW: Samba opened (unrestricted)" fi ensure_ufw_enabled } [[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_samba