Connection Reset by Peer: Chasing SSH connection issues on a Hardened Windows Domain

This post was drafted with AI, based on a real production troubleshooting session but I was too lazy to write it down myself. Details have been generalized/anonymized, but the investigation, root cause, and fix are real.

TL;DR

SSH to a newly hardened Windows domain died with a bare Connection reset by peer — no error, no password prompt, nothing. Firewalls, VPN, and IPv6 were all innocent; a full packet capture showed the connection dying the instant authentication began, right after a clean key exchange.

The real cause: our SSH server used a Kerberos mechanism called S4U2Self to check group membership before the client even sent a password — a passwordless “give me a token for this user” trick. Recent AD hardening (delegation restrictions on sensitive accounts) makes the domain controller flatly refuse that request, and the SSH server responded to the refusal by silently killing the connection instead of falling back to a normal password check.

Fix: stop restricting SSH access by group (which needs that passwordless group-membership check) and restrict by username instead (a plain string match that never touches Kerberos delegation). One line changed in the server config, no AD changes needed — though the username-matching syntax had its own trap worth knowing about (details below).

Lesson: a hard reset with zero application-level error almost always means something crashed or was deliberately killed server-side — go looking there first, and don’t assume “verbose logging” is actually landing where you’re looking for it.


The symptom

We stood up a new, more heavily hardened Windows domain for test automation, migrated our SSH-based tooling over, and hit a wall immediately: every SSH connection attempt against the new machines died with Connection reset by peer right in the middle of the handshake. Not a timeout, not “access denied” — a hard TCP reset, with no SSH-level error message at all.

The frustrating part: the exact same client, the exact same automation account, the exact same SSH server software worked flawlessly against our older, less-hardened domain. Something about the hardening had broken SSH — but nothing in the setup looked obviously wrong.

First instincts, and why they were wrong

The obvious first guesses were all network-shaped: firewall rules, VPN routing, maybe an IPv6 vs IPv4 quirk. We’d actually been burned by a firewall-profile issue on this same domain before (a rule scoped to the wrong network profile), so it was a reasonable place to start.

But the packet trace told a different story. A full ssh -vvv capture showed the TCP handshake completing, the SSH version banners exchanging, and a complete, successful key exchange — algorithm negotiation, Diffie-Hellman, new keys installed on both sides. The connection only died the instant the client sent its first authentication request. That ruled out routing and firewalls outright: you don’t get a clean key exchange over a broken network path.

Narrowing it down: isolate one variable at a time

With network eliminated, the only responsible way forward was to change exactly one thing at a time and see what moved the needle.

Remote vs. local. We reproduced the exact same reset connecting from the server to itself over loopback. That ruled out anything about the client machine, DNS, or the path between two hosts — the bug was entirely inside this one machine’s SSH server process.

Domain account vs. local account. We tried a purely local, non-domain account instead of our usual domain service account, and it also failed to connect. Worth being precise here rather than papering over it: a local account resolves its group membership straight out of the local SAM database — no AD round-trip, no Kerberos, no S4U2Self involved at all. So if you try this yourself and get a clean, named failure (something like a logon-type-not-granted error) instead of a raw reset, that’s not evidence you’ve found a different bug — it’s actually consistent with the same root mechanism, just failing on a different, non-Kerberos code path along the way. Either way, this test did its job: it ruled out a whole category of theories about Active Directory group-membership resolution being broken for domain principals specifically, since local accounts don’t depend on AD at all.

Service vs. foreground process. We stopped the SSH service and ran the server binary directly in a console, in debug mode, as ourselves. That connection succeeded and reached a password prompt — which was actually a red herring, as it turned out later: it succeeded only because a login to “yourself” takes a shortcut in the code and reuses the already-existing process token instead of doing real authentication. Testing with a different account against that same foreground process gave an explicit, clean error message (“unable to generate a token — not running as system”) — informative, but also not the real bug, since the production server runs as the system account and doesn’t hit that particular check.

Each of these tests was individually inconclusive, but together they did their job: they eliminated the network, eliminated AD-account-type as the sole variable, and told us the crash lived specifically in how the actual running service authenticated a different account.

Getting real diagnostic data was its own battle

Windows’ Event Viewer is genuinely painful to extract detail from — no easy copy/paste, and worse, we discovered that even with the SSH daemon’s most verbose logging level enabled, the Windows Event Log for this service only ever recorded a single bland “connection accepted” line per attempt. None of the detailed protocol-level trace you’d expect ever made it into the Event Log at all, service-side.

The fix was to stop looking in Event Viewer entirely and instead point the server at a plain debug log file directly (most SSH server implementations on Windows support redirecting verbose output to a file via a command-line flag, separate from whatever the Windows Service normally does). That was the turning point — it let us capture the real service’s own debug output, in its real execution context, for the very connection that was failing.

