Cómo desplegar RustDesk por GPO en un dominio de Windows con Active Directory

Por Víctor Aracil - septiembre 25, 2026

Si administras una red con Active Directory, instalar RustDesk equipo por equipo no es una opción práctica. Necesitas un despliegue centralizado, repetible y que funcione con independencia del usuario que inicie sesión.

Una GPO con un script de inicio del equipo resuelve ese problema. Windows ejecuta el script con la cuenta SYSTEM, instala RustDesk para todo el equipo y deja el servicio preparado antes de que el usuario empiece a trabajar.

La idea parece sencilla, pero hay varios detalles que pueden impedir la instalación: el script puede estar en la pestaña equivocada, PowerShell puede bloquearlo, la red puede no estar disponible durante el arranque o SYSVOL puede estar desincronizado entre los controladores de dominio.

En esta guía te explico el procedimiento que he utilizado, los errores que aparecieron durante el despliegue y cómo puedes diagnosticarlos sin perder horas revisando el instalador cuando el problema está realmente en la infraestructura de Active Directory.

Qué vas a conseguir

Al terminar tendrás este diseño:

  1. El instalador de RustDesk se guarda en un recurso compartido de solo lectura para los equipos del dominio.
  2. Una GPO vinculada a una OU ejecuta un script de PowerShell al arrancar cada equipo.
  3. El script se ejecuta como NT AUTHORITY\SYSTEM.
  4. RustDesk queda instalado como servicio con inicio automático.
  5. Cada conexión entrante requiere que el empleado pulse Aceptar.
  6. El script genera un registro local que permite saber qué ocurrió en cada ordenador.

En este caso se utilizan los servidores públicos de RustDesk. Si más adelante instalas un servidor propio, podrás conservar la misma estructura y adaptar la configuración de red del cliente.

Por qué debes utilizar un script de inicio del equipo

Dentro de una GPO puedes ejecutar scripts al iniciar el equipo o al iniciar la sesión de un usuario. Para instalar una aplicación que debe estar disponible para todos, utiliza el inicio del equipo.

La ruta es:

Configuración del equipo
└── Directivas
    └── Configuración de Windows
        └── Scripts (Inicio o apagado)
            └── Inicio

Este punto es fundamental. Un script de inicio de sesión se ejecuta dentro del contexto del usuario y puede fallar por falta de permisos administrativos. El script de inicio del equipo se ejecuta como SYSTEM, por lo que no necesitas guardar una cuenta de administrador ni una contraseña dentro del archivo.

Tampoco debes desactivar UAC ni habilitar AlwaysInstallElevated. Ambas medidas rebajan la seguridad de todo el equipo para resolver un problema que la GPO ya puede solucionar correctamente.

Prepara el recurso compartido

Crea una carpeta compartida para el instalador, por ejemplo:

\\servidor\instaladores

Concede permisos de lectura y ejecución al grupo que contiene los equipos que recibirán RustDesk. Puedes utilizar Equipos del dominio o un grupo específico para el despliegue.

Recuerda que, durante el arranque, no accede al recurso compartido el usuario que se sentará delante del ordenador. Accede la cuenta del equipo, con un nombre similar a DOMINIO\EQUIPO-PILOTO$.

Antes de continuar, comprueba desde un equipo piloto que el instalador puede leerse:

Test-Path '\\servidor\instaladores\rustdesk-x86_64.exe'

El resultado debe ser True.

Qué debe hacer el script de despliegue

No conviene limitar el script a ejecutar el instalador desde la red. Un despliegue fiable debería realizar estas tareas:

  • Comprobar que se está ejecutando como SYSTEM.
  • Verificar que la ruta compartida y el instalador existen.
  • Copiar el instalador a una carpeta local.
  • Comparar su SHA-256 con el valor aprobado.
  • Ejecutar la instalación silenciosa.
  • Esperar a que el servicio de RustDesk exista y esté iniciado.
  • Configurar el servicio con inicio automático y cuenta LocalSystem.
  • Exigir aprobación manual para las conexiones.
  • Desactivar el acceso mediante contraseña en la pantalla bloqueada si buscas consentimiento estricto.
  • Eliminar cualquier contraseña permanente.
  • Registrar cada paso y devolver un código de error útil.

RustDesk admite la instalación silenciosa de su ejecutable con:

rustdesk-x86_64.exe --silent-install

Para exigir la aceptación del usuario puedes configurar:

