Public/Configure-VaultServerForLDAPAuth.ps1

<#
    .SYNOPSIS
        This function uses the HashiCorp Vault Server's REST API to configure the Vault Server for
        LDAP Authrntication.
 
    .DESCRIPTION
        See .SYNOPSIS
 
    .NOTES
 
    .PARAMETER VaultServerNetworkLocation
        This parameter is MANDATORY.
 
        This parameter takes a string that represents the network location (IP Address or DNS-Resolvable)
        of the Vault Server.
 
    .PARAMETER VaultServerPort
        This parameter is MANDATORY.
 
        This parameter takes an integer that represents a Port Number (8200, etc). The Vault Server
        typically uses port 8200.
 
    .PARAMETER EncrytNetworkTraffic
        This parameter is OPTIONAL, but is set by default to be $True.
 
        This parameter is a switch. If used, the Vault Server will be configured to encrypt network
        traffic via TLS.
 
        IMPORTANT NOTE: NEVER set this parameter to $False unless you are simply testing the Vault Server
        in Development Mode. In production, you MUST encrypt network traffic to/from the Vault Server,
        and therefore, this parameter must be $True.
 
    .PARAMETER VaultAuthToken
        This parameter is MANDATORY.
 
        This parameter takes a string that represents a Vault Authentiction token with permission to
        configure teh Vault Server for LDAP Authentication.
 
    .PARAMETER VaultLogFileName
        This parameter is OPTIONAL, but is set to 'vault_audit.log' by default.
 
        This parameter takes a string that represents the name of the log file on the Vault Server that
        logs all activity (i.e. Vault Operator Command Line as well as REST API calls).
 
    .PARAMETER VaultLogEndPointName
        This parameter is OPTIONAL, but is set to 'default-audit'.
 
        This parameter takes a string that represents the name of the Vault Server REST API Endpoint
        used to enable and configure the Vault Server activity log. For context, this value is used
        with a REST API URL similar to:
            "$VaultServerBaseUri/sys/audit/$VaultLogEndPointName"
 
    .PARAMETER PerformOptionalSteps
        This parameter is OPTIONAL, but highly recommended.
 
        This parameter is a switch. If used, the following additional configuration operations will
        be performed on the Vault Server:
            - A backup root token with username 'backupadmin' will be created.
            - A 'custom-root' policy will be created and applied to the "VaultAdmins" Group (which must already exist
            in LDAP). This policy effectively grants all users in the "VaultAdmins" Group root access to the Vault Server.
            - A 'vaultusers' policy will be created and applied to the "VaultUsers" Group (which must already exist
            in LDAP). Users in the "VaultUsers" Group will have all permissions except 'delete' and 'sudo'.
 
    .PARAMETER LDAPServerHostNameOrIP
        This parameter is MANDATORY.
 
        This parameter takes a string that represents either the IP Address or DNS-Resolvable name of
        the LDAP Server. In a Windows environment, this would be a Domain Controller.
 
    .PARAMETER LDAPServicePort
        This parameter is MANDATORY.
 
        This parameter takes an integer with possible values: 389, 636, 3268, or 3269. Depending
        on how you have LDAP configured, use the appropriate port number. If you are not sure,
        use the TestLDAP function to determine which ports are in use.
 
    .PARAMETER UseOpenSSL
        This parameter is OPTIONAL.
 
        This parameter is a switch. If you would like to use openssl to determine your LDAP Server's Certificate
        even .Net classes are sufficient in determining this information.
 
    .PARAMETER BindUserDN
        This parameter is MANDATORY.
 
        This parameter takes a string that represents an LDAP Path to a User Account Object - somthing like:
            cn=vault,ou=OrgUsers,dc=zero,dc=lab
 
        This User Account will be used by the Vault Server to search the LDAP database and confirm
        credentials for the user trying to login to the Vault Server against the LDAP database. This
        LDAP account should be dedicated for use by the Vault Server and should not have any other purpose.
 
    .PARAMETER LDAPBindCredentials
        This parameter is MANDATORY.
 
        This parameter takes a PSCredential. Th e UserName should corredpound to the UserName provided to the
        -BindUserDN parameter, but should be in format <DomainPrefix>\<UserName>. So, to be consistent with
        the example provided in the -BindUserDN comment-based-help, you could create the value for
        -LDAPBindCredentials via:
            $Creds = [pscredential]::new("zero\vault",$(Read-Host "Please Enter the Password for 'zero\vault'" -AsSecureString))
 
    .PARAMETER LDAPUserOUDN
        This parameter is MANDATORY.
 
        This parameter takes a string tht represents an LDAP Path to an Organizational Unit (OU) that Vault
        will search in order to find User Accounts. To stay consistent with the example provided in the
        comment-based-help for the -BindUserDN parameter, this would be:
            ou=OrgUsers,dc=zero,dc=lab
 
    .PARAMETER LDAPGroupOUDN
        This parameter is MANDATORY.
 
        This parameter takes a string that represents an LDAP Path to the Organizational Unit (OU) that
        contains the Security Groups "VaultAdmins" and "VaultUsers". This could be something like:
            ou=Groups,dc=zero,dc=lab
 
    .PARAMETER LDAPVaultUsersSecurityGroupDN
        This parameter is OPTIONAL, however, it becomes MANDATORY when the -PerformOptionalSteps parameter is used.
 
        This parameter takes a string that represents the LDAP Path to the "VaultUsers" Security Group. To be
        consistent with the example provided in teh comment-based-help for the -LDAPGroupOUDN parameter, this
        should be something like:
            cn=VaultUsers,ou=Groups,dc=zero,dc=lab
 
        IMPORTANT NOTE: The Common Name (CN) for this LDAP Path MUST be 'VaultUsers'
 
    .PARAMETER LDAPVaultAdminsSecurityGroupDN
        This parameter is OPTIONAL, however, it becomes MANDATORY when the -PerformOptionalSteps parameter is used.
 
        This parameter takes a string that represents the LDAP Path to the "VaultAdmins" Security Group. To be
        consistent with the example provided in teh comment-based-help for the -LDAPGroupOUDN parameter, this
        should be something like:
            cn=VaultAdmins,ou=Groups,dc=zero,dc=lab
 
        IMPORTANT NOTE: The Common Name (CN) for this LDAP Path MUST be 'VaultAdmins'
 
    .EXAMPLE
        # Open an elevated PowerShell Session, import the module, and -
 
        PS C:\Users\zeroadmin> $ConfigureVaultLDAPSplatParams = @{
            VaultServerNetworkLocation = "vaultserver.zero.lab"
            VaultServerPort = 8200
            VaultAuthToken = $VaultAuthToken
            LDAPServerHostNameOrIP = "ZeroDC01.zero.lab"
            LDAPServicePort = 636
            LDAPBindCredentials = $LDAPBindCredentials
            BindUserDN = "cn=vault,ou=OrgUsers,dc=zero,dc=lab"
            LDAPUserOUDN = "ou=OrgUsers,dc=zero,dc=lab"
            LDAPGroupOUDN = "ou=Groups,dc=zero,dc=lab"
            PerformOptionalSteps = $True
            LDAPVaultUsersSecurityGroupDN = "cn=VaultUsers,ou=Groups,dc=zero,dc=lab"
            LDAPVaultAdminsSecurityGroupDN = "cn=VaultAdmins,ou=Groups,dc=zero,dc=lab"
        }
        PS C:\Users\zeroadmin> $ConfigureVaultLDAPResult = Configure-VaultServerForLDAPAuth @ConfigureVaultLDAPSplatParams
         