Getting to that point took a couple of wrong turns worth naming, since none of this was obvious from the documentation:

  • Setting the config’s log level to maximum verbosity wasn’t enough on its own. The server’s own config file supports a “log everything” setting, and we had it on the whole time — it just didn’t matter, because the destination (the Windows Event Log channel) simply doesn’t carry that level of detail for this service, regardless of what the config asks for. Cranking up verbosity is useless if you’re reading it back from a channel that discards most of it.
  • Running the server binary manually in the foreground, in its own debug mode, looked like the obvious workaround — and it was actively misleading. Debug mode refuses to run as a background service (by design, it only handles one connection and exits), so it has to be launched as yourself in a console. That’s the trap: logging in as the same account the process is running as takes an internal shortcut and skips real authentication entirely, which is why our first foreground test “succeeded” for no meaningful reason. Testing with a genuinely different account from that same console instead threw an explicit, different error about the process not running with sufficient privilege — informative, but still not the production failure, because the real service runs with a different, more privileged identity than a console you’re sitting at.
  • Even reproducing that privileged identity by hand (via a tool that lets you open a console as the system account) didn’t line up with production either. It failed, but earlier and differently than the real service did — a strong sign that a manually-launched “as the system account” console isn’t quite the same execution context as a properly service-managed process, even when the account name matches.
  • What actually worked: leave the config’s verbosity setting in place, but change how the service itself is launched so its output goes to a plain file instead of the Event Log — appending a “log to this file” flag directly onto the service’s own startup command (not the foreground-debug flag, which would break the service’s ability to handle more than one connection). That’s a one-line change to how the service is registered, doesn’t touch its actual authentication behavior at all, and — critically — preserves the exact real production context while finally producing full detail.
  • One practical gotcha: the log file can end up locked while the service is actively running, so reading it cleanly sometimes meant briefly stopping the service first. Minor, but worth expecting rather than being confused by.

The smoking gun

Buried in that log, right at the point of failure, were two lines that explained everything:

generate_s4u_user_token: LsaLogonUser() failed. User '<account>' Status: 0xC000006D SubStatus 0.
unable to generate token for user <account>

0xC000006D is STATUS_LOGON_FAILURE — but the interesting part is which API call produced it: not a normal password check, but something called an S4U2Self request.

Fastest way to confirm you’re hitting this: the status code alone isn’t enough — the substatus is what actually tells you whether this is a delegation refusal versus something else entirely (a typo’d account, a disabled account, etc.). If you’re chasing something similar, correlate the same event on the domain controller side (Event ID 4625 logs both the status and substatus together) rather than relying on the client symptom alone. A 0xC000006D with substatus 0 pointed us at a generic logon failure during token generation; other substatus values (e.g. 0xC0000064STATUS_NO_SUCH_USER) would point at a different root cause even though the top-level status code looks identical — don’t stop reading at the status code.

S4U2Self (“Service for User to Self”) is a Kerberos extension that lets a service ask the domain controller, “give me a token for this user,” without needing that user’s password. It’s how our SSH server was checking group membership for an access-control directive (an “only allow these groups to log in” rule) before the client had even sent a password — there’s no credential to check yet at that point, so the server uses this passwordless trick just to peek at what groups the account belongs to.

If that passwordless check fails, the server doesn’t fall back to “well, let’s just ask for the password and find out” — it treats the account as unresolvable and kills the connection outright, with no explanation sent back to the client. That’s the hard reset we’d been chasing the entire time, and it had nothing to do with the actual password, the network, or even (directly) whether the account was local or domain.

Why hardening broke this specifically

S4U2Self is fundamentally a delegation mechanism — a service vouching for a user without the user being present to authenticate themselves. Several well-known, entirely reasonable Active Directory hardening practices explicitly restrict exactly this kind of delegation on sensitive accounts:

  • Marking an account as “This account is sensitive and cannot be delegated” (a single checkbox/attribute on the account)
  • Membership in the Protected Users security group, which blanket-blocks delegation, legacy authentication protocols, and credential caching for its members
  • Authentication Policy Silos that explicitly restrict where and how an account can be used for delegation

Any one of these — all textbook hardening moves for admin or service accounts — will cause the domain controller to flatly refuse an S4U2Self request for that account, which is exactly the failure we captured. The old, less-hardened domain simply never had these protections in place, so the same SSH server code had always worked there without anyone noticing it depended on S4U2Self at all.

If you’re hitting something similar, those three are the concrete, actionable things to go check on the affected account first — “some AD hardening broke it” isn’t a diagnosis you can act on, but “is this account marked non-delegable, in Protected Users, or covered by an auth policy silo” is a five-minute check.

