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
+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',