#!/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 usage() { cat <<'EOF' Usage: sudo ./Enroll-SguLinuxDomainClient.sh --domain-controller [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. --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 ;; --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" # Slick Greeter normally shows only the last/local account tile. Expose a # manual user-name prompt so a first-time AD user can enter AL/AD/DO IDs. 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=false' >"$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" } 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 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' realm list