Category Archives: WindowsPowershell

Setting Up a vSphere Service Account for Pivotal BOSH Director using PowerCLI

BOSH Director requires a fairly powerful vCenter service account to do all of the things it does.

The list of permissions required is here, and it’s extensive.

You can always take the shortcut and make your account an Administrator of the vSphere environment, but that violates the whole “least privilege” principle and I don’t like that in production environments.

I wrote a working PowerCLI code function that will automatically create this vCenter role and add the specified user/group to it.

It greatly reduces the time to set this up.  Hope this helps someone out.

function Add-BoshVCenterAccount()
{
<#
.SYNOPSIS
Grants the correct vSphere permissions to the specified service user/group for BOSH director to function.
.DESCRIPTION
This function creates a new vSphere role called PKS Administrators if it does not exist already. It then assigns the specified local or domain user/group to the role at the root vCenter server object.
.PARAMETER Group
Specifies the Group to assign the role to.
.PARAMETER User
Specifies the User to assign the role to.
.PARAMETER Domain
If specified, then the User or Group specified is assumed to be a domain object. Specify the AD Domain the user/group is a member of.
.OUTPUTS
[VMware.VimAutomation.ViCore.Impl.V1.PermissionManagement.PermissionImpl]
The resultant permission.
.LINK
https://docs.pivotal.io/pivotalcf/2-0/customizing/vsphere-service-account.html
.EXAMPLE
Connect-ViServer -Server myvcenter.domain.com
Add-BoshVCenterAccount -Domain mydomain -User user1
#>
[CmdletBinding(SupportsShouldProcess,DefaultParameterSetName="user")]
param
(
[Parameter(Mandatory,ParameterSetName="user")]
[string] $User,
[Parameter(Mandatory,ParameterSetName="group")]
[string] $Group,
[string] $Domain
)
$version = $Null
if ( (Get-Variable | Where-Object { $_.Name -ieq "global:DefaultViServer" }) -and $DefaultViServer )
{
$version = $defaultViServer.Version
}
else
{
throw ("Use Connect-ViSever first!")
}
# Permissions for 6.5+:
$privileges = @( `
"Manage custom attributes",
"Allocate space",
"Browse datastore",
"Low level file operations",
"Remove file",
"Update virtual machine files",
"Delete folder",
"Create folder",
"Move folder",
"Rename folder",
"Set custom attribute",
"Modify cluster",
"CreateTag",
"EditTag",
"DeleteTag",
"Assign network",
"Assign virtual machine to resource pool",
"Migrate powered off virtual machine",
"Migrate powered on virtual machine",
"Add existing disk",
"Add new disk",
"Add or remove device",
"Advanced",
"Change CPU count",
"Change resource",
"Configure managedBy",
"Disk change tracking",
"Disk lease",
"Display connection settings",
"Extend virtual disk",
"Memory",
"Modify device settings",
"Raw device",
"Reload from path",
"Remove disk",
"Rename",
"Reset guest information",
"Set annotation",
"Settings",
"Swapfile placement",
"Unlock virtual machine",
"Guest Operation Program Execution",
"Guest Operation Modifications",
"Guest Operation Queries",
"Answer question",
"Configure CD media",
"Console interaction",
"Defragment all disks",
"Device connection",
"Guest operating system management by VIX API",
"Power Off",
"Power On",
"Reset",
"Suspend",
"VMware Tools install",
"Create from existing",
"Create new",
"Move",
"Register",
"Remove",
"Unregister",
"Allow disk access",
"Allow read-only disk access",
"Allow virtual machine download",
"Allow virtual machine files upload",
"Clone template",
"Clone virtual machine",
"Customize",
"Deploy template",
"Mark as template",
"Mark as virtual machine",
"Modify customization specification",
"Promote disks",
"Read customization specifications",
"Create snapshot",
"Remove Snapshot",
"Rename Snapshot",
"Revert to snapshot",
"Import",
"vApp application configuration"
)
if ( $version -ilike "6.0*" )
{
# Version 6.0 permissions:
$privileges = $privileges | Where-Object { $_ -inotmatch '^(Create|Edit|Delete)Tag$' }
$privileges += "Create Inventory Service Tag"
$privileges += "Edit Inventory Service Tag"
$privileges += "Delete Inventory Service Tag"
}
$role = Get-ViRole | Where-Object { $_.Name -ieq "PKS Administrators" }
if ( !$role )
{
$role = New-VIRole Name "PKS Administrators" Privilege $privileges
}
$principalParam = @{}
$idFieldName = "Name"
if ( $Domain )
{
$principalParam.Add("Domain", $Domain)
$idFieldName = "Id"
}
if ( $PSCmdlet.ParameterSetName -ieq "user" )
{
$principalParam.Add($idFieldName, $User)
$principalParam.Add("User", $true)
}
else
{
$principalParam.Add($idFieldName, $Group)
$principalParam.Add("Group", $true)
}
$principal = Get-VIAccount @principalParam
if ( $PSCmdlet.ShouldProcess($DefaultViServer.Name, "Add permission to root Vcenter for domain account $($principal.Name) and role PKS Administrators") )
{
New-VIPermission Entity "Datacenters" Principal $principal Role $role
}
}

 

Using the Puppet CA API From Windows

Puppet Enterprise exposes a number of RESTful APIs that can be used to help automate the solution and integrate it with other things. One need I’ve run into is the need to revoke and remove certificates from Puppet nodes in an automated fashion. My previous approach involved using SSH to connect to the Puppet Master server and run the puppet cert clean command, but I’m not a huge fan of that. With some effort, I found out how to talk to the API using Postman and PowerShell in a Windows environment. Postman was good for initial testing of the API, while I use PowerShell to fully automate solutions. I’ve outlined a step-by-step on how to set this up below:

Basics

The base URI for the puppet CA API is:

https://*puppet master server FQDN*:8140/puppet-ca/v1

The default port is 8140, which is configurable.

Authorization

Authorization and authentication were the most difficult parts for me to figure out. Unlike the other API endpoints in Puppet, you don’t use the normal token method. The CA API uses certificate authentication and authorization is granted based on the Subject Name of the certificate your client presents to the Puppet server. By default, the ONLY machine allowed to talk to the endpoint is your Puppet Master server itself, so without modification you can’t do much with the API.

You can change the authorization rules to allow other machines to connect. You can see the configuration for this in the /etc/puppetlabs/puppetserver/conf.d/auth.conf:

{
"allow-unauthenticated": true,
"match-request": {
"method": "get",
"path": "/puppet-ca/v1/certificate/",
"query-params": {},
"type": "path"
},
"name": "puppetlabs certificate",
"sort-order": 500
},
{
"allow": [
"puppetmaster.domain.com"
],
"match-request": {
"method": [
"get",
"put",
"delete"
],
"path": "/puppet-ca/v1/certificate_status",
"query-params": {},
"type": "path"
},
"name": "puppetlabs certificate status",
"sort-order": 500
},
{
"allow-unauthenticated": true,
"match-request": {
"method": "get",
"path": "/puppet-ca/v1/certificate_revocation_list/ca",
"query-params": {},
"type": "path"
},
"name": "puppetlabs crl",
"sort-order": 500
},
{
"allow-unauthenticated": true,
"match-request": {
"method": [
"get",
"put"
],
"path": "/puppet-ca/v1/certificate_request",
"query-params": {},
"type": "path"
},
"name": "puppetlabs csr",
"sort-order": 500
}

You’ll see an array of rules defined in this file, each one granting access to particular API endpoints. In this case, I’m most concerned with the certificate endpoints shown above. (For details on the layout of this file, see Puppet’s Docs here)

The endpoint rules that specify “allow-unauthenticated” are freely-accessible without authentication, so most of this article doesn’t apply to them. Just make a call from Postman or Curl like normal.

However, the certificate_status endpoint has an “allow” property, which lists all of the nodes that are allowed to access the endpoint. By default, it appears the name of your Puppet Master server appears here.

Normally, you could probably add entries to this list, restart your Puppet Master services, and go. The issue is this file is actually managed by Puppet, and your changes would be overwritten the next time the Puppet agent runs.

This setting is actually governed by the puppet_enterprise::profile::certificate_authority::client_whitelist setting. This can be set a couple of ways. The first way is to log into the Puppet Master GUI and do the following:

  1. Go to Inventory and select your Puppet Master server
  2. Select the “Groups” tab and click the PE Certificate Authority Group
  3. Click the “Classes” tab
  4. Set the client_whitelist parameter under puppet_enterprise::profile::certificate_authority

certificate_authorityNormally, this would work, but when the Puppet agent runs you might get the following error:

Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Duplicate declaration: Class[Puppet_enterprise::Profile::Master] is already declared; cannot redeclare on node

The workaround I found in a Q/A article suggested to just add the setting to your common.yaml and have Hiera set the setting instead. This worked well for me. My common.yaml file looks like this:

# Allows the listed machines to communicate with the puppet-ca API:
puppet_enterprise::profile::certificate_authority::client_whitelist:
– server1.mydomain.com
– server2.mydomain.com

Once this was pushed to the Puppet Master server, I did a Puppet agent run using puppet agent -t from the server and it applied the settings. Checking auth.conf again, I now see this:

{
"allow": [
"puppetmaster.domain.com",
"server1.domain.com",
"server2.domain.com"
],
"match-request": {
"method": [
"get",
"put",
"delete"
],
"path": "/puppet-ca/v1/certificate_status",
"query-params": {},
"type": "path"
},

Now that my servers are authorized to access the API, I can make calls using a client certificate to authenticate to the API.

Authentication

The next section shows you how to setup Postman and PowerShell to authenticate to the API. If you setup your authorization correctly as shown above, you should be able to hit the APIs.

Using Postman

To use Client Cert authentication to the Puppet API, you can setup Postman using the following method

Import the cert into Postman:

  1. Click Settings in Postman
  2. Go to Certificates
  3. Click the “Add Certificate link”
  4. Add the cert using the following settings
    • Host – Specify the FQDN of the host you want to present the cert to. Don’t specify any of the URI path, just the FQDN and port.
    • CRT File – Use the PEM file in the certs directory
    • KEY File – Use the PEM file in the private_keys directory
    • NO passphrase

Postman_client_cert

Once that is done, you can issue a GET command to a URI like this and get a response:

https://puppetmasterserver.domain.com:8140/puppet-ca/v1/certificate_statuses/key

The “key” portion of the URI is required, but the word “key” is arbitrary. I think you can pretty much type anything you want there.

This yields a response much like the following:

cert_statuses

If you get a “Forbidden” error, you either have the URI slightly wrong or you don’t have the authorization correct. The array of names in the “allow” section of the API rule MUST match the Subject Name of the certificate.

Using PowerShell

To get this to work with PowerShell, you have to export your Puppet certs as a PFX and reference them in a Invoke-RestMethod call.

To create a PFX from the certs, do the following:

  1. Install Openssl
      • If you have Git for Windows installed, you already have this. Just change to c:\program files\Git\usr\bin
  2. Run the following
C:\Program Files\Git\usr\bin\openssl.exe pkcs12 -export -out "c:\temp\server1.domain.com.pfx" -inkey "C:\ProgramData\PuppetLabs\puppet\etc\ssl\private_keys\server1.domain.com.pem" -in "C:\ProgramData\PuppetLabs\puppet\etc\ssl\certs\server1.domain.com.pem"

Don’t specify a export password.

Once that is done, call the following Cmdlet:

Invoke-RestMethod -Uri "https://puppetmaster.domain.com:8140/puppet-ca/v1/certificate_statuses/key" -Certificate (Get-PfxCertificate -FilePath C:\temp\server1.domain.com.pfx) -Headers @{"Content-Type" = "application/json" }

Viola! That’s it.

References

Disabling SSL Certificate Validation with PowerShell

I’ve run into this issue about a billion times.  Mostly, I see it when I’m coding against a web API on a device with a bad or partially-valid self-signed cert.

I’ve seen several articles on how to disable the SSL validation check, but have had only limited success with them.  I finally found an approach out there that works for all of my use cases, and wrapped a nice function around it.  I’m publishing it here in hopes it helps people out someday.

Basically, call this either to enable or disable SSL certificate validation.  It is safe to run multiple times in the same session and doesn’t throw any errors.

Here it is:

function Set-SslCertificateValidation
{
<#
.SYNOPSIS
This function enables or disables SSL Cert validation in your PowerShell session. Calling this affects SSL validation for ALL function calls in the session!
.PARAMETER Disable
If specified, validation is disabled. If not specified (the default) validation is re-enabled.
.EXAMPLE
Set-SslCertificateValidation -Disable
# Disables SSL Cert validation
.EXAMPLE
Set-SslCertificateValidation
# Re-enables SSL Cert validation again
#>
param
(
[switch] $Disable
)
$type = [AppDomain]::CurrentDomain.GetAssemblies().ExportedTypes | Where-Object { $_.Name -ieq "TrustAllCertsPolicy" }
if ( !$type )
{
# Disable SSL Certificate validation:
Add-Type -TypeDefinition @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy
{
public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate,WebRequest request, int certificateProblem)
{
return true;
}
}
"@
}
if ( $Disable )
{
[System.Net.ServicePointManager]::CertificatePolicy = New-Object -TypeName TrustAllCertsPolicy
}
else
{
[System.Net.ServicePointManager]::CertificatePolicy = $null
}
}

Automating MAK Proxy Activation with PowerShell

I ran into a need recently where I had to activate Windows on new machines in an automated fashion.  The issue was that the environment did not use KMS, but instead activated new machines using a MAK key.  The machines being activated did not have Internet access, so they had to be activated via proxy.

There is a great article on how to do this using the Volume Activation Management Tool (VAMT) here.  Basically, enable Internet access (or at least access to the MS Activation servers) to a machine with the VAMT installed and you can use the GUI to activate it.  If you need to automate it, you can see instructions on the PowerShell commands for VAMT here.

This all works very well, but not complete for my needs.  I needed have a different server other than the VAMT server initiate the activation.  To do this, I wrapped the VAMT commands I needed in a PowerShell function detailed further below.  With this function, you can have any server issue the commands to the VAMT server to add and activate multiple severs on your network in an automated fashion.

I found one big caveat though.  You need to enable Kerberos Delegation for BOTH the VAMT server and the server running this function.  This is done by issuing the command below in PowerShell:

Set-AdComputer -Identity computerName -TrustedForDelegation $true 

The reason for this is the server running this function must pass the credentials of the user running it to the VAMT cmdlets so they can run.  In turn, the Find-VamtManagedMachine cmdlet must also pass those credentials to Active Directory to look the machine up.  If you forget to do this, you will get errors.

Here is the function:

function Invoke-WindowsActivation()
{
<#
.SYNOPSIS
This function reaches out remotely to the specified VAMT server and activates the given machines by proxy. To run this, you must meet the following requirements:
* The ActiveDirectory module from Microsoft be installed on the machine this function runs from. Install with:
Add-WindowsFeature
* It's assumed the machines you are dealing with are on an Active Directory domain.
* You have a server with the VAMT 3.0 installed.
.PARAMETER ComputerName
Specifies one or more computers to activate.
.PARAMETER Domain
Specifies the AD domain the VAMT server and the machines you are activating are on. Default is the current user DNS Domain ($ENV:USERDNSDOMAIN).
.PARAMETER VamtServer
Specifies the machine the VAMT toolset is installed on. This machine needs the Windows Assessment and Deployment Kit (VAMT Tool) installed. See:
https://www.microsoft.com/en-us/download/details.aspx?id=30652
https://technet.microsoft.com/en-us/library/hh825184.aspx
.EXAMPLE
Invoke-WindowsActivation -ComputerName myserver1,myserver2 -VamtServer vamt01
ActionsAllowed : 105
ApplicationName :
ApplicationId : xxxxx
CMID :
ConfirmationId :
ExportGuid : xxxxx
FullyQualifiedDomainName : myserver1.mydomain.com
GenuineStatus : Genuine
GraceExpirationDate : 4/17/2017 9:56:23 PM
InstallationId : xxxxx
KmsHost :
KmsPort :
LastActionStatus : Successfully updated the product information.
LastErrorCode : 0
LastUpdated : 4/17/2017 9:56:23 PM
LicenseFamily : ServerDatacenter
LicenseStatus : Licensed
LicenseStatusLastUpdated : 4/17/2017 9:56:23 PM
LicenseStatusReason : 0
PartialProductKey : xxxx
ProductDescription : Windows(R) Operating System, VOLUME_MAK channel
ProductKeyId : xxx
ProductName : Windows(R), ServerDatacenter edition
ProductKeyType : Mak
ProductVersion : 6.3.9600.17809
Sku : xxxxx
ProductKeyTypeName :
LicenseStatusText :
GenuineStatusText :
ResourceLanguage :
SoftwareProtectionService : SPP
VLActivationType : NeverVolumeActivated
VLActivationTypeEnabled : Default
AdActivationObjectName :
AdActivationObjectDN :
AdActivationCsvlkPid :
AdActivationCsvlkSkuId : 00000000-0000-0000-0000-000000000000
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param
(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)] $ComputerName,
[string] $Domain = $ENV:UserDnsDomain,
[Parameter(Mandatory=$true)] [string] $VamtServer
)
begin
{
function Test-Kerberos()
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true)] $ComputerName
)
Import-Module ActiveDirectory
$c = Get-AdComputer Identity $ComputerName Properties TrustedForDelegation
return ( $c.TrustedForDelegation )
}
if ( !(Test-Kerberos ComputerName $VamtServer) )
{
throw ("The VAMT Server ($VamtServer) does not have Kerberos delegation enabled! Use: Set-AdComputer -Identity $VamtServer -TrustedForDelegation $true")
}
if ( !(Test-Kerberos ComputerName $Env:COMPUTERNAME) )
{
throw ("This client ($Env:COMPUTERNAME) does not have Kerberos delegation enabled! Use: Set-AdComputer -Identity $VamtServer -TrustedForDelegation $true")
}
# You must use a 32-bit PowerShell session! VAMT.psd1 does not support 64-bit.
$session = New-PSSession ComputerName $VamtServer ConfigurationName Microsoft.PowerShell32
$sb = `
{
$psdPath = ""
if ( Test-Path Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\VAMT3" )
{
$psdPath = Get-ItemProperty Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\VAMT3" Name "SchemaFilePath" | Select-Object ExpandProperty SchemaFilePath
}
else
{
throw ("VAMT3 is not installed on the local machine: $($ENV:COMPUTERNAME)!")
}
Write-Verbose ("VAMT Module location: $psdPath")
Import-Module Name (Join-Path Path $psdPath ChildPath "vamt.psd1")
}
$psdPath = Invoke-Command Session $Session ScriptBlock $sb
}
process
{
try
{
foreach ( $comp in $ComputerName )
{
$sb = `
{
param
(
[Parameter(Mandatory=$true)] $ComputerName,
[string] $Domain = $ENV:UserDnsDomain
)
$product = Find-VamtManagedMachine QueryType ActiveDirectory QueryValue $Domain MachineFilter $ComputerName
if ( !$product )
{
throw ("Unable to find a computer in the VAMT Database named $ComputerName! Verify Kerberos delegation is enabled for both $($ENV:ComputerName) and $ComputerName! Set-AdComputer -Identity $ComputerName -TrustedForDelegation `$true ")
}
Write-Host ("Product Entry:")
Write-Host ($product | Format-List | Out-String)
if ( $product.GenuineStatus -ine "Genuine" )
{
# Get the confirmation ID:
$confirmation = $product | Get-VamtConfirmationId
if ( $confirmation.ConfirmationId )
{
$out = Install-VamtConfirmationId Products $confirmation
$output = Find-VamtManagedMachine QueryType ActiveDirectory QueryValue $Domain MachineFilter $ComputerName
Write-Host ("Activated server: ")
Write-Host ($output | Format-List | Out-String)
$output
if ( $output.GenuineStatus -ine "Genuine" )
{
throw ("An error occurred activating Windows OS on $comp. `r`nError message: $($output.LastActionStatus).")
}
}
else
{
throw ("Unable to get a confirmation ID for machine $ComputerName!")
}
}
else
{
Write-Warning ("$ComputerName has already been activated!")
$product
}
}
if ( $PSCmdlet.ShouldProcess($comp, "Activate Windows machine") )
{
Invoke-Command Session $session ScriptBlock $sb ArgumentList $comp,$Domain
}
}
}
catch
{
if ( $session )
{
$session | Remove-PSSession
}
throw $_
}
}
end
{
if ( $session )
{
$session | Remove-PSSession
}
}
}

 

