#!/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_archive_password="" local backup_auto_enabled="true" local backup_auto_input="" 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 if [[ -n "$backup_destination" ]]; then prompt_value backup_archive_password "BACKUP_ARCHIVE_PASSWORD (encrypts group archives)" true read -r -p "Enable automatic daily backups? [Y/n]: " backup_auto_input case "${backup_auto_input,,}" in n|no) backup_auto_enabled="false" ;; *) backup_auto_enabled="true" ;; esac fi 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" < /tmp/bootstrap-smb.conf < /tmp/bootstrap.auth < "$ENV_FILE" <