A tempting shortcut that isn’t worth taking

Early in the investigation, before any of this was understood, the obvious-looking fix on the table was adding the affected accounts (or the servers themselves) to the legacy “Pre-Windows 2000 Compatible Access” built-in group. It’s an old group that grants broad read access to user and group attributes across the directory — which, mechanically, is exactly the kind of access S4U2Self-style group-membership resolution needs. It probably would have made the symptom disappear.

It’s also exactly the kind of fix that undoes the hardening you just did. That group exists for backward compatibility with pre-Active-Directory systems, and it works by granting broad directory-read rights to principals like Authenticated Users (in some configurations, effectively Anonymous Logon too) — the same kind of wide-open enumeration capability that modern AD hardening baselines specifically try to close off. Using it here would have “fixed” this one symptom by quietly reopening a much bigger door than the one it was patching, on a domain that had just been hardened specifically to close doors like it. Security correctly pushed back on this, and that pushback is what forced the deeper investigation that found the real, narrow cause.

It’s a useful gut-check for this kind of problem in general: if the fix you’re reaching for undoes the security property you were trying to add in the first place, it’s not a fix — it’s a rollback wearing a disguise. The fix that actually survived (below) doesn’t touch AD permissions at all, which is the property you want: a fix on a hardened environment should be at least as narrow as the problem it’s solving, not broader.

The fix (and a subtle trap in it)

The access-control directive that triggered the passwordless group check could be replaced with a username-based restriction instead of a group-based one. Matching against a username is a plain string/glob comparison — it never needs to resolve group membership, never touches Kerberos delegation, and so never triggers S4U2Self at all. Once we made that change, connections sailed straight through to a real password prompt with no reset.

There’s a genuinely nasty trap hiding in that fix, though. On this domain, the account name the SSH server actually authenticates against is the fully-qualified form (user@domain), not the bare short name. Username-restriction directives in most SSH server configs treat an @ inside the restriction pattern itself as a special separator, meaning “this user, but only from this specific host” — so writing the restriction as user@domain doesn’t do what it looks like it does; it gets misparsed as a host restriction and silently fails to match the real login:

# Looks right, silently wrong — the @ gets parsed as a host restriction,
# not as part of the username, so this never matches the real login:
AllowUsers svc-account@corp.example

# Works — no @ in the pattern, so the whole string is matched as a
# plain username glob:
AllowUsers svc-account*

The working form needed to avoid an @ in the pattern entirely (a trailing wildcard did the trick), matching the account name as a plain prefix instead.

One caveat worth stating plainly: this trades group-based scalability for a flat, manually-maintained list. That’s a fine trade for a handful of automation accounts, but it’s not a permanent architectural answer — if the allowed-list keeps growing, the actual fix is resolving the delegation restriction on the AD side (or restructuring which accounts need SSH access at all) rather than leaning on AllowUsers indefinitely as a workaround for a problem it doesn’t actually solve.

Takeaways

A few things from this debugging session feel worth carrying forward to the next mystery like it:

  1. A hard TCP reset with zero application-level error is a huge clue on its own. It usually means something crashed or explicitly killed the connection server-side, rather than a graceful “access denied” — which should immediately shift attention away from credentials-are-wrong theories and toward “what is the server doing internally right before it would normally respond.”
  2. Change one variable at a time, even when each individual test feels inconclusive. Loopback vs. remote, local account vs. domain account, foreground process vs. service — none of those tests alone explained the bug, but together they fenced in exactly where it had to be.
  3. Don’t trust that your logging is actually capturing what you think it is. We had the server’s most verbose logging level enabled the entire time and were still missing the actual failure, because the log destination we were checking simply didn’t carry that level of detail. Always sanity-check that a “verbose” log actually contains verbose content for a known event before trusting its absence of evidence.
  4. Access-control conveniences (like “allow this group”) can have expensive, non-obvious implementation costs. A directive that reads as a simple authorization check triggered a full passwordless Kerberos delegation request under the hood — a mechanism most engineers configuring that directive would have no reason to know existed, let alone that it was sensitive to unrelated security hardening applied months later by a different team.
  5. Security hardening and infrastructure tooling need to be validated together, not in isolation. Both the SSH server configuration and the account hardening were individually correct and reasonable. The failure only existed at their intersection, and nothing about either change in isolation would have surfaced it during review.

Horizon Network Editor — Change Portgroup Assignments Without Touching Horizon Admin

