Using PowerCLI in Smart Card Based Environment

ref: https://kb.vmware.com/s/article/67789

Problem

You work in a hardened environment and you don’t have an administrative username and password because you only have smart cards or tokens.

Resolution

According to VMware, this is expected behavior. Uh, what?

Workaround

According to VMware, “Use Windows SSPI to pass through the Windows logged session Smart Card credentials to PowerCLI to authenticate to vCenter.”

What does that even mean? That was a lot of words that didn’t really explain how to “work around” the issue. So, let’s break it down.

Windows SSPI is Security Support Provider Interface. More details can be found here, https://docs.microsoft.com/en-us/windows/win32/rpc/security-support-provider-interface-sspi-

The optimal way to address this is to have a dedicated workstation or server in the data center that you can log into with PowerShell and PowerCLI installed. Since you are hopefully logging in with your administrator token and your administrator account has either been given permission directly on vSphere or through a security group, then you should be able to just log into the server when you try to use Connect-VIServer (https://vdc-repo.vmware.com/vmwb-repository/dcr-public/85a74cac-7b7b-45b0-b850-00ca08d1f238/ae65ebd9-158b-4f31-aa9c-4bbdc724cc38/doc/Connect-VIServer.html). The credential of the currently logged on user, that’s you, will get passed along with the request.

PowerCLI Script to add vCenter Privileges for VMware Horizon 7

ref: https://docs.vmware.com/en/VMware-Horizon-7/7.12/horizon-installation/GUID-A878F876-B359-42FC-9124-A1E34BFB3319.html

ref: https://code.vmware.com/docs/11794/cmdlet-reference/doc/Get-VIPrivilege.html

ref: https://code.vmware.com/docs/11794/cmdlet-reference/doc/New-VIRole.html

ref: https://code.vmware.com/docs/11794/cmdlet-reference/doc/Set-VIRole.html

$VIRoleName = "View Manager Role"
$VIRolePrivileges = @(`
    # Folder  
    'Create Folder', 'Delete Folder',`
    # Datastore
    'Allocate space',`
    # Virtual Machine - Configuration
    'Add or remove device', 'Advanced configuration', 'Modify device settings',`
    # Virtual Machine - Interaction
    'Power off', 'Power on', 'Reset', 'Suspend', 'Perform wipe or shrink operations',`
    # Virtual Machine - Inventory
    'Create new', 'Create from existing', 'Remove',`
    # Virtual Machine - Provisioning
    'Customize guest', 'Deploy template', 'Read customization specifications', 'Clone template', 'Clone Virtual Machine',`
    # Resource
    'Assign virtual machine to resource pool',`
    # Global
    'Act as vCenter Server',`
    # Host
    'Advanced settings',`
    # Profile-driven Storage
    'Profile-driven storage view', 'Profile-driven storage update'
    )

try {
    # Get list of current Roles
    $VIRoles = Get-VIRole

    # Check if Role exists
    foreach($VIRole in $VIRoles) {
        if ($VIRole.Name -like $VIRoleName) {
            # Role exists
            exit
        } 
    }

    # Assume the Role does not exist
    # Create the new Role
    New-VIRole -Name $VIRoleName
    
    # Add the Privileges to the Role
    foreach($VIRolePrivilege in $VIRolePrivileges) { 
        Set-VIRole -Role $VIRoleName -AddPrivilege $VIRolePrivilege 
    }

} catch {
    
}

Copy and paste the contents above to a new PowerShell file. This script will check if the given Role exists and exit or it will create the Role and add the Privileges. This script will not check the current assigned Privileges if the Role exists.

Deploy VMware Unified Access Gateway with PowerShell

Each version of the Unified Access Gateway will also have PowerShell scripts available in a .zip file. For this post, I am using Unified Access Gateway 20.09. The components can be downloaded from https://my.vmware.com/web/vmware/downloads/info/slug/desktop_end_user_computing/vmware_unified_access_gateway/20_09. You will want to the appliance itself as well as the PowerShell scripts. For this post, I am going to use the FIPS version.

You are also going to need the ovftool that can be downloaded from https://code.vmware.com/web/tool/4.4.0/ovf. In this post, we are going to need the newest version, 4.4.1. Make sure you have installed the ovftool on your Windows machine you are going to deploy from. Also, ensure you can access ovftool from the command line. You may have to add this to the System PATH.

Optionally, you can use an Integrated Development Environment (IDE) of your choice. For this post, I am going to use Visual Studio Code, available at https://code.visualstudio.com/download.

I have TLS certificates from Let’s Encrypt, so I am going to deploy with them as well. For me, this is how I have created my directory structure.

  • certs – contains all of my TLS certificates needed
  • ova – contains the ova I am going to deploy
  • uag-001v_setting.ini – this is the settings file I use for one of my Unified Access Gateways (UAG). You would need one per UAG you want to deploy.
  • uagdeploy.ps1 – PowerShell script included in the PowerShell scripts zip
  • uagdeploy.psm1 – PowerShell module included in the PowerShell scripts zip

The following is my uag-001v_settings.ini file. You can get the “barebones” file after deploying and configuring at least one UAG in vSphere. Then you can export the settings and modify as necessary.


[General]
eth0ErrorMsg={"netmask":"SUCCESS","ip":"SUCCESS","defaultGateway":"SUCCESS"}
#netInternet: Portgroup used in vSphere for Internet/DMZ facing interface
netInternet=DMZ
#ip0: IP address for the netInternet interface
ip0=10.10.10.30
diskMode=
#source: The location of the OVA to deploy
source=.\ova\euc-unified-access-gateway-fips-20.09.0.0-16949983_OVF10.ova
#ip1: IP address for the internal interface
ip1=192.168.92.30
#defaultGateway: IP address for the gateway on the netInternet interface
defaultGateway=10.10.10.1
#target: User ([email protected]), 
#target: vCenter(vcenter.aaronrombaut.com), 
#target: host location (/Datacenter/host/Cluster/vmh-001p.aaronrombaut.com)
target=vi://[email protected]:[email protected]/Datacenter/host/Cluster/vmh-001p.aaronrombaut.com
#ds: Datastore to install to
ds=Synology-LUN01
netmask0=255.255.255.0
#netManagementNetwork= Portgroup used in vSphere
netManagementNetwork=LAN
#netBackendNetwork: Portgroup used in vSphere
netBackendNetwork=LAN
ip0AllocationMode=STATICV4
#name: Name of the Unified Access Gateway appliance
name=uag-001v
deploymentOption=twonic
ip1AllocationMode=STATICV4
netmask1=255.255.255.0
authenticationTimeout=300000
fipsEnabled=true
sysLogType=UDP
uagName=uag-001v
clockSkewTolerance=600
locale=en_US
tls12Enabled=true
tls13Enabled=false
ipMode=STATICV4
requestTimeoutMsec=10000
ipModeforNIC2=STATICV4
tls11Enabled=false
clientConnectionIdleTimeout=360
tls10Enabled=false
adminCertRolledBack=false
cookiesToBeCached=none
enableHTTPHealthMonitor=false
snmpEnabled=false
maxSystemCPUAllowed=100
healthCheckUrl=/favicon.ico
quiesceMode=false
#dns: The Domain Name System server in use
dns=192.168.92.10
isTLS13SetByUser=false
isCiphersSetByUser=false
tlsPortSharingEnabled=true
ceipEnabled=false
bodyReceiveTimeoutMsec=15000
monitorInterval=60
maxConnectionsAllowedPerSession=16
cipherSuites=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
adminPasswordExpirationDays=90
httpConnectionTimeout=120
#dnsSearch: The domain name to use in queries
dnsSearch=aaronrombaut.com
isTLS11SetByUser=false
sessionTimeout=36000000
syslogSystemMessagesEnabled=false
ssl30Enabled=false
#ntpServers: Servers for providing time in the infrastructure
ntpServers=time.cloudflare.com
#sshEnabled: Leave this blank to NOT enable ssh which is recommended in Production
sshEnabled=

[Horizon]
#proxyDestinationUrl: URL for the VMware Horizon Connection Server
proxyDestinationUrl=https://hzn7cs-001v.aaronrombaut.com
disableHtmlAccess=false
rewriteOriginHeader=false
healthCheckUrl=/favicon.ico
proxyDestinationIPSupport=IPV4
queryBrokerInterval=300
matchWindowsUserName=false
windowsSSOEnabled=false
pcoipDisableLegacyCertificate=false
gatewayLocation=External
securityHeaders={"X-Frame-Options":"SAMEORIGIN","Strict-Transport-Security":"max-age=63072000; includeSubdomains; preload","X-Content-Type-Options":"nosniff","Content-Security-Policy":"default-src 'self';font-src 'self' data:;script-src 'self' 'unsafe-inline' 'unsafe-eval' data:;style-src 'self' 'unsafe-inline';img-src 'self' blob: data:","X-XSS-Protection":"1; mode=block"}
proxyDestinationUrlThumbprints=
tunnelExternalUrl=https://myhorizon.aaronrombaut.com:443
blastExternalUrl=https://myhorizon.aaronrombaut.com:8443
radiusClassAttributeList=
smartCardHintPrompt=false
logoutOnCertRemoval=false
redirectHostMappingList=
proxyPattern=(/|/view-client(.*)|/portal(.*)|/appblast(.*))
pcoipExternalUrl=72.225.4.11:4172

[SSLCert] #External facing
pemPrivKey=
pemCerts=
#pfxCerts: The location where the certificates are located
pfxCerts=.\certs\myhorizon-prod.p12
pfxCertAlias=

[SSLCertAdmin] #Internal facing
pemPrivKey=
pemCerts=
#pfxCerts: The location where the certificates are located
pfxCerts=.\certs\myhorizon-prod.p12
pfxCertAlias=

[PackageUpdates]
packageUpdatesScheme=OFF

Open an elevated PowerShell window and navigate to the working directory. You will want to type the following to start the deployment:

.\uagdeploy.ps1 .\uag-001v_settings.ini

Enter and re-enter the root password and admin password.

Enter Yes or No if you want to join the VMware Customer Experience Improvement Program (CEIP).

Enter the password for the vCenter you specified in the settings.ini file above.

You should receive a “deployed successfully” message at the end. From here, you should be able to navigate to the appliance in a web browser and if you used certificates, your page should be accessible, securely. For me, I access the UAG at https://uag-001v.aaronrombaut.com:9443.

If you have other Unified Access Gateways to deploy, just modify the settings.ini file and deploy. Make sure you have records for each UAG added to your DNS forward and reverse lookup zones.

Test-ServiceRunning

function Test-ServiceRunning {
    param(
        [Parameter(Mandatory=$true)]    
        [String]$ServiceName
    )

    $ValidServiceNames = Get-Service | Select-Object -Property "Name"

    try {
        # Test if valid service name
        if ($ValidServiceNames.Name -contains $ServiceName) {
            #Write-Host "Service name exists and can be used!"
            $ServiceToCheck = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
            if ($ServiceToCheck | Where-Object {$_.Status -eq "Running"}) {
                #Write-Host "Running"
                return $true
            } else {
                #Write-Host "Not Running"
                return $false
            }
        } else {
            Write-Host "Not a valid service name!"
            exit
        }
    } catch {
        Write-Error "Error in processing request"
    }
}

PowerShell Error Catching

PowerShell error catching has been very frustrating for me. I try to do the right thing by putting code in a Try-Catch-Finally block, but continue to struggle with catching specific errors. I don’t know why the ‘thing’ I need to catch is not output with the error. I have to go through the error and ‘hunt’ for the thing to catch. Below is the basic syntax for a PowerShell Try-Catch-Finally block.

try {
    # Do something
} catch [something here to catch] {
    # Handle the error
} finally {
    # This section will always run
}

The sample below is similar to what you would typically see after an error is caught. Unfortunately, there is nothing in the output that you can use to ‘catch’.

What you end up having to do is use

$Error[0] | Format-List * -Force

This will show a longer output of the error, and more specifically, the ‘thing’ to ‘catch’. See the highlighted text below.

Now that there is a specific Exception to catch, we can add a new catch block to our Try-Catch-Finally block.

try {
    # Do something
} catch [System.Management.Automation.ParameterBindingException] {
    # Handle the specific error
} finally {
    # This section will always run
}

The best help I was able to find for this came from a Spiceworks post by Duffney, https://community.spiceworks.com/how_to/121063-using-try-catch-powershell-error-handling. It was by far the easiest method to discover the specific error to use in the catch block.

I am not quite sure why it is so hard to find the exception to use in the catch block or why it is just not part of the error output. I suppose for someone who codes in PowerShell everyday, this post will be laughable. I just wanted to make sure I recorded a post for future reference because I know for sure, I will struggle with this concept again.

How to Dot Source a PowerShell Script

So I wrote a script the other day in PowerShell ISE and it worked fine. But when I wanted to use the function in a standard PowerShell window, I was perplexed. It is actually a really easy thing to take care of.

In the case of my server, I wrote a PowerShell function and stored it in my Documents folder. So when opening a new PowerShell window, all I had to do was run the following command:

PS C:\Users\<username>\Documents> . .\New-Function.ps1

This made all of the functions written in the script available for use during my current session. For me and this case, I am OK with only running the script when needed.

Change the Canonical Name (CN) of an Active Directory User

One of the most annoying things for me (and I assume many other Systems Administrators) is going into an organization and querying users to find all variations of name formats. Some are ‘first name last name’, ‘last name, first name’, or some other variation. It’s almost like an archeological dig where you can see periods of time that there was one format and then later on another format came along.

What’s frustrating is knowing how easy it is to change the format and that it doesn’t happen. It’s fine if the organization wants to change the format, but if that’s the case, then be sure to change the information already contained to match. There is nothing wrong with periodically running a “cleanup” script over Active Directory to make everything uniform. The great thing about Active Directory is that it is a database, it already contains the information. The DisplayName property and the cn are display properties, they can be changed whenever without affecting the user object. Also, running a script like the one below can clean up and make Active Directory uniform in a matter of seconds, if not less.

	# Where the users are located that you want to change
	$exerciseUsersOU = 'OU=USERS,OU=TEMPORARY,DC=aaronrombaut,DC=com'
	# An array of the users (adjust Properties as needed)
	$exerciseUsers = Get-ADUser -Filter * -SearchBase $exerciseUsersOU -Properties Title, GivenName, Surname
	
	# Loop through all the users
	foreach ($exerciseUser in $exerciseUsers)
	{
		$title = $exerciseUser.Title
		$firstName = $exerciseUser.GivenName
		$lastName = $exerciseUser.Surname
		
		# The following line will adjust the DisplayName
		Set-ADUser -Identity $exerciseUser -DisplayName "$lastName, $firstName, $title"
		
		# The following line will adjust the cn
		Rename-ADObject -Identity $exerciseUser -NewName "$lastName, $firstName"
	}

The following images show a before and after but are only a representation of what you can rename from and to. Your organization may use different naming standards. Either way, when standards change, be sure to adjust the objects already present. This will be much more professional and organized.

Before renaming the User
After renaming the User