Files
ubuntu-post-install/services/samba.sh
Claude f8bfce87d9 Add samba service: shares, dedicated users, LAN-scoped firewall
New services/samba.sh, following the non-Docker service shape
(services/crowdsec.sh) since Samba runs natively (smbd/nmbd), not in
a container:

- Installs the samba package if missing
- Prompts to add one or more shares (path, guest vs. authenticated)
- For authenticated shares, creates a system Linux account (if one
  doesn't already exist) and a separate Samba password via smbpasswd
  for each user, adds them to a sambashare group
- Appends share stanzas to /etc/samba/smb.conf (tagged with a
  # ubuntu-post-install:share:<name> marker for later discovery),
  validates with testparm before restarting smbd/nmbd
- Opens UFW for SMB (137/138 udp, 139/445 tcp), scoped to the
  detected LAN subnet by default rather than the whole internet
- Writes a docs-only README under ~/docker/samba (no compose stack)

Registered under `utilities`, with an is_installed()/install_count()
entry in setup.sh (command -v smbd, matching the glow/crowdsec
pattern for non-Docker services) and a README.md Services table entry.

Also wired as an optional nudge into services/base.sh, alongside the
existing Caddy/CrowdSec/NetBird prompts — offered during the base
install but not unconditional, since (unlike net-tools/ncdu) it needs
real input — a share path and at least one user — to do anything
useful, so it defaults to declined rather than accepted.
2026-08-31 18:39:11 +00:00

366 lines
15 KiB
Bash

#!/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:<name>\` comment right above its \`[<name>]\`
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 <username> # change an existing user's Samba password
sudo pdbedit -L # list all Samba users
sudo smbpasswd -x <username> # remove a user from Samba (leaves the Linux account alone)
\`\`\`
## Connecting
- Windows: \`\\\\<server-ip>\\<share-name>\`
- macOS Finder: Go -> Connect to Server -> \`smb://<server-ip>/<share-name>\`
- Linux: \`smbclient //<server-ip>/<share-name> -U <username>\` 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