#>

function Configure-VaultServerForLDAPAuth {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [string]$VaultServerNetworkLocation, # Should be an IP Address of DNS-Resolvable HostName/FQDN

        [Parameter(Mandatory=$True)]
        [int]$VaultServerPort, # Typically 8200

        [Parameter(Mandatory=$False)]
        [switch]$EncryptNetworkTraffic = $True, # Impacts using http/https, Vault Config, Generating TLS Certificates

        [Parameter(Mandatory=$True)]
        [string]$VaultAuthToken, # Get this via manual step preceeding this function using Vault CmdLine - 'vault operator init'

        [Parameter(Mandatory=$False)]
        [string]$VaultLogFileName = "vault_audit.log",

        [Parameter(Mandatory=$False)]
        [string]$VaultLogEndPointName = "default-audit",

        # Creates backup root token with username 'backupadmin',
        # Creates 'custom-root' policy applied to "VaultAdmins" group (all permissions)
        # Creates 'vaultusers' policy applied to "VaultUsers" group (all permissions except 'delete' and 'sudo')
        [Parameter(Mandatory=$False)]
        [switch]$PerformOptionalSteps,

        [Parameter(Mandatory=$True)]
        [string]$LDAPServerHostNameOrIP,

        [Parameter(Mandatory=$True)]
        [ValidateSet(389,636,3268,3269)]
        [int]$LDAPServicePort,

        [Parameter(Mandatory=$False)]
        [switch]$UseOpenSSL,

        [Parameter(Mandatory=$True)]
        [string]$BindUserDN, # Should be a path to a User Account LDAP object, like cn=vault,ou=OrgUsers,dc=zero,dc=lab

        # Should be a non-privileged LDAP/AD account whose sole purpose is allowing Vault to read the LDAP Database
        [Parameter(Mandatory=$True)]
        [pscredential]$LDAPBindCredentials,
        
        [Parameter(Mandatory=$True)]
        [string]$LDAPUserOUDN, # Something like ou=OrgUsers,dc=zero,dc=lab
    
        [Parameter(Mandatory=$True)]
        [string]$LDAPGroupOUDN, # Something like ou=Groups,dc=zero,dc=lab

        [Parameter(Mandatory=$False)]
        [ValidatePattern("^cn=VaultUsers")]
        [string]$LDAPVaultUsersSecurityGroupDN, # Something like cn=VaultUsers,ou=Groups,dc=zero,dc=lab

        [Parameter(Mandatory=$False)]
        [ValidatePattern("^cn=VaultAdmins")]
        [string]$LDAPVaultAdminsSecurityGroupDN # Something like cn=VaultAdmins,ou=Groups,dc=zero,dc=lab
    )

    #region >> Prep

    if ($PSVersionTable.Platform -eq "Unix" -or $PSVersionTable.OS -match "Darwin" -and $env:SudoPwdPrompt) {
        if (GetElevation) {
            Write-Error "You should not be running the $($MyInvocation.MyCommand.Name) function as root! Halting!"
            $global:FunctionResult = "1"
            return
        }
        RemoveMySudoPwd
        NewCronToAddSudoPwd
        $env:SudoPwdPrompt = $False
    }
    if (!$PSVersionTable.Platform -or $PSVersionTable.Platform -eq "Win32NT") {
        [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"

        if (!$(GetElevation)) {
            Write-Error "The $($MyInvocation.MyCommand.Name) function must be run from an elevated PowerShell session! Halting!"
            $global:FunctionResult = "1"
            return
        }
    }

    # Create $Ouput Hashtable so we can add to it as we go and return whatever was done in case of error
    $Output = [ordered]@{}

    if ($EncryptNetworkTraffic) {
        $VaultServerBaseUri = "https://$VaultServerNetworkLocation" + ":$VaultServerPort/v1"
    }
    else {
        $VaultServerBaseUri = "http://$VaultServerNetworkLocation" + ":$VaultServerPort/v1"
    }

    if ($PerformOptionalSteps) {
        if (!$LDAPVaultUsersSecurityGroupDN -or !$LDAPVaultAdminsSecurityGroupDN) {
            Write-Error "When using the -PerformOptionalSteps switch, you must also supply values for -LDAPVaultUsersSecurityGroupDN and -LDAPVaultAdminsSecurityGroupDN! Halting!"
            $global:FunctionResult = "1"
            return
        }
    }

    # Make sure we can reach the LDAP Server
    try {
        $LDAPServerNetworkInfo = ResolveHost -HostNameOrIP $LDAPServerHostNameOrIP
        if (!$LDAPServerNetworkInfo) {throw "Unable to resolve $LDAPServerHostNameOrIP! Halting!"}
    }
    catch {
        Write-Error $_
        $global:FunctionResult = "1"
        return
    }

    if (!$PSVersionTable.Platform -or $PSVersionTable.Platform -eq "Win32NT") {
        $CurrentlyLoadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies()

        if (![bool]$($CurrentlyLoadedAssemblies -match "System.DirectoryServices.AccountManagement")) {
            Add-Type -AssemblyName System.DirectoryServices.AccountManagement
        }
        $SimpleDomain = $LDAPServerNetworkInfo.Domain
        $SimpleDomainWLDAPPort = $SimpleDomain + ":$LDAPServicePort"
        [System.Collections.ArrayList]$DomainLDAPContainersPrep = @()
        foreach ($Section in $($SimpleDomain -split "\.")) {
            $null = $DomainLDAPContainersPrep.Add($Section)
        }
        $DomainLDAPContainers = $($DomainLDAPContainersPrep | foreach {"DC=$_"}) -join ", "

        # Make sure $LDAPBindCredentials work
        try {
            $SimpleUserName = $($LDAPBindCredentials.UserName -split "\\")[1]
            #$PasswordInPlainText = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($LDAPBindCredentials.Password))
            $BindPassword = $PasswordInPlainText = $LDAPBindCredentials.GetNetworkCredential().Password
            $PrincipleContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new(
                [System.DirectoryServices.AccountManagement.ContextType]::Domain,
                "$SimpleDomainWLDAPPort",
                "$DomainLDAPContainers",
                [System.DirectoryServices.AccountManagement.ContextOptions]::SimpleBind,
                "$($LDAPBindCredentials.UserName)",
                "$PasswordInPlainText"
            )

            try {
                $UserPrincipal = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($PrincipleContext, [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName, "$SimpleUserName")
                $LDAPBindCredentialsAreValid = $True
            }
            catch {
                throw "The credentials provided to the -LDAPBindCredentials parameter are not valid for the domain $SimpleDomain! Halting!"
            }

            if ($LDAPBindCredentialsAreValid) {
                # Determine if the User Account is locked
                $AccountLocked = $UserPrincipal.IsAccountLockedOut()

                if ($AccountLocked -eq $True) {
                    throw "The provided UserName $($LDAPBindCredentials.Username) is locked! Please unlock it before additional attempts at getting working credentials!"
                }
            }
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
            return
        }

        # NOTE: With .Net, LDAP URIs always start with 'LDAP' - never lowercase and never with an 's|S' (i.e. never LDAPS|ldaps),
        # regardless of port
        $LDAPUri = "LDAP://$($LDAPServerNetworkInfo.FQDN):$LDAPServicePort"

        # Make sure $LDAPUserOUDN exists
        try {
            $LDAPUserOUDNDirectoryEntry = [System.DirectoryServices.DirectoryEntry]("$LDAPUri/$LDAPUserOUDN")
            $LDAPUserOUDNDirectoryEntry.Close()
        }
        catch {
            Write-Error "The LDAP Object $LDAPUserOUDN cannot be found! Halting!"
            $global:FunctionResult = "1"
            return
        }

        # Make sure $LDAPGroupOUDN exists
        try {
            $LDAPGroupOUDNDirectoryEntry = [System.DirectoryServices.DirectoryEntry]("$LDAPUri/$LDAPGroupOUDN")
            $LDAPGroupOUDNDirectoryEntry.Close()
        }
        catch {
            Write-Error "The LDAP Object $LDAPGroupOUDN cannot be found! Halting!"
            $global:FunctionResult = "1"
            return
        }
    }
    if ($PSVersionTable.Platform -eq "Unix" -or $PSVersionTable.OS -match "Darwin") {
        # Determine if we have the required Linux commands
        [System.Collections.ArrayList]$LinuxCommands = @(
            "echo"
            "host"
            "hostname"
            "ldapsearch"
        )
        if (!$Domain) {
            $null = $LinuxCommands.Add("domainname")
        }
        [System.Collections.ArrayList]$CommandsNotPresent = @()
        foreach ($CommandName in $LinuxCommands) {
            $CommandCheckResult = command -v $CommandName
            if (!$CommandCheckResult) {
                $null = $CommandsNotPresent.Add($CommandName)
            }
        }

        if ($CommandsNotPresent.Count -gt 0) {
            [System.Collections.ArrayList]$FailedInstalls = @()
            if ($CommandsNotPresent -contains "echo" -or $CommandsNotPresent -contains "whoami") {
                try {
                    #$null = InstallLinuxPackage -PossiblePackageNames "coreutils" -CommandName "echo"
                    $InstallLinuxPackageAsString = ${Function:InstallLinuxPackage}.Ast.Extent.Text
                    $SBAsString = @(
                        "`$GetLocalGroupAndUsersAsString = @'`n$InstallLinuxPackageAsString`n'@"
                        'try {'
                        ' Invoke-Expression $InstallLinuxPackageAsString'
                        ' $null = InstallLinuxPackage -PossiblePackageNames "coreutils" -CommandName "echo"'
                        ' Write-Host "`nOutputStartsBelow`n"'
                        ' "Done" | ConvertTo-Json -Depth 3'
                        '}'
                        'catch {'
                        ' @("ErrorMsg",$_.Exception.Message) | ConvertTo-Json -Depth 3'
                        '}'
                    )
                    $SBAsString = $SBAsString -join "`n"
                    $InstallPackageResultPrep = SudoPwsh -CmdString $SBAsString

                    if ($InstallPackageResultPrep.Output -match "ErrorMsg") {
                        throw $InstallPackageResultPrep.Output[-1]
                    }
                    if ($InstallPackageResultPrep.OutputType -eq "Error") {
                        if ($InstallPackageResultPrep.Output -match "ErrorMsg") {
                            throw $InstallPackageResultPrep.Output[-1]
                        }
                        else {
                            throw $InstallPackageResultPrep.Output
                        }
                    }
                    $InstallPackageResult = $InstallPackageResultPrep.Output
                }
                catch {
                    $null = $FailedInstalls.Add("coreutils")
                }
            }
            if ($CommandsNotPresent -contains "host" -or $CommandsNotPresent -contains "hostname" -or $CommandsNotPresent -contains "domainname") {
                try {
                    #$null = InstallLinuxPackage -PossiblePackageNames @("dnsutils","bindutils","bind-utils","bind-tools") -CommandName "nslookup"
                    $InstallLinuxPackageAsString = ${Function:InstallLinuxPackage}.Ast.Extent.Text
                    $SBAsString = @(
                        "`$GetLocalGroupAndUsersAsString = @'`n$InstallLinuxPackageAsString`n'@"
                        'try {'
                        ' Invoke-Expression $InstallLinuxPackageAsString'
                        ' $null = InstallLinuxPackage -PossiblePackageNames @("dnsutils","bindutils","bind-utils","bind-tools") -CommandName "nslookup"'
                        ' Write-Host "`nOutputStartsBelow`n"'
                        ' "Done" | ConvertTo-Json -Depth 3'
                        '}'
                        'catch {'
                        ' @("ErrorMsg",$_.Exception.Message) | ConvertTo-Json -Depth 3'
                        '}'
                    )
                    $SBAsString = $SBAsString -join "`n"
                    $InstallPackageResultPrep = SudoPwsh -CmdString $SBAsString

                    if ($InstallPackageResultPrep.Output -match "ErrorMsg") {
                        throw $InstallPackageResultPrep.Output[-1]
                    }
                    if ($InstallPackageResultPrep.OutputType -eq "Error") {
                        if ($InstallPackageResultPrep.Output -match "ErrorMsg") {
                            throw $InstallPackageResultPrep.Output[-1]
                        }
                        else {
                            throw $InstallPackageResultPrep.Output
                        }
                    }
                    $InstallPackageResult = $InstallPackageResultPrep.Output
                }
                catch {
                    $null = $FailedInstalls.Add("dnsutils_bindutils_bind-utils_bind-tools")
                }
            }
            if ($CommandsNotPresent -contains "ldapsearch") {
                try {
                    #$null = InstallLinuxPackage -PossiblePackageNames "openldap-clients" -CommandName "ldapsearch"
                    $InstallLinuxPackageAsString = ${Function:InstallLinuxPackage}.Ast.Extent.Text
                    $SBAsString = @(
                        "`$GetLocalGroupAndUsersAsString = @'`n$InstallLinuxPackageAsString`n'@"
                        'try {'
                        ' Invoke-Expression $InstallLinuxPackageAsString'
                        ' $null = InstallLinuxPackage -PossiblePackageNames "openldap-clients" -CommandName "ldapsearch"'
                        ' Write-Host "`nOutputStartsBelow`n"'
                        ' "Done" | ConvertTo-Json -Depth 3'
                        '}'
                        'catch {'
                        ' @("ErrorMsg",$_.Exception.Message) | ConvertTo-Json -Depth 3'
                        '}'
                    )
                    $SBAsString = $SBAsString -join "`n"
                    $InstallPackageResultPrep = SudoPwsh -CmdString $SBAsString

                    if ($InstallPackageResultPrep.Output -match "ErrorMsg") {
                        throw $InstallPackageResultPrep.Output[-1]
                    }
                    if ($InstallPackageResultPrep.OutputType -eq "Error") {
                        if ($InstallPackageResultPrep.Output -match "ErrorMsg") {
                            throw $InstallPackageResultPrep.Output[-1]
                        }
                        else {
                            throw $InstallPackageResultPrep.Output
                        }
                    }
                    $InstallPackageResult = $InstallPackageResultPrep.Output
                }
                catch {
                    $null = $FailedInstalls.Add("openldap-clients")
                }
            }
    
            if ($FailedInstalls.Count -gt 0) {
                Write-Error "The following Linux packages are required, but were not able to be installed:`n$($FailedInstalls -join "`n")`nHalting!"
                $global:FunctionResult = "1"
                return
            }
        }

        [System.Collections.ArrayList]$CommandsNotPresent = @()
        foreach ($CommandName in $LinuxCommands) {
            $CommandCheckResult = command -v $CommandName
            if (!$CommandCheckResult) {
                $null = $CommandsNotPresent.Add($CommandName)
            }
        }
    
        if ($CommandsNotPresent.Count -gt 0) {
            Write-Error "The following Linux commands are required, but not present on $env:ComputerName:`n$($CommandsNotPresent -join "`n")`nHalting!"
            $global:FunctionResult = "1"
            return
        }

        try {
            if ($Domain) {
                $DomainControllerInfo = GetDomainController -Domain $Domain -ErrorAction Stop
            }
            else {
                $DomainControllerInfo = GetDomainController -ErrorAction Stop
            }
    
            if ($DomainControllerInfo.PrimaryDomainController -eq "unknown") {
                $PDC = $DomainControllerInfo.FoundDomainControllers[0]
            }
            else {
                $PDC = $DomainControllerInfo.PrimaryDomainController
            }
    
            $LDAPInfo = TestLDAP -ADServerHostNameOrIP $PDC -ErrorAction Stop
            if (!$DomainControllerInfo) {throw "Problem with GetDomainController function! Halting!"}
            if (!$LDAPInfo) {throw "Problem with TestLDAP function! Halting!"}
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
            return
        }

        $SimpleDomainPrep = $PDC -split "\."
        $SimpleDomain = $SimpleDomainPrep[1..$($SimpleDomainPrep.Count-1)] -join "."
        [System.Collections.ArrayList]$DomainLDAPContainersPrep = @()
        foreach ($Section in $($SimpleDomain -split "\.")) {
            $null = $DomainLDAPContainersPrep.Add($Section)
        }
        $DomainLDAPContainers = $($DomainLDAPContainersPrep | foreach {"DC=$_"}) -join ","
        $BindUserName = $LDAPBindCredentials.UserName
        $BindUserNameForExpect = $BindUserName -replace [regex]::Escape('\'),'\\\'
        $BindPassword = $PasswordInPlainText = $LDAPBindCredentials.GetNetworkCredential().Password

        # Make sure $LDAPBindCredentials work
        $ldapSearchOutput = ldapsearch -x -h $PDC -D $BindUserName -w $BindPassword -b "$DomainLDAPContainers" -s sub "(objectClass=group)" cn
        if ($LASTEXITCODE -ne 0) {
            Write-Error "The credentials provided to the -LDAPBindCredentials parameter are not valid for the domain $SimpleDomain! Halting!"
            $global:FunctionResult = "1"
            return
        }

        # Make sure $LDAPUserOUDN exists
        $ldapSearchOutput = ldapsearch -x -h $PDC -D $BindUserName -w $BindPassword -b "$LDAPUserOUDN" -s sub "(objectClass=user)" cn
        if ($ldapSearchOutput -match "No such object") {
            Write-Error "The LDAP Object $LDAPUserOUDN cannot be found! Halting!"
            $global:FunctionResult = "1"
            return
        }

        # Make sure $LDAPGroupOUDN exists
        $ldapSearchOutput = ldapsearch -x -h $PDC -D $BindUserName -w $BindPassword -b "$LDAPGroupOUDN" -s sub "(objectClass=user)" cn
        if ($ldapSearchOutput -match "No such object") {
            Write-Error "The LDAP Object $LDAPUserOUDN cannot be found! Halting!"
            $global:FunctionResult = "1"
            return
        }
    }

    $HeadersParameters = @{
        "X-Vault-Token" = $VaultAuthToken
    }

    #endregion >> Prep


    #region >> Main Body
    
    # Turn on Vault Audit Log
    # Vault CmdLine Equivalent:
    # vault audit enable file file_path=/vault/logs/vault_audit.log
    $jsonRequest = @"
{
    "type": "file",
    "options": {
        "path": "/vault/logs/$VaultLogFileName"
    }
}
"@

    try {
        # Validate JSON
        $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
    }
    catch {
        Write-Error "There was a problem with the JSON for Turning on the Audit Log! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $IWRSplatParams = @{
        Uri         = "$VaultServerBaseUri/sys/audit/$VaultLogEndPointName"
        Headers     = $HeadersParameters
        Body        = $JsonRequestAsSingleLineString
        Method      = "Put"
    }
    $TurnOnAuditLog = Invoke-RestMethod @IWRSplatParams
    $ConfirmAuditLogIsOn = $(Invoke-RestMethod -Uri "$VaultServerBaseUri/sys/audit" -Headers $HeadersParameters -Method Get).data
    if (!$ConfirmAuditLogIsOn) {
        Write-Error "Cannot confirm that the Vault Audit Log is turned on! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $Output.Add("EnableAuditLog",$ConfirmAuditLogIsOn)

    # Create a new policy that effectively has root access to Vault, and call it 'custom-root'. This policy will be applied
    # to Vault Administrators later on
    $jsonRequest = @"
{
    "policy": "path \"*\" {\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n}"
}
"@

    try {
        # Validate JSON
        $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
    }
    catch {
        Write-Error "There was a problem with the JSON for creating the 'custom-root' policy! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $IWRSplatParams = @{
        Uri         = "$VaultServerBaseUri/sys/policy/custom-root"
        Headers     = $HeadersParameters
        Body        = $JsonRequestAsSingleLineString
        Method      = "Put"
    }
    $RootPolicyResponse = Invoke-RestMethod @IWRSplatParams
    $ConfirmRootPolicy = Invoke-RestMethod -Uri "$VaultServerBaseUri/sys/policy/custom-root" -Headers $HeadersParameters -Method Get
    if (!$ConfirmRootPolicy) {
        Write-Error "Cannot confirm that the Vault policy 'custom-root' has been enabled! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $Output.Add("CreateCustomRootPolicy",$ConfirmRootPolicy)

    # Create a policy that is for typical Vault Users (i.e. not Vault Admins), that allows for everything except
    # delete and sudo. Change according to your preferences.
    $jsonRequest = @"
{
    "policy": "path \"*\" {\n capabilities = [\"create\", \"read\", \"update\", \"list\"]\n}"
}
"@

    try {
        # Validate JSON
        $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
    }
    catch {
        Write-Error "There was a problem with the JSON for creating the 'vaultusers' policy! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $IWRSplatParams = @{
        Uri         = "$VaultServerBaseUri/sys/policy/vaultusers"
        Headers     = $HeadersParameters
        Body        = $JsonRequestAsSingleLineString
        Method      = "Put"
    }
    $VaultUsersPolicyResponse = Invoke-RestMethod @IWRSplatParams
    $ConfirmVaultUsersPolicy = Invoke-RestMethod -Uri "$VaultServerBaseUri/sys/policy/vaultusers" -Headers $HeadersParameters -Method Get
    if (!$ConfirmVaultUsersPolicy) {
        Write-Error "Cannot confirm that the Vault policy 'vaultusers' has been enabled! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $Output.Add("CreateVaultUsersPolicy",$ConfirmVaultUsersPolicy)

    if ($PerformOptionalSteps) {
        # Create a user other than the initial root (i.e. the token $VaultAuthToken that we've been using thus far) that has root privileges
        # via the 'custom-root' policy. This is just for a backup root account for emergencies
        # Vault CmdLine Equivalent:
        # vault token create -policy=custom-root -display-name="backupadmin" -ttl="8760h" -renewable=true -metadata=user=backupadmin
        $jsonRequest = @"
{
    "policies": [
        "custom-root"
    ],
    "meta": {
        "user": "backupadmin"
    },
    "ttl": "8760h",
    "renewable": true
}
"@

        try {
            # Validate JSON
            $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
        }
        catch {
            Write-Error "There was a problem with the JSON for creating the 'backupadmin' Vault Token! Halting!"
            if ($Output.Count -gt 0) {[pscustomobject]$Output}
            $global:FunctionResult = "1"
            return
        }
        $IWRSplatParams = @{
            Uri         = "$VaultServerBaseUri/auth/token/create"
            Headers     = $HeadersParameters
            Body        = $JsonRequestAsSingleLineString
            Method      = "Post"
        }
        $NewUserTokenResponse = Invoke-RestMethod @IWRSplatParams
        if (!$NewUserTokenResponse) {
            Write-Error "There was a problem creating the 'backupadmin' Vault Token! Halting!"
            $global:FunctionResult = "1"
            return
        }
        $Output.Add("BackupRootToken",$NewUserTokenResponse)
    }

    # Enable LDAP Authentication
    # vault auth enable ldap -description="Login with LDAP"
    $jsonRequest = @"
{
    "type": "ldap",
    "description": "Login with LDAP"
}
"@

    try {
        # Validate JSON
        $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
    }
    catch {
        Write-Error "There was a problem with the JSON for enabling the Vault LDAP Authentication Method! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $IWRSplatParams = @{
        Uri         = "$VaultServerBaseUri/sys/auth/ldap"
        Headers     = $HeadersParameters
        Body        = $JsonRequestAsSingleLineString
        Method      = "Post"
    }
    $EnableLDAPResponse = Invoke-RestMethod @IWRSplatParams
    $ConfirmLDAPEnabled = Invoke-RestMethod -Uri "$VaultServerBaseUri/sys/auth" -Headers $HeadersParameters -Method Get
    if (!$ConfirmLDAPEnabled) {
        Write-Error "There was a problem enabling the LDAP Authentication Method for the Vault Server! Halting!"
    }
    $Output.Add("LDAPAuthEngineEnabled",$ConfirmLDAPEnabled)

    # Next, we need the LDAP Server's Root CA Public Certificate
    try {
        $GetLDAPCertSplatParams = @{
            LDAPServerHostNameOrIP      = $LDAPServerNetworkInfo.FQDN
            Port                        = $LDAPServicePort
            ErrorAction                 = "Stop"
        }
        if ($LDAPServicePort -eq 389 -or $LDAPServicePort -eq 3268) {
            $GetLDAPCertSplatParams.Add("AllowOpenSSLInstall",$True)
        }
        if ($UseOpenSSL) {
            $GetLDAPCertSplatParams.Add("UseOpenSSL",$True)
            if ($GetLDAPCertSplatParams.Keys -notcontains "AllowOpenSSLInstall") {
                $GetLDAPCertSplatParams.Add("AllowOpenSSLInstall",$True)
            }
        }

        $GetLDAPCertResult = Get-LDAPCert @GetLDAPCertSplatParams
        if (!$GetLDAPCertResult) {throw "The Get-LDAPCert function failed! Is your LDAP implementation using TLS? Halting!"}
        $RootCertificateInPemFormat = $GetLDAPCertResult.RootCACertificateInfo.PemFormat -join "`n"
        if (!$RootCertificateInPemFormat) {throw "The Get-LDAPCert function failed to get the Root CA Certificate in the LDAP Endpoint's Certificate Chain! Halting!"}
    }
    catch {
        Write-Error $_
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }

    # The Vault Server handles LDAP Uris as expected (as opposed to .Net counterpart in above
    # 'Variable/Parameter Transforms and PreRun Prep' region)
    if ($LDAPServicePort -eq 389 -or $LDAPServicePort -eq 3268) {
        $LDAPUriForVault = "ldap://$($LDAPServerNetworkInfo.FQDN):$LDAPServicePort"
    }
    if ($LDAPServicePort -eq 636 -or $LDAPServicePort -eq 3269) {
        $LDAPUriForVault = "ldaps://$($LDAPServerNetworkInfo.FQDN):$LDAPServicePort"
    }

    $jsonRequest = @"
{
    "url": "$LDAPUriForVault",
    "userattr": "samaccountname",
    "userdn": "$LDAPUserOUDN",
    "discoverdn": "true",
    "groupdn": "$LDAPGroupOUDN",
    "groupfilter": "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))",
    "groupattr": "cn",
    "certificate": "$RootCertificateInPemFormat",
    "insecure_tls": "false",
    "starttls": "true",
    "binddn": "$BindUserDN",
    "bindpass": "$PasswordInPlainText",
    "deny_null_bind": "true",
    "tls_max_version": "tls12",
    "tls_min_version": "tls12"
}
"@

    try {
        # Validate JSON
        $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
    }
    catch {
        Write-Error "There was a problem with the JSON for establishing Vault's LDAP configuration! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $IWRSplatParams = @{
        Uri         = "$VaultServerBaseUri/auth/ldap/config"
        Headers     = $HeadersParameters
        Body        = $JsonRequestAsSingleLineString
        Method      = "Post"
    }
    $LDAPAuthConfigResponse = Invoke-RestMethod @IWRSplatParams
    $ConfirmLDAPAuthConfig = Invoke-RestMethod -Uri "$VaultServerBaseUri/auth/ldap/config" -Headers $HeadersParameters -Method Get
    if (!$ConfirmLDAPAuthConfig) {
        Write-Error "There was a problem setting the Vault LDAP Authentication configuration! Halting!"
        if ($Output.Count -gt 0) {[pscustomobject]$Output}
        $global:FunctionResult = "1"
        return
    }
    $Output.Add("LDAPAuthConfiguration",$ConfirmLDAPAuthConfig)
    # Remove $PasswordInPlainText from Memory as best we can
    $PasswordInPlainText = $null
    $PrincipleContext = $null
    $jsonRequest = $null
    $JsonRequestAsSingleLineString = $null


    if ($PerformOptionalSteps) {
        # Apply the 'custom-root' policy to the AD User Group 'VaultAdmins'
        # Vault Cmdline equivalent is:
        # vault write auth/ldap/groups/VaultAdmins policies=custom-root

        # Make sure $LDAPVaultAdminsSecurityGroupDN exists
        if (!$PSVersionTable.Platform -or $PSVersionTable.Platform -eq "Win32NT") {
            try {
                $LDAPVaultAdminsSecurityGroupDNDirectoryEntry = [System.DirectoryServices.DirectoryEntry]("$LDAPUri/$LDAPVaultAdminsSecurityGroupDN")
                $LDAPVaultAdminsSecurityGroupDNDirectoryEntry.Close()
            }
            catch {
                Write-Error "The LDAP Object $LDAPVaultAdminsSecurityGroupDN cannot be found! Halting!"
                $global:FunctionResult = "1"
                if ($Output.Count -gt 0) {[pscustomobject]$Output}
                return
            }
        }
        if ($PSVersionTable.Platform -eq "Unix" -or $PSVersionTable.OS -match "Darwin") {
            $ldapSearchOutput = ldapsearch -x -h $PDC -D $BindUserName -w $BindPassword -b "$LDAPVaultAdminsSecurityGroupDN" -s sub "(objectClass=user)" cn
            if ($ldapSearchOutput -match "No such object") {
                Write-Error "The LDAP Object $LDAPVaultAdminsSecurityGroupDN cannot be found! Halting!"
                $global:FunctionResult = "1"
                return
            }
        }

        $jsonRequest = @"
{
    "policies": "custom-root"
}
"@

        try {
            # Validate JSON
            $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
        }
        catch {
            Write-Error "There was a problem with the JSON for applying the 'custom-root' policy to the VaultAdmins Security Group! Halting!"
            $global:FunctionResult = "1"
            return
        }
        $IWRSplatParams = @{
            Uri         = "$VaultServerBaseUri/auth/ldap/groups/VaultAdmins"
            Headers     = $HeadersParameters
            Body        = $JsonRequestAsSingleLineString
            Method      = "Post"
        }
        $ApplyPolicyToVaultAdminsGroup = Invoke-WebRequest @IWRSplatParams
        $ConfirmPolicyOnVaultAdmins = Invoke-RestMethod -Uri "$VaultServerBaseUri/auth/ldap/groups/VaultAdmins" -Headers $HeadersParameters -Method Get
        if (!$ConfirmPolicyOnVaultAdmins) {
            Write-Error "Unable to confirm that the 'custom-root' Vault Policy was applied to the LDAP Security Group 'VaultAdmins'! Halting!"
            if ($Output.Count -gt 0) {[pscustomobject]$Output}
            $global:FunctionResult = "1"
            return
        }
        $Output.Add("AppliedVaultAdminsPolicy",$ConfirmPolicyOnVaultAdmins)

        # Apply the 'vaultusers' policy to the AD User Group 'VaultUsers'
        # Vault Cmdline equivalent is:
        # vault write auth/ldap/groups/VaultUsers policies=vaultusers

        # Make sure $LDAPVaultUsersSecurityGroupDN exists
        if (!$PSVersionTable.Platform -or $PSVersionTable.Platform -eq "Win32NT") {
            try {
                $LDAPVaultUsersSecurityGroupDNDirectoryEntry = [System.DirectoryServices.DirectoryEntry]("$LDAPUri/$LDAPVaultUsersSecurityGroupDN")
                $LDAPVaultUsersSecurityGroupDNDirectoryEntry.Close()
            }
            catch {
                Write-Error "The LDAP Object $LDAPVaultUsersSecurityGroupDN cannot be found! Halting!"
                if ($Output.Count -gt 0) {[pscustomobject]$Output}
                $global:FunctionResult = "1"
                return
            }
        }
        if ($PSVersionTable.Platform -eq "Unix" -or $PSVersionTable.OS -match "Darwin") {
            $ldapSearchOutput = ldapsearch -x -h $PDC -D $BindUserName -w $BindPassword -b "$LDAPVaultUsersSecurityGroupDN" -s sub "(objectClass=user)" cn
            if ($ldapSearchOutput -match "No such object") {
                Write-Error "The LDAP Object $LDAPVaultUsersSecurityGroupDN cannot be found! Halting!"
                $global:FunctionResult = "1"
                return
            }
        }

        $jsonRequest = @"
{
    "policies": "vaultusers"
}
"@

        try {
            $JsonRequestAsSingleLineString = $jsonRequest | ConvertFrom-Json -EA Stop | ConvertTo-Json -Compress -EA Stop
        }
        catch {
            Write-Error "There was a problem with the JSON for applying the 'vaultusers' policy to the VaulUsers Security Group! Halting!"
            if ($Output.Count -gt 0) {[pscustomobject]$Output}
            $global:FunctionResult = "1"
            return
        }
        $IWRSplatParams = @{
            Uri         = "$VaultServerBaseUri/auth/ldap/groups/VaultUsers"
            Headers     = $HeadersParameters
            Body        = $JsonRequestAsSingleLineString
            Method      = "Post"
        }
        $ApplyPolicyToVaultUsersGroup = Invoke-WebRequest @IWRSplatParams
        $ConfirmPolicyOnVaultUsers = Invoke-RestMethod -Uri "$VaultServerBaseUri/auth/ldap/groups/VaultUsers" -Headers $HeadersParameters -Method Get
        if (!$ConfirmPolicyOnVaultUsers) {
            Write-Error "Unable to confirm that the 'vaultusers' Vault Policy was applied to the LDAP Security Group 'VaultUsers'! Halting!"
            if ($Output.Count -gt 0) {[pscustomobject]$Output}
            $global:FunctionResult = "1"
            return
        }
        $Output.Add("AppliedVaultUsersPolicy",$ConfirmPolicyOnVaultUsers)
    }

    [pscustomobject]$Output

    #endregion >> Main Body

}