Hopefully, this is of use to others.

Encrypting Credentials In PowerShell Scripts

I have a long-standing dislike of hard-coding credentials in scripts.  In a production environment, it’s never a good idea to leave sensitive account passwords hard-coded in plain text in scripts.  To that end, I’ve developed an easy method in PowerShell to protect sensitive information.

The functions I present below allow you to store usernames and passwords, where the passwords are encrypted, in a form that can be later decrypted inside a script.  By default, only the user account that encrypted the credentials can decrypt them, and only from that same machine.  It all uses native .NET stuff, so you don’t need any third-party stuff to get it working.

Where I find this most useful is for services or scheduled tasks that run as system accounts that execute PowerShell scripts.  You can log into the machine as that service account, encrypt a set of credentials, then when that scheduled task runs as that service account it is able to read them.

Using the export function I show below, you can either export your credentials to an xml file on the file system, or a registry value in the Windows registry.

Here is an example:

First, save the credential to a variable and export it to an xml file:

$cred = Get-Credential username
$cred | Export-PSCredential -Path c:\temp\creds.xml

This outputs the path to the xml file you created with the encrypted credentials:

Export-PSCredential

Alternately, you can export to a registry key instead:

$cred = Get-Credential username
$cred | Export-PSCredential -RegistryPath HKCU:\software\test -Name mycreds

