How does browser know what Certificate to provide for Client Cert Authentication

Ever wonder how the browser provided the client cert when multiple are available for the user?

The answer is the server that is requesting the client cert for authentication has an option to ask for the cert that is signed by a specific Distinguished Name CA, which is part of the server communication. You can see this in wireshark capture, No need for SSL decryption, as the DN CA information is part of server hello messages which happens before the encryption beings between the server and client.

As long as the browser can find the matching cert with a private key signed by DN CA, it has the ability to silently provide to the server based on browser settings. If there is more than one issued by the DN CA, it will prompt the user to make the selection. If the browser finds a matching cert but it doesn’t have the private key, it will skip that cert.

If the server just asks for client cert without providing DN CA, then the browser displays all available client cert that have the private key and prompts the user to make the selection.

Powershell Get Citrix Current Session Logon Time

$code = @'
using System;
using System.Runtime.InteropServices;


namespace WFAPI
{
    
   public  enum WF_INFO_CLASS
    {
            WFVersion,             // OSVERSIONINFO
            WFInitialProgram,
            WFWorkingDirectory,
            WFOEMId,
            WFSessionId,
            WFUserName,
            WFWinStationName,
            WFDomainName,
            WFConnectState,
            WFClientBuildNumber,
            WFClientName,
            WFClientDirectory,
            WFClientProductId,
            WFClientHardwareId,
            WFClientAddress,
            WFClientDisplay,
            WFClientCache,
            WFClientDrives,
            WFICABufferLength,
            WFLicenseEnabler,
            RESERVED2,
            WFApplicationName,
            WFVersionEx,
            WFClientInfo,
            WFUserInfo,
            WFAppInfo,
            WFClientLatency,
            WFSessionTime,
            WFLicensingModel
    } 
    [StructLayout(LayoutKind.Sequential)]
    public struct WF_SESSION_TIME
    {
        public long ConnectTime;
        public long DisconnectTime;
        public long LastInputTime;
        public long LogonTime;
        public long CurrentTime;
    }
    
    public class Program
    {
        [DllImport("wfapi64.dll", CharSet=CharSet.Unicode,SetLastError=true)]
        public static extern bool WFQuerySessionInformation(System.IntPtr hServer, int sessionId, WF_INFO_CLASS WFInfoClass, out System.IntPtr ppBuffer, out uint pBytesReturned);              

        [DllImport("wfapi64.dll", ExactSpelling = true, SetLastError = false)]
        public static extern void WFFreeMemory(IntPtr memory);

        public const int WF_CURRENT_SESSION = -1;

        public static string GetClientName()
        {
            System.IntPtr buffer = IntPtr.Zero;
            uint bytesReturned;
            try
            {
                bool sessionInfo = WFQuerySessionInformation(System.IntPtr.Zero, WF_CURRENT_SESSION, WF_INFO_CLASS.WFClientName, out buffer, out bytesReturned);
                return Marshal.PtrToStringUni(buffer);
            }
            catch
            {
                return string.Empty;
            }
            finally
            {
                WFFreeMemory(buffer);
                buffer = IntPtr.Zero;
            }
        }       
        public static string getSessionLogonTime()
        {
            System.IntPtr buffer = IntPtr.Zero;
            string sessionLogonTime = "";
            uint bytesReturned = 0;            
            try
            {
                bool sessionInfo = WFQuerySessionInformation(System.IntPtr.Zero, WF_CURRENT_SESSION, WF_INFO_CLASS.WFSessionTime, out buffer, out bytesReturned);
                if(sessionInfo)
                {
                    WF_SESSION_TIME WFSessionTime = (WF_SESSION_TIME)Marshal.PtrToStructure(buffer, typeof(WF_SESSION_TIME));
                    sessionLogonTime = Convert.ToString(DateTime.FromFileTime(WFSessionTime.LogonTime));     
                }
            }
            catch
            {
                return string.Empty;
            }
            finally
            {
                WFFreeMemory(buffer);
                buffer = IntPtr.Zero;
            }
            return sessionLogonTime;
        }  

    }
}

'@
Add-Type -TypeDefinition $code -Language CSharp   
[WFAPI.Program]::getSessionLogonTime()

WFAPI disconnect sessions that exceed certain idle time [override policy]

