Files
SGU-CredentialProvider/scripts/Enroll-SguLinuxDomainClient.sh
T

482 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
# Enroll-SguLinuxDomainClient.sh
#
# Idempotently joins a Debian/Ubuntu or RHEL-family Linux workstation to the
# SGU Active Directory laboratory. The join password is always requested by
# realmd; this script never accepts, logs, or stores it.
set -Eeuo pipefail
IFS=$'\n\t'
SCRIPT_DIRECTORY=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
DOMAIN_NAME='lci.lasalle.mx'
DOMAIN_CONTROLLER=''
DOMAIN_DNS=''
COMPUTER_OU='OU=Laboratorio,DC=lci,DC=lasalle,DC=mx'
JOIN_USER='Administrator'
DOMAIN_INTERFACE=''
DOMAIN_ADDRESS=''
COMPUTER_NAME=''
ALLOW_GROUP=''
ENABLE_SSH=false
ENABLE_HYPERV_ENHANCED_SESSION=false
ENABLE_RUSTDESK=true
RUSTDESK_REGISTRATION_SHARE=''
usage() {
cat <<'EOF'
Usage:
sudo ./Enroll-SguLinuxDomainClient.sh --domain-controller <IPv4-or-FQDN> [options]
Required:
--domain-controller VALUE Fixed IPv4 address or DNS name of the AD controller.
Options:
--domain-name VALUE AD DNS domain (default: lci.lasalle.mx).
--domain-dns VALUE DNS server for the AD network (default: domain controller).
--computer-ou DN Destination computer OU.
--join-user USER AD account permitted to join computers (default: Administrator).
--computer-name NAME NetBIOS host name; its FQDN becomes NAME.DOMAIN.
--domain-interface IFACE Private NIC connected to the AD network.
--domain-address CIDR Static IPv4 address for --domain-interface, e.g. 192.168.50.12/24.
--allow-group GROUP Restrict Linux sign-in to this AD group after joining.
--enable-ssh Install, enable, and (when active) permit OpenSSH in the local firewall.
--enable-hyperv-enhanced-session
Install and configure XRDP over Hyper-V sockets for VMConnect.
--disable-rustdesk Do not install the managed RustDesk remote-support client.
--rustdesk-registration-share UNC
Override the protected controller SMB enrollment share.
--help Show this help.
Network safety:
--domain-interface and --domain-address must be supplied together. The selected
interface must not own the default route, so the command cannot replace the
Internet route while attaching a private AD NIC.
The AD password is requested interactively by realmd. It is never accepted as an
argument or written to a file, log, or command line.
EOF
}
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
need_command() {
command -v "$1" >/dev/null 2>&1 || fail "Required command is unavailable: $1"
}
while (($#)); do
case "$1" in
--domain-controller) DOMAIN_CONTROLLER=${2:?Missing value for --domain-controller}; shift 2 ;;
--domain-name) DOMAIN_NAME=${2:?Missing value for --domain-name}; shift 2 ;;
--domain-dns) DOMAIN_DNS=${2:?Missing value for --domain-dns}; shift 2 ;;
--computer-ou) COMPUTER_OU=${2:?Missing value for --computer-ou}; shift 2 ;;
--join-user) JOIN_USER=${2:?Missing value for --join-user}; shift 2 ;;
--computer-name) COMPUTER_NAME=${2:?Missing value for --computer-name}; shift 2 ;;
--domain-interface) DOMAIN_INTERFACE=${2:?Missing value for --domain-interface}; shift 2 ;;
--domain-address) DOMAIN_ADDRESS=${2:?Missing value for --domain-address}; shift 2 ;;
--allow-group) ALLOW_GROUP=${2:?Missing value for --allow-group}; shift 2 ;;
--enable-ssh) ENABLE_SSH=true; shift ;;
--enable-hyperv-enhanced-session) ENABLE_HYPERV_ENHANCED_SESSION=true; shift ;;
--disable-rustdesk) ENABLE_RUSTDESK=false; shift ;;
--rustdesk-registration-share) RUSTDESK_REGISTRATION_SHARE=${2:?Missing value for --rustdesk-registration-share}; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) fail "Unknown argument: $1. Use --help for usage." ;;
esac
done
[[ ${EUID} -eq 0 ]] || fail 'Run this command with sudo or as root.'
[[ -n $DOMAIN_CONTROLLER ]] || fail '--domain-controller is required.'
if [[ -z $DOMAIN_DNS ]]; then
DOMAIN_DNS=$DOMAIN_CONTROLLER
fi
if [[ -n $DOMAIN_INTERFACE || -n $DOMAIN_ADDRESS ]]; then
[[ -n $DOMAIN_INTERFACE && -n $DOMAIN_ADDRESS ]] || \
fail '--domain-interface and --domain-address must be supplied together.'
fi
if [[ -z $COMPUTER_NAME ]]; then
COMPUTER_NAME=$(hostname -s)
fi
COMPUTER_NAME=${COMPUTER_NAME^^}
HOST_FQDN="${COMPUTER_NAME,,}.${DOMAIN_NAME,,}"
install_prerequisites() {
local -a packages=()
if command -v apt-get >/dev/null 2>&1; then
packages=(realmd sssd sssd-tools adcli libnss-sss libpam-sss krb5-user packagekit samba-common-bin)
if [[ $ENABLE_SSH == true ]]; then
packages+=(openssh-server)
fi
if [[ $ENABLE_HYPERV_ENHANCED_SESSION == true ]]; then
packages+=(xrdp xorgxrdp ssl-cert)
# XRDP's Debian post-install script cannot replace a dangling
# certificate symlink left by an interrupted/older installation.
# Remove only dangling links so dpkg can recreate them safely.
local xrdp_link
for xrdp_link in /etc/xrdp/cert.pem /etc/xrdp/key.pem; do
if [[ -L $xrdp_link && ! -e $xrdp_link ]]; then
rm -f -- "$xrdp_link"
fi
done
fi
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y "${packages[@]}"
pam-auth-update --enable mkhomedir --force
return
fi
if command -v dnf >/dev/null 2>&1; then
packages=(realmd sssd sssd-tools adcli oddjob oddjob-mkhomedir samba-common-tools krb5-workstation)
if [[ $ENABLE_SSH == true ]]; then
packages+=(openssh-server)
fi
dnf install -y "${packages[@]}"
authselect select sssd with-mkhomedir --force
return
fi
fail 'Supported package managers are apt-get (Debian/Ubuntu) and dnf (RHEL/Fedora/Rocky/AlmaLinux).'
}
configure_private_ad_interface() {
[[ -n $DOMAIN_INTERFACE ]] || return 0
need_command nmcli
ip link show "$DOMAIN_INTERFACE" >/dev/null 2>&1 || \
fail "Network interface does not exist: $DOMAIN_INTERFACE"
local default_interface
default_interface=$(ip route show default | awk 'NR == 1 { print $5 }')
if [[ $default_interface == "$DOMAIN_INTERFACE" ]]; then
fail "Refusing to reconfigure $DOMAIN_INTERFACE because it owns the default route. Use the private AD NIC."
fi
local connection_name="SGU-Lab-AD-${DOMAIN_INTERFACE}"
if ! nmcli -t -f NAME connection show | grep -Fxq "$connection_name"; then
nmcli connection add type ethernet ifname "$DOMAIN_INTERFACE" con-name "$connection_name"
fi
nmcli connection modify "$connection_name" \
connection.autoconnect yes \
ipv4.method manual \
ipv4.addresses "$DOMAIN_ADDRESS" \
ipv4.dns "$DOMAIN_DNS" \
ipv4.dns-search "$DOMAIN_NAME" \
ipv4.never-default yes \
ipv6.method ignore
nmcli connection up "$connection_name"
}
enable_sssd_dyndns() {
[[ -n $DOMAIN_INTERFACE ]] || return 0
local configuration_directory='/etc/sssd/conf.d'
local configuration_path="${configuration_directory}/90-sgu-dyndns.conf"
local temporary_path
temporary_path=$(mktemp)
printf '%s\n' \
"[domain/${DOMAIN_NAME,,}]" \
"ad_hostname = ${HOST_FQDN}" \
'dyndns_update = True' \
'dyndns_update_ptr = True' \
"dyndns_iface = ${DOMAIN_INTERFACE}" \
'dyndns_refresh_interval = 43200' >"$temporary_path"
install -d -o root -g root -m 700 "$configuration_directory"
install -o root -g root -m 600 "$temporary_path" "$configuration_path"
rm -f "$temporary_path"
}
enable_short_domain_login_names() {
local configuration_path='/etc/sssd/sssd.conf'
[[ -f $configuration_path ]] || return 0
# Institutional account names (AL/AD/DO) are unique in this lab and are
# the identifiers users already know. Keep UPN logins valid while also
# allowing the short form in PAM applications such as XRDP/VMConnect.
if grep -Eq '^[[:space:]]*use_fully_qualified_names[[:space:]]*=' "$configuration_path"; then
sed -Ei 's/^[[:space:]]*use_fully_qualified_names[[:space:]]*=.*/use_fully_qualified_names = False/' \
"$configuration_path"
else
sed -Ei "/^\[domain\/${DOMAIN_NAME//./\\.}\]$/a use_fully_qualified_names = False" \
"$configuration_path"
fi
chmod 600 "$configuration_path"
}
configure_sssd_responder_mode() {
local configuration_path='/etc/sssd/sssd.conf'
[[ -f $configuration_path ]] || return 0
# realmd writes a persistent responder list, while recent Debian-family
# packages can enable the same NSS/PAM responders through systemd sockets.
# Running both modes makes the sockets fail at boot and can leave graphical
# PAM clients unable to contact SSSD reliably. Keep realmd's persistent
# responders and disable only the duplicate socket units when they exist.
local unit
for unit in sssd-nss.socket sssd-pam.socket sssd-pam-priv.socket; do
if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q "^${unit}"; then
systemctl disable --now "$unit" >/dev/null 2>&1 || true
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
fi
done
}
configure_graphical_domain_login() {
local sssd_configuration_directory='/etc/sssd/conf.d'
local temporary_sssd_configuration
local interactive_services='+lightdm,+cinnamon-screensaver'
if [[ $ENABLE_HYPERV_ENHANCED_SESSION == true ]]; then
interactive_services+=',+xrdp-sesman'
fi
temporary_sssd_configuration=$(mktemp)
printf '%s\n' \
"[domain/${DOMAIN_NAME,,}]" \
"ad_gpo_map_interactive = ${interactive_services}" >"$temporary_sssd_configuration"
install -d -o root -g root -m 700 "$sssd_configuration_directory"
install -o root -g root -m 600 "$temporary_sssd_configuration" \
"${sssd_configuration_directory}/91-sgu-graphical-login.conf"
rm -f "$temporary_sssd_configuration"
rm -f "${sssd_configuration_directory}/91-sgu-xrdp.conf"
# Do not disclose a list of local/domain accounts at the console. Slick
# Greeter still provides the explicit manual prompt needed for a first AD
# sign-in (AL/AD/DO identifier and password).
if [[ -d /etc/lightdm/lightdm.conf.d ]]; then
local temporary_lightdm_configuration
temporary_lightdm_configuration=$(mktemp)
printf '%s\n' \
'[Seat:*]' \
'greeter-show-manual-login=true' \
'greeter-hide-users=true' >"$temporary_lightdm_configuration"
install -o root -g root -m 644 "$temporary_lightdm_configuration" \
'/etc/lightdm/lightdm.conf.d/91-sgu-domain-login.conf'
rm -f "$temporary_lightdm_configuration"
fi
}
enable_ssh() {
[[ $ENABLE_SSH == true ]] || return 0
local service_name='sshd'
if systemctl list-unit-files ssh.service >/dev/null 2>&1; then
service_name='ssh'
fi
systemctl enable --now "$service_name"
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active'; then
ufw allow OpenSSH
elif command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
}
configure_hyperv_enhanced_session() {
[[ $ENABLE_HYPERV_ENHANCED_SESSION == true ]] || return 0
command -v xrdp >/dev/null 2>&1 || {
printf 'WARNING: XRDP is unavailable; Hyper-V Enhanced Session was not enabled.\n' >&2
return 0
}
local xrdp_configuration='/etc/xrdp/xrdp.ini'
[[ -f $xrdp_configuration ]] || {
printf 'WARNING: %s is missing; Hyper-V Enhanced Session was not enabled.\n' "$xrdp_configuration" >&2
return 0
}
# VMConnect uses AF_VSOCK rather than TCP. Only change the first occurrence,
# which belongs to [Globals]; later port entries describe XRDP backends.
sed -Ei '0,/^port=.*/s|^port=.*|port=vsock://-1:3389|' "$xrdp_configuration"
if grep -q '^use_vsock=' "$xrdp_configuration"; then
sed -Ei '0,/^use_vsock=.*/s|^use_vsock=.*|use_vsock=true|' "$xrdp_configuration"
else
sed -Ei '/^port=vsock:\/\/-1:3389/a use_vsock=true' "$xrdp_configuration"
fi
sed -Ei '0,/^security_layer=.*/s|^security_layer=.*|security_layer=rdp|' "$xrdp_configuration"
sed -Ei '0,/^crypt_level=.*/s|^crypt_level=.*|crypt_level=none|' "$xrdp_configuration"
# A clean Ubuntu installation can contain XRDP symlinks before the
# snake-oil certificate has actually been generated.
if [[ ! -s /etc/ssl/certs/ssl-cert-snakeoil.pem || \
! -s /etc/ssl/private/ssl-cert-snakeoil.key ]]; then
if command -v make-ssl-cert >/dev/null 2>&1; then
make-ssl-cert generate-default-snakeoil --force-overwrite
else
printf 'WARNING: make-ssl-cert is unavailable; XRDP certificate generation was skipped.\n' >&2
fi
fi
usermod -aG ssl-cert xrdp
# xrdp-sesman (root) and xrdp (the xrdp account) share /run/xrdp. Give the
# directory the shared group/mode so the second service can create its PID
# file instead of timing out while VMConnect remains at "Connecting".
local override_directory='/etc/systemd/system/xrdp-sesman.service.d'
local temporary_override
temporary_override=$(mktemp)
printf '%s\n' \
'[Service]' \
'Group=xrdp' \
'RuntimeDirectory=xrdp' \
'RuntimeDirectoryMode=0775' >"$temporary_override"
install -d -o root -g root -m 755 "$override_directory"
install -o root -g root -m 644 "$temporary_override" \
"${override_directory}/sgu-runtime.conf"
rm -f "$temporary_override"
systemctl daemon-reload
systemctl enable xrdp xrdp-sesman
systemctl restart xrdp
systemctl is-active --quiet xrdp
systemctl is-active --quiet xrdp-sesman
}
install_welcome_wallpaper() {
local source_directory="${SCRIPT_DIRECTORY}/welcome-wallpaper"
local source_script="${source_directory}/Set-SguWelcomeWallpaper.sh"
local source_image="${source_directory}/darkblue.jpg"
local install_directory='/usr/local/lib/sgu-welcome-wallpaper'
local configuration_directory='/etc/sgu'
local autostart_directory='/etc/xdg/autostart'
if [[ ! -r $source_script || ! -r $source_image ]]; then
printf 'WARNING: Welcome wallpaper assets are absent; domain enrollment will continue without desktop branding.\n' >&2
return 0
fi
# Desktop branding is optional and must never invalidate an otherwise valid
# domain join. Install its distribution-specific dependencies best-effort.
if command -v apt-get >/dev/null 2>&1; then
if ! apt-get install -y imagemagick ldap-utils fontconfig; then
printf 'WARNING: Could not install welcome wallpaper dependencies; enrollment remains valid.\n' >&2
return 0
fi
elif command -v dnf >/dev/null 2>&1; then
if ! dnf install -y ImageMagick openldap-clients fontconfig; then
printf 'WARNING: Could not install welcome wallpaper dependencies; enrollment remains valid.\n' >&2
return 0
fi
fi
install -d -o root -g root -m 755 "$install_directory" "$configuration_directory" "$autostart_directory"
install -o root -g root -m 755 "$source_script" "${install_directory}/Set-SguWelcomeWallpaper.sh"
install -o root -g root -m 644 "$source_image" "${install_directory}/darkblue.jpg"
if compgen -G "${source_directory}/fonts/*.[ot]tf" >/dev/null; then
install -d -o root -g root -m 755 "${install_directory}/fonts"
install -o root -g root -m 644 "${source_directory}"/fonts/*.[ot]tf "${install_directory}/fonts/"
fi
local base_dn=''
local component
IFS='.' read -ra domain_components <<<"$DOMAIN_NAME"
for component in "${domain_components[@]}"; do
if [[ -n $base_dn ]]; then
base_dn+=','
fi
base_dn+="DC=${component}"
done
local temporary_configuration
temporary_configuration=$(mktemp)
printf 'DOMAIN_CONTROLLER=%q\nDOMAIN_NAME=%q\nBASE_DN=%q\n' \
"$DOMAIN_CONTROLLER" "$DOMAIN_NAME" "$base_dn" >"$temporary_configuration"
install -o root -g root -m 644 "$temporary_configuration" \
"${configuration_directory}/welcome-wallpaper.conf"
rm -f "$temporary_configuration"
local temporary_autostart
temporary_autostart=$(mktemp)
cat >"$temporary_autostart" <<'EOF'
[Desktop Entry]
Type=Application
Name=SGU welcome wallpaper
Comment=Generate a personalized La Salle laboratory welcome wallpaper
Exec=/usr/local/lib/sgu-welcome-wallpaper/Set-SguWelcomeWallpaper.sh
Terminal=false
NoDisplay=true
X-GNOME-Autostart-enabled=true
X-Cinnamon-Autostart-enabled=true
EOF
install -o root -g root -m 644 "$temporary_autostart" \
"${autostart_directory}/sgu-welcome-wallpaper.desktop"
rm -f "$temporary_autostart"
}
install_managed_rustdesk() {
[[ $ENABLE_RUSTDESK == true ]] || return 0
local installer="${SCRIPT_DIRECTORY}/Install-SguLinuxRustDeskClient.sh"
if [[ ! -r $installer ]]; then
fail 'The managed Linux RustDesk installer is missing from this bootstrap package.'
fi
local -a parameters=(--domain-name "$DOMAIN_NAME")
if [[ -n $RUSTDESK_REGISTRATION_SHARE ]]; then
parameters+=(--registration-share "$RUSTDESK_REGISTRATION_SHARE")
fi
bash "$installer" "${parameters[@]}"
}
verify_domain_connectivity() {
need_command getent
getent ahostsv4 "$DOMAIN_CONTROLLER" >/dev/null || \
fail "Could not resolve the domain controller: $DOMAIN_CONTROLLER"
if command -v resolvectl >/dev/null 2>&1; then
resolvectl query --type=SRV "_ldap._tcp.dc._msdcs.${DOMAIN_NAME}" >/dev/null || \
fail "AD DNS does not provide _ldap._tcp.dc._msdcs.${DOMAIN_NAME}."
fi
}
configure_private_ad_interface
install_prerequisites
verify_domain_connectivity
# Establish a canonical host name before adcli creates or refreshes the
# computer object, SPNs, and keytab entries.
hostnamectl set-hostname "$HOST_FQDN"
if realm list --name-only 2>/dev/null | grep -Fxqi "$DOMAIN_NAME"; then
printf 'Computer is already joined to %s; validating and refreshing configuration.\n' "$DOMAIN_NAME"
else
realm discover "$DOMAIN_NAME" >/dev/null
printf 'Joining %s. realmd will request the password for %s interactively.\n' "$DOMAIN_NAME" "$JOIN_USER"
realm join \
--membership-software=adcli \
--client-software=sssd \
--computer-ou="$COMPUTER_OU" \
--user="$JOIN_USER" \
"$DOMAIN_NAME"
fi
enable_sssd_dyndns
enable_short_domain_login_names
configure_sssd_responder_mode
configure_graphical_domain_login
systemctl enable --now sssd
sssctl config-check
systemctl restart sssd
adcli update --domain="$DOMAIN_NAME" --host-fqdn="$HOST_FQDN" --computer-name="$COMPUTER_NAME"
adcli testjoin --domain="$DOMAIN_NAME"
if [[ -n $ALLOW_GROUP ]]; then
realm deny --all
realm permit --groups "$ALLOW_GROUP"
fi
enable_ssh
configure_hyperv_enhanced_session
install_welcome_wallpaper
install_managed_rustdesk
printf '\nLinux enrollment completed.\n'
printf ' Host: %s\n' "$HOST_FQDN"
printf ' Domain: %s\n' "$DOMAIN_NAME"
printf ' OU: %s\n' "$COMPUTER_OU"
printf ' Login format: %%U@%s\n' "$DOMAIN_NAME"
printf ' Welcome wallpaper: generated at each graphical sign-in when the desktop is supported.\n'
if [[ $ENABLE_RUSTDESK == true ]]; then
printf ' RustDesk: configured and registered in the controller inventory.\n'
fi
realm list