In the registry, you can see your exported credentials:

Export-Registry

https://gist.github.com/BrandonStiff/02cada362bfca007d298b549506f225f.js

The major thing that needs to be understood about this is the encryption key that is used to encrypt these credentials is tied to both the userid used to encrypt them AND the machine you encrypted from.  Unless you specify a keyphrase, you cannot decrypt these credentials as another user or from another machine.  The idea is if you have a script that reads these encrypted credentials, you have to log in as the user the script runs as on the machine the script runs from and encrypt them.  However, as described above, if you provide a keyphrase, you can decrypt them from anywhere as any user.  You just have to somehow protect the keyphrase.

Importing the credentials again is pretty simple:

$cred = Import-PSCredential -Path C:\temp\creds.xml
# OR
$cred = Import-PSCredential -RegistryPath HKCU:\Software\test -Name mycreds

Import-PSCredential

Specifying a keyphrase involves specifying the -KeyPhrase parameter on either the import or export function.

Below is the code.  Simply paste these three functions into your PowerShell session or into your script and away you go.

function Get-EncryptionKey()
{
<#
.SYNOPSIS
Retrieves a 128/192/256-bit encryption key using the given keyphrase.
.PARAMETER KeyPhrase
Specifies the phrase to use to create the 128-bit key.
.PARAMETER Length
Specifies the number of bits to make the length. Use either 128, 192, or 256 bits. Default is 128.
.OUTPUTS
[byte[]]
Returns a 128/192/256-bit (32/48/64-byte) array that represents the keyphrase.
#>
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]
[string] $KeyPhrase,
[ValidateSet(128,192,256)] [int] $Length = 128
)
process
{
$enc = [System.Text.Encoding]::UTF8;
$bytes = $Length / 4;
$KeyPhrase = $KeyPhrase.PadRight($bytes, "0").SubString(0,$bytes);
$enc.GetBytes($KeyPhrase);
}
}