If you manage VMware Horizon instant-clone pools or RDS farms, you’ve probably been in this situation: you need to move a pool to a different portgroup — maybe a VLAN change, a migration to a new segment, or just fixing a misconfiguration. The options are not great. You can either dig through Horizon Console’s pool edit wizard and hope you find the right field, or go via vCenter and touch the golden image directly. Neither is fast, neither is scriptable, and neither gives you a clear view of what all your pools are currently assigned to.

I built Horizon Network Editor to solve exactly that.

What it does

The tool gives you a single window showing every instant-clone desktop pool and RDS farm in your environment, with their current portgroup assignments visible at a glance. Click a row, pick new portgroup(s) from a dropdown, and apply. That’s it.

Key things it handles that the Console doesn’t make easy:

  • Multi-portgroup per NIC — Horizon supports assigning multiple portgroups to a single NIC for load-balancing. The tool exposes this properly.
  • “From Golden Image” — you can revert a NIC to inheriting its network from the base VM rather than having an explicit assignment.
  • Force redeploy — optionally delete all machines/servers immediately after the change so they come back on the new portgroup, rather than waiting for the next lifecycle event.
  • Stale snapshot handling — if the golden image snapshot has been deleted from vCenter, the tool falls back to the v2 NIC API which only needs the base VM, so you’re not blocked.

Under the hood

The tool is built in Python 3.11 with PySide6 and talks directly to the Horizon REST API. Network labels are fetched via GET /rest/external/v1/network-labels scoped to the pool’s host/cluster, NIC definitions via GET /rest/external/v1/network-interface-cards, and changes are applied with PUT /rest/inventory/v8/desktop-pools/{id} (v7 for farms). Credentials are stored in the OS keyring — nothing written to disk in plaintext.

It’s packaged as a self-contained binary via PyInstaller, so there’s no Python installation required on the machine running it.

Getting it

Pre-built binaries for macOS, Windows, and Linux are on the GitHub Releases page. Download, unzip, run.

If you want to build from source yourself:

git clone https://github.com/Magneet/horizon_network_editor.git
cd horizon_network_editor
bash build_mac.sh      # or build_win.bat / build_linux.sh

Requires Python 3.11+ and Horizon 2312 or later.

First run

Go to the Configuration tab, enter your connection server, username, and domain, set your password (stored in keyring), and click Test & Save. The tool will discover all pods and connection servers. After that, hit Connect on either the VDI Pools or RDS Farms tab and your environment loads in.

Horizon Golden Image Deployment Tool — What’s New

A while back I published the first version of the Horizon Golden Image Deployment Tool (HGIDT), a small desktop app to push golden images to VMware Horizon instant-clone pools without having to click through the Horizon Admin console. Since then the tool has grown quite a bit, so it’s time for a proper write-up of everything that’s changed.

You can grab the latest release as a ready-to-run package from the Releases page on GitHub — no Python installation needed.


What the tool does

HGIDT is a desktop application that talks directly to the Horizon REST API. You point it at a Connection Server, give it credentials, and it lets you:

  • Push a new base VM and snapshot to instant-clone VDI desktop pools and RDS farms
  • Enable or disable provisioning per pool or farm
  • Schedule a push for a specific date and time
  • Resize the compute profile (CPU, cores per socket, RAM) as part of a push
  • Manage the secondary image workflow — stage, promote, or cancel All of this from a single window, without touching the Horizon Admin console.

What’s new

  • new gui
  • filtering in golden images list
  • improved performance
  • improved golden image/snapshot selection from api
  • added option to not pull golden images/ snapshots all the time

Platform support: Windows, macOS, and Linux

The tool originally only had a Windows build. This release adds native macOS and Linux builds.

macOS ships as a .dmg containing a .app bundle. Because the app isn’t signed with an Apple Developer certificate, macOS Gatekeeper will warn you on first launch — right-click the app, choose Open, and confirm the prompt.

Linux ships as a ZIP with a standalone folder. The only runtime requirement is a display server (X11 or Wayland) and the Qt XCB platform dependencies, which are typically already present on any desktop Linux system. Password saving requires a running keyring daemon (GNOME Keyring or KWallet); without one the app still works, you just re-enter your password each session.

Windows continues to ship as a ZIP with a standalone folder containing the .exe.

All three are available on the Releases page.


Requirements

  • VMware Horizon 2312 or later
  • The Horizon REST API must be reachable from the machine running the tool
  • An account with sufficient Horizon admin privileges to update desktop pools and farms

Running from source

If you prefer to run from source rather than the pre-built binary:

python3 -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows

pip install -r requirements.txt
python horizon_golden_image_deployment_tool.py


Feedback and pull requests welcome on GitHub.

Calling all vExpert applicants!

Have you applied or are you thinking about applying for vExpert? Starting June 12th Martin Micheelsen and I will be running the vExpert Candidates Open Office Hours again. This is an online zoom call where we discuss what it takes to become a vExpert but also where we can go over your application together and see what can be improved.

