Add managed RustDesk enrollment for Linux clients

This commit is contained in:
2026-09-08 08:27:11 -06:00
parent 675db6bc1e
commit ef9be0f897
7 changed files with 566 additions and 9 deletions
+23 -5
View File
@@ -1,6 +1,6 @@
# Enrolamiento de clientes Linux
El enrolador Linux incorpora una estación Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux a `lci.lasalle.mx` mediante `realmd`, `adcli`, Kerberos y SSSD. No instala el Credential Provider de Windows: Linux conserva su propio inicio de sesión PAM/SSSD.
El enrolador Linux incorpora una estación Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux a `lci.lasalle.mx` mediante `realmd`, `adcli`, Kerberos y SSSD. No instala el Credential Provider de Windows: Linux conserva su propio inicio de sesión PAM/SSSD. También instala RustDesk, lo dirige al servidor RustDesk autoalojado y registra el ID y la contraseña de acceso desatendido en el inventario protegido del controlador de dominio.
La contraseña de la cuenta autorizada para unir equipos se solicita directamente por `realmd`. Nunca se acepta como argumento, ni se escribe en archivos, registros o la línea de comandos.
@@ -44,9 +44,10 @@ de sesión de GDM también debe permanecer deshabilitado.
5. Activa SSSD, creación de directorio personal mediante PAM y valida la contraseña de la cuenta de equipo con `adcli testjoin`. En distribuciones que habilitan los respondedores NSS/PAM de SSSD tanto en `sssd.conf` como mediante sockets de systemd, desactiva los sockets duplicados para evitar una colisión al arrancar.
6. Cuando se proporcionó la NIC privada, activa actualizaciones DNS dinámicas de SSSD en esa interfaz.
7. Con `--enable-ssh`, instala y habilita OpenSSH y abre únicamente el servicio SSH cuando el firewall local ya está activo.
8. Registra `lightdm` y `cinnamon-screensaver` como inicios interactivos ante las políticas GPO de SSSD. En equipos con LightDM también muestra el ingreso manual, necesario para el primer acceso de un usuario del dominio.
8. Registra `lightdm` y `cinnamon-screensaver` como inicios interactivos ante las políticas GPO de SSSD. En equipos con LightDM oculta la lista de cuentas y conserva únicamente el ingreso manual: usuario y contraseña, necesario para el primer acceso de un usuario del dominio.
9. Con `--enable-hyperv-enhanced-session`, configura XRDP sobre Hyper-V sockets para que VMConnect pueda usar **Sesión mejorada**, repara certificados incompletos, registra `xrdp-sesman` en el mismo mapa interactivo y valida ambos servicios XRDP.
10. Instala el fondo azul, las fuentes Indivisa y un autoinicio compatible con Cinnamon, GNOME y XFCE. En cada sesión gráfica genera el saludo con el nombre del usuario y la ubicación/OU del equipo obtenidas de AD.
11. Instala RustDesk 1.4.9 desde el paquete oficial comprobado, configura exclusivamente el servidor institucional y crea una contraseña aleatoria de acceso desatendido. La contraseña no se muestra en Linux: viaja cifrada con la clave pública del controlador y éste la conserva mediante su inventario protegido.
El objeto de equipo aparece como `NOMBRE` en `OU=Laboratorio`. SSSD registra su registro A cuando la actualización DNS dinámica está activada.
@@ -64,9 +65,9 @@ El formato UPN explícito también permanece disponible:
usuario@lci.lasalle.mx
```
En Linux Mint, selecciona **Iniciar sesión** o el campo manual de usuario en
Slick Greeter y escribe la clave corta. El mosaico con el nombre de una cuenta
local no cambia de identidad al escribir únicamente otra contraseña.
En Linux Mint aparece únicamente el ingreso manual. Escribe la clave corta y
su contraseña; no se muestra una lista ni mosaicos de cuentas locales o del
dominio.
La primera sesión crea `/home/usuario@lci.lasalle.mx`. El valor predeterminado de SSSD conserva credenciales para desconexiones breves de la red; las contraseñas no son administradas ni almacenadas por el Auth Broker.
@@ -89,8 +90,25 @@ getent passwd 'usuario@lci.lasalle.mx'
sudo sssctl domain-status lci.lasalle.mx
sudo sssctl user-checks usuario -a acct -s lightdm
sudo sssctl user-checks usuario -a acct -s cinnamon-screensaver
systemctl is-active rustdesk
sudo cat /var/lib/sgu/rustdesk/device.json
```
En el controlador de dominio, el mismo inventario protegido usado por Windows
muestra el ID de un cliente Linux y, únicamente bajo solicitud explícita de un
administrador, su contraseña de RustDesk:
```powershell
& C:\ProgramData\SGU\RustDesk\Get-SguRustDeskDevice.ps1
& C:\ProgramData\SGU\RustDesk\Get-SguRustDeskDevice.ps1 `
-ComputerName ALEX-LMINT -RevealPassword
```
Usa `--disable-rustdesk` sólo cuando una estación deba quedar expresamente sin
soporte remoto. `--rustdesk-registration-share` permite especificar el UNC del
controlador cuando una topología de DNS no puede resolver automáticamente el
controlador de dominio.
Para sacar un equipo del dominio de forma explícita:
```bash
+28 -3
View File
@@ -20,6 +20,8 @@ COMPUTER_NAME=''
ALLOW_GROUP=''
ENABLE_SSH=false
ENABLE_HYPERV_ENHANCED_SESSION=false
ENABLE_RUSTDESK=true
RUSTDESK_REGISTRATION_SHARE=''
usage() {
cat <<'EOF'
@@ -41,6 +43,9 @@ Options:
--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:
@@ -75,6 +80,8 @@ while (($#)); do
--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
@@ -236,15 +243,16 @@ configure_graphical_domain_login() {
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.
# 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=false' >"$temporary_lightdm_configuration"
'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"
@@ -397,6 +405,19 @@ EOF
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 || \
@@ -446,6 +467,7 @@ fi
enable_ssh
configure_hyperv_enhanced_session
install_welcome_wallpaper
install_managed_rustdesk
printf '\nLinux enrollment completed.\n'
printf ' Host: %s\n' "$HOST_FQDN"
@@ -453,4 +475,7 @@ 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
+14 -1
View File
@@ -353,7 +353,9 @@ foreach ($requiredPath in @(
(Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1'),
(Join-Path $scriptsRoot 'Install-SguDomainMonitoring.ps1'),
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
(Join-Path $scriptsRoot 'Install-SguRustDeskLinuxEnrollment.ps1'),
(Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1'),
(Join-Path $scriptsRoot 'Invoke-SguRustDeskLinuxRegistrationProcessor.ps1'),
(Join-Path $scriptsRoot 'Invoke-SguMonitoringMaintenance.ps1'),
(Join-Path $scriptsRoot 'Get-SguRustDeskDevice.ps1'),
(Join-Path $scriptsRoot 'Get-SguUsageReport.ps1'),
@@ -643,10 +645,19 @@ $rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') `
-FirewallRemoteAddress $privateSubnet
$rustDeskManagementRoot = Join-Path $env:ProgramData 'SGU\RustDesk'
New-Item -ItemType Directory -Path $rustDeskManagementRoot -Force | Out-Null
foreach ($scriptName in @('Register-SguRustDeskDevice.ps1', 'Get-SguRustDeskDevice.ps1')) {
foreach ($scriptName in @(
'Register-SguRustDeskDevice.ps1',
'Get-SguRustDeskDevice.ps1',
'Install-SguRustDeskLinuxEnrollment.ps1',
'Invoke-SguRustDeskLinuxRegistrationProcessor.ps1')) {
Copy-Item -LiteralPath (Join-Path $scriptsRoot $scriptName) `
-Destination (Join-Path $rustDeskManagementRoot $scriptName) -Force
}
$rustDeskLinuxEnrollment = & (Join-Path $rustDeskManagementRoot 'Install-SguRustDeskLinuxEnrollment.ps1') `
-DomainName $DomainName `
-ServerAddress $rustDeskDnsName `
-ServerPublicKey $rustDeskServer.PublicKey `
-ProcessorScriptPath (Join-Path $rustDeskManagementRoot 'Invoke-SguRustDeskLinuxRegistrationProcessor.ps1')
$rustDeskServerClient = & (Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1') `
-ServerAddress $rustDeskDnsName `
-ServerPublicKey $rustDeskServer.PublicKey
@@ -680,6 +691,7 @@ $validation = [ordered]@{
RustDeskHbbrTask = $rustDeskServer.HbbrTask
RustDeskHbbsListening = $rustDeskServer.HbbsListening
RustDeskHbbrListening = $rustDeskServer.HbbrListening
RustDeskLinuxRegistrationTask = (Get-ScheduledTask -TaskName $rustDeskLinuxEnrollment.RegistrationTask).State.ToString()
RustDeskServerClientId = $rustDeskServerClient.RustDeskId
EventCollector = (Get-Service Wecsvc).Status.ToString()
EventSubscription = @(& wecutil.exe enum-subscription) -contains 'SGU-Lab-Monitoring'
@@ -698,6 +710,7 @@ if ($validation.BrokerService -ne 'Running' -or
$validation.RemoteDesktop -ne 'Running' -or
$validation.RustDeskHbbsTask -ne 'Running' -or
$validation.RustDeskHbbrTask -ne 'Running' -or
$validation.RustDeskLinuxRegistrationTask -notin @('Ready', 'Running') -or
-not $validation.RustDeskHbbsListening -or
-not $validation.RustDeskHbbrListening -or
$validation.EventCollector -ne 'Running' -or
+279
View File
@@ -0,0 +1,279 @@
#!/usr/bin/env bash
# Install-SguLinuxRustDeskClient.sh
#
# Installs/configures a RustDesk client on an AD-joined Linux workstation and
# registers its randomly generated unattended-access credential with the
# protected inventory on the SGU domain controller. The credential is never
# emitted to stdout and is sent to the controller only in an RSA-OAEP envelope.
set -Eeuo pipefail
IFS=$'\n\t'
DOMAIN_NAME='lci.lasalle.mx'
REGISTRATION_SHARE=''
STATE_ROOT='/var/lib/sgu/rustdesk'
CLIENT_VERSION='1.4.9'
DOWNLOAD_URI='https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-x86_64.deb'
EXPECTED_SHA256='7244BA47C40E804172044BFBE659467C54CE46554C98E78C8C0406F1D612FDA3'
usage() {
cat <<'EOF'
Usage:
sudo ./Install-SguLinuxRustDeskClient.sh [options]
Options:
--domain-name VALUE AD DNS domain (default: lci.lasalle.mx).
--registration-share UNC SMB enrollment share. Defaults to the first
AD domain controller's SGU RustDesk share.
--state-root PATH Root-owned local RustDesk state directory.
--help Show this help.
The computer must already be joined to Active Directory. The script uses the
machine keytab to authenticate to the enrollment share, configures the
self-hosted RustDesk server, creates an unattended-access password, and waits
for the controller to confirm protected inventory registration.
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-name) DOMAIN_NAME=${2:?Missing value for --domain-name}; shift 2 ;;
--registration-share) REGISTRATION_SHARE=${2:?Missing value for --registration-share}; shift 2 ;;
--state-root) STATE_ROOT=${2:?Missing value for --state-root}; 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.'
[[ -r /etc/krb5.keytab ]] || fail 'The AD machine keytab is missing. Join the computer to the domain first.'
install_prerequisites() {
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y curl openssl smbclient dnsutils
return
fi
if command -v dnf >/dev/null 2>&1; then
dnf install -y curl openssl samba-client bind-utils
return
fi
fail 'RustDesk enrollment supports apt-get (Debian/Ubuntu) and dnf (RHEL/Fedora/Rocky/AlmaLinux).'
}
resolve_registration_share() {
if [[ -n $REGISTRATION_SHARE ]]; then
return
fi
local controller
controller=$(host -t SRV "_ldap._tcp.dc._msdcs.${DOMAIN_NAME}" 2>/dev/null |
awk '/SRV record/ { print $NF; exit }' | sed 's/\.$//')
[[ -n $controller ]] || controller=$DOMAIN_NAME
REGISTRATION_SHARE="//${controller}/SGU-RustDesk-Enrollment$"
}
initialize_machine_kerberos() {
local principal
# adcli places the machine-account principal in the keytab. Prefer it to
# host/FQDN: some AD deployments retain the latter locally even when its
# SPN is not accepted by the KDC for an initial ticket request.
principal=$(klist -k /etc/krb5.keytab 2>/dev/null |
awk '$NF ~ /^[^/@]+\$@/ { print $NF; exit }')
if [[ -z $principal ]]; then
principal=$(klist -k /etc/krb5.keytab 2>/dev/null |
awk '$NF ~ /^host\// { print $NF; exit }')
fi
[[ -n $principal ]] || fail 'No host principal was found in /etc/krb5.keytab.'
KRB5CCNAME="FILE:${STATE_ROOT}/machine-krb5cc"
export KRB5CCNAME
rm -f -- "${KRB5CCNAME#FILE:}"
kinit -k -t /etc/krb5.keytab "$principal"
}
smb_get() {
local remote_name=$1
local local_path=$2
smbclient --use-kerberos=required -N "$REGISTRATION_SHARE" \
-c "get ${remote_name} ${local_path}" >/dev/null
}
smb_put() {
local local_path=$1
local remote_name=$2
smbclient --use-kerberos=required -N "$REGISTRATION_SHARE" \
-c "put ${local_path} ${remote_name}" >/dev/null
}
install_rustdesk() {
local installer_path="${STATE_ROOT}/rustdesk-${CLIENT_VERSION}-x86_64.deb"
local installed_version=''
if command -v rustdesk >/dev/null 2>&1; then
installed_version=$(rustdesk --version 2>/dev/null | head -n 1 || true)
fi
if [[ $installed_version != *"${CLIENT_VERSION}"* ]]; then
curl --fail --location --proto '=https' --tlsv1.2 \
--output "$installer_path" "$DOWNLOAD_URI"
local actual_hash
actual_hash=$(sha256sum "$installer_path" | awk '{ print toupper($1) }')
[[ $actual_hash == "$EXPECTED_SHA256" ]] || fail 'RustDesk package SHA-256 verification failed.'
if command -v apt-get >/dev/null 2>&1; then
dpkg -i "$installer_path" || apt-get install -f -y
else
fail 'The pinned RustDesk package is currently provided as a Debian package only.'
fi
fi
need_command rustdesk
systemctl enable rustdesk
}
read_server_configuration() {
local configuration_path="${STATE_ROOT}/rustdesk-client.json"
smb_get 'rustdesk-client.json' "$configuration_path"
RUSTDESK_SERVER_ADDRESS=$(python3 - "$configuration_path" <<'PY'
import json
import sys
with open(sys.argv[1], encoding='utf-8') as source:
value = json.load(source)
address = value.get('ServerAddress', '')
key = value.get('ServerPublicKey', '')
if not isinstance(address, str) or not isinstance(key, str) or not address or not key:
raise SystemExit('The controller RustDesk configuration is incomplete.')
print(address)
PY
)
RUSTDESK_SERVER_PUBLIC_KEY=$(python3 - "$configuration_path" <<'PY'
import json
import sys
with open(sys.argv[1], encoding='utf-8') as source:
print(json.load(source)['ServerPublicKey'])
PY
)
}
configure_rustdesk() {
local configuration
configuration=$(cat <<EOF
rendezvous_server = '${RUSTDESK_SERVER_ADDRESS}:21116'
nat_type = 1
serial = 0
[options]
custom-rendezvous-server = '${RUSTDESK_SERVER_ADDRESS}:21116'
relay-server = '${RUSTDESK_SERVER_ADDRESS}:21117'
key = '${RUSTDESK_SERVER_PUBLIC_KEY}'
EOF
)
# The Linux service runs as root. Keep the system copy as a diagnostic and
# the root profile copy as the configuration consumed by the service.
install -d -o root -g root -m 700 /root/.config/rustdesk /etc/rustdesk
printf '%s\n' "$configuration" | install -o root -g root -m 600 /dev/stdin \
/root/.config/rustdesk/RustDesk2.toml
printf '%s\n' "$configuration" | install -o root -g root -m 644 /dev/stdin \
/etc/rustdesk/RustDesk2.toml
systemctl restart rustdesk
systemctl is-active --quiet rustdesk || fail 'The RustDesk service did not start.'
}
set_access_password() {
local secret_path="${STATE_ROOT}/access.secret"
if [[ -r $secret_path ]]; then
ACCESS_PASSWORD=$(<"$secret_path")
else
ACCESS_PASSWORD=$(openssl rand -hex 24)
umask 077
printf '%s' "$ACCESS_PASSWORD" >"$secret_path"
chmod 600 "$secret_path"
fi
rustdesk --password "$ACCESS_PASSWORD" >/dev/null
RUSTDESK_ID=$(rustdesk --get-id 2>/dev/null | tail -n 1 | tr -d '[:space:]')
[[ $RUSTDESK_ID =~ ^[0-9]+$ ]] || fail "RustDesk returned an invalid device ID: $RUSTDESK_ID"
}
register_with_controller() {
local certificate_path="${STATE_ROOT}/registration-public.cer"
local public_key_path="${STATE_ROOT}/registration-public.pem"
local request_path="${STATE_ROOT}/registration.request"
local encrypted_request_path="${STATE_ROOT}/registration.request.enc"
local result_path="${STATE_ROOT}/registration.result.json"
local request_id
request_id=$(cat /proc/sys/kernel/random/uuid)
local computer_name
computer_name=$(hostname -s | tr '[:lower:]' '[:upper:]')
[[ $computer_name =~ ^[A-Z0-9][A-Z0-9-]{0,62}$ ]] || fail 'The Linux computer name is not valid for RustDesk inventory.'
smb_get 'registration-public.cer' "$certificate_path"
openssl x509 -inform DER -in "$certificate_path" -pubkey -noout >"$public_key_path"
chmod 600 "$public_key_path"
# AccessPassword is hexadecimal and the other values are constrained, so
# this compact JSON is safe to construct without echoing sensitive data.
printf '{"ComputerName":"%s","RustDeskId":"%s","AccessPassword":"%s","RequestId":"%s"}' \
"$computer_name" "$RUSTDESK_ID" "$ACCESS_PASSWORD" "$request_id" >"$request_path"
openssl pkeyutl -encrypt -pubin -inkey "$public_key_path" \
-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 \
-in "$request_path" -out "$encrypted_request_path"
chmod 600 "$request_path" "$encrypted_request_path"
local remote_request="${computer_name}-${request_id}.request"
smb_put "$encrypted_request_path" "Requests/${remote_request}"
local attempt=0
while ((attempt < 18)); do
rm -f -- "$result_path"
if smb_get "Requests/${request_id}.result.json" "$result_path" 2>/dev/null; then
python3 - "$result_path" "$computer_name" "$RUSTDESK_ID" <<'PY'
import json
import sys
with open(sys.argv[1], encoding='utf-8') as source:
result = json.load(source)
if result.get('Status') != 'Registered':
raise SystemExit(result.get('Error', 'The controller rejected the RustDesk registration.'))
if result.get('ComputerName') != sys.argv[2] or result.get('RustDeskId') != sys.argv[3]:
raise SystemExit('The controller response did not match this computer or RustDesk ID.')
PY
rm -f -- "$request_path" "$encrypted_request_path" "$public_key_path" "$certificate_path" "$result_path"
return
fi
sleep 5
((attempt+=1))
done
fail 'RustDesk was configured locally, but the domain controller did not confirm inventory registration within 90 seconds.'
}
install -d -o root -g root -m 700 "$STATE_ROOT"
trap 'if [[ -n ${KRB5CCNAME:-} ]]; then rm -f -- "${KRB5CCNAME#FILE:}"; fi' EXIT
install_prerequisites
resolve_registration_share
initialize_machine_kerberos
install_rustdesk
read_server_configuration
configure_rustdesk
set_access_password
register_with_controller
device_path="${STATE_ROOT}/device.json"
printf '{"ComputerName":"%s","RustDeskId":"%s","ServerAddress":"%s","ConfiguredAt":"%s"}\n' \
"$(hostname -s | tr '[:lower:]' '[:upper:]')" "$RUSTDESK_ID" "$RUSTDESK_SERVER_ADDRESS" \
"$(date --iso-8601=seconds)" >"$device_path"
chmod 600 "$device_path"
printf 'RustDesk enrollment completed. ID: %s\n' "$RUSTDESK_ID"
@@ -0,0 +1,128 @@
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9.-]*$')]
[string]$DomainName,
[Parameter(Mandatory)]
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9.-]*$')]
[string]$ServerAddress,
[Parameter(Mandatory)]
[ValidatePattern('^[A-Za-z0-9+/=]+$')]
[string]$ServerPublicKey,
[string]$RegistrationShareName = 'SGU-RustDesk-Enrollment$',
[string]$DataRoot = "$env:ProgramData\SGU\RustDesk\LinuxEnrollment",
[string]$ProcessorScriptPath = (Join-Path $PSScriptRoot 'Invoke-SguRustDeskLinuxRegistrationProcessor.ps1')
)
$ErrorActionPreference = 'Stop'
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Only a local administrator can install Linux RustDesk enrollment.'
}
}
function Get-EnrollmentCertificate {
param([Parameter(Mandatory)][string]$FriendlyName)
$certificate = Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object FriendlyName -eq $FriendlyName |
Where-Object HasPrivateKey |
Select-Object -First 1
if (-not $certificate) {
$certificate = New-SelfSignedCertificate `
-Subject 'CN=SGU RustDesk Linux enrollment' `
-FriendlyName $FriendlyName `
-CertStoreLocation 'Cert:\LocalMachine\My' `
-KeyAlgorithm RSA `
-KeyLength 3072 `
-KeyUsage KeyEncipherment,DigitalSignature `
-NotAfter (Get-Date).AddYears(5)
}
return $certificate
}
function Set-EnrollmentDirectoryAcl {
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$DomainNetbiosName
)
New-Item -ItemType Directory -Path $Path -Force | Out-Null
$arguments = @(
"`"$Path`"", '/inheritance:r',
'/grant:r', 'SYSTEM:(OI)(CI)(F)',
'BUILTIN\Administrators:(OI)(CI)(F)',
"$DomainNetbiosName\Domain Computers:(OI)(CI)(M)"
)
& icacls.exe @arguments | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Could not secure the Linux RustDesk enrollment directory $Path."
}
}
Assert-Administrator
Import-Module ActiveDirectory -ErrorAction Stop
if (-not (Test-Path -LiteralPath $ProcessorScriptPath -PathType Leaf)) {
throw "The Linux RustDesk registration processor is missing: $ProcessorScriptPath"
}
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install the protected Linux RustDesk enrollment endpoint')) {
return
}
$domain = Get-ADDomain -Identity $DomainName
$publicRoot = Join-Path $DataRoot 'Public'
$requestsRoot = Join-Path $publicRoot 'Requests'
$archiveRoot = Join-Path $publicRoot 'Archive'
$rejectedRoot = Join-Path $publicRoot 'Rejected'
foreach ($path in @($DataRoot, $publicRoot, $requestsRoot, $archiveRoot, $rejectedRoot)) {
Set-EnrollmentDirectoryAcl -Path $path -DomainNetbiosName $domain.NetBIOSName
}
$certificate = Get-EnrollmentCertificate -FriendlyName 'SGU RustDesk Linux enrollment'
$publicCertificatePath = Join-Path $publicRoot 'registration-public.cer'
Export-Certificate -Cert $certificate -FilePath $publicCertificatePath -Force | Out-Null
$clientConfiguration = [ordered]@{
ServerAddress = $ServerAddress
ServerPublicKey = $ServerPublicKey
RegistrationShare = "\\$env:COMPUTERNAME\$RegistrationShareName"
UpdatedAt = (Get-Date).ToString('o')
}
[IO.File]::WriteAllText((Join-Path $publicRoot 'rustdesk-client.json'),
($clientConfiguration | ConvertTo-Json), [Text.UTF8Encoding]::new($false))
$share = Get-SmbShare -Name $RegistrationShareName -ErrorAction SilentlyContinue
if (-not $share) {
New-SmbShare -Name $RegistrationShareName -Path $publicRoot `
-FullAccess @('SYSTEM', 'BUILTIN\Administrators') `
-ChangeAccess "$($domain.NetBIOSName)\Domain Computers" | Out-Null
}
elseif ($share.Path -ne $publicRoot) {
throw "The existing SMB share $RegistrationShareName points to $($share.Path), not $publicRoot."
}
$installedProcessor = Join-Path $DataRoot 'Invoke-SguRustDeskLinuxRegistrationProcessor.ps1'
Copy-Item -LiteralPath $ProcessorScriptPath -Destination $installedProcessor -Force
$processorArguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$installedProcessor`" -DataRoot `"$DataRoot`" -CertificateThumbprint $($certificate.Thumbprint)"
$action = New-ScheduledTaskAction -Execute (Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe') `
-Argument $processorArguments
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 1) -RepetitionDuration (New-TimeSpan -Days 3650)
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'SGU-RustDesk-LinuxRegistration' -Action $action -Trigger $trigger `
-Principal $principal -Description 'Registers encrypted RustDesk credentials sent by domain-joined Linux computers.' -Force | Out-Null
New-NetFirewallRule -DisplayName 'SGU RustDesk Linux enrollment SMB' -Group 'SGU RustDesk' `
-Direction Inbound -Action Allow -Protocol TCP -LocalPort 445 -Profile Domain -ErrorAction SilentlyContinue | Out-Null
[pscustomobject]@{
RegistrationShare = "\\$env:COMPUTERNAME\$RegistrationShareName"
PublicCertificatePath = $publicCertificatePath
RegistrationTask = 'SGU-RustDesk-LinuxRegistration'
CertificateThumbprint = $certificate.Thumbprint
}
@@ -0,0 +1,90 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$DataRoot,
[Parameter(Mandatory)]
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
[string]$CertificateThumbprint
)
$ErrorActionPreference = 'Stop'
$requestsRoot = Join-Path $DataRoot 'Public\Requests'
$archiveRoot = Join-Path $DataRoot 'Public\Archive'
$rejectedRoot = Join-Path $DataRoot 'Public\Rejected'
$registrationScript = Join-Path $env:ProgramData 'SGU\RustDesk\Register-SguRustDeskDevice.ps1'
function Write-Result {
param(
[Parameter(Mandatory)][string]$RequestId,
[Parameter(Mandatory)][hashtable]$Value
)
$path = Join-Path $requestsRoot "$RequestId.result.json"
[IO.File]::WriteAllText($path, ($Value | ConvertTo-Json), [Text.UTF8Encoding]::new($false))
}
function Get-ComputerNameFromOwner {
param([Parameter(Mandatory)][string]$Owner)
if ($Owner -notmatch '^[^\\]+\\(?<Name>[A-Za-z0-9][A-Za-z0-9-]{0,62})\$$') {
throw 'The request file owner is not an Active Directory computer account.'
}
return $Matches.Name.ToUpperInvariant()
}
if (-not (Test-Path -LiteralPath $registrationScript -PathType Leaf)) {
throw "The RustDesk inventory registration script is missing: $registrationScript"
}
Import-Module ActiveDirectory -ErrorAction Stop
$certificate = Get-Item -LiteralPath "Cert:\LocalMachine\My\$CertificateThumbprint" -ErrorAction Stop
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate)
if (-not $rsa) {
throw 'The Linux RustDesk enrollment certificate does not have an RSA private key.'
}
New-Item -ItemType Directory -Path $requestsRoot, $archiveRoot, $rejectedRoot -Force | Out-Null
Get-ChildItem -LiteralPath $requestsRoot -Filter '*.request' -File | ForEach-Object {
$requestFile = $_
$requestIdMatch = [regex]::Match($requestFile.BaseName,
'(?<Id>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$')
if (-not $requestIdMatch.Success) {
Move-Item -LiteralPath $requestFile.FullName -Destination (Join-Path $rejectedRoot $requestFile.Name) -Force
return
}
$requestId = $requestIdMatch.Groups['Id'].Value
try {
$ownerComputerName = Get-ComputerNameFromOwner -Owner (Get-Acl -LiteralPath $requestFile.FullName).Owner
$plainText = [Text.Encoding]::UTF8.GetString($rsa.Decrypt(
[IO.File]::ReadAllBytes($requestFile.FullName),
[Security.Cryptography.RSAEncryptionPadding]::OaepSHA256))
$request = $plainText | ConvertFrom-Json -ErrorAction Stop
$computerName = [string]$request.ComputerName
$rustDeskId = [string]$request.RustDeskId
$accessPassword = [string]$request.AccessPassword
$declaredRequestId = [string]$request.RequestId
if ($computerName -notmatch '^[A-Za-z0-9][A-Za-z0-9-]{0,62}$' -or
$computerName.ToUpperInvariant() -ne $ownerComputerName -or
$rustDeskId -notmatch '^\d+$' -or
$accessPassword.Length -lt 12 -or
$declaredRequestId -notmatch '^[0-9a-fA-F-]{36}$') {
throw 'The encrypted Linux RustDesk registration payload is invalid.'
}
Get-ADComputer -Identity $ownerComputerName -ErrorAction Stop | Out-Null
& $registrationScript -ComputerName $ownerComputerName -RustDeskId $rustDeskId `
-AccessPassword $accessPassword -Confirm:$false | Out-Null
Write-Result -RequestId $declaredRequestId -Value @{
Status = 'Registered'
ComputerName = $ownerComputerName
RustDeskId = $rustDeskId
RegisteredAt = (Get-Date).ToString('o')
}
Move-Item -LiteralPath $requestFile.FullName -Destination (Join-Path $archiveRoot $requestFile.Name) -Force
}
catch {
$safeError = $_.Exception.Message -replace '(?i)password[^\r\n]*', 'credential validation failed'
Write-Result -RequestId $requestId -Value @{
Status = 'Rejected'
Error = $safeError
}
Move-Item -LiteralPath $requestFile.FullName -Destination (Join-Path $rejectedRoot $requestFile.Name) -Force -ErrorAction SilentlyContinue
}
}
+4
View File
@@ -143,6 +143,8 @@ Compress-Archive -Path (Join-Path $clientRoot '*') -DestinationPath $clientZip `
# Linux administrator never receives Windows binaries or certificate material.
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Enroll-SguLinuxDomainClient.sh') `
-Destination (Join-Path $linuxClientRoot 'Enroll-SguLinuxDomainClient.sh')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Install-SguLinuxRustDeskClient.sh') `
-Destination (Join-Path $linuxClientRoot 'Install-SguLinuxRustDeskClient.sh')
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\linux-client-enrollment.md') `
-Destination (Join-Path $linuxClientRoot 'README.md')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.sh') `
@@ -168,7 +170,9 @@ $serverScripts = @(
'Get-SguBrokerLog.ps1',
'Install-SguDomainMonitoring.ps1',
'Install-SguRustDeskClient.ps1',
'Install-SguRustDeskLinuxEnrollment.ps1',
'Install-SguRustDeskServer.ps1',
'Invoke-SguRustDeskLinuxRegistrationProcessor.ps1',
'Invoke-SguMonitoringMaintenance.ps1',
'New-LabCertificate.ps1',
'Get-SguRustDeskDevice.ps1',