Back to Blog

Onboarding Discovered Hosts: Enrollment Links and Gateway Push over WinRM and SSH

Christopher 9 min read
agent-deploymentenrollmentwinrmgateway

Network discovery reports the operating-system hosts on a gateway's local network and marks which are not running the ET Ducky agent. Getting an agent onto those unmanaged hosts is a separate function, because installing software across a network has a different risk profile than reading the network. There are now two ways to do it. This post describes both, the transport and credential mechanics behind the push, and the specific WinRM constraints that turned out to matter.

Two paths

An unmanaged host can be onboarded by pulling or by pushing.

Both are launched from the Network Discovery view and both are admin-triggered. The enrollment link is the default because it adds nothing to the target. The push exists for cases where the host cannot be touched directly.

Enrollment link

An enrollment link is a single-use, short-lived capability. Generating one mints a fresh registration token scoped to the organization and the target operating system, with a use count of one and an expiry that matches the link. The link itself is an opaque token that references that registration token. It is encrypted with AES-GCM and hex-encoded so it is safe to carry in a URL path, and it is validated before anything is served. There is no separate table for links. The registration token is the record, and the encrypted reference plus its expiry is the gate.

The link points at an endpoint under /api/enroll/. When the target requests it, the endpoint decrypts the token, validates the registration token under a root tenant context with an explicit organization predicate, and returns a small script. On Windows the script is PowerShell, run with irm '<link>' | iex. On Linux it is shell, run with curl -sSL '<link>' | sudo bash. The script sets TLS 1.2, downloads the signed installer from the download endpoint over HTTPS, and runs it silently with the registration token on the command line. Any validation failure returns a 404 rather than an error that would reveal whether the token exists.

The only requirement on the target is outbound HTTPS to the download endpoint, or to an on-premises distribution hub for organizations with data-residency constraints. There is no inbound port, no gateway on the path, and no target credential involved. After the install the agent maintains an outbound connection to the cloud, so nothing is left listening. The link is delivered however the operator already reaches the box, which can be a remote session, a login script, or an existing management tool.

Gateway push

The push routes through the out-of-band gateway that also runs discovery, either a dedicated device or a regular agent with the gateway role enabled. It authenticates to the target with credentials the operator enters once. The sequence has four parts.

First, the operator's target password is placed in a one-time encrypted relay and a deploy command is queued. The password is not carried in the command and is not written to logs. The relay record is AES-GCM encrypted, valid for fifteen minutes, and consumed exactly once. The command record and the audit log store a redacted payload.

Second, the owning gateway claims the command. It does this by polling, not by receiving a pushed message. The cloud queues the command and the gateway pulls it on its next poll over an agent-bearer, organization-scoped channel. This mirrors how patch jobs are delivered. An earlier design pushed the command over a WebSocket, which failed when a connection went stale without either side noticing. The send succeeded into a dead socket and the command was lost. Pulling removes that failure mode. A gateway that is briefly offline claims the command when it reconnects, and the command stays queued rather than disappearing.

Third, the gateway consumes the relay, selects a transport, and validates the credentials before installing anything. WinRM is used for Windows and SSH for Linux. The pre-check opens a session and confirms the account works. Bad credentials or an unreachable target fail here in a few seconds with a clear message, before any installer runs.

Fourth, the gateway downloads the signed installer to the target, runs it, and reports the exit code. The installer source is chosen automatically. The default is the cloud download endpoint over TLS. For organizations that keep artifacts on-premises, it is served from their own distribution hub using a short-lived read grant, so the bytes stay on the network.

What WinRM requires

The push over WinRM ran into several constraints that are worth stating plainly, because each one is a common reason a first attempt fails.