When I became one in 2016 I had no idea what was needed and the vExpert Pro’s where not around yet. Several years ago the vExpert team setup this vExpert Pro program that is aimed at enabling candidates to be able to create the application they need to become a vExpert.

You can find more information on the vExpert site here: https://vexpert.vmware.com/

On December 18th 2025, January 8th 2026 and January 22nd 2026 we will be running the vExpert Candidates Open Office Hours calls at 8PM CET via zoom.

If you are interested in joining the vExpert Office Hours drop me an email at vexpertofficehours@gmail.com so I can send you a calendar invitation.

FAQ

  • How often will you run this?
    • Three times on the posted dates
  • How late is this in my time zone?
    • You can use this link to convert the time to your local time.
  • Are there any other options to talk to vExpert Pro’s?
    • YES! We just started a VMware Community DIscord which has a channel for applicants. For more information see the vExpert website https://vexpert.vmware.com/

Omnissa Horizon REST api throttling maximums

Some people who read release notes will have noticed that the last few version of Horizon have a api limit these days for the rest api’s. With soap being depracated and the q3 release of this year probably being the last that will have soap api in it these maximums have become very relevant. As I was working on moving The ControlUp for VDI api implementation to rest api I asked Omnissa support for what the actual maximums are.

In the picture below you can find the default maximums per 60 seconds and keep in mind that every page counts as an api call so it’s important to keep page sizes as big as possible.

If you want to change the maximums this is the documentation that I received for it courtesy of the great support from Omnissa

Create an object of type pae-APIThrottlingRules under OU=Properties,OU=APIThrottlingRules with the cn name in the format <category><resource><httpMethod> eg for Inventory/v*/desktop-pools , cn can be InventoryDesktopPoolsList, for inventory/v*/desktop-pools/{id}, cn can be InventoryDesktopPoolsGet

In the object populate the following attributes:

  • pae-Enabled – by default the rule will be enabled. Set to 0 to disable the rule.
     
  • pae-APIEndpoint – endpoint on which API throttling needs to be applied, replace V1,V2 with v* and any path param with {id}. keep everything in lowercase.
  • Few examples – config/v*/gateways, config/v*/gateways/{id}, federation/v*/pods/{id}/endpoints/{id}
  • pae-HttpMethod – REST HTTP method for the API denoted by number as follows
    • 0 – GET
    • 1 – DELETE
    • 2 – POST
    • 3 – PUT
  • pae-ThrottlingRules – no of request allowed in time duration (in seconds).
    • MaxRequests=<no of requests allowed>,TimePeriodSecs=<time in seconds>