function Export-PSCredential
{
<#
.SYNOPSIS
Exports a credential object into an XML file or registry value with an encrypted password. An important note is that the encrypted password can ONLY be read by the user who created the exported file
unless a passphrase is provided.
.PARAMETER Credential
Specifies the Credential to export to a file. Use Get-Credential to supply this.
.PARAMETER Path
Specifies the file to export to. Default is (CurrentDir)\encrypted.xml.
.PARAMETER RegistryPath
Specifies the path to the registry to export the credentials to. Use HKLM and HCKU for HKEY_LOCAL_MACHINE and HKEY_CURRENT_USER respectively. Example: HKCU:\Software\Acme Inc\MyCredentials
.PARAMETER Name
Specifies the name of the registry value to store the credentials under. Only specify with RegistryPath.
.PARAMETER KeyPhrase
Specifies the key phrase to use to encrypt the password. If not specified, then a key derived from the user's account is used. This makes the password only decryptable by the user who encrypted it.
If a key is specified, then anybody with the key can decrypt it.
.EXAMPLE
PS> (Get-Credential bsti) | Export-PSCredential
# Encrypts the credential for username bsti and exports to the current directory as encrypted.xml
.EXAMPLE
PS> (Get-Credential bsti) | Export-PSCredential -Path C:\temp\mycreds.xml
# Encrypts the credential for username bsti and exports to the current directory as encrypted.xml
.EXAMPLE
PS> (Get-Credential bsti) | Export-PSCredential -RegistryPath "HKCU:\Software\Acme Inc\MyCreds" -Name "switch1"
# Encrypts the credential for username bsti and exports to the registry at the given path, under the value switch1.
.EXAMPLE
PS> (Get-Credential bsti) | Export-PSCredential -Path C:\temp\mycreds.xml -KeyPhrase "ThisisMyEncryptionPassword123"
# Encrypts the credential for username bsti and exports it to the filesystem. Anyone with the keyphrase can decrypt it.
.OUTPUTS
Returns the [System.IO.FileInfo] object representing file that was created or the path to the registry key the credentials were exported to.
#>
[CmdletBinding(SupportsShouldProcess=$true,DefaultParameterSetName="filesystem")]
param
(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[Management.Automation.PSCredential] $Credential,
[Parameter(ParameterSetName="filesystem")]
[ValidateScript({ Test-Path Path (Split-Path Path $_) PathType Container } )]
[string] $Path = $(Join-Path Path (Get-Location) ChildPath "encrypted.xml"),
[Parameter(Mandatory=$true,ParameterSetName="registry")]
[string] $RegistryPath,
[Parameter(Mandatory=$true,ParameterSetName="registry")]
[string] $Name,
[string] $KeyPhrase
)
process
{
foreach ( $cred in $Credential )
{
# Create temporary object to be serialized to disk
$export = "" | Select-Object Username, EncryptedPassword
# Give object a type name which can be identified later
$export.PSObject.TypeNames.Insert(0,"ExportedPSCredential")
$export.Username = $Credential.Username
# Encrypt SecureString password using Data Protection API
# Only the current user account can decrypt this cipher unless a key is specified:
$params = @{}
if ( $KeyPhrase )
{
$params.Add("Key", (Get-EncryptionKey KeyPhrase $KeyPhrase))
}
$export.EncryptedPassword = $Credential.Password | ConvertFrom-SecureString @params
if ( $PSCmdlet.ParameterSetName -ieq "registry" )
{
# Export to registry
# Make sure the registry key exists:
if ( !(Test-Path Path $RegistryPath) )
{
New-Item Path $RegistryPath Force | Out-Null
}
# Set/Update the credential in the registry store:
Set-ItemProperty Path $RegistryPath Name $Name Value ("{0}:{1}" -f $export.UserName, $export.EncryptedPassword) Force
}
else
{
# Export using the Export-Clixml cmdlet
$export | Export-Clixml $Path
# Return FileInfo object referring to saved credentials
Get-Item Path $Path
}
}
}
}

