Securing your Horizon Universal Access Gateway (UAG) with a genuine SSL certificate from a recognised vendor is an important process. It enables your users to be sure they’re connecting to the correct VDI infrastructure, and that the communications between their endpoint and remote desktop are secure.
However, SSL certificates are often not cheap and replacing them can be an administrative burden.
In this post, I will show you how to leverage Let’s Encrypt to provide free SSL certificates, and how the renewal and replacement process can be automated.
Let’s Encrypt
Let’s Encrypt is a not-for-profit certificate authority offering free SSL certs valid up to ninety days. Users can renew their certificates any time during this period.
The aim of the service is to reduce complexity and the management overhead of acquiring and installing SSL certificates on servers. To facilitate this, the Automated Certificate Management Environment (ACME) protocol is used to validate domain ownership. This is typically done by placing a file in a web server’s root directory or the creation of a TXT record in the domain’s DNS zone. Third-party add-ons can be used to interface with well-known web and DNS providers such as Amazon Web Services Route 53, which I will be using this in this post.
Getting Started
What we’ll need:
- PowerShell ACME Client (Posh-ACME) by Ryan Bolger
- Authentication method (in this case I’ll be using Route 53 from Amazon Web Services)
- PowerShell script
AWS Configuration
To enable Posh-ACME to communicate with AWS, we need to create an API key and secret.
Login into the AWS Console and navigate to Route 53. In the following screenshot you can see I have two hosted zones:

Route 53
Click on Hosted Zones and make a note of your Hosted Zone ID(s):

Hosted Zone ID(s)
Navigate to the AWS IAM and select Users, then click Add User. Give the user a name, select Programmatic access and then click Next
Under Set Permissions, click Attach Existing Policies Directly, then click Create Policy. A new window will open. Click the JSON tab and paste in the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"route53:ListHostedZones", | |
"route53:GetChange" | |
], | |
"Resource": [ | |
"*" | |
] | |
}, | |
{ | |
"Effect" : "Allow", | |
"Action" : [ | |
"route53:ChangeResourceRecordSets", | |
"route53:ListResourceRecordSets" | |
], | |
"Resource" : [ | |
"arn:aws:route53:::hostedzone/YOURHOSTEDZONEID" | |
] | |
} | |
] | |
} |
Remember to replace YOURHOSTEDZONEID with the actual ID of your zone(s).
Click Review Policy, and then give it a name:
Click Create Policy.
Switch back to the original IAM window and click the refresh icon. Filter the policies to find the one you just created and check to select it, then click Next.
Click Next again, followed by Create User. The success screen will appear:
Make a note of the Access Key ID and the Secret Access Key, as you will need these later on.
Replacing the Certificates
The last step in the process is our script. It takes two parameters. The first is the external/public hostname of your UAG(s). This hostname often points to load-balanced address and is backed by more than one UAG.
The second parameter is the list of UAGs you wish to upload the certificate to. The certificate is only applied to the Internet interface, as it is common for the admin interface (on port 9443) to use a different certificate which is typically supplied from an internal CA.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Uploads freshly minted certs to Horizon UAG | |
.DESCRIPTION | |
Generates an SSL certificate from Let's Encrypt, then connects to each UAG using the REST API and applies the certificate to the Internet interface. The Admin interface (port 9443) is unaffected. | |
.PARAMETER dnsName | |
The public DNS name of the UAG | |
.PARAMETER uags | |
Comma-seperated list of UAGs | |
.EXAMPLE | |
Update-UAGCerts.ps1 <DNS name> <UAGs> | |
.NOTES | |
Author: Mark Brookfield (@virtualhobbit) | |
#> | |
param( | |
[Parameter(Position=0,Mandatory=$true)] | |
[string]$dnsName, | |
[Parameter(Position=1,Mandatory=$true)] | |
[string[]]$uags | |
) | |
if (!$dnsName) { | |
Write-Error "No DNS name supplied – aborting" | |
exit | |
} | |
if (!$uags) { | |
Write-Error "No UAGs supplied – aborting" | |
exit | |
} | |
# Define Lets Encrypt parameters | |
$psMod = "Posh-ACME" | |
$dnsPlugin = "Route53" | |
$r53Params = @{R53AccessKey='YOURACCESSKEY'; R53SecretKey='YOURSECRETKEY'} | |
$email = "you@youremail.com" | |
# Define UAG credentials | |
$user = 'admin' | |
Write-Host "Please enter the UAG admin password. Please note this must be the same for all UAGs." | |
$pass = Read-Host –AsSecureString "Admin password" –Force | |
$pass = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass)) | |
$creds = "$($user):$($pass)" | |
# Encode credentials | |
$encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($creds)) | |
if (!(Get-InstalledModule –Name $psMod)) { | |
# Install the Posh-ACME module | |
Install-Module –Name $psMod –Scope CurrentUser –Force | |
} | |
# Set Let's Encrypt server | |
Set-PAServer LE_PROD | |
# Order the certificate | |
New-PACertificate $dnsName –AcceptTOS –DnsPlugin $dnsPlugin –PluginArgs $r53Params –Contact $email –Verbose –Force | |
$newCert = Get-PACertificate | |
# Convert private key to one-liner | |
$privKey = [IO.File]::ReadAllText($newCert.KeyFile) | |
$privKeyReplace = $privKey.Replace("`n",'\n') | |
# Convert SSL certificate to one-liner | |
$cert = [IO.File]::ReadAllText($newCert.FullChainFile) | |
$certReplace = $cert.Replace("`n",'\n') | |
# Create JSON body | |
$json = '{"privateKeyPem":"' + $privKeyReplace + '","certChainPem":"' + $certReplace + '"}' | |
# Define API parameters | |
$params = @{ | |
Headers = @{ 'Authorization' = "Basic $encodedCreds" } | |
Method = 'PUT' | |
Body = $json | |
ContentType = 'application/json' | |
} | |
ForEach ($uag in $uags){ | |
# Define the URI | |
$Uri = "https://" + $uag + ':9443/rest/v1/config/certs/ssl' | |
# Display UAG | |
Write-Host "UAG is: " $uag | |
# Connect to each UAG and replace SSL certificate and private key | |
Invoke-RestMethod $uri @params | |
} |
Don’t forget to replace the following variables:
- $r53Params
I hope this was useful. Please reach out on Twitter (virtualhobbit) if you have any issues, or more importantly if you can improve the process/script!
Happy minting!
Pingback: Newsletter: December 27, 2019 – Notes from MWhite
Pingback: Newsletter: January 4, 2020 – Notes from MWhite