Add all the rules under OU=Properties,OU=APIThrottlingRules. Once done, Notify the server to take up the latest rules from LDAP.

  1. Set pae-APIThrottlingRefresh to 1 under OU=Properties,OU=Global,CN=Common
  2. Server will set pae-APIThrottlingRefresh to 0 once rules are updated in the server.

    to connect to ldap see this kb article: https://kb.omnissa.com/s/article/2012377?lang=en_US&queryTerm=connect%20to%20ADAM

    Calling all vExpert hopefulls!

    Have you ever dreamt about becoming a vExpert? When I became one in 2016 I had no idea what was needed and the vExpert Pro’s where not around yet. Several years ago the vExpert team setup this vExpert Pro program that is aimed at enabling hopefulls to be able to create the application they need to become a vExpert. You can find more information on the vExpert site here: https://vexpert.vmware.com/

    Starting next week June 27th I will be running a weekly vExpert office hour zoom call where people can come with questions or if they want to discuss their application we can also review it. Even if you’re not there yet content wise we can also give you directions on what you could do for the next round of applications. These sessions will be held between 20.00h and 21.00h ECT +1 on Thursday evenings and I will try to get multiple vExpert Pro’s in there to help you out as much as possible.

    If you are interested in joining the vExpert Office Hours drop me an email at vexpertofficehours@gmail.com so I can send you a calendar invitation.

    or join us on the days via zoom using these details:

    Join Zoom Meeting
    https://controlup.zoom.us/j/81590596106?pwd=czDDGaUqbcIZmdKnpim7bskRZdw0sb.1

    Meeting ID: 815 9059 6106
    Passcode: 487061

    FAQ

    • How often will you run this?
      • This will be weekly untill applications close
    • How late is this in my time zone?
      • You can use this link to convert the time to your local time.
    • Are there any other options to talk to vExpert Pro’s?
      • YES! We just started a VMware Community DIscord which has a channel for applicants. For more information see the vExpert website https://vexpert.vmware.com/

    New Horizon Golden Image Deployment Tool released

    Today at VMware Explore I have launched a new version of the Horizon Golden Image Deployment Tool aka hgidt. This new version is a complete rewrite of the old one as I moved the code base to Python. As base I used a converted framework of the old tool (using ChatGpt :D) in Tkinter and my existing python module for Horizon.

    Why the rewrite?

    I decided for a rewrite as I wante dto be able to publish the tool as an app and most anti malware tools will flag an exeutable that turns out to be a powershell script as malicious. Besides that I thought it would be easier to make the tool more flexible and more important faster. I also wanted to be able to use it from a mac. While the tool itself works flawless when started as a script I did run into issues compiling it into a Mac App so for now I don’t have that yet.

    Changes

    • Switch from Powershell 7 to Python
    • replaced the selector for machines to deploy a secondary image to to a dropdown and number for amount or percentage of machines
    • published as an executable
    • Credentials storing made optional
    • Moved storing of credentials to the local credentials store
    • Connect button connects and gets info for both VDI & RDS

    Where can I get it?

    For now a zip file with the executable (and a ton of other files, it was either that or a slower starting app.) can be found on my Github which also has my (messy) source code.

    Did anything change in how I use it?

    Only some tiny bits like the option that I added to store the password or not and some slight changes on the VDI/RDS but I think these will be self explanatory.

    Pics or it didn’t happen

    The executable among the 1000’s of other files needed for Python

    ← Back

    Thank you for your response. ✨

    The configuration page, note the checkbox to store the password.
    With the popup to ask for the password
    After testing the credentials

    The VDI & RDS Tabs after connecting. Take note of the already selected resizing options. These will be read from the current config so you get the same size when redeploying.

    After selecting a new snapshot and enabling the secondary image to be pushed to selected machines
    Deploy button was hit
    And after a refresh

    Todo list & bugs

    • FIx the percentage of machines that sometimes fails
    • add more logging
    • find a method to package into an app for macos

    Used Python Modules

    These are the modules I am using in this application

    • tkinter
    • tkcalendar
    • datetime
    • configparser
    • os
    • horizon_functions (own custom module)
    • keyring
    • requests
    • threading
    • time
    • sys
    • math
    • loguru
    • ttkthemes

    Getting Horizon Events using the REST API

    In a previous post I described on how to configure the Horizon Event database using the REST API’s. In this post I will describe on how you can retreive those events using a script that I have created. To get the first few events is easy, just use the /external/v1/audit-events api cmdlet and you get the first batch of events in an unsorted fashion. The script that I have created will get the events since a certain date and if you want only gets the types with a certain severity.

    The script is created for Powershell 7 and has been tested with 7.3.4

    Parameters

    I have written 4 parameters into this script, 2 are mandatory and 2 are optional

    • Credential
      • This optional parameter needs to be a credential object from get-credential. If this is not supplied you will be asked to provide credentials in domain\username and password.
    • ConnectionServerFQDN
      • This mandatory parameter needs to be a string object with the fqdn of the connection server to connetc to i.e. server.domain.dom
    • SinceDate
      • This mandatory parameter needs to be a datetime object for the earliest date to get events for. for example use (get-date).adddays(-100) to get events up to 100 days old.
    • AuditSeverityTypes
      • This optional parameter needs to be an array with SeverityTypes to get events for. Allowed types are : INFO,WARNING,ERROR,AUDIT_SUCCESS,AUDIT_FAIL,UNKNOWN.

    Usage

    First I get my credentials using get-credential, you cna also import them from an xml using import-clixml creds.xml for example

    $credentials = get-credential

    Next I get all events for the last day using:

    .\Horizon_Rest_Get_Events.ps1 -ConnectionServerFQDN pod1cbr1.loft.lab -sincedate (get-date).AddDays(-1) -Credential $credentials

    Or just the ERROR and INFO events using:

    .\Horizon_Rest_Get_Events.ps1 -ConnectionServerFQDN pod1cbr1.loft.lab -sincedate (get-date).AddDays(-100) -Credential $credentials -auditseveritytypes "ERROR","AUDIT_FAIL"

    Yes I had to get back in days some further to get error events.

    The Script

    The script itself can be found on my github .

    My portable Lab

    For a long time I have been wanting to have some kind of portable lab but I had a few requirements that where limiting me:

    • has kvm (for troubleshooting as things always break for me)
    • low power consumption
    • Does’t require a dedicated suitcase for transportation

    Not so long ago I got a newer laptop from ControlUp to replace my (not that old) annoying heat and noise generating Dell Lattitude 5401. I never could get this thing even a bit quiet without hurting performance. While I found it annoying to work daily on I thought it might be a good match to turn this laptop in a portable lab.

    The Host

    One of the advantages was that the laptop did have an intel nic built in so I decided to see if I could get ESXi installed on it. After some tinkering I found out that the only thing required for this was to change the disk mode in the bios from raid to ahci. (Why dell? why? the thing has only one nvme…) and after that change ESXi installed without issues.

    Now with the 16GB that my machine had I could run ESXi but it was far from enough for what I was going to need and neither was the storage at 512GB. I did have an intel nvme 1TB 660P nvme in one of my servers that I honestly wasn’t even using so that was swapped in quickly. I also had a Gigabite Brix box with 64GB that I had planned to run 24/7 but with the rising energy costs I never did that and it was mostly gathering dust as my regular homelab has more than enough cpu/memory. These where 2 Lexar Modules and after another quick swap you might have seen this picture on twitter.

    https://twitter.com/Magneet_nl/status/1637753984642437120

    Specs

    Connectivity

    Routing

    So I had my host with build-in KVM & UPS, now I also needed connectivity. When using it at home it has it’s own dedicated vlan but on the go I needed some kind of router that can use the same vlan. I did not wat to go for a virtual router as I do not want to potentially connect an ESXi host to a for me unknown network. Again I had a few requirements:

    • USB powered (Requiring a single socket is already enough)
    • Small
    • 2 ports (wan and lan side)
    • gbit
    • wifi (so my regular laptop can connect)
    • Bonus : usb tethering for my phone in case all other connection options fail

    In the end I decided to order the GL.iNet GL-AR300M16. While there might have been cheaper options there aren’t that many that have multi rj45 ports and also have openwrt as that is a very flexible OS for routers like these. The only thing I have had to change oimn the router was the ip addresses that it uses + I had to disablke dhcp as I wanted the domain controller to do that. It switches very easily between wired and tethering so it’s checkeng all the boxes that I needed.

    Storage

    Yes I know I said this machine is a mobile lab but I did want to have an option to connect shared storage in case I want to do maintenance etc. I did not want to use the built-in nic for this so I went looking for a usb to rj45 adapter that supports 2,5gbps as I have a Qnap TS-464 with 2,5gbps connectivity. The first step was to check the usb nic fling for what adapters will work. Now this list doesn’t include the speed but a quick look at amazon showed me that the RealTek 8156 will work and the images (not the text!) in this amazon CY USB-C to 2,5GBPS adapter showed that it should work. So another quick amazon order later I was able to prove it worked and I had connectivity to my NAS.

    The Setup

    The host

    On Host level I didn’t need to make too much changes besides network configuration. The only things done so far was to install the USB NIC Fling, configure NTP, add storage and last but not least enable TPS! For a LAB this small any gain in memory availability is essential so TPS is really needed.

    vCenter

    I went with the smalles vCenter option available and for now it’s running ok with 2cpu’s and 14GB of ram.

    The domain

    The first VM to be deployed was my domain controller as that’s needed for just about everything I do. I went for a domain called LoaL.lab wich is an abbreviation for Lab On A Laptop. As always I name my vm’s with the environment name in it so the first VM to be deployed was LoaLDC which was quickly transformed into a domain controller, dns and dhcp server.

    VM’s

    The goal for this portable lab was to have something that can showcase ControlUp Integrated with vSphere, Horizon, App Volumes & DEM so I ended up with these VM’s. As you can see the connection server more or less is the bitch of this setup and has multiple functions:

    NameFunctionCPUMemory
    LoaLVCvCenter (Tiny)214
    LoalDCDomain Controller26
    LoaLCSVMware Horizon Connection Server, SQL Server, File server212
    LoaLAPPApp Volumes Server26
    LoalCUControlUp Monitor26
    LoalRecVMware Horizon Session Recorder (needed for demo)24
    LoalW10Static management VM, also available via Horizon28

    Desktop Pools & RDS Farms

    So besides the manual desktop pool for the mgmt VM I also have an Instant Clone Pool and and RDS Farm. For both I have an App Volume available with notepad ++ and one of the test users also has a writable volume. All iof this still works while I have made the desktops and rds machines as small as possible: 1 cpu and 1gb of ram! Don’t expect that you are able to do a lot but I can login and start notepad++ and that’s enough here. In the VDI pool there are 2 machines while there is a single RDS host deployed. Both Golden Images where optimized to dead with just about everything selected in the VMware OS Optimization Tool.

    On the RDS Host the app volume is published using the per user on-demand integration that was added for Horizon 2206. See this article: Revolutionize virtual apps by publishing apps on demand on generic RDSH servers – VMware End-User Computing Blog

    Notepad ++ as on-demand App Volume
    The login sequence
    And as seen from App Volumes

    Resource Consumption

    So with 64GB of ram this is clearly the bottleneck for this system but how is it looking after it has been running for a few hours? I am receiving a warning about memory consumption but the current usage is 58GB but I don’t see any swapping or ballooning so that’s nice. I also like the Shared Common metric

    Power Consumption

    While it might be less relevant for a mobile lab I am interested in the power consumption and this is the graph for the current period. This data is coming from a Blitzwolf smart plug connected to my Homey. So the peak at boot time comes close to 80W but seems to stabalize after that.

    The ControlUp setup

    As a ControlUp employee one of this lab’s usecases was to display what we can do with ControlUp. This is a brand new CU environment that is mainly using Real-Time DX but for fun I have also deployed the Edge DX Agent to a few servers.

    Introducing the Horizon Golden Image Deployment Tool

    Intro

    Some of you might have seen previews on the socials but for the last few months I have been working hard on a GUI based tool to deploy golden images to VMware Horizon Instant Clone Desktop Pools and RDS Farms. This because who isn’t sick and tired of having to go into each and every Pod admin interface to o a million clicks to deploy a few new golden images?

    The work started mid December and as usual the first 75% was done pretty quickly so mid January I had a first working version. While I expected a first version that would only support Desktop Pools to be available mid February (Vmug Virtual EUC day someone?) this build actually already had most of the parts in place to support both Desktop Pools and RDS Farms. After this first build my regular work for ControlUp started picking up again so I had less time to fix the numerous bugs that I encountered. Well bugs? Most where logic errors on my part but most of them have been ironed out by now. All in all the version that you can use today has cost me ~80hrs in building & testing and many many more hours thinking about solutions.

    Is the tool perfect? Definitely not but it works and it’s more than what we’ve had before and I will be looking into impriving it even more.

    The Tool

    I guess you got bored with the talking and would like to see the tool now. The content of thos blog post might get it’s own page later for documentation purposes. The tool itself can be downloaded from this GitHub repository. Make sure to grab both the xaml and ps1 files, put them in a single folder and you should be good. The tool entirely runs on Powershell and is using the WPF Framework for the GUI.

    Quick jumps:

    Requirements

    There are only 2 real requirements:

    • Powershell 7.3 or later (for performance reasons I used some PS 7.3 options so the tool WILL break with an older version.)
    • Horizon 8 2206 or later (the reason for this is that otherwise the secondary images wouldn’t be available and that’s a feature I wanted in there for sure.)

    Deplying a new golden Image

    For normal usage you can just start the ps1 file.

    This will bring you to this tab, before the first use though you need to go to the configuration page.

    Fill in one of your cconnection servers, credentials and hit the test button. If you have a cloud pod setup the other pods will be automatically detected and make sure to check the Ignore Certificate Errors checkbox in case that’s needed!

    If the test was successfull you are good to go to either the Desktop Pools or RDS Farms tabs. Both are almost completely the same so I will only show desktop pools

    Hit the connect button and all Instant Clone Pools or Farms from all pods will be auto populated in the first pull down menu.

    The second pulldown menu allows you to select a new source VM

    And the third pull down the snapshot (I guess you guessed that already, I might need to add labels but they are so ugly in WPF)

    To the right you have all kinds of options that you should recognize from the regular gui including add vTPM that was added in Horizon 2206. This checkbox isn’t in the RDS Farms as we currently simply don’t have the option to have Horizon add a vTPM there. If the options are valid ( like more cores that cores/socket and if those numbers will work (can’t do 3 cores and 2 cores per socket!) the Deploy Golden Image becomes available. Hit this to start the deployment, you can check the status by hitting the refresh button. (don’t tell anyone but the functions does exactly the same as a connect)

    Handling Secondary Images

    By default a secondary image will not be pushed to any machines. Just select the Push As Secondary Image checkbox and hit Deploy Golden Image button.

    What you can also do is select one or more of the machines and deploy the Golden Images ot those machines

    Once a Secondary Image has been deployed the three other buttons come available.

    From top to bottom you can either cancel the secondary image completely, apply the golden image to more desktops (selecting ones that already has it won’t break anything and will just deploy it when possible) and promote the Secondary Image to the Primary golden image for the pool. The latter will cause a rebuild of ALL machines including the ones already running on the image. In the future I will also add a button to configure a machine to run the original image.

    Settings and logs location

    All settings are stored in an xml file in %appdata%\HGIDTool this includes the password configured in the Settings tab as a regular Encrypted PowerShell Credentials object. If you didn’t hit save on the settings tab this will be automatically done when closing the tool.

    In case you want to borrow some of my code the log files contain every API call that I do to get date, push an image or handlke secondary images. This is the same output as you’d get when running in -verbose mode so that’s only needed when you’re troubleshooting the tool.