function Import-PSCredential
{
<#
.SYNOPSIS
Imports a credential exported by Export-PSCredential and returns a Credential.
.PARAMETER Path
Specifies one or more files to convert from XML files to credentials.
.PARAMETER RegistryPath
Specifies the path in the registry to look for the encrypted credentials.
.PARAMETER Name
Specifies the registry key the credentials are stored under.
.PARAMETER KeyPhrase
Specifies the key phrase to use to encrypt the password. If not specified, then a key derived from the user's account is used. This makes the password only decryptable by the user who encrypted it.
If a key is specified, then anybody with the key can decrypt it.
.EXAMPLE
Import-PSCredential -Path C:\temp\mycreds.xml
# Retrieves encrypted credenials from the given file.
.EXAMPLE
Get-ChildItem C:\temp\credstore | Import-PSCredential
# Retrieves encrypted credenials from files in the given directory.
.EXAMPLE
Import-PSCredential -RegistryPath "HKCU:\Software\Acme Inc\MyCreds" -Name switch1
# Retrieves encrypted credenials from the registry path: "HKCU:\Software\Acme Inc\MyCreds" Key switch1
.EXAMPLE
Import-PSCredential -Path C:\temp\mycreds.xml -KeyPhrase "test12345"
# Retrieves encrypted credenials from the filesystem. Decrypts them using the given key.
.OUTPUTS
[System.Management.Automation.Credential]
Outputs a credential object representing the cached credentials. Use GetPlainTextPassword() to retrieve the plain text password.
#>
[CmdletBinding(DefaultParameterSetName="filesystem")]
param
(
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ParameterSetName="filesystem")]
[ValidateScript({ Test-Path Path $_ PathType Leaf } )] [String[]] $Path,
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ParameterSetName="registry")]
[string] $RegistryPath,
[Parameter(Mandatory=$true,ParameterSetName="registry")]
[string] $Name,
[string] $KeyPhrase
)
begin
{
$paths = @()
}
process
{
if ( $PSCmdlet.ParameterSetName -ieq "registry" )
{
$paths += $RegistryPath
}
else
{
$paths += $Path
}
foreach ( $p in $paths )
{
$import = $null
if ( $PSCmdlet.ParameterSetName -ieq "registry" )
{
# Imported from registry:
$import = "" | Select-Object "UserName","EncryptedPassword"
# Make sure the registry key exists:
if ( Test-Path Path $p )
{
$regValue = Get-ItemProperty Path $p | Where-Object { $_.$Name }
if ( $regValue )
{
$credsAsString = (Get-ItemProperty Path $p).$Name
if ( ($credsAsString -split ":").Count -lt 2 )
{
throw ("Credential was stored in an invalid format!")
}
$import.UserName = ($credsAsString -split ":")[0]
$import.EncryptedPassword = ($credsAsString -split ":")[1]
}
}
}
else
{
$fileFullPath = $p
if ( $p -is [System.IO.FileInfo] )
{
$fileFullPath = $p.FullName
}
# Import credential file
$import = Import-Clixml $fileFullPath
}
if ( $import -and $import.UserName -and $import.EncryptedPassword )
{
$userName = $import.Username
# Decrypt the password and store as a SecureString object for safekeeping
try
{
$params = @{};
if ( $KeyPhrase )
{
$params.Add("Key",(Get-EncryptionKey KeyPhrase $KeyPhrase));
}
$securePass = $import.EncryptedPassword | ConvertTo-SecureString ErrorAction Stop @params;
}
catch [System.FormatException]
{
throw ("An invalid encryption key was supplied! If this credential was encrypted with a KeyPhrase, you must use the correct keyphrase to decrypt it!");
}
catch [System.Security.Cryptography.CryptographicException]
{
throw ("Invalid encryption key! If no key is specified, then only the user that exported the credential in file $fileFullPath can retrieve it! Current user $($env:UserDomain)\$($env:UserName) may not have access!");
}
catch
{
throw $_;
}
# Build the new credential object
Get-PSCredential Credential (New-Object System.Management.Automation.PSCredential $userName, $securePass);
}
}
}
}