The channel has to be encrypted. A default WinRM service ships with AllowUnencrypted set to false and rejects cleartext WS-Man. There are two ways to satisfy it. The first is an HTTPS listener on port 5986, where TLS provides the encryption and a default host works without further change. The gateway prefers 5986 automatically when the target exposes it. The second is WinRM message encryption over plain 5985, where each SOAP body is sealed with the negotiated security context and framed as a multipart/encrypted payload. This is what the native Windows client and libraries such as pywinrm and pypsrp do. The current build implements it, so plain 5985 is encrypted by default and a stock host needs no preparation at all. Two details decide whether the target accepts the sealed message. The wire framing is a four-byte little-endian signature length followed by the signature and then the sealed bytes, while .NET's NegotiateAuthentication.Wrap emits the signature and sealed bytes with no length prefix, so the gateway prepends it. The inner MIME part must carry the exact protocol string application/HTTP-SPNEGO-session-encrypted. The alternate spelling ending in -encryption gets the envelope past the listener but fails decryption with an HTTP 500, which is a slow one to find because the byte framing looks correct. With both right, a target left at the default AllowUnencrypted of false enrolls with no changes on the host.

Environment variables belong on the shell, not the command. The installer command passes a few values, such as the download URL and the token, as environment variables so the token does not appear in the command string. In the WinRS protocol those variables are part of the rsp:Shell element created when the shell opens, not part of rsp:CommandLine. Placing an Environment element on the command, even an empty one, produces a schema validation fault and the command never runs. The fix is to declare the environment at shell-creation time and keep the command line to the command itself.

Local accounts need the machine as the authority. For a workgroup target authenticated by a local account, the account name has to be scoped to the target computer rather than the client. Using a bare user name or a leading .\ form fails, because the leading dot means the local machine from the client's point of view and does not resolve when the gateway authenticates remotely. The account name qualified with the target's computer name is what NTLM matches against the target's local database.

SSH has none of this. It is reachable on port 22 with the supplied credentials, and if the account is not root the install script runs under sudo with the same password.

Credential handling

The push is deliberate, administrator-initiated remote code execution against a host on the local network. It runs an installer with administrative rights on the target, so the credential path is built to minimise exposure.

PropertyDetail
At restAES-GCM encrypted in a single-use relay record. The plaintext password is never persisted, and the command record and audit log hold a redacted payload only.
In transitDelivered to the gateway once, over TLS, only when it consumes the relay to run the install. It is never in the queued command and never logged.
LifetimeOne-time use, expires after fifteen minutes, held only for the single install and wiped from gateway memory afterward.
EnrollmentThe installed agent enrolls with a scoped, revocable registration token rather than a standing secret.

Initiating a push requires a paid out-of-band subscription and the Operator role or higher. Read-only members cannot start one. Every push is recorded with operator, target, gateway, time, and outcome, with the password redacted.

Closing the WinRM window

Enabling WinRM to run a one-off push leaves a management service listening after the install is done. The push has an optional teardown for this. On a Windows push, an operator can request that WinRM be disabled after the install. The gateway registers a scheduled task on the target, before the install runs, that disables WinRM and removes its firewall rule after a fixed window and then deletes itself. It runs as a delayed SYSTEM task, so it fires even if the install fails or the gateway never reconnects, and the window is long enough that it never interrupts the install.

The teardown covers the disable side only. Enabling WinRM in the first place is still a manual or policy-driven step on the target, because a push needs WinRM already running to execute. That is one of the reasons the enrollment link is the default. It never needs an inbound service to exist at all.

Choosing between them

Both paths install the same signed agent and both scope enrollment to a revocable registration token. The difference is the direction of the connection. The enrollment link has the host reach out, so it needs no gateway, no open port, and no target credential, and it leaves nothing listening. The push reaches in, which is more capable for unattended and bulk rollout but requires a management service on the target, administrator credentials, and, on Windows, the encryption and account handling above. The link is the recommended default and the push is the fallback for hosts that cannot be reached any other way.

The prerequisites, step-by-step setup, and troubleshooting for both are in the Agent Deployment documentation.

Acknowledgments

The WinRM message-encryption details in this post were pinned down against the open-source work of jborean93, whose libraries pywinrm, pypsrp, and pyspnego implement this protocol correctly and document it in code. Their reference was what let us confirm the exact wire framing and the protocol string, and isolate the one-word difference that was failing decryption. We are grateful for that work.

ET Ducky

Documentation and pricing are available on this site.

View Pricing