& 'C:\Program Files\RustDesk\rustdesk.exe' --option approve-mode 'click'
& 'C:\Program Files\RustDesk\rustdesk.exe' --option allow-logon-screen-password 'N'
& 'C:\Program Files\RustDesk\rustdesk.exe' --password ''

No confíes solo en el código de salida del instalador. En algunas versiones o formas de invocación puede no devolverse como esperas. Comprueba también el resultado real: ejecutable presente, servicio creado, tipo de inicio automático y opciones aplicadas.

Un registro local como el siguiente facilita mucho el diagnóstico:

C:\Program Files\RustDesk-Deployment\despliegue.log

Script completo de despliegue

Antes de utilizarlo, sustituye todos los valores escritos entre corchetes. Debes indicar el servidor y el recurso compartido, el hash SHA-256 aprobado, la versión que has probado y el nombre exacto del instalador.

  • [SERVIDOR]: nombre DNS o dirección del servidor de archivos.
  • [RECURSO_COMPARTIDO]: nombre del recurso que contiene el instalador.
  • [SHA256_DEL_INSTALADOR]: hash SHA-256 del ejecutable validado.
  • [VERSION_RUSTDESK]: versión exacta que se va a instalar.
  • [NOMBRE_COMPLETO_DEL_INSTALADOR.exe]: nombre exacto del archivo descargado.

El script está pensado para una GPO de inicio del equipo. No lo publiques con valores de ejemplo sin completar y prueba cualquier versión nueva en una OU piloto.