Note the Get-EncryptionKey function is required for both the import and export functions!

Creating a Directory Tree in Puppet

As you can probably tell from the flurry of blog posts I’ve made concerning Puppet, I’m going through the process of learning and setting up Puppet Enterprise.

One thing that irked me early on is the inability of the file resource to create a directory if the parent directory does not exist.  For example:

file { 'mydirectory' :
  ensure         => 'directory',
  path           => 'c:/parentdir/childdir'
}

If c:\parentdir does not exist, this fails.

Error: Cannot create C:/parentdir/childdir; parent directory C:/parentdir does not exist
Error: /Stage[main]/Profile::Myclass/File[mydirectory]/ensure: change from absent to directory failed: Cannot create C:/parentdir/childdir; parent directory C:/parentdir does not exist

You can alternately specify it like this to get it to work:

file { ['c:/parentdir', 'c:/parentdir/childdir'] :
  ensure         => 'directory'
}

This works, and for the most part is OK.  In my case though, I have the user provide the directory name through a class parameter:

class myclass ([String] $mydirectory)
{
  file { 'mydirectory' :
   ensure         => 'directory',
   path           => $mydirectory
  }
}

If the user specifies c:/parentdir/childdir, and c:/parentdir does not exist, it explodes.  I could adjust the code and advise my users to pass in arrays of strings representing the directories, but that’s not very clear or clean.

