Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save naamancampbell/b33af1f2e566b014e2ba8deff898b939 to your computer and use it in GitHub Desktop.
Save naamancampbell/b33af1f2e566b014e2ba8deff898b939 to your computer and use it in GitHub Desktop.
Generates and distributes encrypted passwords for use with PowerShell scripts used in CloudFormation templates.
<#
.SYNOPSIS
Generates and distributes encrypted passwords for use with PowerShell scripts used in CloudFormation templates.
.DESCRIPTION
The CloudFormation-PowerShell-Creds script is designed to be run prior to running a CloudFormation stack
then called again from within the CloudFormation template. The purpose is to avoid the use of cleartext
passwords in CloudFormation templates/PowerShell scripts.
There are three main components of this script:
1. Prepare Credentials
This phase stores encrypted passwords in a restricted folder before uploading to AWS S3.
Re-running this phase will generate a new unique credential folder.
2. Download Credentials
This phase is recreates the restricted folder structure on the target system before downloading from AWS S3.
3. Access Credentials
This phase is used to access credentials (as PSCredential objects) from the local credential store.
Author: Naaman Campbell (naaman@clancampbell.id.au)
Change Log:
07/10/2017 0.1 NC Initial Release
Forked from: Script-Template-WithCreds.ps1
Author: David Lee (david.lee@kloud.com.au)
https://gist.github.com/davefunkel/415a4a09165b8a6027a297085bf812c5
.PARAMETER PrepareCredentials
Records and prepares credentials for use within AWS CloudFormation templates (and other scripts)
.PARAMETER DownloadCredentials
Downloads credentials from AWS S3 for use on target system
.PARAMETER AccessCredential
Retrieves the specified credential from the local credential store
.PARAMETER CredentialPath
Path to credential store folder
.PARAMETER S3Bucket
Name of S3 Bucket to stage credentials
.PARAMETER AWSAccessKey
AWS Access Key for user with S3 permissions for -S3Bucket
.PARAMETER AWSSecretKey
AWS Secret Key for -AWSAccessKey
.PARAMETER AWSRegion
AWS Region for -S3Bucket
.EXAMPLE
Prompts for credentials to save to the secure files in C:\Admin\Keys before copying to the specified AWS S3 bucket:
.\CloudFormation-PowerShell-Creds.ps1 -PrepareCredentials -CredentialPath "C:\Admin\Keys" -S3Bucket "cf-templates-1234deadbeef1-ap-southeast-2" -AWSAccessKey "XXXX" -AWSSecretKey "XXXX" -AWSRegion "ap-southeast-2"
.EXAMPLE
Downloads the secure files from the specified AWS S3 bucket to the matching local folder path (eg. C:\Admin\Keys\CF-Creds-20171018T0954207027):
.\CloudFormation-PowerShell-Creds.ps1 -DownloadCredentials -CredentialPath "C:\Admin\Keys\CF-Creds-20171018T0954207027" -S3Bucket "cf-templates-1234deadbeef1-ap-southeast-2"
.EXAMPLE
Retrieves "ADFSSVC" user credential (as $ADFSSVC-Cred) from local secure credential store
$ADFSSVC-Cred = .\CloudFormation-PowerShell-Creds.ps1 -AccessCredential "ADFSSVC" -CredentialPath "C:\Admin\Keys\CF-Creds-20171018T0954207027"
#>
[CmdletBinding(
HelpUri = 'https://github.com/naamancampbell/CloudFormationPowerShellCreds/')]
Param(
# Prepare credentials for use within CloudFormation templates
[Parameter(Position = 0,
Mandatory = $true,
ParameterSetName = "Prepare")]
[switch]
$PrepareCredentials,
# Download credentials from within CloudFormation template
[Parameter(Position = 0,
Mandatory = $true,
ParameterSetName = "Download")]
[switch]
$DownloadCredentials,
# Access credentials from within CloudFormation template
[Parameter(Position = 0,
Mandatory = $true,
ParameterSetName = "Access")]
[String]
$AccessCredential,
# Path to store credentials
#
# - with -PrepareCredentials:
# eg. C:\Admin\Keys
#
# - with -DownloadCredentials & -AccessCredential (includes Credential dir):
# eg. C:\Admin\Keys\CF-Creds-20171018T0305398200
#
[Parameter(Mandatory = $true,
ParameterSetName = "Prepare")]
[Parameter(Mandatory = $true,
ParameterSetName = "Download")]
[Parameter(Mandatory = $true,
ParameterSetName = "Access")]
[String]
$CredentialPath,
# S3 Bucket to stage credentials
[Parameter(Mandatory = $true,
ParameterSetName = "Prepare")]
[Parameter(Mandatory = $true,
ParameterSetName = "Download")]
[String]
$S3Bucket,
# AWS Access Key to upload to S3
[Parameter(ParameterSetName = "Prepare")]
[String]
$AWSAccessKey,
# AWS Secret Key to upload to S3
[Parameter(ParameterSetName = "Prepare")]
[String]
$AWSSecretKey,
# AWS Region to upload to S3
[Parameter(ParameterSetName = "Prepare")]
[String]
$AWSRegion,
)
<##====================================================================================
GLOBAL CONFIGURATION
##===================================================================================#>
$erroractionpreference = "stop"
$debugFlag = $PSBoundParameters.Debug.IsPresent
$verboseFlag = $PSBoundParameters.Verbose.IsPresent
# Use this to control whether to break on debugs or not
if($debugFlag -eq $true) {
# If you want a break prompt on each Write-Debug entry, use "inquire"
# Otherwise use "continue" to simply output debug log (recommended)
$debugPreference = "continue"
}
# Output Log File Name: DescriptiveName_yyyy-MM-ddTHHmm.log
$outputLogFileName = "$scriptName_" + (get-date -f yyyy-MM-ddTHHmm) + ".log"
# Change Log File Directory as necessary
$rootDir = Split-Path -Path $CredentialPath -Parent
$outputLogDir = "$rootDir\CF-Creds-Logs"
# Full Path of Log File
$outputLogPath = "$outputLogDir\$outputLogFileName"
# Log Age Limit (in days). Log files older than this will be auto deleted
$logAgeLimit = 30
# This is where I put global variables and the like for easy access an updates
<##====================================================================================
FUNCTIONS
##===================================================================================#>
<#
.SYNOPSIS
Initializes the output log file.
.DESCRIPTION
Execute this function at the beginning of your main code to ensure the log file path
exists, and if it doesn't, create the folder paths to ensure future Write-Log calls
will run without issue
This function can also be modified to create log file headers
.PARAMETER overwrite
Set this flag to overwrite any existing log files. Otherwise default is to append
to the existing log file
.PARAMETER headerText
Optional ability to define some text at the start of eveyr initialization of the log file
Useful if you are not overwriting the log file each time script is run]
#>
function Start-LogFile([switch]$overwrite, $headerText) {
# Check if log dir exists, if not, create it
if(!(Test-Path $outputLogDir)) {
New-Item -type Directory $outputLogDir | out-null
}
Write-Output $headerText
try {
if($overwrite -eq $true) {
set-content $outputLogPath $headerText
} else {
add-content $outputLogPath $headerText
}
} catch {
Write-Warning "Could not initialize the log file: $outputLogPath"
}
}
<#
.SYNOPSIS
Writes to an entry into the output log file, and to the console if -Verbose is used
.DESCRIPTION
This is a useful function for log file outputs. Change the structure of this output
as necessary for your scripts.
If the -Verbose flag is set, then the log file message will also be written to the console
using the Write-Verbose command
.PARAMETER type
Optional, but can use to tag the log entry type. Recommend to use one of INFO,WARNING,ERROR
Will default to INFO if non specified.
NOTE: If you wish to write a DEBUG log entry, use the Write-DebugLog function
.PARAMETER message
The text to write to your ouput file
#>
function Write-Log($message, $type) {
if($type -eq $null -or $type -eq "") {
$type = "INFO"
}
try {
# Log Entry Structure: [Date] [TYPE] [MESSAGE]
$logEntry = (Get-Date -format u) + "`t" + $type.ToUpper() + "`t" + $message
if ($type -eq "WARNING") {
Write-Host -foregroundcolor Yellow $logEntry
} elseif ($type -eq "ERROR") {
Write-Host -foregroundcolor Red $logEntry
} else {
Write-Host $logEntry
}
Add-Content $outputLogPath $logEntry
} catch {
Write-Warning "Could not write entry to output log file: $outputLogPath `nLog Entry:$message"
}
}
<#
.SYNOPSIS
Writes to an entry ino the output log file only if the -debug parameter was set in the script
.DESCRIPTION
This is a useful function for performing debug logging. It will use both the in built Write-Debug
function as well as creating an entry to the log file with a DEBUG type
.PARAMETER message
The debug text to write to your ouput file
#>
function Write-DebugLog($message) {
Write-Debug $message
try {
# Only write to the log file if the -Debug parameter has been set
if($script:debugFlag -eq $true) {
# Log Entry Structure: [Date] [TYPE] [MESSAGE]
$logEntry = (Get-Date -format u) + "`t" + "DEBUG" + "`t" + $message
Add-Content $outputLogPath $logEntry
}
} catch {
Write-Warning "Could not write entry to output log file: $outputLogPath `nLog Entry:$message"
}
}
<#
.SYNOPSIS
Cleans up log files
.DESCRIPTION
This is a useful function when scripts are run regularly and thus create lots of log files.
Based on a log age date, it will remove all log files older than that period
Uses a Global variable for the log age date
.PARAMETER logPath
Folder location for log files to remove
.PARAMETER fileExtension
Type of files to delete. Use a wildcard format like "*.log"
#>
function Cleanup-LogFiles($logPath, $fileExtension) {
# Determine the date of which files older than specific period will be deleted
$dateToDelete = (Get-Date).AddDays(-$logAgeLimit)
$filesToDelete = Get-ChildItem $logPath -Include $fileExtension -Recurse | where { $_.LastWriteTime -le $dateToDelete }
foreach ($file in $filesToDelete) {
$filePath = $file.FullName
try {
Write-DebugLog "Deleting $file..."
Remove-Item $filePath -force | out-null
} catch {
Write-Log "Failed to delete old log file ($filePath)" -type WARNING
}
}
}
<#
.SYNOPSIS
Uploads files to S3 Bucket
.DESCRIPTION
This function uses the EC2 Instance-attached IAM Role to upload files to AWS S3 storage.
.PARAMETER uploadPath
Directory/file location to upload
#>
function Upload-FilesToS3($uploadPath) {
# Determine if AWS Tools for PowerShell are installed
if (!(Get-AWSPowerShellVersion)) {
Write-Log "AWS Tools for PowerShell not installed. Installing from PowerShell Gallery for current user.." -type INFO
# test if AWS Access Key, AWS Secret Key and AWS Region were provided
if (!($AWSAccessKey -and $AWSSecretKey -and $AWSRegion)) {
Write-Log "Failed to Install AWS Tools for PowerShell. Re-run with -AWSAccessKey, -AWSSecretKey and -AWSRegion set." -type ERROR
Write-Host -foreground red "Failed to Install AWS Tools for PowerShell. Re-run with -AWSAccessKey, -AWSSecretKey and -AWSRegion set."
Exit -1
}
try {
Set-PSRepository -InstallationPolicy Trusted -Name "PSGallery"
Install-Package -Name AWSPowerShell -Scope CurrentUser
Set-AWSCredentials -StoreAs CF-Creds -AccessKey $AWSAccessKey -SecretKey $AWSSecretKey
Initialize-AWSDefaults -ProfileName 'CF-Creds' -Region $AWSRegion
} catch {
$errText = $error[0]
Write-Log "Failed to Install AWS Tools for PowerShell. Error Message was: $errText" -type ERROR
Write-Host -foreground red "Failed to Install AWS Tools for PowerShell. Please check Log File."
Exit -1
}
}
try {
# Upload credential directory to AWS S3 Bucket and encrypt using AWS managed keys
$folderName = Split-Path -Path $uploadPath -Leaf
Write-S3Object -BucketName $S3Bucket -Folder $uploadPath -KeyPrefix $folderName -Recurse -CannedACLName private -ServerSideEncryption AES256
} catch {
$errText = $error[0]
Write-Log "Failed to Upload Credentials to AWS S3 Bucket. Error Message was: $errText" -type ERROR
Write-Host -foreground red "Failed to Upload to AWS S3. Please check Log File."
Exit -1
}
Write-Log "Credentials successfully uploaded to AWS S3 Bucket." -Type INFO
Write-Host -foreground Green "Credentials uploaded to AWS S3."
}
<#
.SYNOPSIS
Downloads files from S3 Bucket
.DESCRIPTION
This function uses the EC2 Instance-attached IAM Role to download files from AWS S3 storage.
#>
function Download-FilesFromS3() {
# Determine if AWS Tools for PowerShell are installed
if (!(Get-AWSPowerShellVersion)) {
Write-Log "AWS Tools for PowerShell not installed. Installing from PowerShell Gallery for current user.." -type INFO
try {
Set-PSRepository -InstallationPolicy Trusted -Name "PSGallery"
Install-Package -Name AWSPowerShell -Scope CurrentUser
} catch {
$errText = $error[0]
Write-Log "Failed to Install AWS Tools for PowerShell. Error Message was: $errText" -type ERROR
Write-Host -foreground red "Failed to Install AWS Tools for PowerShell. Please check Log File."
Exit -1
}
}
try {
# Download credential directory to AWS S3 Bucket and encrypt using AWS managed keys
$folderName = Split-Path -Path $CredentialPath -Leaf
Read-S3Object -BucketName $S3Bucket -KeyPrefix $folderName -Folder $CredentialPath
} catch {
$errText = $error[0]
Write-Log "Failed to Download Credentials from AWS S3 Bucket. Error Message was: $errText" -type ERROR
Write-Host -foreground red "Failed to Download from AWS S3. Please check Log File."
Exit -1
}
Write-Log "Credentials successfully downloaded from AWS S3 Bucket." -Type INFO
Write-Host -foreground Green "Credentials downloaded from AWS S3."
}
<##====================================================================================
MAIN CODE
##===================================================================================#>
if ($PrepareCredentials -eq $true) {
####
# Generates the secure files and uploads to AWS S3.
####
try {
$headerText = (Get-Date -format u) + "`t" + "INIT" + "`t" + "****$scriptName log initialised in PrepareCredentials mode.****"
Start-LogFile -headerText $headerText # Initialize the log file with a header
# This will be used in file paths below, so avoid using spaces and special characters
$ScriptPath = "$PSCommandPath"
# Datestamp
$DateStamp = Get-Date -Format FileDateTime
# Initialise Credential Store
$CredentialDir = "$CredentialPath\CF-Creds-$DateStamp"
$CredentialKey = "$CredentialDir\AES.key"
$CredentialStore = "$CredentialDir\SecureStore.txt"
mkdir -p $CredentialDir
New-Item -Path $CredentialKey -ItemType "file"
New-Item -Path $CredentialStore -ItemType "file"
Copy-Item -Path $ScriptPath -Destination $CredentialDir
# Secure Credential Store
# - restrict access to current user (chmod 700 dir)
$ScriptUser = "$env:userdomain\$env:username"
Invoke-Expression -Command:"icacls $CredentialDir /grant:r ${ScriptUser}:F /T"
Invoke-Expression -Command:"icacls $CredentialDir /inheritance:r /T"
# Generate a random AES Encryption Key.
$AESKey = New-Object Byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($AESKey)
Add-Content $CredentialKey $AESKey
Write-Log "Collecting Credentials to create a secure credential file..." -Type INFO
# Display credential prompt until closed by user
do {
$creds = $Host.UI.PromptForCredential('CloudFormation PowerShell Credentials',
'Close window to finish collecting credentials.',
'', '')
# exit credentials loop if prompt is closed
if (!$creds) {
break
}
# skip blank credentials
if ((!$creds.UserName) -or (!$creds.Password)) {
Write-Log "Blank Credentials Entered. Skipping.." -type WARNING
Write-Host -foreground yellow "Skipping Missing Credentials.."
continue
}
# Store the details in a hashed format
$userName = $creds.UserName
$passwordSecureString = $creds.Password
$password = $passwordSecureString | ConvertFrom-SecureString -Key $AESKey
Add-Content $CredentialStore "${userName}:${password}"
} while ($creds -ne $null)
Write-Log "Credentials collected and stored." -Type INFO
Write-Host -foreground Green "Credentials collected and stored."
Write-Log "Upload Credentials to AWS S3 Bucket" -Type INFO
Write-Host -foreground Yellow "Uploading Credentials to AWS S3"
Upload-FilesToS3 $CredentialDir
} catch {
$errText = $error[0]
Write-Log "Failed to Prepare Credentials. Error Message was: $errText" -type ERROR
Write-Host -foreground red "Failed to Prepare Credentials. Please check Log File."
Exit -1
}
} elseif ($DownloadCredentials -eq $true) {
####
# Downloads secure files from AWS S3 into secured folder
####
$headerText = (Get-Date -format u) + "`t" + "INIT" + "`t" + "****$scriptName log initialised in AccessCredentials mode****"
Start-LogFile -headerText $headerText # Initialize the log file with a header
# Initialise/Secure Credential Directory
$CredentialDir = $CredentialPath
mkdir -p $CredentialDir
$ScriptUser = "$env:userdomain\$env:username"
Invoke-Expression -Command:"icacls $CredentialDir /grant:r ${ScriptUser}:F /T"
Invoke-Expression -Command:"icacls $CredentialDir /inheritance:r /T"
# Download secure files from AWS S3
Download-FilesFromS3
# Secure downloaded files in credential directory
Invoke-Expression -Command:"icacls $CredentialDir /grant:r ${ScriptUser}:F /T"
Invoke-Expression -Command:"icacls $CredentialDir /inheritance:r /T"
} elseif ($AccessCredential -ne $null) {
####
# Retrieves password from secured folder as a SecureString
####
# Check to ensure we have a secure credential directory (i.e. -DownloadCredentials has been run) and that the contents are valid
if(!(Test-Path $CredentialPath)) {
Write-Log "Could not find a secure credential directory at $CredentialPath. Exiting." -Type ERROR
Write-Host -foreground red "[ERROR] Could not find a secure credential directory at $CredentialPath. Ensure that you have run the -DownloadCredentials parameter at least once for this script."
exit -1
}
$CredentialFilePath = "$CredentialPath\SecureStore.txt"
$AESKeyFilePath = "$CredentialPath\AES.key"
try {
Write-DebugLog "Reading secure credential file at $CredentialFilePath."
$AESKey = Get-Content $AESKeyFilePath
$CredLine = Select-String -Path $CredentialFilePath -Pattern $AccessCredential | %{ $_.ToString() }
$UserName = $CredLine.Split(':')[3]
$Password = $CredLine.Split(':')[4] | ConvertTo-SecureString -Key $AESKey
Write-DebugLog "Creating credential object..."
$CredObject = New-Object System.Management.Automation.PSCredential -ArgumentList $UserName, $Password
Write-DebugLog "Credential store successfully read for username: $UserName"
return $CredObject
} catch {
$errText = $error[0]
Write-Log "Could not execute in AccessCredential mode. Error Message was: $errText" -type ERROR
exit -1
}
}
<##====================================================================================
END OF CODE
##===================================================================================#>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment