├── PowerVault ├── PowerVault.psd1 └── PowerVault.psm1 ├── LICENSE ├── README.md ├── .gitignore └── Tests └── PowerVault.Tests.ps1 /PowerVault/PowerVault.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdhunt/PowerVault/HEAD/PowerVault/PowerVault.psd1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chris Hunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerVault 2 | PowerShell Client for HashiCorp Vault 3 | 4 | This is a PowerShell client for [HashiCorp Vault](https://www.vaultproject.io/). It interfaces with the HTTP API and does not require vault.exe. 5 | 6 | The HTTP API is not stable, so this module is not stable. 7 | 8 | The module only contains basic CRUD functionality. 9 | 10 | ## Examples 11 | 12 | ```powershell 13 | # Create and Read 14 | PS C:\> $vault = Get-Vault -Address 127.0.0.1 -Token 46e231ee-49bb-189d-c58d-f276743ececa 15 | 16 | PS C:\> Set-Secret -VaultObject $vault -Path secret/new -Secret @{value="secret"} 17 | 18 | PS C:\> Get-Secret $vault secret/new 19 | 20 | value 21 | ----- 22 | secret 23 | ``` 24 | 25 | ```powershell 26 | # Retun the Secret as a [PSCredential] 27 | PS C:\> Set-Secret -VaultObject $vault -Path secret/username -Secret @{password="P@55w0rd"} 28 | 29 | PS C:\> Get-Secret -VaultObject $vault -Path secret/username -AsCredential 30 | 31 | UserName Password 32 | -------- -------- 33 | username System.Security.SecureString 34 | ``` 35 | 36 | ## Testing 37 | The tests depend on Vault.exe to launch a local Dev server. The script will pull down the binaries from HashiCorp to TestDrive:\ and start up a Vault.exe process. All of the files should be cleaned up when the tests complete. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | 198 | *.zip 199 | -------------------------------------------------------------------------------- /Tests/PowerVault.Tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\PowerVault\PowerVault.psd1 -Force 2 | 3 | if (-not (Test-Path -Path "$PSScriptRoot\Vault.exe") ) { 4 | throw "You need a copy of Vault.exe in $PSScriptRoot" 5 | } 6 | 7 | Describe 'API Compatability' { 8 | 9 | $process = Start-Process -FilePath "$PSScriptRoot\Vault.exe" -ArgumentList @('server','-dev') -RedirectStandardOutput TestDrive:\stdout.txt -WindowStyle Hidden -PassThru 10 | 11 | Start-Sleep -Seconds 2 12 | 13 | try 14 | { 15 | 16 | $addr = (Get-Content -Path TestDrive:\stdout.txt | Select-String -SimpleMatch VAULT_ADDR | Select-Object -ExpandProperty Line -First 1).Split('=')[1].Trim("'") 17 | $token = (Get-Content -Path TestDrive:\stdout.txt | Select-String -SimpleMatch "Root Token:" | Select-Object -ExpandProperty Line -First 1).Split(':')[1].Trim() 18 | 19 | $env:VAULT_ADDR = $addr 20 | $env:VAULT_TOKEN = $token 21 | 22 | # Pre-populate with some known secrets 23 | & "$PSScriptRoot\Vault.exe" write secret/hello value=world 24 | & "$PSScriptRoot\Vault.exe" write secret/delete value=me 25 | & "$PSScriptRoot\Vault.exe" write secret/update value=was 26 | & "$PSScriptRoot\Vault.exe" write secret/testwithusername username=usernametest password=p@55w0rd 27 | & "$PSScriptRoot\Vault.exe" write secret/testwithoutusername password=p@55w0rd 28 | 29 | $vault = Get-Vault 30 | 31 | Context 'Test Get-Vault Parameters' { 32 | 33 | It 'Should work with $env' { 34 | $result = Get-Vault 35 | 36 | $result.uri | Should BeExactly "$addr/v1/" 37 | $result.auth_header.'X-Vault-Token' | Should BeExactly $token 38 | } 39 | 40 | It 'Should work with parametres' { 41 | $result = Get-Vault -Address http://127.0.0.1:8300 -Token abc-123 42 | 43 | $result.uri | Should BeExactly 'http://127.0.0.1:8300/v1/' 44 | $result.auth_header.'X-Vault-Token' | Should BeExactly 'abc-123' 45 | } 46 | } 47 | 48 | 49 | Context 'Verify Server Started' { 50 | $result = Test-Vault $vault 51 | 52 | It 'Should be Initialized' { 53 | $result.initialized | Should Be $true 54 | } 55 | It "Should not be Sealed" { 56 | $result.sealed | Should Be $false 57 | } 58 | } 59 | 60 | Context 'Create' { 61 | Set-Secret $vault secret/new @{value='secret'} 62 | 63 | $json = & "$PSScriptRoot\Vault.exe" --% read -format=json secret/new 64 | $result = $json | ConvertFrom-Json 65 | 66 | It 'Should contain a new secret' { 67 | $result | Should Not BeNullOrEmpty 68 | $result.data.value | Should BeExactly 'secret' 69 | } 70 | } 71 | 72 | Context "Read" { 73 | 74 | $result = Get-Secret $vault secret/hello 75 | 76 | It "Should have a 'value' property" { 77 | $output = $result | Get-Member -Name value 78 | 79 | $output | Should Not BeNullOrEmpty 80 | } 81 | 82 | It 'Should return hello world' { 83 | 84 | $result.value | Should BeExactly 'world' 85 | } 86 | } 87 | 88 | Context 'Update' { 89 | Set-Secret $vault secret/update @{value='now'} 90 | 91 | $json = & "$PSScriptRoot\Vault.exe" --% read -format=json secret/update 92 | $result = $json | ConvertFrom-Json 93 | 94 | It "Should contain a new secret" { 95 | $result | Should Not BeNullOrEmpty 96 | $result.data.value | Should BeExactly 'now' 97 | } 98 | 99 | } 100 | 101 | Context 'Delete' { 102 | Remove-Secret $vault secret/delete 103 | 104 | $json = & "$PSScriptRoot\Vault.exe" 'read', '-format=json', 'secret/delete' 2> TestDrive:\stderr.txt 105 | 106 | It 'Should be not contain the secret' { 107 | $json | Should BeNullOrEmpty 108 | } 109 | 110 | } 111 | 112 | Context 'Secret with username property' { 113 | 114 | $result = Get-Secret $vault -Path secret/testwithusername -AsCredential 115 | 116 | It 'Should be a PSCredential' { 117 | $result.GetType().Name | Should Be 'PSCredential' 118 | } 119 | 120 | It "Should contain a username property that equals 'usernametest'" { 121 | $result.UserName | Should BeExactly 'usernametest' 122 | } 123 | } 124 | 125 | Context 'Secret without username property' { 126 | 127 | $result = Get-Secret $vault -Path secret/testwithoutusername -AsCredential 128 | 129 | It 'Should be a PSCredential' { 130 | $result.GetType().Name | Should Be 'PSCredential' 131 | } 132 | 133 | It "Should contain a username property that equals 'testwithoutusername'" { 134 | $result.UserName | Should BeExactly 'testwithoutusername' 135 | } 136 | } 137 | 138 | Context 'Secret does not exist' { 139 | 140 | It 'Should throw an exception for missing Path' { 141 | 142 | { Get-Secret $vault -Path secret/nohello } | Should Throw 143 | 144 | } 145 | } 146 | } 147 | catch { 148 | Write-Host "Error occurred:`n`n`n$($_ | Out-String)`n`n`n" 149 | 150 | } 151 | finally { 152 | $process.Kill() 153 | Start-Sleep -Seconds 2 154 | } 155 | 156 | } 157 | 158 | Remove-Module PowerVault -------------------------------------------------------------------------------- /PowerVault/PowerVault.psm1: -------------------------------------------------------------------------------- 1 | $prefix = '/v1/' 2 | 3 | <# 4 | .Synopsis 5 | Return an Object containing Vault connection details. 6 | .DESCRIPTION 7 | This is session variable required by all other Cmdlets. 8 | .EXAMPLE 9 | PS C:\> $vault = Get-Vault -Address 127.0.0.1 -Token 46e231ee-49bb-189d-c58d-f276743ececa 10 | #> 11 | function Get-Vault 12 | { 13 | [CmdletBinding()] 14 | [Alias()] 15 | [OutputType([PSCustomObject])] 16 | Param 17 | ( 18 | # Server Address 19 | [Parameter(Position=0)] 20 | [String] 21 | $Address = $env:VAULT_ADDR, 22 | 23 | # Client token 24 | [Parameter(Position=1)] 25 | [String] 26 | $Token = $env:VAULT_TOKEN 27 | ) 28 | 29 | 30 | [PSCustomObject]@{'uri'= $Address + $prefix 31 | 'auth_header' = @{'X-Vault-Token'=$Token} 32 | } | 33 | Write-Output 34 | 35 | } 36 | 37 | <# 38 | .Synopsis 39 | Test connectivity to the Vault server. 40 | .DESCRIPTION 41 | This method returns the health of the server if it can connect. 42 | .EXAMPLE 43 | PS C:\> Test-Vault $vault 44 | 45 | initialized sealed standby 46 | ----------- ------ ------- 47 | True False False 48 | #> 49 | function Test-Vault 50 | { 51 | [CmdletBinding()] 52 | [Alias()] 53 | [OutputType([Object])] 54 | Param 55 | ( 56 | # The Object containing Vault access details. 57 | [Parameter(Mandatory, Position=0)] 58 | [PSCustomObject] 59 | $VaultObject 60 | 61 | ) 62 | 63 | $uri = $VaultObject.uri + 'sys/health' 64 | 65 | Write-Debug $uri 66 | 67 | Invoke-RestMethod -Uri $uri -Headers $VaultObject.auth_header | Write-Output 68 | } 69 | 70 | <# 71 | .Synopsis 72 | Access a Secret in the Vault 73 | .DESCRIPTION 74 | This can return a [PSCustomObject] base don the raw Secret or attempt to return a [PSCredential] object. 75 | .EXAMPLE 76 | PS C:\> Get-Secret -VaultObject $vault -Path secret/hello 77 | 78 | value 79 | ----- 80 | world 81 | .EXAMPLE 82 | PS C:\> Get-Secret -VaultObject $vault -Path secret/username -AsCredential 83 | 84 | UserName Password 85 | -------- -------- 86 | username System.Security.SecureString 87 | .LINK 88 | https://www.vaultproject.io/docs/http/index.html 89 | Get-Vault 90 | .NOTES 91 | At the current version, Vault does not yet promise backwards compatibility even with the v1 prefix. We'll remove this warning when this policy changes. We expect we'll reach API stability by Vault 0.3. 92 | #> 93 | function Get-Secret 94 | { 95 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] 96 | [CmdletBinding()] 97 | [Alias()] 98 | [OutputType([Object])] 99 | Param 100 | ( 101 | # The Object containing Vault access details. 102 | [Parameter(Mandatory, Position=0)] 103 | [PSCustomObject] 104 | $VaultObject, 105 | 106 | # The Path to the secret as you would pass to Vault Read. 107 | [Parameter(Mandatory, Position=1)] 108 | [String] 109 | $Path, 110 | 111 | # Attempt to convert the Secret to a [PSCredential]. If the Secret contains a username property that will be used else the function will fall back to using the Secret name. 112 | [Parameter()] 113 | [switch] 114 | $AsCredential 115 | ) 116 | 117 | $uri = $VaultObject.uri + $Path + '/?list=true' 118 | 119 | $result = [string]::Empty 120 | 121 | Write-Debug $uri 122 | 123 | try 124 | { 125 | $result = Invoke-RestMethod -Uri $uri -Headers $VaultObject.auth_header 126 | } 127 | catch 128 | { 129 | Throw ("Failed to get secret from " + $uri) 130 | } 131 | 132 | if ($result) 133 | { 134 | if ($result.GetType().Name -eq 'PSCustomObject') 135 | { 136 | if ($result | Get-Member -Name data) 137 | { 138 | $data = $result | Select-Object -ExpandProperty data 139 | 140 | if ($AsCredential) 141 | { 142 | $username = [string]::Empty 143 | 144 | if ($data | Get-Member -Name username) 145 | { 146 | $username = $data.username 147 | Write-Verbose "Found a username property in the results. [$username]" 148 | } 149 | else 150 | { 151 | $username = $Path.Split('/')[-1] 152 | Write-Verbose "Did not find a username property, parsing path. [$username]" 153 | } 154 | 155 | if ($data | Get-Member -Name password) 156 | { 157 | New-Object -TypeName System.Management.Automation.PSCredential ` 158 | -ArgumentList $username, ($data.password | ConvertTo-SecureString -AsPlainText -Force) 159 | } 160 | else 161 | { 162 | Write-Debug $result 163 | throw "The data did not contain a password property." 164 | } 165 | } 166 | else 167 | { 168 | Write-Output -InputObject $data 169 | } 170 | } 171 | } 172 | else 173 | { 174 | throw $result 175 | } 176 | } 177 | else 178 | { 179 | Write-Debug $result 180 | Write-Verbose "No Secret found. [$Path]" 181 | } 182 | 183 | } 184 | 185 | <# 186 | .Synopsis 187 | Create or update a Secret 188 | .DESCRIPTION 189 | This will set the contents of a Secret. 190 | .EXAMPLE 191 | PS C:\> Set-Secret -VaultObject $vault -Path secret/new -Secret @{value="secret"} 192 | 193 | PS C:\> Get-Secret $vault secret/new 194 | 195 | value 196 | ----- 197 | secret 198 | #> 199 | function Set-Secret 200 | { 201 | [CmdletBinding()] 202 | [Alias()] 203 | Param 204 | ( 205 | # The Object containing Vault access details. 206 | [Parameter(Mandatory, Position=0)] 207 | [PSCustomObject] 208 | $VaultObject, 209 | 210 | # The Path to the Secret as you would pass to Vault Read. 211 | [Parameter(Mandatory, Position=1)] 212 | [String] 213 | $Path, 214 | 215 | # The Secret. This will be converted to JSON. A simple Hash works best. 216 | [Parameter(Mandatory, Position=2)] 217 | [Object] 218 | $Secret 219 | ) 220 | 221 | $uri = $VaultObject.uri + $Path 222 | 223 | Write-Debug $uri 224 | 225 | try 226 | { 227 | $data = $Secret | ConvertTo-Json 228 | 229 | Write-Debug $data 230 | } 231 | catch 232 | { 233 | throw "Cannot convert Secret to JSON" 234 | } 235 | 236 | Invoke-RestMethod -Uri $uri -Method Post -Headers $VaultObject.auth_header -Body $data | Write-Output 237 | 238 | } 239 | 240 | 241 | <# 242 | .Synopsis 243 | Delete a Secret 244 | .DESCRIPTION 245 | This will set the delete of a Secret. 246 | .EXAMPLE 247 | PS C:\> Remove-Secret $vault secret/new 248 | 249 | PS C:\> Get-Secret $vault secret/new -Verbose 250 | 251 | VERBOSE: No Secret found. [secret/new] 252 | #> 253 | function Remove-Secret 254 | { 255 | [CmdletBinding()] 256 | [Alias()] 257 | Param 258 | ( 259 | # The Object containing Vault access details. 260 | [Parameter(Mandatory, Position=0)] 261 | [PSCustomObject] 262 | $VaultObject, 263 | 264 | # The Path to the Secret as you would pass to Vault Read. 265 | [Parameter(Mandatory, Position=1)] 266 | [String] 267 | $Path 268 | ) 269 | 270 | $uri = $VaultObject.uri + $Path 271 | 272 | Write-Debug $uri 273 | 274 | 275 | Invoke-RestMethod -Uri $uri -Method Delete -Headers $VaultObject.auth_header| Write-Output 276 | 277 | } 278 | --------------------------------------------------------------------------------