Fortunately, Puppet supports PowerShell and PowerShell is awesome:

class myclass (String $directory)
{
   exec { 'mydirectory' :
     command => "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe -noprofile -noninteractive -command \"New-Item -ItemType Directory -Path \"$directory\" \"",
     onlyif  => "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe -noprofile -noninteractive -command \"if (Test-Path -Path \"$directory\" -PathType Container) { exit 99 }\""
   } 
}

This code block creates the entire directory tree without issue.  The onlyif parameter ensures that the exec block is not fired off if the directory already exists.

Enjoy!

Powershell Module for Logging to a File

Despite out best efforts as coders, automated processes sometimes fail.  One of the principle ways to troubleshoot such processes is to log data to a file so you can follow what happened after-the-fact.  I have a TON of scripts that have to do this, so it made sense to cobble together some functions that make doing this easier.

To this end, I’ve written a script module called bsti.logging.  It features the following functions:

logging_functions

Once you import the module, you call New-LogFile to setup a new file to write to.  You can specify options to append the weekday to the file or a more specific timestamp (e.g. MyLog_mmddyyyyhhmmss.log) to the log file.  For timestamped log files, you can also setup retention so old log files get automatically deleted after a period of time or after so many accumulate.

logging_new_logfile

I have three basic types of ways to handle log file naming that you need to be clear on to get good use out of the module:

1) Standard – The log file path you pass is will be unchanged by the function.  The purging parameters are ignored, you must use -Append or it will be overwritten if it exists already.

2) Circular – The log file will have _weekday appended to the file name before the extension.  If you pass in C:\temp\log\MyLogFile.log for example, you get:
MyLogFile_Monday.log
MyLogFIle_Tuesday.log
etc…
When you call New-LogFile with circular naming and the *same* log file path again that same day, it will automatically be appended to.  When the next Monday rolls by, it *automatically* overwrites it.

This scheme is good if you call the script that writes to the log file frequently, don’t want to manage a large number of log files, and don’t need more than 7 days of log file history.

3) DateStamped – This appends a detailed datestamp to the log file name before the extension.  In the example above, you get:
MyLogFile_03292015200500.log (Assuming 3/29/2015 10:05 PM)
This means every time you call New-LogFile (Assuming you wait at least 1 second!), you get a unique log file.  Append is essentially ignored.
The PurgeAfter and KeepNumberOfFIles, if specified, will cause the New-LogFile function to call Remove-LogFiles automatically and keep your log files trimmed as you specify.  If you specify both PurgeAfter AND KeepNumberOfFiles, both thresholds are observed (meaning the file needs to be older than what you specified with PurgeAfter AND you have to have KeepNumberOfFiles remaining).

This scheme is good if you need a specified history of log files and want individual log files for each run of your process.  The automatic cleanup is a bonus.

Once you’ve setup your new log file, you call the following functions to write to it.  These functions also echo to the console, so you can replace any calls to Write-Host with these functions and get messages to your console and to a log file:

Write-Message
Write-Object
Write-Banner
Write-BlankLines

logging_write

Makes things pretty simple.

As with all of my modules and module functions, I heavily document them using Powershell comments-based help.  Just try:

Get-Help New-LogFile -Full

This module does depend on my bsti.conversion module, so if you use this module as a whole you need both modules.  I posted about that module here.

Here is a link to the new bsti.logging module.
Here is a link to the bsti.conversion module.

TimeSpan Conversion Function and Module

I have a ton of Powershell code I’ve written over the last 6 years or so that I’m in the process of cleaning up and looking to share.  I plan on re-presenting them as a set of scripts and script modules I’ll be featuring in a series of blog posts coming in the near future.

I’ll start simply.  I have a new script module called bsti.conversion that has a single function:  ConvertTo-TimeSpan

I always thought it was neat that you could type in 1kb and Powershell would convert that value to 1024.  You can also use mb (megabytes), tb (terabytes), and pb (petabytes).  I don’t see that eb (exabytes) works.  In any case, I always wished I could do the same with time units like this:

12m (minutes) or 24h (hours) or 7d (days)

The ConvertTo-TimeSpan function I’ve included in this module does just that.

What I use this for is I have functions and scripts I like to write that require the user to pass in an easy time unit value.

This functionality can also be achieved by Timespan conversion like so:

[Timespan](“1.00:00:00”)  # 1 day
[Timespan](“1.6:00:00”)  # 1 day, 6 hours
[Timespan](“1:22:00”)  # 1 hour, 22 minutes

The conversion function is a little less precise, but a bit more human-readable, which is important to me since most of my users are not .NET programmers and don’t understand the format of a timespan object right offhand.

The function in this module supports both formats:

bsti.conversion1

The module files can be downloaded here.
Once downloaded, extract the folder to the C:\windows\system32\windowspowershell\v1.0\modules directory.  The final structure should look like this:
C:\windows\system32\windowspowershell\v1.0\modules\bsti.conversion

Then just use the Import-Module bsti.conversion command as shown above.

Not bad for a start, hope you enjoy.

