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.
366 lines
15 KiB
Bash
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
|