#requires -Version 5.1
<#
GPO de INICIO DEL EQUIPO (SYSTEM). Windows x64 y version de RustDesk validada por el administrador.
Rellenar Sha256Esperado con el hash del instalador previamente validado.
No ejecuta instalaciones desde la red ni guarda contrasenas.
No actualiza otras versiones: las registra como error para revision separada.
#>
[CmdletBinding()]
param(
    [string]$RutaCompartida = '\\[SERVIDOR]\[RECURSO_COMPARTIDO]',
    [string]$Sha256Esperado = '[SHA256_DEL_INSTALADOR]'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$VersionObjetivo = '[VERSION_RUSTDESK]'
$InstaladorName = '[NOMBRE_COMPLETO_DEL_INSTALADOR.exe]'
$CarpetaTrabajo = Join-Path $env:ProgramFiles 'RustDesk-Deployment'
$Exe = Join-Path $env:ProgramFiles 'RustDesk\rustdesk.exe'
$Log = Join-Path $CarpetaTrabajo 'despliegue.log'
$ConfiguracionIniciada = $false
$BloqueoDespliegue = $null
$CodigoFinal = 1

function Write-Log([string]$Texto) {
    $Linea = '{0:u} {1} {2}' -f (Get-Date), $env:COMPUTERNAME, $Texto
    Add-Content -LiteralPath $Log -Value $Linea -Encoding UTF8
}

function Invoke-RustDesk([string]$Archivo, [string]$Argumentos, [int]$Segundos = 30) {
    # Argumentos es una cadena controlada por este script, nunca entrada del usuario.
    $IdTemporal = [Guid]::NewGuid().ToString('N')
    $Salida = Join-Path $CarpetaTrabajo ("salida-{0}.txt" -f $IdTemporal)
    $ErrorCli = Join-Path $CarpetaTrabajo ("error-{0}.txt" -f $IdTemporal)
    $Proceso = $null
    try {
        $Proceso = Start-Process -FilePath $Archivo -ArgumentList $Argumentos `
            -WindowStyle Hidden -PassThru -RedirectStandardOutput $Salida `
            -RedirectStandardError $ErrorCli
        # Conservar el handle antes de esperar: Windows PowerShell puede perder
        # el codigo de salida de procesos lanzados con Start-Process -PassThru.
        $null = $Proceso.Handle
        if (-not $Proceso.WaitForExit($Segundos * 1000)) {
            # No se mata un instalador a medias. Revisar procesos antes de reintentar.
            throw "Tiempo agotado: $Argumentos. Revisar proceso PID $($Proceso.Id)."
        }
        $Proceso.WaitForExit()
        $CodigoSalida = $Proceso.ExitCode
        $Proceso.Dispose()
        $Proceso = $null
        if ($null -eq $CodigoSalida) {
            throw "No se pudo recuperar el codigo de salida de $Argumentos; no se confirma su resultado."
        }
        if ($CodigoSalida -ne 0) {
            throw "Fallo de comando $Argumentos; codigo $CodigoSalida."
        }
        $TextoSalida = [System.IO.File]::ReadAllText($Salida)
        return $TextoSalida.Trim()
    } finally {
        if ($null -ne $Proceso) { $Proceso.Dispose() }
        Remove-Item -LiteralPath $Salida,$ErrorCli -Force -ErrorAction SilentlyContinue
    }
}

function Get-VersionInstalada {
    if (-not (Test-Path -LiteralPath $Exe -PathType Leaf)) { return '' }
    $Info = (Get-Item -LiteralPath $Exe).VersionInfo.ProductVersion
    if ($Info -match '^(\d+\.\d+\.\d+)(?:\D|$)') { return $Matches[1] }
    throw "No se pudo interpretar la version instalada: $Info"
}

function Wait-RustDeskService([int]$Intentos = 30, [int]$Pausa = 2) {
    # CIM no conserva un ServiceController abierto mientras el instalador
    # elimina y vuelve a crear el servicio. Exigir tres lecturas consecutivas.
    $Consecutivas = 0
    for ($i = 0; $i -lt $Intentos; $i++) {
        $Estado = Get-CimInstance Win32_Service -Filter "Name='RustDesk'"
        if ($null -ne $Estado) { $Consecutivas++ } else { $Consecutivas = 0 }
        if ($Consecutivas -ge 3) { return $true }
        if ($i -lt ($Intentos - 1)) { Start-Sleep -Seconds $Pausa }
    }
    return $false
}

function Start-VerifiedRustDeskService([int]$Intentos = 30, [int]$Pausa = 2) {
    $Consecutivas = 0
    $UltimoError = 'Servicio no disponible.'
    for ($i = 0; $i -lt $Intentos; $i++) {
        try {
            $Estado = Get-CimInstance Win32_Service -Filter "Name='RustDesk'"
            if ($null -eq $Estado) { throw 'Servicio todavia ausente.' }
            if ($Estado.StartMode -ne 'Auto') {
                Set-Service -Name RustDesk -StartupType Automatic
            }
            if ($Estado.State -eq 'Stopped') { Start-Service -Name RustDesk }
            $Estado = Get-CimInstance Win32_Service -Filter "Name='RustDesk'"
            if ($null -ne $Estado -and $Estado.State -eq 'Running' -and $Estado.StartMode -eq 'Auto') {
                $Consecutivas++
                if ($Consecutivas -ge 3) { return }
            } else {
                $Consecutivas = 0
                $UltimoError = 'El servicio aun no esta Running/Auto.'
            }
        } catch {
            $Consecutivas = 0
            $UltimoError = $_.Exception.Message
        }
        if ($i -lt ($Intentos - 1)) { Start-Sleep -Seconds $Pausa }
    }
    throw "No se estabilizo el servicio RustDesk: $UltimoError Si acaba de desinstalarse, reiniciar el puesto antes de reintentar."
}

function Set-VerifiedOption([string]$Clave, [AllowEmptyString()][string]$Valor) {
    # Las claves y valores proceden exclusivamente de las constantes de abajo.
    $Argumentos = '--option {0} "{1}"' -f $Clave, $Valor
    for ($Intento = 1; $Intento -le 5; $Intento++) {
        $null = Invoke-RustDesk $Exe $Argumentos
        Start-Sleep -Seconds 1
        $Actual = Invoke-RustDesk $Exe "--option $Clave"
        if ($Actual -ceq $Valor) { return }
        Start-Sleep -Seconds 2
    }
    throw "No se pudo verificar la opcion $Clave."
}

try {
    $Identidad = [Security.Principal.WindowsIdentity]::GetCurrent()
    $Principal = New-Object Security.Principal.WindowsPrincipal($Identidad)
    if (-not $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw 'Ejecutar como SYSTEM mediante GPO de equipo o como administrador en el piloto.'
    }
    if (-not [Environment]::Is64BitOperatingSystem -or -not [Environment]::Is64BitProcess) {
        throw 'Se requiere Windows x64 y PowerShell de 64 bits.'
    }
    # Carpeta bajo Program Files, protegida por sus ACL normales de Windows.
    $null = New-Item -ItemType Directory -Path $CarpetaTrabajo -Force
    if ((Get-Item -LiteralPath $CarpetaTrabajo).Attributes -band [IO.FileAttributes]::ReparsePoint) {
        throw 'La carpeta de trabajo no puede ser un enlace o punto de reanalisis.'
    }
    # Mantener un bloqueo exclusivo durante toda la ejecucion. Si la GPO tiene
    # el script duplicado, la segunda instancia termina sin interferir.
    try {
        $RutaBloqueo = Join-Path $CarpetaTrabajo 'despliegue.lock'
        $BloqueoDespliegue = [System.IO.File]::Open(
            $RutaBloqueo,
            [System.IO.FileMode]::OpenOrCreate,
            [System.IO.FileAccess]::ReadWrite,
            [System.IO.FileShare]::None
        )
    } catch [System.IO.IOException] {
        exit 0
    }
    Write-Log "INICIO revision 5. Identidad: $($Identidad.Name)"
    $Version = Get-VersionInstalada
    if ($Version -and $Version -ne $VersionObjetivo) {
        throw "Version existente ${Version}: este piloto no actualiza ni degrada otras versiones."
    }
    if (-not $Version) {
        if ($Sha256Esperado -notmatch '^[A-Fa-f0-9]{64}$') {
            throw 'Rellenar Sha256Esperado antes de instalar.'
        }
        $Origen = Join-Path $RutaCompartida $InstaladorName
        $Local = Join-Path $CarpetaTrabajo $InstaladorName
        $Copiado = $false
        for ($Intento = 1; $Intento -le 6; $Intento++) {
            try {
                Copy-Item -LiteralPath $Origen -Destination $Local -Force
                $Copiado = $true
                break
            } catch {
                Write-Log "Copia: intento $Intento fallido. $($_.Exception.Message)"
                if ($Intento -lt 6) { Start-Sleep -Seconds 10 }
            }
        }
        if (-not $Copiado) { throw 'No se pudo copiar el instalador desde el recurso compartido.' }
        if ((Get-FileHash -LiteralPath $Local -Algorithm SHA256).Hash -ine $Sha256Esperado) {
            throw 'El hash del instalador no coincide con el aprobado.'
        }
        Write-Log 'Instalacion silenciosa desde copia local.'
        $null = Invoke-RustDesk $Local '--silent-install' 180
        $Limite = (Get-Date).AddSeconds(30)
        while (-not (Test-Path -LiteralPath $Exe) -and (Get-Date) -lt $Limite) {
            Start-Sleep -Seconds 2
        }
        if ((Get-VersionInstalada) -ne $VersionObjetivo) {
            throw 'No se ha confirmado la instalacion de la version objetivo.'
        }
        Write-Log 'EXE instalado. Esperando el registro del servicio por el instalador.'
        $ServicioRegistrado = Wait-RustDeskService
        if (-not $ServicioRegistrado) { Write-Log 'El instalador no ha dejado un servicio estable; se intentara registrarlo una vez.' }
    }
    # Tambien se comprueba y configura cuando el EXE ya existe.
    $ConfiguracionIniciada = $true
    if ($null -eq (Get-CimInstance Win32_Service -Filter "Name='RustDesk'")) {
        Write-Log 'Registrando el servicio con --install-service.'
        $null = Invoke-RustDesk $Exe '--install-service' 60
        if (-not (Wait-RustDeskService)) {
            throw 'El servicio no aparece tras --install-service. Reiniciar el puesto si hay una desinstalacion pendiente y revisar el registro System.'
        }
    }
    Write-Log 'Esperando servicio Running/Auto antes de configurar.'
    Start-VerifiedRustDeskService
    Write-Log 'Servicio preparado. Configurando consentimiento.'

    # El servicio debe estar arrancado: la CLI habla con el mediante IPC.
    Set-VerifiedOption 'approve-mode' 'click'
    Set-VerifiedOption 'allow-logon-screen-password' 'N'
    # Retirar una posible contrasena permanente heredada del script original.
    $Respuesta = Invoke-RustDesk $Exe '--password ""'
    if ($Respuesta -cne 'Done!') { throw 'No se ha confirmado la retirada de la contrasena permanente.' }

    # Cliente oficial con servidores publicos. No importar perfiles de otro equipo.
    foreach ($Clave in @('custom-rendezvous-server', 'relay-server', 'api-server', 'key')) {
        Set-VerifiedOption $Clave ''
    }

    Restart-Service -Name RustDesk
    Start-VerifiedRustDeskService
    Start-Sleep -Seconds 3
    if ((Invoke-RustDesk $Exe '--option approve-mode') -cne 'click' -or
        (Invoke-RustDesk $Exe '--option allow-logon-screen-password') -cne 'N') {
        throw 'Los ajustes de consentimiento no se han conservado tras reiniciar el servicio.'
    }
    $Estado = Get-CimInstance Win32_Service -Filter "Name='RustDesk'"
    if ($Estado.State -ne 'Running' -or $Estado.StartMode -ne 'Auto') {
        throw 'El servicio no esta en ejecucion y con inicio automatico.'
    }
    Write-Log 'OK: version, servicio y opciones de consentimiento comprobados. Falta prueba funcional del piloto.'
    $CodigoFinal = 0
} catch {
    $Mensaje = $_.Exception.Message
    if ($null -ne $_.Exception.InnerException) {
        $Mensaje += " Detalle: $($_.Exception.InnerException.Message)"
    }
    $Mensaje += " Ubicacion: $($_.InvocationInfo.ScriptLineNumber)"
    # Si falla la configuracion, no dejar el servicio aceptando conexiones con ajustes inciertos.
    if ($ConfiguracionIniciada) {
        try {
            if ($null -ne (Get-CimInstance Win32_Service -Filter "Name='RustDesk'")) {
                Set-Service -Name RustDesk -StartupType Disabled
                Stop-Service -Name RustDesk -Force
            }
        } catch {
            $Mensaje += " No se pudo deshabilitar/detener el servicio: $($_.Exception.Message)"
        }
    }
    if (Test-Path -LiteralPath $CarpetaTrabajo) {
        try { Write-Log "ERROR: $Mensaje" } catch { }
    }
    Write-Error $Mensaje -ErrorAction Continue
    $CodigoFinal = 1
} finally {
    if ($null -ne $BloqueoDespliegue) {
        $BloqueoDespliegue.Dispose()
    }
}
exit $CodigoFinal

Crea una OU y una GPO piloto

No empieces vinculando la GPO a todo el dominio. Crea una OU piloto y mueve allí dos o tres ordenadores representativos.

Las directivas vinculadas a la nueva OU se suman a las heredadas desde el dominio y desde las OU superiores, salvo que exista un bloqueo de herencia, una opción forzada o un filtrado que cambie ese comportamiento.

Abre Administración de directivas de grupo con gpmc.msc. La GPO se crea y se vincula desde esta consola, no desde Usuarios y equipos de Active Directory.

Sigue estos pasos:

  1. Localiza la OU piloto.
  2. Haz clic con el botón derecho sobre ella.
  3. Selecciona Crear un GPO en este dominio y vincularlo aquí.
  4. Asigna un nombre descriptivo, como EQUIPOS - RustDesk - Piloto.
  5. Edita la nueva GPO.

Copia el script dentro de SYSVOL

En el editor de la GPO entra en:

Configuración del equipo > Directivas > Configuración de Windows >
Scripts (Inicio o apagado) > Inicio

Abre la pestaña Scripts de PowerShell y pulsa Mostrar archivos. Windows abrirá la carpeta de esa GPO dentro de SYSVOL.

Copia allí el archivo .ps1. Después vuelve a la ventana de propiedades, pulsa Agregar y selecciona el script.

Deja los parámetros vacíos si el archivo ya contiene sus valores de configuración. En Para este GPO, ejecutar los scripts en el orden siguiente puedes dejar No configurado cuando solo existe este script.

Comprueba que el archivo aparece una sola vez en la lista. En nuestro piloto quedó registrado dos veces y Windows lanzó dos copias casi simultáneas. Ambas intentaron utilizar el mismo archivo temporal y apareció este error:

El proceso no puede obtener acceso al archivo porque está siendo utilizado en otro proceso.

Eliminar la entrada duplicada resolvió el problema. Si quieres reforzar el script, añade además un mutex o bloqueo para impedir ejecuciones concurrentes.

Haz que Windows espere a la red

El equipo necesita acceder a SYSVOL y al recurso compartido antes de ejecutar el instalador. Habilita esta directiva:

Configuración del equipo > Directivas > Plantillas administrativas >
Sistema > Inicio de sesión >
Esperar siempre a la red al iniciar el equipo y al iniciar sesión

Esto resulta especialmente útil cuando el adaptador tarda en obtener dirección IP, existe autenticación de red o el equipo arranca más rápido que la conexión con el controlador de dominio.

Revisa la política de ejecución de PowerShell

Consulta la configuración efectiva en el equipo piloto:

Get-ExecutionPolicy -List

Si una directiva corporativa exige scripts firmados, firma el archivo con un certificado de firma de código y distribuye la confianza en ese certificado. No cambies toda la organización a Unrestricted para evitar el problema.

Con RemoteSigned, un script ejecutado directamente desde una ruta de red puede considerarse remoto y bloquearse si no está firmado. Para una prueba manual controlada puedes copiarlo a una carpeta local, verificar su hash, desbloquearlo y ejecutarlo:

$Origen = '\\servidor\instaladores\despliegue_rustdesk.ps1'
$Local = 'C:\RustDesk-Piloto\despliegue_rustdesk.ps1'

New-Item -ItemType Directory -Path (Split-Path $Local) -Force | Out-Null
Copy-Item -LiteralPath $Origen -Destination $Local -Force
Get-FileHash -LiteralPath $Local -Algorithm SHA256
Unblock-File -LiteralPath $Local
& $Local

Utiliza esta secuencia para diagnosticar. En producción, la opción más limpia es firmar el script o aplicar una política corporativa coherente.

Aplica la GPO y valida el piloto

En el equipo piloto ejecuta:

gpupdate /target:computer /force
gpresult /scope computer /r

La GPO de RustDesk debe aparecer en Objetos de directiva de grupo aplicados. Después reinicia el ordenador: una actualización de directivas no sustituye el evento de inicio que dispara el script.

Tras el reinicio, comprueba:

Test-Path 'C:\Program Files\RustDesk\rustdesk.exe'

Get-CimInstance Win32_Service -Filter "Name='RustDesk'" |
    Select-Object Name, State, StartMode, StartName, PathName

Get-Content 'C:\Program Files\RustDesk-Deployment\despliegue.log' -Tail 30

El servicio debería aparecer iniciado, en modo automático y con LocalSystem como cuenta.

También puedes consultar la configuración y el identificador del equipo:

& 'C:\Program Files\RustDesk\rustdesk.exe' --option approve-mode
& 'C:\Program Files\RustDesk\rustdesk.exe' --option allow-logon-screen-password
& 'C:\Program Files\RustDesk\rustdesk.exe' --get-id

La última comprobación es funcional: inicia una conexión desde el puesto de un técnico y verifica que el empleado recibe la ventana de aceptación. Prueba también Rechazar, dejar la petición sin responder, bloquear la sesión y una acción que provoque UAC. Así sabrás exactamente qué experiencia tendrá el usuario.

Si no se instala, localiza primero la capa que falla

Cuando RustDesk no aparece después del reinicio, no modifiques el instalador a ciegas. Sigue este orden.

La GPO no aparece en gpresult

Revisa el vínculo con la OU, el filtrado de seguridad, los filtros WMI y la ubicación de la cuenta del equipo. La GPO debe aplicarse al objeto equipo, no solo al usuario.

La GPO aparece, pero no existe el registro local

El script no llegó a ejecutarse. Comprueba que está en Inicio del equipo &gt; Scripts de PowerShell, que existe dentro de la carpeta de SYSVOL abierta con Mostrar archivos y que solo figura una vez en la lista.

Aparecen los eventos 1058 o 1130

Si Windows indica que no puede leer gpt.ini o el script dentro de:

\\dominio.local\SYSVOL\dominio.local\Policies\{GUID-DE-LA-GPO}

el problema no está en las variables del script ni en la ruta del instalador. El cliente no puede leer la parte de archivos de la GPO.

Compara la carpeta y gpt.ini directamente en cada controlador de dominio. Es posible que la GPO exista en Active Directory, pero falte en el SYSVOL de uno de los controladores.

Una salida correcta de repadmin /replsummary solo confirma la replicación de Active Directory. No demuestra por sí sola que SYSVOL esté sano. En dominios antiguos que todavía utilizan FRS, revisa además el registro File Replication Service y eventos como 13508.

No amplíes el despliegue hasta corregir esta situación. Un equipo puede instalar RustDesk cuando contacta con un controlador y fallar cuando contacta con el otro.

El servicio todavía no existe

La instalación y el alta del servicio no siempre terminan al mismo tiempo. Añade una espera con reintentos y vuelve a consultar el servicio antes de configurarlo. Evita dar por fallida la instalación por una única comprobación inmediata.

PowerShell muestra un error con una salida vacía

Una orden correcta puede no devolver texto. Si el script llama a métodos como .Trim() sobre un valor nulo, fallará aunque RustDesk se haya configurado. Lee el archivo de salida como texto y trata la salida vacía como una cadena válida.

No aparecen las Plantillas administrativas

Un error al recopilar las plantillas administrativas suele indicar una incompatibilidad entre archivos ADMX y ADML o un almacén central incompleto. Es un problema distinto del script de RustDesk. Corrige las plantillas con cuidado y no copies archivos al almacén central sin inventario y copia de seguridad.

Exporta las conexiones de los técnicos a un inventario compartido

RustDesk guarda localmente las conexiones conocidas en archivos TOML. El siguiente script lee el ID, alias, nombre del equipo, usuario y plataforma, y combina la información de todos los técnicos en un CSV separado por punto y coma.

No lee ni publica contraseñas. La carpeta de inventario debe existir previamente y los informáticos necesitan permisos de modificación únicamente sobre esa carpeta.

Sustituye [SERVIDOR], [RECURSO_COMPARTIDO] y [CARPETA_INVENTARIO] por los valores de tu entorno. Si el CSV está abierto en Excel durante la actualización, ciérralo y vuelve a ejecutar el script.

#requires -Version 5.1
<#
Exporta las conexiones de la interfaz local de RustDesk y actualiza un CSV
compartido. Conserva los registros publicados por los demas tecnicos.
No lee ni exporta contrasenas.
#>
[CmdletBinding()]
param(
    [string]$CarpetaPeers = (Join-Path $env:APPDATA 'RustDesk\config\peers'),
    [string]$Destino = '\\[SERVIDOR]\[RECURSO_COMPARTIDO]\[CARPETA_INVENTARIO]\Conexiones_RustDesk.csv'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Tecnico = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$EquipoOrigen = $env:COMPUTERNAME
$CarpetaDestino = Split-Path -Parent $Destino
$Bloqueo = Join-Path $CarpetaDestino '.Conexiones_RustDesk.lock'
$FlujoBloqueo = $null
$Temporal = $null

function ConvertFrom-TomlText([string]$Texto) {
    $Valor = $Texto.Trim()
    if ($Valor.Length -ge 2 -and $Valor[0] -eq '"' -and $Valor[$Valor.Length - 1] -eq '"') {
        try { return ($Valor | ConvertFrom-Json) } catch { return $Valor.Substring(1, $Valor.Length - 2) }
    }
    if ($Valor.Length -ge 2 -and $Valor[0] -eq "'" -and $Valor[$Valor.Length - 1] -eq "'") {
        return $Valor.Substring(1, $Valor.Length - 2)
    }
    return $Valor
}

if (-not (Test-Path -LiteralPath $CarpetaPeers -PathType Container)) {
    throw "No existe la carpeta de conexiones de RustDesk: $CarpetaPeers"
}
if (-not (Test-Path -LiteralPath $CarpetaDestino -PathType Container)) {
    throw "No existe la carpeta compartida: $CarpetaDestino"
}

$Archivos = @(Get-ChildItem -LiteralPath $CarpetaPeers -Filter '*.toml' -File)
if ($Archivos.Count -eq 0) {
    throw "No hay conexiones TOML en $CarpetaPeers"
}

$FilasLocales = @(foreach ($Archivo in $Archivos) {
    $Seccion = ''
    $Campos = @{}

    foreach ($Linea in [IO.File]::ReadAllLines($Archivo.FullName)) {
        if ($Linea -match '^\s*\[([^\]]+)\]\s*(?:#.*)?$') {
            $Seccion = $Matches[1].Trim().ToLowerInvariant()
            continue
        }
        if ($Linea -match '^\s*([A-Za-z0-9_-]+)\s*=\s*(.+?)\s*$') {
            $Clave = $Matches[1].ToLowerInvariant()
            if (($Seccion -eq 'info' -and $Clave -in @('username','hostname','platform')) -or
                ($Seccion -eq 'options' -and $Clave -eq 'alias')) {
                $Campos["$Seccion.$Clave"] = ConvertFrom-TomlText $Matches[2]
            }
        }
    }

    $Alias = [string]$Campos['options.alias']
    $Hostname = [string]$Campos['info.hostname']
    $Username = [string]$Campos['info.username']
    $NombreVisible = if ($Alias) { $Alias } elseif ($Hostname) { $Hostname } elseif ($Username) { $Username } else { $Archivo.BaseName }

    [pscustomobject][ordered]@{
        Tecnico       = $Tecnico
        EquipoOrigen  = $EquipoOrigen
        Nombre        = $NombreVisible
        ID            = $Archivo.BaseName
        Alias         = $Alias
        EquipoRemoto  = $Hostname
        UsuarioRemoto = $Username
        Plataforma    = [string]$Campos['info.platform']
        UltimoCambio  = $Archivo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')
        PublicadoUTC  = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')
    }
})

try {
    # Impide que dos tecnicos modifiquen el CSV central simultaneamente.
    for ($Intento = 1; $Intento -le 30 -and $null -eq $FlujoBloqueo; $Intento++) {
        try {
            $FlujoBloqueo = [IO.File]::Open(
                $Bloqueo,
                [IO.FileMode]::OpenOrCreate,
                [IO.FileAccess]::ReadWrite,
                [IO.FileShare]::None
            )
        } catch [IO.IOException] {
            if ($Intento -eq 30) {
                throw 'Otro tecnico esta actualizando el inventario. Vuelve a intentarlo dentro de unos segundos.'
            }
            Start-Sleep -Seconds 1
        }
    }

    $Existentes = @()
    if (Test-Path -LiteralPath $Destino -PathType Leaf) {
        try {
            $Existentes = @(Import-Csv -LiteralPath $Destino -Delimiter ';')
        } catch {
            throw "No se pudo leer el inventario existente. Comprueba que el CSV no este abierto en Excel. $($_.Exception.Message)"
        }
    }

    # Sustituir solamente la publicacion anterior de este usuario en este PC.
    $Conservadas = @($Existentes | Where-Object {
        $_.Tecnico -ne $Tecnico -or $_.EquipoOrigen -ne $EquipoOrigen
    })
    $Inventario = @($Conservadas) + @($FilasLocales)

    $Temporal = Join-Path $CarpetaDestino ('.Conexiones_RustDesk-{0}.tmp' -f [Guid]::NewGuid().ToString('N'))
    $Inventario |
        Sort-Object Tecnico,EquipoOrigen,Nombre,ID |
        Export-Csv -LiteralPath $Temporal -NoTypeInformation -Encoding UTF8 -Delimiter ';'

    try {
        Copy-Item -LiteralPath $Temporal -Destination $Destino -Force
    } catch {
        throw "No se pudo actualizar $Destino. Si esta abierto en Excel, cierralo y vuelve a ejecutar el script. $($_.Exception.Message)"
    }

    Write-Output "Publicadas $($FilasLocales.Count) conexiones. Inventario actualizado: $Destino"
} finally {
    if ($null -ne $FlujoBloqueo) { $FlujoBloqueo.Dispose() }
    if ($null -ne $Temporal) { Remove-Item -LiteralPath $Temporal -Force -ErrorAction SilentlyContinue }
}

Pasa del piloto a producción por fases

Cuando el piloto sea estable, amplía el despliegue de forma gradual:

  1. Equipo del responsable técnico.
  2. Dos o tres equipos del departamento de informática.
  3. Un grupo pequeño de usuarios con perfiles y sedes diferentes.
  4. Una OU completa.
  5. El resto de equipos del dominio.

Antes de cada fase confirma estos puntos:

  • SYSVOL contiene la GPO y el script en todos los controladores.
  • Los equipos pueden leer el instalador con su cuenta de máquina.
  • El hash del instalador coincide con el aprobado.
  • Solo existe una entrada del script en la GPO.
  • El servicio se inicia automáticamente.
  • No existe contraseña permanente.
  • La conexión exige aceptación del usuario.
  • El registro local muestra un resultado correcto.

Conserva además un procedimiento de retirada: desvincular la GPO, detener y eliminar el servicio si procede, desinstalar RustDesk y borrar los archivos de trabajo. Pruébalo en el piloto antes de necesitarlo.

EXE o MSI para un despliegue nuevo

El procedimiento descrito se validó con el instalador EXE y el parámetro --silent-install. RustDesk también dispone de un paquete MSI orientado a despliegues administrados.

Si empiezas ahora un proyecto nuevo, evalúa el MSI porque ofrece instalación silenciosa estándar con msiexec y registro detallado. Si ya has validado una versión EXE concreta, no cambies de formato durante la ampliación sin repetir el piloto.

En ambos casos fija la versión, guarda su hash y prueba las actualizaciones antes de sustituir el paquete compartido.

Conclusión

Desplegar RustDesk mediante GPO te permite preparar todos los equipos del dominio sin depender de los privilegios del usuario. La clave está en ejecutar la instalación al iniciar el equipo, copiar el script dentro de SYSVOL, validar el paquete y comprobar el resultado real del servicio.

Los fallos más difíciles no suelen estar en RustDesk. Una entrada duplicada puede ejecutar el script dos veces; una política de PowerShell puede bloquear el archivo; y una avería de SYSVOL puede hacer que la misma GPO funcione o falle según el controlador de dominio elegido por el cliente.

Si separas esas capas y haces el despliegue por fases, tendrás una instalación predecible, auditable y preparada para prestar soporte remoto con el consentimiento del usuario.

Referencias

  • Comparte:

Puede que también te guste

0 Reviews

Vuelve arriba