UPDATE:  I’m adding my stuff to GitHub.  Bear with me while I come up-to-speed on how to use it.  Find this module project here:

https://github.com/Roadkill4545/bsti.Powershell/tree/master/bsti.conversion

Automatically Transcripting all Powershell Sessions

If you love Windows Powershell as much as I do, you probably find yourself using it to complete day-to-day management tasks in addition to scripting and automation.

More and more hardware vendors, like NetApp and VmWare, provide very robust Powershell toolsets.  Because I’m such a command line guy, and these Powershell libraries are so powerful, I perform nearly all of my management tasks from Powershell.  I find tasks like provisioning and destroying storage, creating clones, managing snapshots, and getting data from virtual machines much easier via the command line in many cases.

Because this is a day-to-day thing to me, it’s useful to have all of the code I type in and the output I get back automatically logged to a file for future perusal.  I liken it to the administrators who manage a lot via SSH, and setup Putty to log all sessions to a file.  This allows me to look at past actions, remember how I did stuff, or see what I did wrong if something got messed up.  It can also helpful for auditing if you need to track who did what using Powershell.

Below is a procedure I use on all management stations I use Powershell from.  This procedure automatically logs all activity from the command line to a file.

By default, it creates a new transcript file in the C:\users\myusername\LogFiles\Powershell\computername directory.

You can override this if you want to go to a central location, by calling the Set-TranscriptFilePath function.

For instance, to transcript everything to a central file share:

Set-TranscriptFilePath -path "\\myserver\LogFiles\Powershell\server1"

It creates one new transcript file per session you launch, so you won’t have multiple sessions writing over the same file.

To set this up, create a new file called profile.ps1 in one of the following directories:

Apply to just the current user:
C:\users\myusername\documents\WindowsPowershell\profile.ps1   only

Or to apply to all users on the computer:
c:\windows\system32\WindowsPowershell\v1.0\profile.ps1

Copy the following script text to the profile script you created:

<# 
  .SYNOPSIS 
  Windows Powershell console profile script. This script is generic enough to be run from any machine. It sets up console logging to a network share for servers and locally for workstations. 
  NOTE: This will not transcript in Powershell ISE! Transcripting in ISE is supported in the current (early) version of Windows Management Framework 5.0 however. 
#>

$script:TranscriptFileKey = "HKLM:\SOFTWARE\PowershellManagement\Powershell"

#############################################################################################################################################
# FUNCTIONS
#############################################################################################################################################

function Get-TranscriptFilePath()
{
  <# 
    .SYNOPSIS 
    This function returns the location where Powershell session transcript log files go. 
  #>

  [CmdletBinding()]
  param
  (
  )

  $transcriptFilePath = ""

  # User can override log path in registry:
  if ( Test-Path -Path $script:TranscriptFileKey )
  {
    $regKey = Get-Item -Path $script:TranscriptFileKey
    if ( $regKey.Property -icontains "RootTranscriptPath" )
    {
      $transcriptFilePath = (Get-ItemProperty -Path $script:TranscriptFileKey -Name "RootTranscriptPath").RootTranscriptPath
    }
  }

  if ( !$transcriptFilePath )
  {
    # Station is a workstation, use local path:
    $transcriptFilePath = Join-Path $Env:USERPROFILE -ChildPath "LogFiles\Powershell\$($Env:ComputerName)"
  }

  # Create the log file path if it does not exist:
  if ( !(Test-Path -Path $transcriptFilePath) )
  {
    New-Item -ItemType directory -Path $transcriptFilePath -Force | Out-Null
  }

  $transcriptFilePath
}

function New-TranscriptFilePath()
{
  Join-Path -Path (Get-TranscriptFilePath) -ChildPath ("Powershell {0:MM dd yyyy hh mm ss} {1:00000}.log" -f (Get-Date),$PID)
}

function Test-Administrator
{
  $user = [Security.Principal.WindowsIdentity]::GetCurrent()
  (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}

function Set-TranscriptFilePath()
{
  <# 
    .SYNOPSIS 
    This function sets the transcript file path from the default. This affects all future Powershell sessions. 
    
    .PARAMETER Path 
    Specifies the new path where future transcript files get saved to. This will remain the path until it is changed. Set this to "" to reset to the default path: C:\users\username\LogFiles\Powershell\computername 
  #>

  [CmdletBinding(SupportsShouldProcess=$true)]
  param
  (
    [string] $Path
  )

  if ( !(Test-Administrator) )
  {
    throw ("You must launch this console as an administrator to execute this function!")
  }

  if ( $Path )
  {
    if ( !(Test-Path -Path $Path) )
    {
      # Create the registry path:
      New-Item -ItemType Directory -Path $script:TranscriptFileKey -Force -ErrorAction Stop | Out-Null
    }
  }

  Set-ItemProperty -Path $script:TranscriptFileKey -Name RootTranscriptPath -Value $Path -ErrorAction Stop | Out-Null

  if ( !$Path )
  {
    $Path = Join-Path $Env:USERPROFILE -ChildPath "LogFiles\Powershell\$($Env:ComputerName)"
  }

  Write-Host ("Future transcript files will be saved to the following directory: $Path") -ForegroundColor Green
}

Once complete, every new Powershell console session you launch will be automatically transcripted!

Note: As of Powershell 4.0, you can’t transcript from the Powershell session that gets launched from Powershell ISE.  This is due to a difference in the console.  However, I have noticed it works OK in the preview of Windows Management Framework 5.0, so i suspect support for this is coming soon.

I’ve uploaded the script here if you’d rather not cut-and-paste.