/*
Author - Siva Mulpuru [sivamulpuru.com]
Usage - WFAPI "maxIdleTimeInMin" "GatewaySNIP"
Comment - Workaround for Citrix Virtal Apps [FKA XenApp] to disconnect AGW connections when exceed passed value. Designed to run as scheduled job. 
          Allows non AGW connections to use serverIdleTimeout policy while providing override for AGW connections.
 
 */

using System;
using System.Runtime.InteropServices;

namespace WFAPI
{
    class Program
    {
        [DllImport("wfapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern bool WFQuerySessionInformation(System.IntPtr hServer, int sessionId, WF_INFO_CLASS WFInfoClass, out System.IntPtr ppBuffer, out uint pBytesReturned);

        [DllImport("wfapi.dll", ExactSpelling = true, SetLastError = false)]
        public static extern void WFFreeMemory(IntPtr memory);

        [DllImport("wfapi.dll", ExactSpelling = true, SetLastError = false)]
        public static extern bool WFDisconnectSession(System.IntPtr hServer, int sessionId, bool bWait);

        [DllImport("wfapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern bool WFEnumerateSessions(IntPtr hServer, 
            [MarshalAs(UnmanagedType.U4)] Int32 Reserved,  
            [MarshalAs(UnmanagedType.U4)] Int32 Version,  
            ref IntPtr pSessionInfo,  
            [MarshalAs(UnmanagedType.U4)] 
            ref Int32 pCount);   

        public enum WF_INFO_CLASS    
        {
            WFVersion,             
            WFInitialProgram,
            WFWorkingDirectory,
            WFOEMId,
            WFSessionId,
            WFUserName,
            WFWinStationName,
            WFDomainName,
            WFConnectState,
            WFClientBuildNumber,
            WFClientName,
            WFClientDirectory,
            WFClientProductId,
            WFClientHardwareId,
            WFClientAddress,
            WFClientDisplay,
            WFClientCache,
            WFClientDrives,
            WFICABufferLength,
            WFLicenseEnabler,
            RESERVED2,
            WFApplicationName,
            WFVersionEx,
            WFClientInfo,
            WFUserInfo,
            WFAppInfo,
            WFClientLatency,
            WFSessionTime,
            WFLicensingModel
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct WF_SESSION_TIME
        {
            public long ConnectTime;
            public long DisconnectTime;
            public long LastInputTime;
            public long LogonTime;
            public long CurrentTime;
        }

        public enum  WF_CONNECTSTATE_CLASS
        {
            WFActive,              // User logged on to WinStation
            WFConnected,           // WinStation connected to client
            WFConnectQuery,        // In the process of connecting to client
            WFShadow,              // Shadowing another WinStation
            WFDisconnected,        // WinStation logged on without client
            WFIdle,                // Waiting for client to connect
            WFListen,              // WinStation is listening for connection
            WFReset,               // WinStation is being reset
            WFDown,                // WinStation is down due to error
            WFInit                 // WinStation in initialization
        }
        
        [StructLayout(LayoutKind.Sequential)]
        public struct WF_CLIENT_ADDRESS
        {
            public int AddressFamily;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
            public byte[] Address;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct WF_SESSION_INFO
        {
            public Int32 SessionID;
            [MarshalAs(UnmanagedType.LPStr)]
            public String pWinStationName;
            public WF_CONNECTSTATE_CLASS State;
        }
        static DateTime getSessionLastInteractionTime(int sessionid)
        {
            IntPtr buffer = IntPtr.Zero;
            DateTime lastInteractionTime = DateTime.Now;
            uint bytesReturned = 0;
            try
            {
                if (WFQuerySessionInformation(IntPtr.Zero, sessionid, WF_INFO_CLASS.WFSessionTime, out buffer, out bytesReturned))
                {
                    WF_SESSION_TIME WFSessionTime = (WF_SESSION_TIME)Marshal.PtrToStructure(buffer, typeof(WF_SESSION_TIME));
                    lastInteractionTime = DateTime.FromFileTime(WFSessionTime.LastInputTime);
                }
            }
            finally
            {
                WFFreeMemory(buffer);                
            }
            return lastInteractionTime;
        }
        
        static string getClientIP(int sessionid)
        {
            IntPtr buffer = IntPtr.Zero;
            uint bytesReturned;
            string clientIPAddress = "0.0.0.0";
            try
            {
                if (WFQuerySessionInformation(IntPtr.Zero, sessionid, WF_INFO_CLASS.WFClientAddress, out buffer, out bytesReturned))
                {

                    WF_CLIENT_ADDRESS si = (WF_CLIENT_ADDRESS)Marshal.PtrToStructure((System.IntPtr)buffer, typeof(WF_CLIENT_ADDRESS));
                    clientIPAddress = si.Address[2] + "." + si.Address[3] + "." + si.Address[4] + "." + si.Address[5];
                }
            }
            finally
            {
                WFFreeMemory(buffer);
            }
            return clientIPAddress;
        }
        
        static void Main(string[] args)
        {
            int maxIdleTimeInMin = Convert.ToInt32(args[0]);
            string gatewaySNIP = args[1];
            //Console.WriteLine("{0} {1}", maxIdleTimeInMin, gatewaySNIP);
            IntPtr pSessionInfo = IntPtr.Zero;
            int Count = 0;
            if (WFEnumerateSessions(IntPtr.Zero,0,1,ref pSessionInfo,ref Count))
            {
                try
                {
                    Int32 dataSize = Marshal.SizeOf(typeof(WF_SESSION_INFO));
                    Int64 current = (int)pSessionInfo;
                    for (int i = 0; i < Count; i++)
                    {
                        WF_SESSION_INFO si = (WF_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WF_SESSION_INFO));
                        current += dataSize;                        
                        if (si.State == WF_CONNECTSTATE_CLASS.WFActive)
                        {
                            //Console.WriteLine("{0} {1} {2}", si.SessionID, getSessionLastInteractionTime(si.SessionID), getClientIP(si.SessionID));                            
                            if(gatewaySNIP.CompareTo(getClientIP(si.SessionID)) == 0 && (DateTime.Now - getSessionLastInteractionTime(si.SessionID)).TotalMinutes > maxIdleTimeInMin)
                            {
                               bool result = WFDisconnectSession(IntPtr.Zero, si.SessionID, false);
                               Console.WriteLine("{0} {1} is {2}", "Disconnect result for SessionID", si.SessionID, result);
                            }
                        }
                    }
                }
                finally
                {
                    WFFreeMemory(pSessionInfo);
                }
            } 
        }
    }
}

Automate Citrix Session Launch for Load tests or health probing

Requirements

  • Citrix Workspace app Version 1808 for Windows or later.
  • Storefront Store needs to have http auth module enabled.

Step 1: Retrieve Resource Name of the app/desktop you would like to automate

“C:\Program Files (x86)\Citrix\ICA Client\AuthManager\storebrowse.exe” -U <username> -P <password> -D <domain> -M 0x2000 -E “https://<storefrontserver>/Citrix/<storename>/discovery”

First column of the output would have the resource name. In My case it’s a windows 10 VDI >
Controller.W10-QA $S2-4‘ ‘W10-QA’ ‘\’ ” https://storefront/Citrix/XDOnly-Internal/resources/v2/Q29udHJvbGxlci5XMTAtUUEgJFMyLTQ-/launch/ica

Step 2: Get the ICA file for the desired Resource Name

“C:\Program Files (x86)\Citrix\ICA Client\AuthManager\storebrowse.exe” -U <username> -P <password> -D <domain> -L “Controller.W10-QA $S2-4” “https://<storefrontserver>/Citrix/<storename>/discovery”

-L with resource name will retrieve the ICA file and save it to %AppData%\Local\Citrix\storebrowse\cache\launch.ica

Step 3: Launch the ICA file

%AppData%\Local\Citrix\storebrowse\cache\launch.ica

Reference > https://docs.citrix.com/en-us/citrix-workspace-app-for-windows/store-browse.html

How to prevent creation of System and Recovery partitions for VDI Master

During the initial windows install screen, press Shift-F10 to launch the command window, and using diskpart create primary partition.

select disk 0
create partition primary
exit

ref > https://www.howtogeek.com/192772/what-is-the-system-reserved-partition-and-can-you-delete-it/

Citrix ADC Load Balance Config for CyberArk PSM

At the time of this post, CyberArk does not have documentation on load balancing Privileged Session Management (PSM) traffic for Citrix ADC, though they provide an example config for F5, it doesn’t translate to Citrix. Hopefully, this would help someone that is trying to do this for ADC.

## Add PSM Backend Servers ##
add server PSM-01 x.x.x.x -comment cyberark
add server PSM-02 x.x.x.x -comment cyberark

## Create  RDS Health Monitor & PSM Health Monitor ##
add lb monitor CyberArk-PSMHealth-monitor HTTP-ECV -send "GET /psm/api/health" -recv PASS -LRTM DISABLED -interval 30 -resptimeout 10 -destPort 443 -secure YES -sslProfile ns_default_ssl_profile_backend
add lb monitor CyberArk-RDSHealth-monitor USER -scriptName nsrdp.pl -dispatcherIP 127.0.0.1 -dispatcherPort 3013 -LRTM DISABLED -interval 30 -resptimeout 10

## Create TCP Service Group, bind backend servers and health monitors with service group monitor threshold/weight of 2 ##
add serviceGroup CyberArkPSM-TCP3389-SG TCP -maxClient 0 -maxReq 0 -cip DISABLED -usip NO -useproxyport YES -cltTimeout 9000 -svrTimeout 9000 -CKA NO -TCPB NO -CMP NO -monThreshold 2 -downStateFlush DISABLED -comment "SM: CyberArk PSM Service Group"
bind serviceGroup CyberArkPSM-TCP3389-SG PSM-01 3389
bind serviceGroup CyberArkPSM-TCP3389-SG PSM-01 3389
bind serviceGroup CyberArkPSM-TCP3389-SG -monitorName CyberArk-PSMHealth-monitor
bind serviceGroup CyberArkPSM-TCP3389-SG -monitorName CyberArk-RDSHealth-monitor

## Create LB TCP VIP ##
add lb vserver CyberarkPSM_TCP3389_VS TCP x.x.x.x 3389 -persistenceType NONE -cltTimeout 9000
bind lb vserver CyberarkPSM_TCP3389_VS CyberArkPSM-TCP3389-SG

Couple of things to call out, the PSM monitor presumes the PSM Health Check backend server setup is configured with ResponseMode of Classic. Notice the PSM monitor has the destPort override of 443, without it service group port would be used causing the probe to fail. Also the monThreshold for the service group set to 2, both health checks need to pass for ADC to mark the backend server as up.

References >
Deploy PSM Health Check | CyberArk Docs
Example for configuring a load balancer | CyberArk Docs
Set up PSM high availability | CyberArk Docs

Troubleshooting > If the service group gets marked as down, start with the monitor details on the group, Last response should provide some guidence.

Sign Scripts/EXE’s with Powershell

Set-AuthenticodeSignature -FilePath .\App-Management.ps1 -TimestampServer http://timestamp.digicert.com -Certificate (Get-ChildItem cert:\CurrentUser\my -CodeSigningCert)

Note: codesign cert needs to exist under the user’s personal store. TimestampServer is recommended as the script signing would still be honored even the cert that used for signing expires in the future, timestamp server from DigiCert is used as an example, any popular public timestamp server will do.

Migrating Bookmarks/Favorites from IE11 to Microsoft Edge Chromium

The New Microsoft Edge does not support folder redirection for Favorites/Bookmarks, the AutoImportAtFirstRun policy could do a one-time import of the current IE bookmarks silently on the first launch but the challenge is to keep them in sync across various devices when users roam.

Automatically import another browser’s data and settings at first run
SOFTWARE\Policies\Microsoft\Edge\AutoImportAtFirstRun REG_DWORD 0x00000001
Group Policy (ADMX) info
  • GP unique name: AutoImportAtFirstRun
  • GP name: Automatically import another browser’s data and settings at first run
  • GP Option Mapping: FromInternetExplorer (1) = Automatically imports all supported datatypes and settings from Internet Explorer
  • GP path (Mandatory): Administrative Templates/Microsoft Edge/
  • GP ADMX file name: MSEdge.admx

https://docs.microsoft.com/en-us/deployedge/microsoft-edge-policies#autoimportatfirstrun

For the users that roam to different PCs or use VDI, Sync to Azure, or a personal Microsoft account can be enabled, this is the easiest option. However, if you like to keep the sync data local then on-premises only sync is configurable. This is similar to favorites folder redirection, with a couple of additional steps to get the same experience.

Step 1. Enable BrowserSignin

SOFTWARE\Policies\Microsoft\Edge\BrowserSignin REG_DWORD 0x00000001
Group Policy (ADMX) info
  • GP unique name: BrowserSignin
  • GP name: Browser sign-in settings
  • GP Option Mapping: Enable (1) = Enable browser sign-in
  • GP path (Mandatory): Administrative Templates/Microsoft Edge/
  • GP ADMX file name: MSEdge.admx

https://docs.microsoft.com/en-us/deployedge/microsoft-edge-policies#browsersignin

Step 2. Associate Edge profile with Active Directory (AD) account

SOFTWARE\Policies\Microsoft\Edge\ConfigureOnPremisesAccountAutoSignIn REG_DWORD 0x00000001
Group Policy (ADMX) info
  • GP unique name: ConfigureOnPremisesAccountAutoSignIn
  • GP name: Configure automatic sign in with an Active Directory domain account when there is no Azure AD domain account
  • GP Option Mapping: SignInAndMakeDomainAccountNonRemovable (1) = Sign in and make domain account non-removable
  • GP path (Mandatory): Administrative Templates/Microsoft Edge/
  • GP ADMX file name: MSEdge.admx

https://docs.microsoft.com/en-us/deployedge/microsoft-edge-policies#configureonpremisesaccountautosignin

Step 3. Enable Roaming Profile Support

SOFTWARE\Policies\Microsoft\Edge\RoamingProfileSupportEnabled REG_DWORD 0x00000001
Group Policy (ADMX) info
  • GP unique name: RoamingProfileSupportEnabled
  • GP name: Enable using roaming copies for Microsoft Edge profile data
  • GP path (Mandatory): Administrative Templates/Microsoft Edge/
  • GP ADMX file name: MSEdge.admx

https://docs.microsoft.com/en-us/deployedge/microsoft-edge-policies#roamingprofilesupportenabled

Step 4. Copy profile.pb to network share on Logout

by default profile.pb in %APPDATA%/Microsoft/Edge. Using GP logout action, copy the file to the user-specific network location.

Step 5. Copy profile.pb from network share on Login

Using GP login action, copy down the profile.pb file to %APPDATA%/Microsoft/Edge.

Add Disclaimer to Citrix ADC RfWebUI theme easily.

RfWebUI in ADC is a mobile-friendly theme, adapts fluidly to different screen layouts, and supports Citrix Receiver Extension APIs making customizing a lot easier. Moreover this is a supported approach from Citrix based on their nfactor extensibility framework.

The two files we are interested in adding a disclaimer to the login page are scripts.js and style.css. The JS file would hold the actual text and the CSS file would hold the styling code. Sample below. <RfWebUI-Custom> would be your custom theme name.

/var/netscaler/logon/themes/<RfWebUI-Custom>/scripts.js

$('.customAuthFooter').html("Disclaimer: All information, software, services, and comments provided on the site are for informational and self-help purposes only and are not intended to be a substitute for professional advice.");

customAuthFooter is an out of the box HTML ID for RfWebUI theme, others ID your might be interested are customAuthBottom and customAuthHeader HTML IDs.

/var/netscaler/logon/themes/<RfWebUI-Custom>/style.css

.customAuthFooter {
bottom: 0;
font-size: 12px;
color: white;
text-align: center;
background-color: black;
}

End Result

Invalidate staticobjects cache to see the changes right away

Note: Event if integrated cache is not enabled, gateway still uses it. Make sure to invalidate.

References >
https://www.citrix.com/content/dam/citrix/en_us/citrix-developer/documents/receiver-apis.html
https://docs.citrix.com/en-us/citrix-adc/current-release/aaa-tm/authentication-methods/multi-factor-nfactor-authentication/nfactor-extensibility.html

Update Root Certs from Windows Update when “Turn off Automatic Root Certificates Update” is Enable

Powshell snippet

$repoPath = \\<fileserver>\<share>\WindowsUpdate-RootCertsRepo
Certutil -syncWithWU -f $repoPath 
$sstStore = ( Get-ChildItem -Path $repoPath\*.crt )
$sstStore | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root

Note: avoid importing SST file using generateSSTFromWU cerutil switch as it appears to corrupt microsoft root certs. 
ref > https://social.technet.microsoft.com/Forums/en-US/13dc04f9-0f53-4071-8440-7d90d6ec9c6e/microsoft-root-certificate-authority-reported-as-revoked?forum=win10itprosecurity