Add managed RustDesk enrollment for Linux clients
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user