Files
ad-ds-simple-file-server/setup
T

443 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ENV_FILE=".env"
SERVICE_ACCOUNT_NAME="FileShare_ServiceAccount"
BOOTSTRAP_ENV_FILE=""
ORIGINAL_ENV_BACKUP=""
BOOTSTRAP_ENV_WRITTEN=0
FINAL_ENV_WRITTEN=0
ENV_PREEXISTED=0
cleanup() {
if [[ -n "$BOOTSTRAP_ENV_FILE" && -f "$BOOTSTRAP_ENV_FILE" ]]; then
rm -f "$BOOTSTRAP_ENV_FILE"
fi
if [[ "$BOOTSTRAP_ENV_WRITTEN" -eq 1 && "$FINAL_ENV_WRITTEN" -eq 0 ]]; then
if [[ "$ENV_PREEXISTED" -eq 1 && -n "$ORIGINAL_ENV_BACKUP" && -f "$ORIGINAL_ENV_BACKUP" ]]; then
cp "$ORIGINAL_ENV_BACKUP" "$ENV_FILE"
chmod 600 "$ENV_FILE"
printf "Restored original %s after setup failure.\n" "$ENV_FILE" >&2
else
rm -f "$ENV_FILE"
printf "Removed temporary %s after setup failure.\n" "$ENV_FILE" >&2
fi
fi
if [[ -n "$ORIGINAL_ENV_BACKUP" && -f "$ORIGINAL_ENV_BACKUP" ]]; then
rm -f "$ORIGINAL_ENV_BACKUP"
fi
}
trap cleanup EXIT
sanitize_netbios_name() {
local raw_name="$1"
local upper_name="${raw_name^^}"
local cleaned_name
cleaned_name="$(printf '%s' "$upper_name" | tr -cd 'A-Z0-9')"
if [[ -z "$cleaned_name" ]]; then
cleaned_name="ADSAMBAFSRV"
fi
printf '%s' "${cleaned_name:0:15}"
}
sanitize_sam_account_name() {
local raw_name="$1"
local cleaned_name
cleaned_name="$(printf '%s' "$raw_name" | tr -cd 'A-Za-z0-9._-')"
if [[ "$cleaned_name" == "FileShare_ServiceAccount" ]]; then
printf '%s' "FileShare_ServiceAcc"
return
fi
if [[ -z "$cleaned_name" ]]; then
cleaned_name="FileShareSvc"
fi
printf '%s' "${cleaned_name:0:20}"
}
parse_route_source_ip() {
local route="$1"
local previous=""
local token
for token in $route; do
if [[ "$previous" == "src" ]]; then
printf '%s\n' "$token"
return 0
fi
previous="$token"
done
return 1
}
detect_ad_dns_ip() {
local lookup_name="$1"
local target_ip=""
local route=""
if ! command -v ip >/dev/null 2>&1; then
return 1
fi
if [[ -n "$lookup_name" ]] && command -v getent >/dev/null 2>&1; then
while read -r target_ip _; do
if [[ ! "$target_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
continue
fi
route="$(ip -o -4 route get "$target_ip" 2>/dev/null || true)"
parse_route_source_ip "$route" && return 0
done < <(getent ahostsv4 "$lookup_name" 2>/dev/null)
fi
route="$(ip -o -4 route get 1.1.1.1 2>/dev/null || true)"
parse_route_source_ip "$route"
}
prompt_value() {
local var_name="$1"
local prompt_text="$2"
local is_secret="${3:-false}"
local value=""
while [[ -z "$value" ]]; do
if [[ "$is_secret" == "true" ]]; then
read -r -s -p "$prompt_text: " value
printf "\n"
else
read -r -p "$prompt_text: " value
fi
done
printf -v "$var_name" '%s' "$value"
}
write_env_file() {
local realm=""
local workgroup=""
local domain=""
local admin_user=""
local admin_password=""
local domain_users_sid=""
local domain_admins_sid=""
local fslogix_group_sid=""
local backup_destination=""
local backup_start_hour="2"
local backup_retention_daily="3"
local backup_retention_weekly="2"
local backup_retention_monthly="2"
local backup_retention_yearly="1"
local samba_hostname="adsambafsrv"
local netbios_name="ADSAMBAFSRV"
local ad_dns_ip=""
local ad_dns_name=""
local ad_dns_ip_auto="0"
local service_password=""
local web_hostname=""
local web_https_port="443"
local web_jwt_secret=""
local acme_ca_server=""
local acme_ca_certificates_source=""
local acme_ca_certificates_source_dir=""
local acme_ca_certificates_url=""
local acme_ca_certificates_insecure_download="false"
local acme_ca_certificates_insecure_input=""
local acme_http_port="80"
local acme_http_port_input=""
local web_hostname_input=""
local service_account_sam=""
local fslogix_group_prompt=""
local samba_hostname_input=""
local netbios_name_input=""
local ad_dns_ip_input=""
local ad_dns_name_input=""
local detected_ad_dns_ip=""
local sanitized_netbios_name=""
prompt_value realm "REALM (e.g. EXAMPLE.COM)"
prompt_value workgroup "WORKGROUP (NetBIOS, e.g. EXAMPLE)"
prompt_value domain "DOMAIN (AD DNS name or reachable DC FQDN)"
prompt_value admin_user "Initial admin user (for provisioning service account)"
prompt_value admin_password "Initial admin password" true
prompt_value domain_users_sid "DOMAIN_USERS_SID (e.g. ...-513)"
prompt_value domain_admins_sid "DOMAIN_ADMINS_SID (e.g. ...-512)"
fslogix_group_prompt="FSLOGIX_GROUP_SID (press Enter to reuse DOMAIN_USERS_SID)"
read -r -p "${fslogix_group_prompt}: " fslogix_group_sid
if [[ -z "$fslogix_group_sid" ]]; then
fslogix_group_sid="$domain_users_sid"
fi
read -r -p "SAMBA_HOSTNAME [adsambafsrv]: " samba_hostname_input
if [[ -n "${samba_hostname_input:-}" ]]; then
samba_hostname="$samba_hostname_input"
fi
read -r -p "NETBIOS_NAME [ADSAMBAFSRV]: " netbios_name_input
if [[ -n "${netbios_name_input:-}" ]]; then
netbios_name="$netbios_name_input"
fi
sanitized_netbios_name="$(sanitize_netbios_name "$netbios_name")"
if [[ "$sanitized_netbios_name" != "$netbios_name" ]]; then
printf "Using sanitized NETBIOS_NAME: %s\n" "$sanitized_netbios_name"
fi
netbios_name="$sanitized_netbios_name"
detected_ad_dns_ip="$(detect_ad_dns_ip "$domain" || true)"
if [[ -n "$detected_ad_dns_ip" ]]; then
read -r -p "AD_DNS_IP (host LAN IP to publish in AD DNS) [${detected_ad_dns_ip}]: " ad_dns_ip_input
if [[ -n "$ad_dns_ip_input" ]]; then
ad_dns_ip="$ad_dns_ip_input"
else
ad_dns_ip="$detected_ad_dns_ip"
ad_dns_ip_auto="1"
fi
else
prompt_value ad_dns_ip "AD_DNS_IP (host LAN IP to publish in AD DNS)"
fi
ad_dns_name="${samba_hostname}.${domain}"
read -r -p "AD_DNS_NAME [${ad_dns_name}]: " ad_dns_name_input
if [[ -n "$ad_dns_name_input" ]]; then
ad_dns_name="$ad_dns_name_input"
fi
read -r -p "BACKUP_DESTINATION (optional URL, press Enter to disable): " backup_destination
read -r -p "BACKUP_START_HOUR [2]: " backup_start_hour
backup_start_hour="${backup_start_hour:-2}"
read -r -p "BACKUP_RETENTION_DAILY [3]: " backup_retention_daily
backup_retention_daily="${backup_retention_daily:-3}"
read -r -p "BACKUP_RETENTION_WEEKLY [2]: " backup_retention_weekly
backup_retention_weekly="${backup_retention_weekly:-2}"
read -r -p "BACKUP_RETENTION_MONTHLY [2]: " backup_retention_monthly
backup_retention_monthly="${backup_retention_monthly:-2}"
read -r -p "BACKUP_RETENTION_YEARLY [1]: " backup_retention_yearly
backup_retention_yearly="${backup_retention_yearly:-1}"
web_hostname="$ad_dns_name"
read -r -p "WEB_HOSTNAME [${web_hostname}]: " web_hostname_input
web_hostname="${web_hostname_input:-$web_hostname}"
read -r -p "WEB_HTTPS_PORT [443]: " web_https_port
web_https_port="${web_https_port:-443}"
prompt_value acme_ca_server "ACME_CA_SERVER (e.g. https://ca.internal/acme/acme/directory)"
read -r -p "ACME_CA_CERTIFICATES_URL (optional one-time download; Enter to import a local PEM file): " acme_ca_certificates_url
if [[ -n "$acme_ca_certificates_url" ]]; then
case "$acme_ca_certificates_url" in
https://*) ;;
*)
printf "ACME_CA_CERTIFICATES_URL must use https://\n" >&2
return 1
;;
esac
read -r -p "Allow untrusted TLS for this one-time root download? [y/N]: " acme_ca_certificates_insecure_input
case "$acme_ca_certificates_insecure_input" in
y|Y|yes|YES) acme_ca_certificates_insecure_download=true ;;
*) acme_ca_certificates_insecure_download=false ;;
esac
else
prompt_value acme_ca_certificates_source "ACME CA root bundle (PEM file on this host)"
if [[ ! -s "$acme_ca_certificates_source" ]]; then
printf "ACME CA root bundle is not a readable file: %s\n" "$acme_ca_certificates_source" >&2
return 1
fi
acme_ca_certificates_source_dir="$(cd "$(dirname "$acme_ca_certificates_source")" && pwd)"
acme_ca_certificates_source="${acme_ca_certificates_source_dir}/$(basename "$acme_ca_certificates_source")"
fi
read -r -p "ACME_HTTP_PORT (HTTP-01 host port) [80]: " acme_http_port_input
acme_http_port="${acme_http_port_input:-80}"
web_jwt_secret="$(python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(48))
PY
)"
service_account_sam="$(sanitize_sam_account_name "$SERVICE_ACCOUNT_NAME")"
if [[ "$service_account_sam" != "$SERVICE_ACCOUNT_NAME" ]]; then
printf "Using sAMAccountName '%s' (AD limit is 20 chars; requested '%s').\n" "$service_account_sam" "$SERVICE_ACCOUNT_NAME"
fi
service_password="$(python3 - <<'PY'
import secrets
import string
alphabet = string.ascii_letters + string.digits + '@#%+=:_-'
print(''.join(secrets.choice(alphabet) for _ in range(48)))
PY
)"
if [[ -f "$ENV_FILE" ]]; then
ENV_PREEXISTED=1
ORIGINAL_ENV_BACKUP="$(mktemp)"
cp "$ENV_FILE" "$ORIGINAL_ENV_BACKUP"
chmod 600 "$ORIGINAL_ENV_BACKUP"
fi
BOOTSTRAP_ENV_FILE="$(mktemp)"
chmod 600 "$BOOTSTRAP_ENV_FILE"
cat > "$BOOTSTRAP_ENV_FILE" <<EOF
REALM=${realm}
WORKGROUP=${workgroup}
DOMAIN=${domain}
JOIN_USER=${admin_user}
JOIN_PASSWORD=${admin_password}
SERVICE_ACCOUNT_NAME=${SERVICE_ACCOUNT_NAME}
SERVICE_ACCOUNT_SAM=${service_account_sam}
SERVICE_ACCOUNT_PASSWORD=${service_password}
DOMAIN_USERS_SID=${domain_users_sid}
DOMAIN_ADMINS_SID=${domain_admins_sid}
FSLOGIX_GROUP_SID=${fslogix_group_sid}
AD_DNS_IP=${ad_dns_ip}
AD_DNS_NAME=${ad_dns_name}
AD_DNS_IP_AUTO=${ad_dns_ip_auto}
BACKUP_DESTINATION=${backup_destination}
BACKUP_START_HOUR=${backup_start_hour}
BACKUP_RETENTION_DAILY=${backup_retention_daily}
BACKUP_RETENTION_WEEKLY=${backup_retention_weekly}
BACKUP_RETENTION_MONTHLY=${backup_retention_monthly}
BACKUP_RETENTION_YEARLY=${backup_retention_yearly}
SAMBA_HOSTNAME=${samba_hostname}
NETBIOS_NAME=${netbios_name}
WEB_ENABLED=true
WEB_HOSTNAME=${web_hostname}
WEB_HTTPS_PORT=${web_https_port}
WEB_JWT_SECRET=${web_jwt_secret}
WEB_TLS_MODE=acme
ACME_CA_SERVER=${acme_ca_server}
ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem
ACME_CA_CERTIFICATES_URL=${acme_ca_certificates_url}
ACME_CA_CERTIFICATES_INSECURE_DOWNLOAD=${acme_ca_certificates_insecure_download}
ACME_HTTP_LISTEN=:80
ACME_HTTP_PORT=${acme_http_port}
EOF
cp "$BOOTSTRAP_ENV_FILE" "$ENV_FILE"
chmod 600 "$ENV_FILE"
BOOTSTRAP_ENV_WRITTEN=1
printf "Building image...\n"
docker compose build samba
if [[ -n "$acme_ca_certificates_source" ]]; then
printf "Importing ACME CA root bundle...\n"
docker compose run --rm --no-deps -T \
--entrypoint /bin/bash samba -lc \
'install -d -m 0755 "$(dirname "$ACME_CA_CERTIFICATES")" && dd of="$ACME_CA_CERTIFICATES" status=none && chmod 0644 "$ACME_CA_CERTIFICATES"' \
< "$acme_ca_certificates_source"
fi
printf "Provisioning service account %s...\n" "$SERVICE_ACCOUNT_NAME"
docker compose run --rm --entrypoint /bin/bash samba -lc '
set -euo pipefail
cat > /tmp/bootstrap-smb.conf <<EOF
[global]
security = ADS
kerberos method = secrets and keytab
realm = ${REALM}
workgroup = ${WORKGROUP}
netbios name = ${NETBIOS_NAME}
EOF
run_net() {
net -s /tmp/bootstrap-smb.conf -A /tmp/bootstrap.auth -S "$DOMAIN" "$@"
}
cat > /tmp/bootstrap.auth <<EOF
username = ${JOIN_USER}
password = ${JOIN_PASSWORD}
domain = ${WORKGROUP}
EOF
chmod 600 /tmp/bootstrap.auth
if run_net ads search "(&(objectClass=user)(sAMAccountName=${SERVICE_ACCOUNT_SAM}))" sAMAccountName | grep -q "^sAMAccountName: ${SERVICE_ACCOUNT_SAM}$"; then
run_net ads password "${SERVICE_ACCOUNT_SAM}" "${SERVICE_ACCOUNT_PASSWORD}"
else
run_net ads user add "${SERVICE_ACCOUNT_SAM}" "${SERVICE_ACCOUNT_PASSWORD}"
fi
'
cat > "$ENV_FILE" <<EOF
REALM=${realm}
WORKGROUP=${workgroup}
DOMAIN=${domain}
JOIN_USER=${service_account_sam}
JOIN_PASSWORD=${service_password}
DOMAIN_USERS_SID=${domain_users_sid}
DOMAIN_ADMINS_SID=${domain_admins_sid}
FSLOGIX_GROUP_SID=${fslogix_group_sid}
AD_DNS_IP=${ad_dns_ip}
AD_DNS_NAME=${ad_dns_name}
AD_DNS_IP_AUTO=${ad_dns_ip_auto}
BACKUP_DESTINATION=${backup_destination}
BACKUP_START_HOUR=${backup_start_hour}
BACKUP_RETENTION_DAILY=${backup_retention_daily}
BACKUP_RETENTION_WEEKLY=${backup_retention_weekly}
BACKUP_RETENTION_MONTHLY=${backup_retention_monthly}
BACKUP_RETENTION_YEARLY=${backup_retention_yearly}
SAMBA_HOSTNAME=${samba_hostname}
NETBIOS_NAME=${netbios_name}
WEB_ENABLED=true
WEB_HOSTNAME=${web_hostname}
WEB_HTTPS_PORT=${web_https_port}
WEB_JWT_SECRET=${web_jwt_secret}
WEB_TLS_MODE=acme
ACME_CA_SERVER=${acme_ca_server}
ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem
ACME_CA_CERTIFICATES_URL=${acme_ca_certificates_url}
ACME_CA_CERTIFICATES_INSECURE_DOWNLOAD=${acme_ca_certificates_insecure_download}
ACME_HTTP_LISTEN=:80
ACME_HTTP_PORT=${acme_http_port}
# Optional overrides:
# LDAP_URI=ldaps://${domain}
# LDAP_BASE_DN=DC=example,DC=com
# PRIVATE_SKIP_USERS=svc_backup,svc_sql
# PRIVATE_SKIP_PREFIXES=svc_,sql_
# BACKUP_DESTINATION=rsync://user:pass@backup.example.com/samba-backups
# BACKUP_DESTINATION=smb://DOMAIN%5Cuser:pass@backup.example.com/Backups/samba
# BACKUP_DESTINATION=davfs://user:pass@webdav.example.com/remote.php/dav/files/backup
# BACKUP_DESTINATION=sftp://user:pass@sftp.example.com/exports/samba
# BACKUP_START_HOUR=2
# BACKUP_RETENTION_DAILY=3
# BACKUP_RETENTION_WEEKLY=2
# BACKUP_RETENTION_MONTHLY=2
# BACKUP_RETENTION_YEARLY=1
# BACKUP_LOG_FILE=/var/log/backup.log
# BACKUP_PROGRESS=auto
# BACKUP_PROGRESS_INTERVAL_SECONDS=10
# WEB_JWT_TTL_SECONDS=28800
# WEB_USAGE_SCAN_INTERVAL_SECONDS=900
# WEB_DIRECTORY_CACHE_SECONDS=300
# STATE_DB_PATH=/state/shares.db
# AUDIT_QUERY_MAX_DAYS=31
# AUDIT_SKIP_USER_SUFFIXES=_svc,_ServiceAcc
# ACME_RENEW_CHECK_SECONDS=900
# WEB_TLS_CERT_FILE=/state/tls/web.crt
# WEB_TLS_KEY_FILE=/state/tls/web.key
EOF
chmod 600 "$ENV_FILE"
FINAL_ENV_WRITTEN=1
printf "Created %s\n" "$ENV_FILE"
}
if [[ -f "$ENV_FILE" ]]; then
read -r -p ".env already exists. Overwrite? [y/N]: " overwrite
case "$overwrite" in
y|Y|yes|YES)
write_env_file
;;
*)
printf "Keeping existing .env\n"
;;
esac
else
write_env_file
fi
docker compose up -d
printf "Samba service started.\n"