diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 43e1a1c97..05c116f2d 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -158,6 +158,8 @@ function humanize(categoryId) { return "1Password Connect"; case "amazon-chime": return "Amazon Chime"; + case "akeyless": + return "Akeyless"; case "ansible": return "Ansible"; case "apexsql": diff --git a/step-templates/akeyless-access-key-login.json b/step-templates/akeyless-access-key-login.json new file mode 100644 index 000000000..ff22e899a --- /dev/null +++ b/step-templates/akeyless-access-key-login.json @@ -0,0 +1,54 @@ +{ + "Id": "f8e3a1b2-4c5d-6e7f-8a9b-0c1d2e3f4a01", + "Name": "Akeyless - Access Key Login", + "Description": "This step authenticates to [Akeyless](https://www.akeyless.io) using an Access ID and Access Key.\n\nThe API token from the response is stored as a sensitive [output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) named AkeylessAuthToken for use in other step templates.\n\nThis step template uses the [Akeyless REST API](https://docs.akeyless.io/reference/auth), so no other dependencies are needed.\n\n---\n\n**Required:**\n- Akeyless Access ID\n- Akeyless Access Key\n\n**Optional:**\n- Gateway/API URL (default: https://api.akeyless.io)\n\n**Notes:**\n- Tested on PowerShell Desktop and PowerShell Core.\n- Pair this step with **Akeyless - Retrieve Static Secrets** or **Akeyless - Retrieve Dynamic Secret**.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nfunction Get-AkeylessApiErrorBody {\n param ($RequestError)\n\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $rawResponse = $reader.ReadToEnd()\n try { return ($rawResponse | ConvertFrom-Json) } catch { return $rawResponse }\n }\n return $null\n }\n\n return $RequestError.ErrorDetails.Message\n}\n\nfunction Format-AkeylessApiError {\n param (\n [string]$Action,\n [System.Management.Automation.ErrorRecord]$ErrorRecord\n )\n\n $message = \"An error occurred during $Action`: $($ErrorRecord.Exception.Message)\"\n $body = Get-AkeylessApiErrorBody -RequestError $ErrorRecord\n if ($null -ne $body) {\n if ($body.error) {\n $message += \"`n`tDetail: $($body.error)\"\n }\n elseif ($body.PSObject.Properties.Name -contains 'errors') {\n $message += \"`n`tDetail: $($body.errors -Join ',')\"\n }\n elseif ($body -is [string] -and -not [string]::IsNullOrWhiteSpace($body)) {\n $message += \"`n`tDetail: $body\"\n }\n }\n\n return $message\n}\n\nfunction Invoke-AkeylessApi {\n param (\n [Parameter(Mandatory = $true)][string]$GatewayUrl,\n [Parameter(Mandatory = $true)][string]$Path,\n [Parameter(Mandatory = $true)][hashtable]$Body\n )\n\n $base = $GatewayUrl.TrimEnd('/')\n $apiPath = $Path.TrimStart('/')\n $uri = \"$base/$apiPath\"\n $json = $Body | ConvertTo-Json -Depth 20 -Compress:$false\n\n try {\n return Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType 'application/json'\n }\n catch {\n $detail = Format-AkeylessApiError -Action \"POST $Path\" -ErrorRecord $_\n Write-Error $detail -Category ConnectionError\n }\n}\n\nfunction Normalize-AkeylessPath {\n param ([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) {\n return $Path\n }\n\n $normalized = $Path.Trim()\n if (-not $normalized.StartsWith('/')) {\n $normalized = \"/$normalized\"\n }\n\n return $normalized\n}\n\nfunction ConvertTo-AkeylessOutputVariableName {\n param (\n [string]$SecretPath,\n [string]$FieldName = '',\n [string]$OverrideName = ''\n )\n\n if (-not [string]::IsNullOrWhiteSpace($OverrideName)) {\n return $OverrideName.Trim()\n }\n\n $base = (Normalize-AkeylessPath $SecretPath).Trim('/').Replace('/', '.')\n if ([string]::IsNullOrWhiteSpace($FieldName)) {\n return $base\n }\n\n return \"$base.$($FieldName.Trim())\"\n}\n\nfunction Get-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [int]$Version = 0\n )\n\n $path = Normalize-AkeylessPath $SecretPath\n $body = @{\n token = $Token\n names = @($path)\n }\n\n if ($Version -gt 0) {\n $body.version = $Version\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'get-secret-value' -Body $body\n if ($null -eq $response) {\n throw \"Empty response retrieving secret '$path'\"\n }\n\n if ($response.PSObject.Properties.Name -contains $path) {\n return $response.$path\n }\n\n foreach ($property in $response.PSObject.Properties) {\n if (-not [string]::IsNullOrWhiteSpace([string]$property.Value)) {\n return $property.Value\n }\n }\n\n throw \"Secret '$path' was not found in the API response\"\n}\n\nfunction Set-AkeylessSensitiveOutput {\n param (\n [string]$Name,\n [string]$Value,\n [string]$StepName,\n [bool]$PrintVariableNames\n )\n\n if ([string]::IsNullOrWhiteSpace($Name)) {\n throw 'Output variable name cannot be empty'\n }\n\n Set-OctopusVariable -Name $Name -Value $Value -Sensitive\n if ($PrintVariableNames) {\n Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$Name}\"\n }\n}\n\nfunction Publish-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @(),\n [int]$Version = 0\n )\n\n $raw = Get-AkeylessSecretValue -GatewayUrl $GatewayUrl -Token $Token -SecretPath $SecretPath -Version $Version\n $created = 0\n $fieldsSpecified = ($Fields.Count -gt 0)\n\n if ($raw -is [string]) {\n $trimmed = $raw.Trim()\n if ($fieldsSpecified) {\n $parsed = $null\n try { $parsed = $trimmed | ConvertFrom-Json } catch {}\n if ($null -eq $parsed) {\n throw \"Secret '$SecretPath' is not JSON but field names were specified\"\n }\n $raw = $parsed\n }\n }\n\n if ($raw -is [pscustomobject] -or $raw -is [hashtable]) {\n $properties = if ($raw -is [hashtable]) { $raw.Keys } else { $raw.PSObject.Properties.Name }\n\n if ($fieldsSpecified) {\n foreach ($field in $Fields) {\n $fieldName = $field.Name\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n foreach ($fieldName in $properties) {\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n\n return $created\n }\n\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$raw) -StepName $StepName -PrintVariableNames $PrintVariableNames\n return 1\n}\n\nfunction Invoke-AkeylessListItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @()\n )\n\n $path = Normalize-AkeylessPath $FolderPath\n $items = @()\n $folders = @()\n $paginationToken = ''\n\n do {\n $body = @{\n token = $Token\n path = $path\n 'current-folder' = $true\n }\n\n if ($Types.Count -gt 0) {\n $body.type = $Types\n }\n if (-not [string]::IsNullOrWhiteSpace($paginationToken)) {\n $body.'pagination-token' = $paginationToken\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'list-items' -Body $body\n if ($null -eq $response) {\n break\n }\n\n if ($null -ne $response.items) {\n $items += @($response.items | ForEach-Object { $_.item_name })\n }\n if ($null -ne $response.folders) {\n $folders += @($response.folders)\n }\n\n $paginationToken = ''\n if ($response.PSObject.Properties.Name -contains 'next_page') {\n $paginationToken = [string]$response.next_page\n }\n } while (-not [string]::IsNullOrWhiteSpace($paginationToken))\n\n return [pscustomobject]@{\n Items = $items\n Folders = $folders\n }\n}\n\nfunction Get-AkeylessFolderItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @('static-secret')\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath -Types $Types\n return @($listing.Items)\n}\n\nfunction Get-AkeylessFolderChildren {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath\n return @($listing.Folders)\n}\n\nfunction Parse-AkeylessFieldDefinitions {\n param ([string]$RawValue)\n\n $fields = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $fields\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $name = $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($name)) {\n throw \"Unable to establish field name from: '$_'\"\n }\n $fields += [pscustomobject]@{\n Name = $name\n VariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $fields\n}\n\nfunction Parse-AkeylessSecretDefinitions {\n param ([string]$RawValue)\n\n $secrets = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $secrets\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $path = Normalize-AkeylessPath $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($path)) {\n throw \"Unable to establish secret path from: '$_'\"\n }\n $secrets += [pscustomobject]@{\n Path = $path\n OutputVariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $secrets\n}\n\nfunction Get-AkeylessSecretsRecursively {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [bool]$Recursive\n )\n\n $results = @()\n $folder = Normalize-AkeylessPath $FolderPath\n\n $items = Get-AkeylessFolderItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($item in $items) {\n $results += $item\n }\n\n if ($Recursive) {\n $children = Get-AkeylessFolderChildren -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($child in $children) {\n $results += Get-AkeylessSecretsRecursively -GatewayUrl $GatewayUrl -Token $Token -FolderPath $child -Recursive $true\n }\n }\n\n return $results\n}\n\nfunction Complete-AkeylessLogin {\n param (\n [string]$GatewayUrl,\n [hashtable]$AuthBody,\n [string]$StepName\n )\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'auth' -Body $AuthBody\n if ($null -eq $response -or [string]::IsNullOrWhiteSpace($response.token)) {\n throw 'Authentication succeeded but no token was returned'\n }\n\n Set-AkeylessSensitiveOutput -Name 'AkeylessAuthToken' -Value $response.token -StepName $StepName -PrintVariableNames $true\n Write-Host 'Authenticated to Akeyless successfully'\n}\n\nfunction Publish-AkeylessStructuredSecretResponse {\n param (\n [object]$Response,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @()\n )\n\n $created = 0\n if ($Response -is [pscustomobject] -or $Response -is [hashtable]) {\n $properties = if ($Response -is [hashtable]) { @($Response.Keys) } else { @($Response.PSObject.Properties.Name) }\n if ($Fields.Count -gt 0) {\n foreach ($field in $Fields) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$field.Name] } else { $Response.$($field.Name) }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $field.Name -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n $useSingleOutputName = (-not [string]::IsNullOrWhiteSpace($OutputVariableName)) -and ($properties.Count -eq 1)\n foreach ($fieldName in $properties) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$fieldName] } else { $Response.$fieldName }\n if ($null -ne $fieldValue) {\n $overrideName = if ($useSingleOutputName) { $OutputVariableName } else { '' }\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $overrideName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n }\n else {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$Response) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created = 1\n }\n\n return $created\n}\n\nfunction Get-AwsHmacSha256Bytes {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n $hmac = New-Object System.Security.Cryptography.HMACSHA256 (, $Key)\n return $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Message))\n}\n\nfunction Get-AwsHmacSha256Hex {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n return ([BitConverter]::ToString((Get-AwsHmacSha256Bytes -Key $Key -Message $Message))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsSigningKey {\n param (\n [string]$SecretKey,\n [string]$DateStamp,\n [string]$Region,\n [string]$Service\n )\n\n $kSecret = [Text.Encoding]::UTF8.GetBytes(\"AWS4$SecretKey\")\n $kDate = Get-AwsHmacSha256Bytes -Key $kSecret -Message $DateStamp\n $kRegion = Get-AwsHmacSha256Bytes -Key $kDate -Message $Region\n $kService = Get-AwsHmacSha256Bytes -Key $kRegion -Message $Service\n return Get-AwsHmacSha256Bytes -Key $kService -Message 'aws4_request'\n}\n\nfunction Get-AwsSha256Hex {\n param ([string]$Text)\n\n $sha = [System.Security.Cryptography.SHA256]::Create()\n return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsCredentialChain {\n $accessKeyId = $env:AWS_ACCESS_KEY_ID\n $secretAccessKey = $env:AWS_SECRET_ACCESS_KEY\n $sessionToken = $env:AWS_SESSION_TOKEN\n\n if (-not [string]::IsNullOrWhiteSpace($accessKeyId) -and -not [string]::IsNullOrWhiteSpace($secretAccessKey)) {\n return [pscustomobject]@{\n AccessKeyId = $accessKeyId.Trim()\n SecretAccessKey = $secretAccessKey.Trim()\n SessionToken = if ([string]::IsNullOrWhiteSpace($sessionToken)) { '' } else { $sessionToken.Trim() }\n }\n }\n\n try {\n $imdsToken = Invoke-RestMethod -Method Put -Uri 'http://169.254.169.254/latest/api/token' -Headers @{ 'X-aws-ec2-metadata-token-ttl-seconds' = '21600' } -TimeoutSec 2\n $roleName = Invoke-RestMethod -Uri 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n $roleCreds = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$roleName\" -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n if ($null -eq $roleCreds -or [string]::IsNullOrWhiteSpace($roleCreds.AccessKeyId)) {\n throw 'EC2 instance metadata returned no IAM credentials'\n }\n\n return [pscustomobject]@{\n AccessKeyId = [string]$roleCreds.AccessKeyId\n SecretAccessKey = [string]$roleCreds.SecretAccessKey\n SessionToken = [string]$roleCreds.Token\n }\n }\n catch {\n throw \"AWS credentials were not found in environment variables or EC2 instance metadata: $($_.Exception.Message)\"\n }\n}\n\nfunction New-AwsIamCloudId {\n param (\n [string]$AccessKeyId,\n [string]$SecretAccessKey,\n [string]$SessionToken = '',\n [string]$Region = 'us-east-1',\n [string]$StsUrl = 'https://sts.amazonaws.com/'\n )\n\n $service = 'sts'\n $method = 'POST'\n $hostName = ([Uri]$StsUrl).Host\n $body = 'Action=GetCallerIdentity&Version=2011-06-15'\n $amzDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')\n $dateStamp = $amzDate.Substring(0, 8)\n $payloadHash = Get-AwsSha256Hex -Text $body\n\n $headers = [ordered]@{\n Host = $hostName\n 'Content-Type' = 'application/x-www-form-urlencoded; charset=utf-8'\n 'Content-Length' = [string]$body.Length\n 'X-Amz-Date' = $amzDate\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $headers['X-Amz-Security-Token'] = $SessionToken\n }\n\n $canonicalHeaders = ($headers.GetEnumerator() | ForEach-Object { \"$($_.Key.ToLowerInvariant()):$($_.Value)\" }) -join \"`n\"\n $signedHeaders = (($headers.Keys | ForEach-Object { $_.ToLowerInvariant() }) | Sort-Object) -join ';'\n $canonicalRequest = @(\n $method\n '/'\n ''\n \"$canonicalHeaders`n\"\n $signedHeaders\n $payloadHash\n ) -join \"`n\"\n\n $credentialScope = \"$dateStamp/$Region/$service/aws4_request\"\n $stringToSign = @(\n 'AWS4-HMAC-SHA256'\n $amzDate\n $credentialScope\n (Get-AwsSha256Hex -Text $canonicalRequest)\n ) -join \"`n\"\n\n $signingKey = Get-AwsSigningKey -SecretKey $SecretAccessKey -DateStamp $dateStamp -Region $Region -Service $service\n $signature = Get-AwsHmacSha256Hex -Key $signingKey -Message $stringToSign\n $authorization = \"AWS4-HMAC-SHA256 Credential=$AccessKeyId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature\"\n\n $requestHeaders = @{\n Authorization = @($authorization)\n 'Content-Length' = @([string]$body.Length)\n Host = @($hostName)\n 'Content-Type' = @('application/x-www-form-urlencoded; charset=utf-8')\n 'X-Amz-Date' = @($amzDate)\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $requestHeaders['X-Amz-Security-Token'] = @($SessionToken)\n }\n\n $payload = [ordered]@{\n sts_request_method = $method\n sts_request_url = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($StsUrl))\n sts_request_body = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body))\n sts_request_headers = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($requestHeaders | ConvertTo-Json -Compress)))\n }\n\n return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Compress)))\n}\n\nfunction Resolve-AwsIamCloudId {\n param (\n [string]$CloudId,\n [string]$Region,\n [string]$StsUrl\n )\n\n if (-not [string]::IsNullOrWhiteSpace($CloudId)) {\n return $CloudId.Trim()\n }\n\n $credentials = Get-AwsCredentialChain\n return New-AwsIamCloudId -AccessKeyId $credentials.AccessKeyId -SecretAccessKey $credentials.SecretAccessKey -SessionToken $credentials.SessionToken -Region $Region -StsUrl $StsUrl\n}\n\n$GATEWAY_URL = $OctopusParameters['Akeyless.Auth.AccessKey.GatewayUrl']\n$ACCESS_ID = $OctopusParameters['Akeyless.Auth.AccessKey.AccessId']\n$ACCESS_KEY = $OctopusParameters['Akeyless.Auth.AccessKey.AccessKey']\n$StepName = $OctopusParameters['Octopus.Step.Name']\n\nif ([string]::IsNullOrWhiteSpace($GATEWAY_URL)) {\n $GATEWAY_URL = 'https://api.akeyless.io'\n}\nif ([string]::IsNullOrWhiteSpace($ACCESS_ID)) {\n throw 'Required parameter Access ID not specified'\n}\nif ([string]::IsNullOrWhiteSpace($ACCESS_KEY)) {\n throw 'Required parameter Access Key not specified'\n}\n\n$body = @{\n 'access-id' = $ACCESS_ID\n 'access-key' = $ACCESS_KEY\n 'access-type' = 'access_key'\n}\n\nComplete-AkeylessLogin -GatewayUrl $GATEWAY_URL -AuthBody $body -StepName $StepName" + }, + "Parameters": [ + { + "HelpText": "The Akeyless API or Gateway URL. For SaaS, use https://api.akeyless.io.", + "Id": "10001000-0000-0000-0000-100010001001", + "Label": "Gateway URL", + "DefaultValue": "https://api.akeyless.io", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.AccessKey.GatewayUrl" + }, + { + "HelpText": "The Akeyless Access ID used for authentication.", + "Id": "10001000-0000-0000-0000-100010001002", + "Label": "Access ID", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.AccessKey.AccessId" + }, + { + "HelpText": "The Akeyless Access Key used for authentication.", + "Id": "10001000-0000-0000-0000-100010001003", + "Label": "Access Key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Name": "Akeyless.Auth.AccessKey.AccessKey" + } + ], + "LastModifiedBy": "akeyless-community", + "LastModifiedAt": "2026-06-17T04:32:01.568Z", + "$Meta": { + "ExportedAt": "2026-06-17T04:32:01.568Z", + "OctopusVersion": "2024.4.0", + "Type": "ActionTemplate" + }, + "Category": "akeyless" +} diff --git a/step-templates/akeyless-aws-iam-login.json b/step-templates/akeyless-aws-iam-login.json new file mode 100644 index 000000000..5d137743a --- /dev/null +++ b/step-templates/akeyless-aws-iam-login.json @@ -0,0 +1,74 @@ +{ + "Id": "f8e3a1b2-4c5d-6e7f-8a9b-0c1d2e3f4a05", + "Name": "Akeyless - AWS IAM Login", + "Description": "This step authenticates to [Akeyless](https://www.akeyless.io) using an AWS IAM auth method.\n\nThe API token from the response is stored as a sensitive [output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) named AkeylessAuthToken for use in other step templates.\n\n---\n\n**Cloud ID**\n\nIf **Cloud ID** is left blank, the step builds it automatically from:\n\n- AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and optional AWS_SESSION_TOKEN, or\n- the EC2 instance profile when running on an AWS worker or deployment target\n\nYou can also supply a pre-generated Cloud ID from \u0007keyless get-cloud-identity --cloud-provider aws_iam.\n\n---\n\n**Required:**\n- Akeyless Access ID for an AWS IAM auth method\n- AWS credentials or a supplied Cloud ID\n\n**Optional:**\n- AWS region used for STS signing (default us-east-1)\n- Custom STS endpoint URL", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nfunction Get-AkeylessApiErrorBody {\n param ($RequestError)\n\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $rawResponse = $reader.ReadToEnd()\n try { return ($rawResponse | ConvertFrom-Json) } catch { return $rawResponse }\n }\n return $null\n }\n\n return $RequestError.ErrorDetails.Message\n}\n\nfunction Format-AkeylessApiError {\n param (\n [string]$Action,\n [System.Management.Automation.ErrorRecord]$ErrorRecord\n )\n\n $message = \"An error occurred during $Action`: $($ErrorRecord.Exception.Message)\"\n $body = Get-AkeylessApiErrorBody -RequestError $ErrorRecord\n if ($null -ne $body) {\n if ($body.error) {\n $message += \"`n`tDetail: $($body.error)\"\n }\n elseif ($body.PSObject.Properties.Name -contains 'errors') {\n $message += \"`n`tDetail: $($body.errors -Join ',')\"\n }\n elseif ($body -is [string] -and -not [string]::IsNullOrWhiteSpace($body)) {\n $message += \"`n`tDetail: $body\"\n }\n }\n\n return $message\n}\n\nfunction Invoke-AkeylessApi {\n param (\n [Parameter(Mandatory = $true)][string]$GatewayUrl,\n [Parameter(Mandatory = $true)][string]$Path,\n [Parameter(Mandatory = $true)][hashtable]$Body\n )\n\n $base = $GatewayUrl.TrimEnd('/')\n $apiPath = $Path.TrimStart('/')\n $uri = \"$base/$apiPath\"\n $json = $Body | ConvertTo-Json -Depth 20 -Compress:$false\n\n try {\n return Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType 'application/json'\n }\n catch {\n $detail = Format-AkeylessApiError -Action \"POST $Path\" -ErrorRecord $_\n Write-Error $detail -Category ConnectionError\n }\n}\n\nfunction Normalize-AkeylessPath {\n param ([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) {\n return $Path\n }\n\n $normalized = $Path.Trim()\n if (-not $normalized.StartsWith('/')) {\n $normalized = \"/$normalized\"\n }\n\n return $normalized\n}\n\nfunction ConvertTo-AkeylessOutputVariableName {\n param (\n [string]$SecretPath,\n [string]$FieldName = '',\n [string]$OverrideName = ''\n )\n\n if (-not [string]::IsNullOrWhiteSpace($OverrideName)) {\n return $OverrideName.Trim()\n }\n\n $base = (Normalize-AkeylessPath $SecretPath).Trim('/').Replace('/', '.')\n if ([string]::IsNullOrWhiteSpace($FieldName)) {\n return $base\n }\n\n return \"$base.$($FieldName.Trim())\"\n}\n\nfunction Get-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [int]$Version = 0\n )\n\n $path = Normalize-AkeylessPath $SecretPath\n $body = @{\n token = $Token\n names = @($path)\n }\n\n if ($Version -gt 0) {\n $body.version = $Version\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'get-secret-value' -Body $body\n if ($null -eq $response) {\n throw \"Empty response retrieving secret '$path'\"\n }\n\n if ($response.PSObject.Properties.Name -contains $path) {\n return $response.$path\n }\n\n foreach ($property in $response.PSObject.Properties) {\n if (-not [string]::IsNullOrWhiteSpace([string]$property.Value)) {\n return $property.Value\n }\n }\n\n throw \"Secret '$path' was not found in the API response\"\n}\n\nfunction Set-AkeylessSensitiveOutput {\n param (\n [string]$Name,\n [string]$Value,\n [string]$StepName,\n [bool]$PrintVariableNames\n )\n\n if ([string]::IsNullOrWhiteSpace($Name)) {\n throw 'Output variable name cannot be empty'\n }\n\n Set-OctopusVariable -Name $Name -Value $Value -Sensitive\n if ($PrintVariableNames) {\n Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$Name}\"\n }\n}\n\nfunction Publish-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @(),\n [int]$Version = 0\n )\n\n $raw = Get-AkeylessSecretValue -GatewayUrl $GatewayUrl -Token $Token -SecretPath $SecretPath -Version $Version\n $created = 0\n $fieldsSpecified = ($Fields.Count -gt 0)\n\n if ($raw -is [string]) {\n $trimmed = $raw.Trim()\n if ($fieldsSpecified) {\n $parsed = $null\n try { $parsed = $trimmed | ConvertFrom-Json } catch {}\n if ($null -eq $parsed) {\n throw \"Secret '$SecretPath' is not JSON but field names were specified\"\n }\n $raw = $parsed\n }\n }\n\n if ($raw -is [pscustomobject] -or $raw -is [hashtable]) {\n $properties = if ($raw -is [hashtable]) { $raw.Keys } else { $raw.PSObject.Properties.Name }\n\n if ($fieldsSpecified) {\n foreach ($field in $Fields) {\n $fieldName = $field.Name\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n foreach ($fieldName in $properties) {\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n\n return $created\n }\n\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$raw) -StepName $StepName -PrintVariableNames $PrintVariableNames\n return 1\n}\n\nfunction Invoke-AkeylessListItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @()\n )\n\n $path = Normalize-AkeylessPath $FolderPath\n $items = @()\n $folders = @()\n $paginationToken = ''\n\n do {\n $body = @{\n token = $Token\n path = $path\n 'current-folder' = $true\n }\n\n if ($Types.Count -gt 0) {\n $body.type = $Types\n }\n if (-not [string]::IsNullOrWhiteSpace($paginationToken)) {\n $body.'pagination-token' = $paginationToken\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'list-items' -Body $body\n if ($null -eq $response) {\n break\n }\n\n if ($null -ne $response.items) {\n $items += @($response.items | ForEach-Object { $_.item_name })\n }\n if ($null -ne $response.folders) {\n $folders += @($response.folders)\n }\n\n $paginationToken = ''\n if ($response.PSObject.Properties.Name -contains 'next_page') {\n $paginationToken = [string]$response.next_page\n }\n } while (-not [string]::IsNullOrWhiteSpace($paginationToken))\n\n return [pscustomobject]@{\n Items = $items\n Folders = $folders\n }\n}\n\nfunction Get-AkeylessFolderItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @('static-secret')\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath -Types $Types\n return @($listing.Items)\n}\n\nfunction Get-AkeylessFolderChildren {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath\n return @($listing.Folders)\n}\n\nfunction Parse-AkeylessFieldDefinitions {\n param ([string]$RawValue)\n\n $fields = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $fields\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $name = $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($name)) {\n throw \"Unable to establish field name from: '$_'\"\n }\n $fields += [pscustomobject]@{\n Name = $name\n VariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $fields\n}\n\nfunction Parse-AkeylessSecretDefinitions {\n param ([string]$RawValue)\n\n $secrets = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $secrets\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $path = Normalize-AkeylessPath $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($path)) {\n throw \"Unable to establish secret path from: '$_'\"\n }\n $secrets += [pscustomobject]@{\n Path = $path\n OutputVariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $secrets\n}\n\nfunction Get-AkeylessSecretsRecursively {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [bool]$Recursive\n )\n\n $results = @()\n $folder = Normalize-AkeylessPath $FolderPath\n\n $items = Get-AkeylessFolderItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($item in $items) {\n $results += $item\n }\n\n if ($Recursive) {\n $children = Get-AkeylessFolderChildren -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($child in $children) {\n $results += Get-AkeylessSecretsRecursively -GatewayUrl $GatewayUrl -Token $Token -FolderPath $child -Recursive $true\n }\n }\n\n return $results\n}\n\nfunction Complete-AkeylessLogin {\n param (\n [string]$GatewayUrl,\n [hashtable]$AuthBody,\n [string]$StepName\n )\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'auth' -Body $AuthBody\n if ($null -eq $response -or [string]::IsNullOrWhiteSpace($response.token)) {\n throw 'Authentication succeeded but no token was returned'\n }\n\n Set-AkeylessSensitiveOutput -Name 'AkeylessAuthToken' -Value $response.token -StepName $StepName -PrintVariableNames $true\n Write-Host 'Authenticated to Akeyless successfully'\n}\n\nfunction Publish-AkeylessStructuredSecretResponse {\n param (\n [object]$Response,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @()\n )\n\n $created = 0\n if ($Response -is [pscustomobject] -or $Response -is [hashtable]) {\n $properties = if ($Response -is [hashtable]) { @($Response.Keys) } else { @($Response.PSObject.Properties.Name) }\n if ($Fields.Count -gt 0) {\n foreach ($field in $Fields) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$field.Name] } else { $Response.$($field.Name) }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $field.Name -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n $useSingleOutputName = (-not [string]::IsNullOrWhiteSpace($OutputVariableName)) -and ($properties.Count -eq 1)\n foreach ($fieldName in $properties) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$fieldName] } else { $Response.$fieldName }\n if ($null -ne $fieldValue) {\n $overrideName = if ($useSingleOutputName) { $OutputVariableName } else { '' }\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $overrideName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n }\n else {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$Response) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created = 1\n }\n\n return $created\n}\n\nfunction Get-AwsHmacSha256Bytes {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n $hmac = New-Object System.Security.Cryptography.HMACSHA256 (, $Key)\n return $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Message))\n}\n\nfunction Get-AwsHmacSha256Hex {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n return ([BitConverter]::ToString((Get-AwsHmacSha256Bytes -Key $Key -Message $Message))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsSigningKey {\n param (\n [string]$SecretKey,\n [string]$DateStamp,\n [string]$Region,\n [string]$Service\n )\n\n $kSecret = [Text.Encoding]::UTF8.GetBytes(\"AWS4$SecretKey\")\n $kDate = Get-AwsHmacSha256Bytes -Key $kSecret -Message $DateStamp\n $kRegion = Get-AwsHmacSha256Bytes -Key $kDate -Message $Region\n $kService = Get-AwsHmacSha256Bytes -Key $kRegion -Message $Service\n return Get-AwsHmacSha256Bytes -Key $kService -Message 'aws4_request'\n}\n\nfunction Get-AwsSha256Hex {\n param ([string]$Text)\n\n $sha = [System.Security.Cryptography.SHA256]::Create()\n return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsCredentialChain {\n $accessKeyId = $env:AWS_ACCESS_KEY_ID\n $secretAccessKey = $env:AWS_SECRET_ACCESS_KEY\n $sessionToken = $env:AWS_SESSION_TOKEN\n\n if (-not [string]::IsNullOrWhiteSpace($accessKeyId) -and -not [string]::IsNullOrWhiteSpace($secretAccessKey)) {\n return [pscustomobject]@{\n AccessKeyId = $accessKeyId.Trim()\n SecretAccessKey = $secretAccessKey.Trim()\n SessionToken = if ([string]::IsNullOrWhiteSpace($sessionToken)) { '' } else { $sessionToken.Trim() }\n }\n }\n\n try {\n $imdsToken = Invoke-RestMethod -Method Put -Uri 'http://169.254.169.254/latest/api/token' -Headers @{ 'X-aws-ec2-metadata-token-ttl-seconds' = '21600' } -TimeoutSec 2\n $roleName = Invoke-RestMethod -Uri 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n $roleCreds = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$roleName\" -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n if ($null -eq $roleCreds -or [string]::IsNullOrWhiteSpace($roleCreds.AccessKeyId)) {\n throw 'EC2 instance metadata returned no IAM credentials'\n }\n\n return [pscustomobject]@{\n AccessKeyId = [string]$roleCreds.AccessKeyId\n SecretAccessKey = [string]$roleCreds.SecretAccessKey\n SessionToken = [string]$roleCreds.Token\n }\n }\n catch {\n throw \"AWS credentials were not found in environment variables or EC2 instance metadata: $($_.Exception.Message)\"\n }\n}\n\nfunction New-AwsIamCloudId {\n param (\n [string]$AccessKeyId,\n [string]$SecretAccessKey,\n [string]$SessionToken = '',\n [string]$Region = 'us-east-1',\n [string]$StsUrl = 'https://sts.amazonaws.com/'\n )\n\n $service = 'sts'\n $method = 'POST'\n $hostName = ([Uri]$StsUrl).Host\n $body = 'Action=GetCallerIdentity&Version=2011-06-15'\n $amzDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')\n $dateStamp = $amzDate.Substring(0, 8)\n $payloadHash = Get-AwsSha256Hex -Text $body\n\n $headers = [ordered]@{\n Host = $hostName\n 'Content-Type' = 'application/x-www-form-urlencoded; charset=utf-8'\n 'Content-Length' = [string]$body.Length\n 'X-Amz-Date' = $amzDate\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $headers['X-Amz-Security-Token'] = $SessionToken\n }\n\n $canonicalHeaders = ($headers.GetEnumerator() | ForEach-Object { \"$($_.Key.ToLowerInvariant()):$($_.Value)\" }) -join \"`n\"\n $signedHeaders = (($headers.Keys | ForEach-Object { $_.ToLowerInvariant() }) | Sort-Object) -join ';'\n $canonicalRequest = @(\n $method\n '/'\n ''\n \"$canonicalHeaders`n\"\n $signedHeaders\n $payloadHash\n ) -join \"`n\"\n\n $credentialScope = \"$dateStamp/$Region/$service/aws4_request\"\n $stringToSign = @(\n 'AWS4-HMAC-SHA256'\n $amzDate\n $credentialScope\n (Get-AwsSha256Hex -Text $canonicalRequest)\n ) -join \"`n\"\n\n $signingKey = Get-AwsSigningKey -SecretKey $SecretAccessKey -DateStamp $dateStamp -Region $Region -Service $service\n $signature = Get-AwsHmacSha256Hex -Key $signingKey -Message $stringToSign\n $authorization = \"AWS4-HMAC-SHA256 Credential=$AccessKeyId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature\"\n\n $requestHeaders = @{\n Authorization = @($authorization)\n 'Content-Length' = @([string]$body.Length)\n Host = @($hostName)\n 'Content-Type' = @('application/x-www-form-urlencoded; charset=utf-8')\n 'X-Amz-Date' = @($amzDate)\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $requestHeaders['X-Amz-Security-Token'] = @($SessionToken)\n }\n\n $payload = [ordered]@{\n sts_request_method = $method\n sts_request_url = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($StsUrl))\n sts_request_body = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body))\n sts_request_headers = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($requestHeaders | ConvertTo-Json -Compress)))\n }\n\n return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Compress)))\n}\n\nfunction Resolve-AwsIamCloudId {\n param (\n [string]$CloudId,\n [string]$Region,\n [string]$StsUrl\n )\n\n if (-not [string]::IsNullOrWhiteSpace($CloudId)) {\n return $CloudId.Trim()\n }\n\n $credentials = Get-AwsCredentialChain\n return New-AwsIamCloudId -AccessKeyId $credentials.AccessKeyId -SecretAccessKey $credentials.SecretAccessKey -SessionToken $credentials.SessionToken -Region $Region -StsUrl $StsUrl\n}\n\n$GATEWAY_URL = $OctopusParameters['Akeyless.Auth.AwsIam.GatewayUrl']\n$ACCESS_ID = $OctopusParameters['Akeyless.Auth.AwsIam.AccessId']\n$CLOUD_ID = $OctopusParameters['Akeyless.Auth.AwsIam.CloudId']\n$AWS_REGION = $OctopusParameters['Akeyless.Auth.AwsIam.AwsRegion']\n$STS_URL = $OctopusParameters['Akeyless.Auth.AwsIam.StsUrl']\n$StepName = $OctopusParameters['Octopus.Step.Name']\n\nif ([string]::IsNullOrWhiteSpace($GATEWAY_URL)) {\n $GATEWAY_URL = 'https://api.akeyless.io'\n}\nif ([string]::IsNullOrWhiteSpace($ACCESS_ID)) {\n throw 'Required parameter Access ID not specified'\n}\nif ([string]::IsNullOrWhiteSpace($AWS_REGION)) {\n $AWS_REGION = 'us-east-1'\n}\nif ([string]::IsNullOrWhiteSpace($STS_URL)) {\n $STS_URL = 'https://sts.amazonaws.com/'\n}\n\n$cloudId = Resolve-AwsIamCloudId -CloudId $CLOUD_ID -Region $AWS_REGION.Trim() -StsUrl $STS_URL.Trim()\n\n$body = @{\n 'access-id' = $ACCESS_ID\n 'access-type' = 'aws_iam'\n 'cloud-id' = $cloudId\n}\n\nComplete-AkeylessLogin -GatewayUrl $GATEWAY_URL -AuthBody $body -StepName $StepName" + }, + "Parameters": [ + { + "HelpText": "The Akeyless API or Gateway URL. For SaaS, use https://api.akeyless.io.", + "Id": "10005000-0000-0000-0000-100050001001", + "Label": "Gateway URL", + "DefaultValue": "https://api.akeyless.io", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.AwsIam.GatewayUrl" + }, + { + "HelpText": "The Akeyless Access ID for the AWS IAM auth method.", + "Id": "10005000-0000-0000-0000-100050001002", + "Label": "Access ID", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.AwsIam.AccessId" + }, + { + "HelpText": "Optional pre-generated AWS Cloud ID. Leave blank to derive credentials from the worker environment.", + "Id": "10005000-0000-0000-0000-100050001003", + "Label": "Cloud ID (optional)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Name": "Akeyless.Auth.AwsIam.CloudId" + }, + { + "HelpText": "Region used when signing the STS GetCallerIdentity request.", + "Id": "10005000-0000-0000-0000-100050001004", + "Label": "AWS region", + "DefaultValue": "us-east-1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.AwsIam.AwsRegion" + }, + { + "HelpText": "STS endpoint used to build the Cloud ID payload.", + "Id": "10005000-0000-0000-0000-100050001005", + "Label": "STS URL", + "DefaultValue": "https://sts.amazonaws.com/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.AwsIam.StsUrl" + } + ], + "LastModifiedBy": "akeyless-community", + "LastModifiedAt": "2026-06-17T04:32:01.625Z", + "$Meta": { + "ExportedAt": "2026-06-17T04:32:01.625Z", + "OctopusVersion": "2024.4.0", + "Type": "ActionTemplate" + }, + "Category": "akeyless" +} diff --git a/step-templates/akeyless-jwt-login.json b/step-templates/akeyless-jwt-login.json new file mode 100644 index 000000000..757495a3a --- /dev/null +++ b/step-templates/akeyless-jwt-login.json @@ -0,0 +1,65 @@ +{ + "Id": "f8e3a1b2-4c5d-6e7f-8a9b-0c1d2e3f4a04", + "Name": "Akeyless - JWT Login", + "Description": "This step authenticates to [Akeyless](https://www.akeyless.io) using a JSON Web Token (JWT) or OIDC token.\n\nThe API token from the response is stored as a sensitive [output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) named AkeylessAuthToken for use in other step templates.\n\n---\n\n**Required:**\n- Akeyless Access ID configured for JWT or OIDC authentication\n- JWT or OIDC bearer token\n\n**Optional:**\n- Access type (jwt or oidc, default jwt)\n- Gateway/API URL (default: https://api.akeyless.io)\n\n**Notes:**\n- Bind the JWT from an Octopus OIDC account, a prior script step, or a sensitive project variable.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nfunction Get-AkeylessApiErrorBody {\n param ($RequestError)\n\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $rawResponse = $reader.ReadToEnd()\n try { return ($rawResponse | ConvertFrom-Json) } catch { return $rawResponse }\n }\n return $null\n }\n\n return $RequestError.ErrorDetails.Message\n}\n\nfunction Format-AkeylessApiError {\n param (\n [string]$Action,\n [System.Management.Automation.ErrorRecord]$ErrorRecord\n )\n\n $message = \"An error occurred during $Action`: $($ErrorRecord.Exception.Message)\"\n $body = Get-AkeylessApiErrorBody -RequestError $ErrorRecord\n if ($null -ne $body) {\n if ($body.error) {\n $message += \"`n`tDetail: $($body.error)\"\n }\n elseif ($body.PSObject.Properties.Name -contains 'errors') {\n $message += \"`n`tDetail: $($body.errors -Join ',')\"\n }\n elseif ($body -is [string] -and -not [string]::IsNullOrWhiteSpace($body)) {\n $message += \"`n`tDetail: $body\"\n }\n }\n\n return $message\n}\n\nfunction Invoke-AkeylessApi {\n param (\n [Parameter(Mandatory = $true)][string]$GatewayUrl,\n [Parameter(Mandatory = $true)][string]$Path,\n [Parameter(Mandatory = $true)][hashtable]$Body\n )\n\n $base = $GatewayUrl.TrimEnd('/')\n $apiPath = $Path.TrimStart('/')\n $uri = \"$base/$apiPath\"\n $json = $Body | ConvertTo-Json -Depth 20 -Compress:$false\n\n try {\n return Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType 'application/json'\n }\n catch {\n $detail = Format-AkeylessApiError -Action \"POST $Path\" -ErrorRecord $_\n Write-Error $detail -Category ConnectionError\n }\n}\n\nfunction Normalize-AkeylessPath {\n param ([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) {\n return $Path\n }\n\n $normalized = $Path.Trim()\n if (-not $normalized.StartsWith('/')) {\n $normalized = \"/$normalized\"\n }\n\n return $normalized\n}\n\nfunction ConvertTo-AkeylessOutputVariableName {\n param (\n [string]$SecretPath,\n [string]$FieldName = '',\n [string]$OverrideName = ''\n )\n\n if (-not [string]::IsNullOrWhiteSpace($OverrideName)) {\n return $OverrideName.Trim()\n }\n\n $base = (Normalize-AkeylessPath $SecretPath).Trim('/').Replace('/', '.')\n if ([string]::IsNullOrWhiteSpace($FieldName)) {\n return $base\n }\n\n return \"$base.$($FieldName.Trim())\"\n}\n\nfunction Get-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [int]$Version = 0\n )\n\n $path = Normalize-AkeylessPath $SecretPath\n $body = @{\n token = $Token\n names = @($path)\n }\n\n if ($Version -gt 0) {\n $body.version = $Version\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'get-secret-value' -Body $body\n if ($null -eq $response) {\n throw \"Empty response retrieving secret '$path'\"\n }\n\n if ($response.PSObject.Properties.Name -contains $path) {\n return $response.$path\n }\n\n foreach ($property in $response.PSObject.Properties) {\n if (-not [string]::IsNullOrWhiteSpace([string]$property.Value)) {\n return $property.Value\n }\n }\n\n throw \"Secret '$path' was not found in the API response\"\n}\n\nfunction Set-AkeylessSensitiveOutput {\n param (\n [string]$Name,\n [string]$Value,\n [string]$StepName,\n [bool]$PrintVariableNames\n )\n\n if ([string]::IsNullOrWhiteSpace($Name)) {\n throw 'Output variable name cannot be empty'\n }\n\n Set-OctopusVariable -Name $Name -Value $Value -Sensitive\n if ($PrintVariableNames) {\n Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$Name}\"\n }\n}\n\nfunction Publish-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @(),\n [int]$Version = 0\n )\n\n $raw = Get-AkeylessSecretValue -GatewayUrl $GatewayUrl -Token $Token -SecretPath $SecretPath -Version $Version\n $created = 0\n $fieldsSpecified = ($Fields.Count -gt 0)\n\n if ($raw -is [string]) {\n $trimmed = $raw.Trim()\n if ($fieldsSpecified) {\n $parsed = $null\n try { $parsed = $trimmed | ConvertFrom-Json } catch {}\n if ($null -eq $parsed) {\n throw \"Secret '$SecretPath' is not JSON but field names were specified\"\n }\n $raw = $parsed\n }\n }\n\n if ($raw -is [pscustomobject] -or $raw -is [hashtable]) {\n $properties = if ($raw -is [hashtable]) { $raw.Keys } else { $raw.PSObject.Properties.Name }\n\n if ($fieldsSpecified) {\n foreach ($field in $Fields) {\n $fieldName = $field.Name\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n foreach ($fieldName in $properties) {\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n\n return $created\n }\n\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$raw) -StepName $StepName -PrintVariableNames $PrintVariableNames\n return 1\n}\n\nfunction Invoke-AkeylessListItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @()\n )\n\n $path = Normalize-AkeylessPath $FolderPath\n $items = @()\n $folders = @()\n $paginationToken = ''\n\n do {\n $body = @{\n token = $Token\n path = $path\n 'current-folder' = $true\n }\n\n if ($Types.Count -gt 0) {\n $body.type = $Types\n }\n if (-not [string]::IsNullOrWhiteSpace($paginationToken)) {\n $body.'pagination-token' = $paginationToken\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'list-items' -Body $body\n if ($null -eq $response) {\n break\n }\n\n if ($null -ne $response.items) {\n $items += @($response.items | ForEach-Object { $_.item_name })\n }\n if ($null -ne $response.folders) {\n $folders += @($response.folders)\n }\n\n $paginationToken = ''\n if ($response.PSObject.Properties.Name -contains 'next_page') {\n $paginationToken = [string]$response.next_page\n }\n } while (-not [string]::IsNullOrWhiteSpace($paginationToken))\n\n return [pscustomobject]@{\n Items = $items\n Folders = $folders\n }\n}\n\nfunction Get-AkeylessFolderItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @('static-secret')\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath -Types $Types\n return @($listing.Items)\n}\n\nfunction Get-AkeylessFolderChildren {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath\n return @($listing.Folders)\n}\n\nfunction Parse-AkeylessFieldDefinitions {\n param ([string]$RawValue)\n\n $fields = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $fields\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $name = $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($name)) {\n throw \"Unable to establish field name from: '$_'\"\n }\n $fields += [pscustomobject]@{\n Name = $name\n VariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $fields\n}\n\nfunction Parse-AkeylessSecretDefinitions {\n param ([string]$RawValue)\n\n $secrets = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $secrets\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $path = Normalize-AkeylessPath $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($path)) {\n throw \"Unable to establish secret path from: '$_'\"\n }\n $secrets += [pscustomobject]@{\n Path = $path\n OutputVariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $secrets\n}\n\nfunction Get-AkeylessSecretsRecursively {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [bool]$Recursive\n )\n\n $results = @()\n $folder = Normalize-AkeylessPath $FolderPath\n\n $items = Get-AkeylessFolderItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($item in $items) {\n $results += $item\n }\n\n if ($Recursive) {\n $children = Get-AkeylessFolderChildren -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($child in $children) {\n $results += Get-AkeylessSecretsRecursively -GatewayUrl $GatewayUrl -Token $Token -FolderPath $child -Recursive $true\n }\n }\n\n return $results\n}\n\nfunction Complete-AkeylessLogin {\n param (\n [string]$GatewayUrl,\n [hashtable]$AuthBody,\n [string]$StepName\n )\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'auth' -Body $AuthBody\n if ($null -eq $response -or [string]::IsNullOrWhiteSpace($response.token)) {\n throw 'Authentication succeeded but no token was returned'\n }\n\n Set-AkeylessSensitiveOutput -Name 'AkeylessAuthToken' -Value $response.token -StepName $StepName -PrintVariableNames $true\n Write-Host 'Authenticated to Akeyless successfully'\n}\n\nfunction Publish-AkeylessStructuredSecretResponse {\n param (\n [object]$Response,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @()\n )\n\n $created = 0\n if ($Response -is [pscustomobject] -or $Response -is [hashtable]) {\n $properties = if ($Response -is [hashtable]) { @($Response.Keys) } else { @($Response.PSObject.Properties.Name) }\n if ($Fields.Count -gt 0) {\n foreach ($field in $Fields) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$field.Name] } else { $Response.$($field.Name) }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $field.Name -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n $useSingleOutputName = (-not [string]::IsNullOrWhiteSpace($OutputVariableName)) -and ($properties.Count -eq 1)\n foreach ($fieldName in $properties) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$fieldName] } else { $Response.$fieldName }\n if ($null -ne $fieldValue) {\n $overrideName = if ($useSingleOutputName) { $OutputVariableName } else { '' }\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $overrideName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n }\n else {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$Response) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created = 1\n }\n\n return $created\n}\n\nfunction Get-AwsHmacSha256Bytes {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n $hmac = New-Object System.Security.Cryptography.HMACSHA256 (, $Key)\n return $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Message))\n}\n\nfunction Get-AwsHmacSha256Hex {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n return ([BitConverter]::ToString((Get-AwsHmacSha256Bytes -Key $Key -Message $Message))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsSigningKey {\n param (\n [string]$SecretKey,\n [string]$DateStamp,\n [string]$Region,\n [string]$Service\n )\n\n $kSecret = [Text.Encoding]::UTF8.GetBytes(\"AWS4$SecretKey\")\n $kDate = Get-AwsHmacSha256Bytes -Key $kSecret -Message $DateStamp\n $kRegion = Get-AwsHmacSha256Bytes -Key $kDate -Message $Region\n $kService = Get-AwsHmacSha256Bytes -Key $kRegion -Message $Service\n return Get-AwsHmacSha256Bytes -Key $kService -Message 'aws4_request'\n}\n\nfunction Get-AwsSha256Hex {\n param ([string]$Text)\n\n $sha = [System.Security.Cryptography.SHA256]::Create()\n return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsCredentialChain {\n $accessKeyId = $env:AWS_ACCESS_KEY_ID\n $secretAccessKey = $env:AWS_SECRET_ACCESS_KEY\n $sessionToken = $env:AWS_SESSION_TOKEN\n\n if (-not [string]::IsNullOrWhiteSpace($accessKeyId) -and -not [string]::IsNullOrWhiteSpace($secretAccessKey)) {\n return [pscustomobject]@{\n AccessKeyId = $accessKeyId.Trim()\n SecretAccessKey = $secretAccessKey.Trim()\n SessionToken = if ([string]::IsNullOrWhiteSpace($sessionToken)) { '' } else { $sessionToken.Trim() }\n }\n }\n\n try {\n $imdsToken = Invoke-RestMethod -Method Put -Uri 'http://169.254.169.254/latest/api/token' -Headers @{ 'X-aws-ec2-metadata-token-ttl-seconds' = '21600' } -TimeoutSec 2\n $roleName = Invoke-RestMethod -Uri 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n $roleCreds = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$roleName\" -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n if ($null -eq $roleCreds -or [string]::IsNullOrWhiteSpace($roleCreds.AccessKeyId)) {\n throw 'EC2 instance metadata returned no IAM credentials'\n }\n\n return [pscustomobject]@{\n AccessKeyId = [string]$roleCreds.AccessKeyId\n SecretAccessKey = [string]$roleCreds.SecretAccessKey\n SessionToken = [string]$roleCreds.Token\n }\n }\n catch {\n throw \"AWS credentials were not found in environment variables or EC2 instance metadata: $($_.Exception.Message)\"\n }\n}\n\nfunction New-AwsIamCloudId {\n param (\n [string]$AccessKeyId,\n [string]$SecretAccessKey,\n [string]$SessionToken = '',\n [string]$Region = 'us-east-1',\n [string]$StsUrl = 'https://sts.amazonaws.com/'\n )\n\n $service = 'sts'\n $method = 'POST'\n $hostName = ([Uri]$StsUrl).Host\n $body = 'Action=GetCallerIdentity&Version=2011-06-15'\n $amzDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')\n $dateStamp = $amzDate.Substring(0, 8)\n $payloadHash = Get-AwsSha256Hex -Text $body\n\n $headers = [ordered]@{\n Host = $hostName\n 'Content-Type' = 'application/x-www-form-urlencoded; charset=utf-8'\n 'Content-Length' = [string]$body.Length\n 'X-Amz-Date' = $amzDate\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $headers['X-Amz-Security-Token'] = $SessionToken\n }\n\n $canonicalHeaders = ($headers.GetEnumerator() | ForEach-Object { \"$($_.Key.ToLowerInvariant()):$($_.Value)\" }) -join \"`n\"\n $signedHeaders = (($headers.Keys | ForEach-Object { $_.ToLowerInvariant() }) | Sort-Object) -join ';'\n $canonicalRequest = @(\n $method\n '/'\n ''\n \"$canonicalHeaders`n\"\n $signedHeaders\n $payloadHash\n ) -join \"`n\"\n\n $credentialScope = \"$dateStamp/$Region/$service/aws4_request\"\n $stringToSign = @(\n 'AWS4-HMAC-SHA256'\n $amzDate\n $credentialScope\n (Get-AwsSha256Hex -Text $canonicalRequest)\n ) -join \"`n\"\n\n $signingKey = Get-AwsSigningKey -SecretKey $SecretAccessKey -DateStamp $dateStamp -Region $Region -Service $service\n $signature = Get-AwsHmacSha256Hex -Key $signingKey -Message $stringToSign\n $authorization = \"AWS4-HMAC-SHA256 Credential=$AccessKeyId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature\"\n\n $requestHeaders = @{\n Authorization = @($authorization)\n 'Content-Length' = @([string]$body.Length)\n Host = @($hostName)\n 'Content-Type' = @('application/x-www-form-urlencoded; charset=utf-8')\n 'X-Amz-Date' = @($amzDate)\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $requestHeaders['X-Amz-Security-Token'] = @($SessionToken)\n }\n\n $payload = [ordered]@{\n sts_request_method = $method\n sts_request_url = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($StsUrl))\n sts_request_body = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body))\n sts_request_headers = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($requestHeaders | ConvertTo-Json -Compress)))\n }\n\n return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Compress)))\n}\n\nfunction Resolve-AwsIamCloudId {\n param (\n [string]$CloudId,\n [string]$Region,\n [string]$StsUrl\n )\n\n if (-not [string]::IsNullOrWhiteSpace($CloudId)) {\n return $CloudId.Trim()\n }\n\n $credentials = Get-AwsCredentialChain\n return New-AwsIamCloudId -AccessKeyId $credentials.AccessKeyId -SecretAccessKey $credentials.SecretAccessKey -SessionToken $credentials.SessionToken -Region $Region -StsUrl $StsUrl\n}\n\n$GATEWAY_URL = $OctopusParameters['Akeyless.Auth.Jwt.GatewayUrl']\n$ACCESS_ID = $OctopusParameters['Akeyless.Auth.Jwt.AccessId']\n$JWT = $OctopusParameters['Akeyless.Auth.Jwt.Jwt']\n$ACCESS_TYPE = $OctopusParameters['Akeyless.Auth.Jwt.AccessType']\n$StepName = $OctopusParameters['Octopus.Step.Name']\n\nif ([string]::IsNullOrWhiteSpace($GATEWAY_URL)) {\n $GATEWAY_URL = 'https://api.akeyless.io'\n}\nif ([string]::IsNullOrWhiteSpace($ACCESS_ID)) {\n throw 'Required parameter Access ID not specified'\n}\nif ([string]::IsNullOrWhiteSpace($JWT)) {\n throw 'Required parameter JWT not specified'\n}\nif ([string]::IsNullOrWhiteSpace($ACCESS_TYPE)) {\n $ACCESS_TYPE = 'jwt'\n}\n\n$body = @{\n 'access-id' = $ACCESS_ID\n 'access-type' = $ACCESS_TYPE.Trim()\n jwt = $JWT\n}\n\nComplete-AkeylessLogin -GatewayUrl $GATEWAY_URL -AuthBody $body -StepName $StepName" + }, + "Parameters": [ + { + "HelpText": "The Akeyless API or Gateway URL. For SaaS, use https://api.akeyless.io.", + "Id": "10004000-0000-0000-0000-100040001001", + "Label": "Gateway URL", + "DefaultValue": "https://api.akeyless.io", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.Jwt.GatewayUrl" + }, + { + "HelpText": "The Akeyless Access ID for the JWT/OIDC auth method.", + "Id": "10004000-0000-0000-0000-100040001002", + "Label": "Access ID", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Auth.Jwt.AccessId" + }, + { + "HelpText": "Bearer token used for authentication.", + "Id": "10004000-0000-0000-0000-100040001003", + "Label": "JWT / OIDC token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Name": "Akeyless.Auth.Jwt.Jwt" + }, + { + "HelpText": "Set to `oidc` when your Akeyless auth method requires OIDC instead of JWT.", + "Id": "10004000-0000-0000-0000-100040001004", + "Label": "Access type", + "DefaultValue": "jwt", + "DisplaySettings": { + "Octopus.SelectOptions": "jwt|JWT\noidc|OIDC", + "Octopus.ControlType": "Select" + }, + "Name": "Akeyless.Auth.Jwt.AccessType" + } + ], + "LastModifiedBy": "akeyless-community", + "LastModifiedAt": "2026-06-17T04:32:01.624Z", + "$Meta": { + "ExportedAt": "2026-06-17T04:32:01.624Z", + "OctopusVersion": "2024.4.0", + "Type": "ActionTemplate" + }, + "Category": "akeyless" +} diff --git a/step-templates/akeyless-retrieve-dynamic-secret.json b/step-templates/akeyless-retrieve-dynamic-secret.json new file mode 100644 index 000000000..f8498ec59 --- /dev/null +++ b/step-templates/akeyless-retrieve-dynamic-secret.json @@ -0,0 +1,104 @@ +{ + "Id": "f8e3a1b2-4c5d-6e7f-8a9b-0c1d2e3f4a03", + "Name": "Akeyless - Retrieve Dynamic Secret", + "Description": "This step retrieves a **dynamic secret** from Akeyless and creates sensitive [output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for use in later deployment or runbook steps.\n\nThis step template uses the [Akeyless REST API](https://docs.akeyless.io/reference/getdynamicsecretvalue), so no other dependencies are needed.\n\n---\n\n**Authentication token**\n\nUse the **Akeyless - Access Key Login** step template first, then bind the Auth Token parameter to:\n\n#{Octopus.Action[].Output.AkeylessAuthToken}\n\n---\n\n**Dynamic secret arguments**\n\nOptional provisioning arguments can be supplied one per line.\n\n---\n\n**Required:**\n- Authentication token\n- Dynamic secret path", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nfunction Get-AkeylessApiErrorBody {\n param ($RequestError)\n\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $rawResponse = $reader.ReadToEnd()\n try { return ($rawResponse | ConvertFrom-Json) } catch { return $rawResponse }\n }\n return $null\n }\n\n return $RequestError.ErrorDetails.Message\n}\n\nfunction Format-AkeylessApiError {\n param (\n [string]$Action,\n [System.Management.Automation.ErrorRecord]$ErrorRecord\n )\n\n $message = \"An error occurred during $Action`: $($ErrorRecord.Exception.Message)\"\n $body = Get-AkeylessApiErrorBody -RequestError $ErrorRecord\n if ($null -ne $body) {\n if ($body.error) {\n $message += \"`n`tDetail: $($body.error)\"\n }\n elseif ($body.PSObject.Properties.Name -contains 'errors') {\n $message += \"`n`tDetail: $($body.errors -Join ',')\"\n }\n elseif ($body -is [string] -and -not [string]::IsNullOrWhiteSpace($body)) {\n $message += \"`n`tDetail: $body\"\n }\n }\n\n return $message\n}\n\nfunction Invoke-AkeylessApi {\n param (\n [Parameter(Mandatory = $true)][string]$GatewayUrl,\n [Parameter(Mandatory = $true)][string]$Path,\n [Parameter(Mandatory = $true)][hashtable]$Body\n )\n\n $base = $GatewayUrl.TrimEnd('/')\n $apiPath = $Path.TrimStart('/')\n $uri = \"$base/$apiPath\"\n $json = $Body | ConvertTo-Json -Depth 20 -Compress:$false\n\n try {\n return Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType 'application/json'\n }\n catch {\n $detail = Format-AkeylessApiError -Action \"POST $Path\" -ErrorRecord $_\n Write-Error $detail -Category ConnectionError\n }\n}\n\nfunction Normalize-AkeylessPath {\n param ([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) {\n return $Path\n }\n\n $normalized = $Path.Trim()\n if (-not $normalized.StartsWith('/')) {\n $normalized = \"/$normalized\"\n }\n\n return $normalized\n}\n\nfunction ConvertTo-AkeylessOutputVariableName {\n param (\n [string]$SecretPath,\n [string]$FieldName = '',\n [string]$OverrideName = ''\n )\n\n if (-not [string]::IsNullOrWhiteSpace($OverrideName)) {\n return $OverrideName.Trim()\n }\n\n $base = (Normalize-AkeylessPath $SecretPath).Trim('/').Replace('/', '.')\n if ([string]::IsNullOrWhiteSpace($FieldName)) {\n return $base\n }\n\n return \"$base.$($FieldName.Trim())\"\n}\n\nfunction Get-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [int]$Version = 0\n )\n\n $path = Normalize-AkeylessPath $SecretPath\n $body = @{\n token = $Token\n names = @($path)\n }\n\n if ($Version -gt 0) {\n $body.version = $Version\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'get-secret-value' -Body $body\n if ($null -eq $response) {\n throw \"Empty response retrieving secret '$path'\"\n }\n\n if ($response.PSObject.Properties.Name -contains $path) {\n return $response.$path\n }\n\n foreach ($property in $response.PSObject.Properties) {\n if (-not [string]::IsNullOrWhiteSpace([string]$property.Value)) {\n return $property.Value\n }\n }\n\n throw \"Secret '$path' was not found in the API response\"\n}\n\nfunction Set-AkeylessSensitiveOutput {\n param (\n [string]$Name,\n [string]$Value,\n [string]$StepName,\n [bool]$PrintVariableNames\n )\n\n if ([string]::IsNullOrWhiteSpace($Name)) {\n throw 'Output variable name cannot be empty'\n }\n\n Set-OctopusVariable -Name $Name -Value $Value -Sensitive\n if ($PrintVariableNames) {\n Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$Name}\"\n }\n}\n\nfunction Publish-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @(),\n [int]$Version = 0\n )\n\n $raw = Get-AkeylessSecretValue -GatewayUrl $GatewayUrl -Token $Token -SecretPath $SecretPath -Version $Version\n $created = 0\n $fieldsSpecified = ($Fields.Count -gt 0)\n\n if ($raw -is [string]) {\n $trimmed = $raw.Trim()\n if ($fieldsSpecified) {\n $parsed = $null\n try { $parsed = $trimmed | ConvertFrom-Json } catch {}\n if ($null -eq $parsed) {\n throw \"Secret '$SecretPath' is not JSON but field names were specified\"\n }\n $raw = $parsed\n }\n }\n\n if ($raw -is [pscustomobject] -or $raw -is [hashtable]) {\n $properties = if ($raw -is [hashtable]) { $raw.Keys } else { $raw.PSObject.Properties.Name }\n\n if ($fieldsSpecified) {\n foreach ($field in $Fields) {\n $fieldName = $field.Name\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n foreach ($fieldName in $properties) {\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n\n return $created\n }\n\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$raw) -StepName $StepName -PrintVariableNames $PrintVariableNames\n return 1\n}\n\nfunction Invoke-AkeylessListItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @()\n )\n\n $path = Normalize-AkeylessPath $FolderPath\n $items = @()\n $folders = @()\n $paginationToken = ''\n\n do {\n $body = @{\n token = $Token\n path = $path\n 'current-folder' = $true\n }\n\n if ($Types.Count -gt 0) {\n $body.type = $Types\n }\n if (-not [string]::IsNullOrWhiteSpace($paginationToken)) {\n $body.'pagination-token' = $paginationToken\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'list-items' -Body $body\n if ($null -eq $response) {\n break\n }\n\n if ($null -ne $response.items) {\n $items += @($response.items | ForEach-Object { $_.item_name })\n }\n if ($null -ne $response.folders) {\n $folders += @($response.folders)\n }\n\n $paginationToken = ''\n if ($response.PSObject.Properties.Name -contains 'next_page') {\n $paginationToken = [string]$response.next_page\n }\n } while (-not [string]::IsNullOrWhiteSpace($paginationToken))\n\n return [pscustomobject]@{\n Items = $items\n Folders = $folders\n }\n}\n\nfunction Get-AkeylessFolderItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @('static-secret')\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath -Types $Types\n return @($listing.Items)\n}\n\nfunction Get-AkeylessFolderChildren {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath\n return @($listing.Folders)\n}\n\nfunction Parse-AkeylessFieldDefinitions {\n param ([string]$RawValue)\n\n $fields = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $fields\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $name = $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($name)) {\n throw \"Unable to establish field name from: '$_'\"\n }\n $fields += [pscustomobject]@{\n Name = $name\n VariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $fields\n}\n\nfunction Parse-AkeylessSecretDefinitions {\n param ([string]$RawValue)\n\n $secrets = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $secrets\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $path = Normalize-AkeylessPath $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($path)) {\n throw \"Unable to establish secret path from: '$_'\"\n }\n $secrets += [pscustomobject]@{\n Path = $path\n OutputVariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $secrets\n}\n\nfunction Get-AkeylessSecretsRecursively {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [bool]$Recursive\n )\n\n $results = @()\n $folder = Normalize-AkeylessPath $FolderPath\n\n $items = Get-AkeylessFolderItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($item in $items) {\n $results += $item\n }\n\n if ($Recursive) {\n $children = Get-AkeylessFolderChildren -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($child in $children) {\n $results += Get-AkeylessSecretsRecursively -GatewayUrl $GatewayUrl -Token $Token -FolderPath $child -Recursive $true\n }\n }\n\n return $results\n}\n\nfunction Complete-AkeylessLogin {\n param (\n [string]$GatewayUrl,\n [hashtable]$AuthBody,\n [string]$StepName\n )\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'auth' -Body $AuthBody\n if ($null -eq $response -or [string]::IsNullOrWhiteSpace($response.token)) {\n throw 'Authentication succeeded but no token was returned'\n }\n\n Set-AkeylessSensitiveOutput -Name 'AkeylessAuthToken' -Value $response.token -StepName $StepName -PrintVariableNames $true\n Write-Host 'Authenticated to Akeyless successfully'\n}\n\nfunction Publish-AkeylessStructuredSecretResponse {\n param (\n [object]$Response,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @()\n )\n\n $created = 0\n if ($Response -is [pscustomobject] -or $Response -is [hashtable]) {\n $properties = if ($Response -is [hashtable]) { @($Response.Keys) } else { @($Response.PSObject.Properties.Name) }\n if ($Fields.Count -gt 0) {\n foreach ($field in $Fields) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$field.Name] } else { $Response.$($field.Name) }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $field.Name -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n $useSingleOutputName = (-not [string]::IsNullOrWhiteSpace($OutputVariableName)) -and ($properties.Count -eq 1)\n foreach ($fieldName in $properties) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$fieldName] } else { $Response.$fieldName }\n if ($null -ne $fieldValue) {\n $overrideName = if ($useSingleOutputName) { $OutputVariableName } else { '' }\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $overrideName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n }\n else {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$Response) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created = 1\n }\n\n return $created\n}\n\nfunction Get-AwsHmacSha256Bytes {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n $hmac = New-Object System.Security.Cryptography.HMACSHA256 (, $Key)\n return $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Message))\n}\n\nfunction Get-AwsHmacSha256Hex {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n return ([BitConverter]::ToString((Get-AwsHmacSha256Bytes -Key $Key -Message $Message))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsSigningKey {\n param (\n [string]$SecretKey,\n [string]$DateStamp,\n [string]$Region,\n [string]$Service\n )\n\n $kSecret = [Text.Encoding]::UTF8.GetBytes(\"AWS4$SecretKey\")\n $kDate = Get-AwsHmacSha256Bytes -Key $kSecret -Message $DateStamp\n $kRegion = Get-AwsHmacSha256Bytes -Key $kDate -Message $Region\n $kService = Get-AwsHmacSha256Bytes -Key $kRegion -Message $Service\n return Get-AwsHmacSha256Bytes -Key $kService -Message 'aws4_request'\n}\n\nfunction Get-AwsSha256Hex {\n param ([string]$Text)\n\n $sha = [System.Security.Cryptography.SHA256]::Create()\n return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsCredentialChain {\n $accessKeyId = $env:AWS_ACCESS_KEY_ID\n $secretAccessKey = $env:AWS_SECRET_ACCESS_KEY\n $sessionToken = $env:AWS_SESSION_TOKEN\n\n if (-not [string]::IsNullOrWhiteSpace($accessKeyId) -and -not [string]::IsNullOrWhiteSpace($secretAccessKey)) {\n return [pscustomobject]@{\n AccessKeyId = $accessKeyId.Trim()\n SecretAccessKey = $secretAccessKey.Trim()\n SessionToken = if ([string]::IsNullOrWhiteSpace($sessionToken)) { '' } else { $sessionToken.Trim() }\n }\n }\n\n try {\n $imdsToken = Invoke-RestMethod -Method Put -Uri 'http://169.254.169.254/latest/api/token' -Headers @{ 'X-aws-ec2-metadata-token-ttl-seconds' = '21600' } -TimeoutSec 2\n $roleName = Invoke-RestMethod -Uri 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n $roleCreds = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$roleName\" -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n if ($null -eq $roleCreds -or [string]::IsNullOrWhiteSpace($roleCreds.AccessKeyId)) {\n throw 'EC2 instance metadata returned no IAM credentials'\n }\n\n return [pscustomobject]@{\n AccessKeyId = [string]$roleCreds.AccessKeyId\n SecretAccessKey = [string]$roleCreds.SecretAccessKey\n SessionToken = [string]$roleCreds.Token\n }\n }\n catch {\n throw \"AWS credentials were not found in environment variables or EC2 instance metadata: $($_.Exception.Message)\"\n }\n}\n\nfunction New-AwsIamCloudId {\n param (\n [string]$AccessKeyId,\n [string]$SecretAccessKey,\n [string]$SessionToken = '',\n [string]$Region = 'us-east-1',\n [string]$StsUrl = 'https://sts.amazonaws.com/'\n )\n\n $service = 'sts'\n $method = 'POST'\n $hostName = ([Uri]$StsUrl).Host\n $body = 'Action=GetCallerIdentity&Version=2011-06-15'\n $amzDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')\n $dateStamp = $amzDate.Substring(0, 8)\n $payloadHash = Get-AwsSha256Hex -Text $body\n\n $headers = [ordered]@{\n Host = $hostName\n 'Content-Type' = 'application/x-www-form-urlencoded; charset=utf-8'\n 'Content-Length' = [string]$body.Length\n 'X-Amz-Date' = $amzDate\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $headers['X-Amz-Security-Token'] = $SessionToken\n }\n\n $canonicalHeaders = ($headers.GetEnumerator() | ForEach-Object { \"$($_.Key.ToLowerInvariant()):$($_.Value)\" }) -join \"`n\"\n $signedHeaders = (($headers.Keys | ForEach-Object { $_.ToLowerInvariant() }) | Sort-Object) -join ';'\n $canonicalRequest = @(\n $method\n '/'\n ''\n \"$canonicalHeaders`n\"\n $signedHeaders\n $payloadHash\n ) -join \"`n\"\n\n $credentialScope = \"$dateStamp/$Region/$service/aws4_request\"\n $stringToSign = @(\n 'AWS4-HMAC-SHA256'\n $amzDate\n $credentialScope\n (Get-AwsSha256Hex -Text $canonicalRequest)\n ) -join \"`n\"\n\n $signingKey = Get-AwsSigningKey -SecretKey $SecretAccessKey -DateStamp $dateStamp -Region $Region -Service $service\n $signature = Get-AwsHmacSha256Hex -Key $signingKey -Message $stringToSign\n $authorization = \"AWS4-HMAC-SHA256 Credential=$AccessKeyId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature\"\n\n $requestHeaders = @{\n Authorization = @($authorization)\n 'Content-Length' = @([string]$body.Length)\n Host = @($hostName)\n 'Content-Type' = @('application/x-www-form-urlencoded; charset=utf-8')\n 'X-Amz-Date' = @($amzDate)\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $requestHeaders['X-Amz-Security-Token'] = @($SessionToken)\n }\n\n $payload = [ordered]@{\n sts_request_method = $method\n sts_request_url = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($StsUrl))\n sts_request_body = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body))\n sts_request_headers = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($requestHeaders | ConvertTo-Json -Compress)))\n }\n\n return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Compress)))\n}\n\nfunction Resolve-AwsIamCloudId {\n param (\n [string]$CloudId,\n [string]$Region,\n [string]$StsUrl\n )\n\n if (-not [string]::IsNullOrWhiteSpace($CloudId)) {\n return $CloudId.Trim()\n }\n\n $credentials = Get-AwsCredentialChain\n return New-AwsIamCloudId -AccessKeyId $credentials.AccessKeyId -SecretAccessKey $credentials.SecretAccessKey -SessionToken $credentials.SessionToken -Region $Region -StsUrl $StsUrl\n}\n\n$GATEWAY_URL = $OctopusParameters['Akeyless.Retrieve.Dynamic.GatewayUrl']\n$AUTH_TOKEN = $OctopusParameters['Akeyless.Retrieve.Dynamic.AuthToken']\n$SECRET_PATH = $OctopusParameters['Akeyless.Retrieve.Dynamic.SecretPath']\n$OUTPUT_VARIABLE_NAME = $OctopusParameters['Akeyless.Retrieve.Dynamic.OutputVariableName']\n$TIMEOUT_TEXT = $OctopusParameters['Akeyless.Retrieve.Dynamic.Timeout']\n$DYNAMIC_ARGS = $OctopusParameters['Akeyless.Retrieve.Dynamic.DynamicSecretArgs']\n$FIELD_VALUES = $OctopusParameters['Akeyless.Retrieve.Dynamic.FieldValues']\n$PRINT_VARIABLE_NAMES = $OctopusParameters['Akeyless.Retrieve.Dynamic.PrintVariableNames']\n$StepName = $OctopusParameters['Octopus.Step.Name']\n\nif ([string]::IsNullOrWhiteSpace($GATEWAY_URL)) {\n $GATEWAY_URL = 'https://api.akeyless.io'\n}\nif ([string]::IsNullOrWhiteSpace($AUTH_TOKEN)) {\n throw 'Required parameter Auth Token not specified'\n}\nif ([string]::IsNullOrWhiteSpace($SECRET_PATH)) {\n throw 'Required parameter Secret Path not specified'\n}\nif ([string]::IsNullOrWhiteSpace($PRINT_VARIABLE_NAMES)) {\n $PRINT_VARIABLE_NAMES = 'False'\n}\n\n$printNames = ($PRINT_VARIABLE_NAMES -eq 'True')\n$path = Normalize-AkeylessPath $SECRET_PATH\n$body = @{\n token = $AUTH_TOKEN\n name = $path\n}\n\nif (-not [string]::IsNullOrWhiteSpace($TIMEOUT_TEXT)) {\n $timeout = 0\n if (-not [int]::TryParse($TIMEOUT_TEXT, [ref]$timeout)) {\n throw \"Timeout must be an integer, got '$TIMEOUT_TEXT'\"\n }\n if ($timeout -gt 0) {\n $body.timeout = $timeout\n }\n}\n\n$argsList = @()\nif (-not [string]::IsNullOrWhiteSpace($DYNAMIC_ARGS)) {\n $argsList = @(($DYNAMIC_ARGS -Split \"`n\").Trim() | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })\n if ($argsList.Count -gt 0) {\n $body.args = $argsList\n }\n}\n\n$response = Invoke-AkeylessApi -GatewayUrl $GATEWAY_URL -Path 'get-dynamic-secret-value' -Body $body\nif ($null -eq $response) {\n throw \"Empty response retrieving dynamic secret '$path'\"\n}\n\n$fields = Parse-AkeylessFieldDefinitions -RawValue $FIELD_VALUES\n$created = Publish-AkeylessStructuredSecretResponse -Response $response -SecretPath $path -StepName $StepName -PrintVariableNames $printNames -OutputVariableName $OUTPUT_VARIABLE_NAME -Fields $fields\nWrite-Host \"Created $created output variables\"" + }, + "Parameters": [ + { + "HelpText": "The Akeyless API or Gateway URL. For SaaS, use https://api.akeyless.io.", + "Id": "10003000-0000-0000-0000-100030001001", + "Label": "Gateway URL", + "DefaultValue": "https://api.akeyless.io", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Dynamic.GatewayUrl" + }, + { + "HelpText": "Authentication token from a previous Akeyless login step.", + "Id": "10003000-0000-0000-0000-100030001002", + "Label": "Auth Token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Name": "Akeyless.Retrieve.Dynamic.AuthToken" + }, + { + "HelpText": "Full path to the dynamic secret, e.g. `/production/database/dynamic`", + "Id": "10003000-0000-0000-0000-100030001003", + "Label": "Secret path", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Dynamic.SecretPath" + }, + { + "HelpText": "Optional output variable name when the dynamic secret returns a single value.", + "Id": "10003000-0000-0000-0000-100030001004", + "Label": "Output variable name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Dynamic.OutputVariableName" + }, + { + "HelpText": "Optional timeout for dynamic secret provisioning.", + "Id": "10003000-0000-0000-0000-100030001005", + "Label": "Timeout (seconds)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Dynamic.Timeout" + }, + { + "HelpText": "Optional provisioning arguments, one per line.", + "Id": "10003000-0000-0000-0000-100030001006", + "Label": "Dynamic secret args", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Name": "Akeyless.Retrieve.Dynamic.DynamicSecretArgs" + }, + { + "HelpText": "Choose specific response fields in the format FieldName | OutputVariableName.", + "Id": "10003000-0000-0000-0000-100030001007", + "Label": "Field names", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Name": "Akeyless.Retrieve.Dynamic.FieldValues" + }, + { + "HelpText": "Write created output variable names to the task log.", + "Id": "10003000-0000-0000-0000-100030001008", + "Label": "Print output variable names", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Name": "Akeyless.Retrieve.Dynamic.PrintVariableNames" + } + ], + "LastModifiedBy": "akeyless-community", + "LastModifiedAt": "2026-06-17T04:32:01.621Z", + "$Meta": { + "ExportedAt": "2026-06-17T04:32:01.621Z", + "OctopusVersion": "2024.4.0", + "Type": "ActionTemplate" + }, + "Category": "akeyless" +} diff --git a/step-templates/akeyless-retrieve-rotated-secret.json b/step-templates/akeyless-retrieve-rotated-secret.json new file mode 100644 index 000000000..0e283f9aa --- /dev/null +++ b/step-templates/akeyless-retrieve-rotated-secret.json @@ -0,0 +1,104 @@ +{ + "Id": "f8e3a1b2-4c5d-6e7f-8a9b-0c1d2e3f4a06", + "Name": "Akeyless - Retrieve Rotated Secret", + "Description": "This step retrieves a **rotated secret** from Akeyless and creates sensitive [output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for use in later deployment or runbook steps.\n\nThis step template uses the [Akeyless REST API](https://docs.akeyless.io/reference/getrotatedsecretvalue), so no other dependencies are needed.\n\n---\n\n**Authentication token**\n\nUse any Akeyless login step template first, then bind the Auth Token parameter to:\n\n#{Octopus.Action[].Output.AkeylessAuthToken}\n\n---\n\n**Linked targets**\n\nFor rotated secrets associated with linked targets, set **Host** to the target host name.\n\n---\n\n**Required:**\n- Authentication token\n- Rotated secret path", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nfunction Get-AkeylessApiErrorBody {\n param ($RequestError)\n\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $rawResponse = $reader.ReadToEnd()\n try { return ($rawResponse | ConvertFrom-Json) } catch { return $rawResponse }\n }\n return $null\n }\n\n return $RequestError.ErrorDetails.Message\n}\n\nfunction Format-AkeylessApiError {\n param (\n [string]$Action,\n [System.Management.Automation.ErrorRecord]$ErrorRecord\n )\n\n $message = \"An error occurred during $Action`: $($ErrorRecord.Exception.Message)\"\n $body = Get-AkeylessApiErrorBody -RequestError $ErrorRecord\n if ($null -ne $body) {\n if ($body.error) {\n $message += \"`n`tDetail: $($body.error)\"\n }\n elseif ($body.PSObject.Properties.Name -contains 'errors') {\n $message += \"`n`tDetail: $($body.errors -Join ',')\"\n }\n elseif ($body -is [string] -and -not [string]::IsNullOrWhiteSpace($body)) {\n $message += \"`n`tDetail: $body\"\n }\n }\n\n return $message\n}\n\nfunction Invoke-AkeylessApi {\n param (\n [Parameter(Mandatory = $true)][string]$GatewayUrl,\n [Parameter(Mandatory = $true)][string]$Path,\n [Parameter(Mandatory = $true)][hashtable]$Body\n )\n\n $base = $GatewayUrl.TrimEnd('/')\n $apiPath = $Path.TrimStart('/')\n $uri = \"$base/$apiPath\"\n $json = $Body | ConvertTo-Json -Depth 20 -Compress:$false\n\n try {\n return Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType 'application/json'\n }\n catch {\n $detail = Format-AkeylessApiError -Action \"POST $Path\" -ErrorRecord $_\n Write-Error $detail -Category ConnectionError\n }\n}\n\nfunction Normalize-AkeylessPath {\n param ([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) {\n return $Path\n }\n\n $normalized = $Path.Trim()\n if (-not $normalized.StartsWith('/')) {\n $normalized = \"/$normalized\"\n }\n\n return $normalized\n}\n\nfunction ConvertTo-AkeylessOutputVariableName {\n param (\n [string]$SecretPath,\n [string]$FieldName = '',\n [string]$OverrideName = ''\n )\n\n if (-not [string]::IsNullOrWhiteSpace($OverrideName)) {\n return $OverrideName.Trim()\n }\n\n $base = (Normalize-AkeylessPath $SecretPath).Trim('/').Replace('/', '.')\n if ([string]::IsNullOrWhiteSpace($FieldName)) {\n return $base\n }\n\n return \"$base.$($FieldName.Trim())\"\n}\n\nfunction Get-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [int]$Version = 0\n )\n\n $path = Normalize-AkeylessPath $SecretPath\n $body = @{\n token = $Token\n names = @($path)\n }\n\n if ($Version -gt 0) {\n $body.version = $Version\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'get-secret-value' -Body $body\n if ($null -eq $response) {\n throw \"Empty response retrieving secret '$path'\"\n }\n\n if ($response.PSObject.Properties.Name -contains $path) {\n return $response.$path\n }\n\n foreach ($property in $response.PSObject.Properties) {\n if (-not [string]::IsNullOrWhiteSpace([string]$property.Value)) {\n return $property.Value\n }\n }\n\n throw \"Secret '$path' was not found in the API response\"\n}\n\nfunction Set-AkeylessSensitiveOutput {\n param (\n [string]$Name,\n [string]$Value,\n [string]$StepName,\n [bool]$PrintVariableNames\n )\n\n if ([string]::IsNullOrWhiteSpace($Name)) {\n throw 'Output variable name cannot be empty'\n }\n\n Set-OctopusVariable -Name $Name -Value $Value -Sensitive\n if ($PrintVariableNames) {\n Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$Name}\"\n }\n}\n\nfunction Publish-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @(),\n [int]$Version = 0\n )\n\n $raw = Get-AkeylessSecretValue -GatewayUrl $GatewayUrl -Token $Token -SecretPath $SecretPath -Version $Version\n $created = 0\n $fieldsSpecified = ($Fields.Count -gt 0)\n\n if ($raw -is [string]) {\n $trimmed = $raw.Trim()\n if ($fieldsSpecified) {\n $parsed = $null\n try { $parsed = $trimmed | ConvertFrom-Json } catch {}\n if ($null -eq $parsed) {\n throw \"Secret '$SecretPath' is not JSON but field names were specified\"\n }\n $raw = $parsed\n }\n }\n\n if ($raw -is [pscustomobject] -or $raw -is [hashtable]) {\n $properties = if ($raw -is [hashtable]) { $raw.Keys } else { $raw.PSObject.Properties.Name }\n\n if ($fieldsSpecified) {\n foreach ($field in $Fields) {\n $fieldName = $field.Name\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n foreach ($fieldName in $properties) {\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n\n return $created\n }\n\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$raw) -StepName $StepName -PrintVariableNames $PrintVariableNames\n return 1\n}\n\nfunction Invoke-AkeylessListItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @()\n )\n\n $path = Normalize-AkeylessPath $FolderPath\n $items = @()\n $folders = @()\n $paginationToken = ''\n\n do {\n $body = @{\n token = $Token\n path = $path\n 'current-folder' = $true\n }\n\n if ($Types.Count -gt 0) {\n $body.type = $Types\n }\n if (-not [string]::IsNullOrWhiteSpace($paginationToken)) {\n $body.'pagination-token' = $paginationToken\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'list-items' -Body $body\n if ($null -eq $response) {\n break\n }\n\n if ($null -ne $response.items) {\n $items += @($response.items | ForEach-Object { $_.item_name })\n }\n if ($null -ne $response.folders) {\n $folders += @($response.folders)\n }\n\n $paginationToken = ''\n if ($response.PSObject.Properties.Name -contains 'next_page') {\n $paginationToken = [string]$response.next_page\n }\n } while (-not [string]::IsNullOrWhiteSpace($paginationToken))\n\n return [pscustomobject]@{\n Items = $items\n Folders = $folders\n }\n}\n\nfunction Get-AkeylessFolderItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @('static-secret')\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath -Types $Types\n return @($listing.Items)\n}\n\nfunction Get-AkeylessFolderChildren {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath\n return @($listing.Folders)\n}\n\nfunction Parse-AkeylessFieldDefinitions {\n param ([string]$RawValue)\n\n $fields = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $fields\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $name = $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($name)) {\n throw \"Unable to establish field name from: '$_'\"\n }\n $fields += [pscustomobject]@{\n Name = $name\n VariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $fields\n}\n\nfunction Parse-AkeylessSecretDefinitions {\n param ([string]$RawValue)\n\n $secrets = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $secrets\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $path = Normalize-AkeylessPath $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($path)) {\n throw \"Unable to establish secret path from: '$_'\"\n }\n $secrets += [pscustomobject]@{\n Path = $path\n OutputVariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $secrets\n}\n\nfunction Get-AkeylessSecretsRecursively {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [bool]$Recursive\n )\n\n $results = @()\n $folder = Normalize-AkeylessPath $FolderPath\n\n $items = Get-AkeylessFolderItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($item in $items) {\n $results += $item\n }\n\n if ($Recursive) {\n $children = Get-AkeylessFolderChildren -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($child in $children) {\n $results += Get-AkeylessSecretsRecursively -GatewayUrl $GatewayUrl -Token $Token -FolderPath $child -Recursive $true\n }\n }\n\n return $results\n}\n\nfunction Complete-AkeylessLogin {\n param (\n [string]$GatewayUrl,\n [hashtable]$AuthBody,\n [string]$StepName\n )\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'auth' -Body $AuthBody\n if ($null -eq $response -or [string]::IsNullOrWhiteSpace($response.token)) {\n throw 'Authentication succeeded but no token was returned'\n }\n\n Set-AkeylessSensitiveOutput -Name 'AkeylessAuthToken' -Value $response.token -StepName $StepName -PrintVariableNames $true\n Write-Host 'Authenticated to Akeyless successfully'\n}\n\nfunction Publish-AkeylessStructuredSecretResponse {\n param (\n [object]$Response,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @()\n )\n\n $created = 0\n if ($Response -is [pscustomobject] -or $Response -is [hashtable]) {\n $properties = if ($Response -is [hashtable]) { @($Response.Keys) } else { @($Response.PSObject.Properties.Name) }\n if ($Fields.Count -gt 0) {\n foreach ($field in $Fields) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$field.Name] } else { $Response.$($field.Name) }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $field.Name -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n $useSingleOutputName = (-not [string]::IsNullOrWhiteSpace($OutputVariableName)) -and ($properties.Count -eq 1)\n foreach ($fieldName in $properties) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$fieldName] } else { $Response.$fieldName }\n if ($null -ne $fieldValue) {\n $overrideName = if ($useSingleOutputName) { $OutputVariableName } else { '' }\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $overrideName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n }\n else {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$Response) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created = 1\n }\n\n return $created\n}\n\nfunction Get-AwsHmacSha256Bytes {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n $hmac = New-Object System.Security.Cryptography.HMACSHA256 (, $Key)\n return $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Message))\n}\n\nfunction Get-AwsHmacSha256Hex {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n return ([BitConverter]::ToString((Get-AwsHmacSha256Bytes -Key $Key -Message $Message))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsSigningKey {\n param (\n [string]$SecretKey,\n [string]$DateStamp,\n [string]$Region,\n [string]$Service\n )\n\n $kSecret = [Text.Encoding]::UTF8.GetBytes(\"AWS4$SecretKey\")\n $kDate = Get-AwsHmacSha256Bytes -Key $kSecret -Message $DateStamp\n $kRegion = Get-AwsHmacSha256Bytes -Key $kDate -Message $Region\n $kService = Get-AwsHmacSha256Bytes -Key $kRegion -Message $Service\n return Get-AwsHmacSha256Bytes -Key $kService -Message 'aws4_request'\n}\n\nfunction Get-AwsSha256Hex {\n param ([string]$Text)\n\n $sha = [System.Security.Cryptography.SHA256]::Create()\n return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsCredentialChain {\n $accessKeyId = $env:AWS_ACCESS_KEY_ID\n $secretAccessKey = $env:AWS_SECRET_ACCESS_KEY\n $sessionToken = $env:AWS_SESSION_TOKEN\n\n if (-not [string]::IsNullOrWhiteSpace($accessKeyId) -and -not [string]::IsNullOrWhiteSpace($secretAccessKey)) {\n return [pscustomobject]@{\n AccessKeyId = $accessKeyId.Trim()\n SecretAccessKey = $secretAccessKey.Trim()\n SessionToken = if ([string]::IsNullOrWhiteSpace($sessionToken)) { '' } else { $sessionToken.Trim() }\n }\n }\n\n try {\n $imdsToken = Invoke-RestMethod -Method Put -Uri 'http://169.254.169.254/latest/api/token' -Headers @{ 'X-aws-ec2-metadata-token-ttl-seconds' = '21600' } -TimeoutSec 2\n $roleName = Invoke-RestMethod -Uri 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n $roleCreds = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$roleName\" -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n if ($null -eq $roleCreds -or [string]::IsNullOrWhiteSpace($roleCreds.AccessKeyId)) {\n throw 'EC2 instance metadata returned no IAM credentials'\n }\n\n return [pscustomobject]@{\n AccessKeyId = [string]$roleCreds.AccessKeyId\n SecretAccessKey = [string]$roleCreds.SecretAccessKey\n SessionToken = [string]$roleCreds.Token\n }\n }\n catch {\n throw \"AWS credentials were not found in environment variables or EC2 instance metadata: $($_.Exception.Message)\"\n }\n}\n\nfunction New-AwsIamCloudId {\n param (\n [string]$AccessKeyId,\n [string]$SecretAccessKey,\n [string]$SessionToken = '',\n [string]$Region = 'us-east-1',\n [string]$StsUrl = 'https://sts.amazonaws.com/'\n )\n\n $service = 'sts'\n $method = 'POST'\n $hostName = ([Uri]$StsUrl).Host\n $body = 'Action=GetCallerIdentity&Version=2011-06-15'\n $amzDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')\n $dateStamp = $amzDate.Substring(0, 8)\n $payloadHash = Get-AwsSha256Hex -Text $body\n\n $headers = [ordered]@{\n Host = $hostName\n 'Content-Type' = 'application/x-www-form-urlencoded; charset=utf-8'\n 'Content-Length' = [string]$body.Length\n 'X-Amz-Date' = $amzDate\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $headers['X-Amz-Security-Token'] = $SessionToken\n }\n\n $canonicalHeaders = ($headers.GetEnumerator() | ForEach-Object { \"$($_.Key.ToLowerInvariant()):$($_.Value)\" }) -join \"`n\"\n $signedHeaders = (($headers.Keys | ForEach-Object { $_.ToLowerInvariant() }) | Sort-Object) -join ';'\n $canonicalRequest = @(\n $method\n '/'\n ''\n \"$canonicalHeaders`n\"\n $signedHeaders\n $payloadHash\n ) -join \"`n\"\n\n $credentialScope = \"$dateStamp/$Region/$service/aws4_request\"\n $stringToSign = @(\n 'AWS4-HMAC-SHA256'\n $amzDate\n $credentialScope\n (Get-AwsSha256Hex -Text $canonicalRequest)\n ) -join \"`n\"\n\n $signingKey = Get-AwsSigningKey -SecretKey $SecretAccessKey -DateStamp $dateStamp -Region $Region -Service $service\n $signature = Get-AwsHmacSha256Hex -Key $signingKey -Message $stringToSign\n $authorization = \"AWS4-HMAC-SHA256 Credential=$AccessKeyId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature\"\n\n $requestHeaders = @{\n Authorization = @($authorization)\n 'Content-Length' = @([string]$body.Length)\n Host = @($hostName)\n 'Content-Type' = @('application/x-www-form-urlencoded; charset=utf-8')\n 'X-Amz-Date' = @($amzDate)\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $requestHeaders['X-Amz-Security-Token'] = @($SessionToken)\n }\n\n $payload = [ordered]@{\n sts_request_method = $method\n sts_request_url = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($StsUrl))\n sts_request_body = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body))\n sts_request_headers = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($requestHeaders | ConvertTo-Json -Compress)))\n }\n\n return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Compress)))\n}\n\nfunction Resolve-AwsIamCloudId {\n param (\n [string]$CloudId,\n [string]$Region,\n [string]$StsUrl\n )\n\n if (-not [string]::IsNullOrWhiteSpace($CloudId)) {\n return $CloudId.Trim()\n }\n\n $credentials = Get-AwsCredentialChain\n return New-AwsIamCloudId -AccessKeyId $credentials.AccessKeyId -SecretAccessKey $credentials.SecretAccessKey -SessionToken $credentials.SessionToken -Region $Region -StsUrl $StsUrl\n}\n\n$GATEWAY_URL = $OctopusParameters['Akeyless.Retrieve.Rotated.GatewayUrl']\n$AUTH_TOKEN = $OctopusParameters['Akeyless.Retrieve.Rotated.AuthToken']\n$SECRET_PATH = $OctopusParameters['Akeyless.Retrieve.Rotated.SecretPath']\n$OUTPUT_VARIABLE_NAME = $OctopusParameters['Akeyless.Retrieve.Rotated.OutputVariableName']\n$HOST_NAME = $OctopusParameters['Akeyless.Retrieve.Rotated.Host']\n$VERSION_TEXT = $OctopusParameters['Akeyless.Retrieve.Rotated.Version']\n$FIELD_VALUES = $OctopusParameters['Akeyless.Retrieve.Rotated.FieldValues']\n$PRINT_VARIABLE_NAMES = $OctopusParameters['Akeyless.Retrieve.Rotated.PrintVariableNames']\n$StepName = $OctopusParameters['Octopus.Step.Name']\n\nif ([string]::IsNullOrWhiteSpace($GATEWAY_URL)) {\n $GATEWAY_URL = 'https://api.akeyless.io'\n}\nif ([string]::IsNullOrWhiteSpace($AUTH_TOKEN)) {\n throw 'Required parameter Auth Token not specified'\n}\nif ([string]::IsNullOrWhiteSpace($SECRET_PATH)) {\n throw 'Required parameter Secret Path not specified'\n}\nif ([string]::IsNullOrWhiteSpace($PRINT_VARIABLE_NAMES)) {\n $PRINT_VARIABLE_NAMES = 'False'\n}\n\n$printNames = ($PRINT_VARIABLE_NAMES -eq 'True')\n$path = Normalize-AkeylessPath $SECRET_PATH\n$body = @{\n token = $AUTH_TOKEN\n names = $path\n}\n\nif (-not [string]::IsNullOrWhiteSpace($HOST_NAME)) {\n $body.host = $HOST_NAME.Trim()\n}\n\nif (-not [string]::IsNullOrWhiteSpace($VERSION_TEXT)) {\n $version = 0\n if (-not [int]::TryParse($VERSION_TEXT, [ref]$version)) {\n throw \"Version must be an integer, got '$VERSION_TEXT'\"\n }\n if ($version -gt 0) {\n $body.version = $version\n }\n}\n\n$response = Invoke-AkeylessApi -GatewayUrl $GATEWAY_URL -Path 'get-rotated-secret-value' -Body $body\nif ($null -eq $response) {\n throw \"Empty response retrieving rotated secret '$path'\"\n}\n\n$fields = Parse-AkeylessFieldDefinitions -RawValue $FIELD_VALUES\n$created = Publish-AkeylessStructuredSecretResponse -Response $response -SecretPath $path -StepName $StepName -PrintVariableNames $printNames -OutputVariableName $OUTPUT_VARIABLE_NAME -Fields $fields\nWrite-Host \"Created $created output variables\"" + }, + "Parameters": [ + { + "HelpText": "The Akeyless API or Gateway URL. For SaaS, use https://api.akeyless.io.", + "Id": "10006000-0000-0000-0000-100060001001", + "Label": "Gateway URL", + "DefaultValue": "https://api.akeyless.io", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Rotated.GatewayUrl" + }, + { + "HelpText": "Authentication token from a previous Akeyless login step.", + "Id": "10006000-0000-0000-0000-100060001002", + "Label": "Auth Token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Name": "Akeyless.Retrieve.Rotated.AuthToken" + }, + { + "HelpText": "Full path to the rotated secret, e.g. `/production/database/rotated`", + "Id": "10006000-0000-0000-0000-100060001003", + "Label": "Secret path", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Rotated.SecretPath" + }, + { + "HelpText": "Optional output variable name when the rotated secret returns a single value.", + "Id": "10006000-0000-0000-0000-100060001004", + "Label": "Output variable name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Rotated.OutputVariableName" + }, + { + "HelpText": "Optional linked-target host name.", + "Id": "10006000-0000-0000-0000-100060001005", + "Label": "Host", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Rotated.Host" + }, + { + "HelpText": "Optional rotated secret version.", + "Id": "10006000-0000-0000-0000-100060001006", + "Label": "Secret version", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Rotated.Version" + }, + { + "HelpText": "Choose specific response fields in the format FieldName | OutputVariableName.", + "Id": "10006000-0000-0000-0000-100060001007", + "Label": "Field names", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Name": "Akeyless.Retrieve.Rotated.FieldValues" + }, + { + "HelpText": "Write created output variable names to the task log.", + "Id": "10006000-0000-0000-0000-100060001008", + "Label": "Print output variable names", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Name": "Akeyless.Retrieve.Rotated.PrintVariableNames" + } + ], + "LastModifiedBy": "akeyless-community", + "LastModifiedAt": "2026-06-17T04:32:01.636Z", + "$Meta": { + "ExportedAt": "2026-06-17T04:32:01.636Z", + "OctopusVersion": "2024.4.0", + "Type": "ActionTemplate" + }, + "Category": "akeyless" +} diff --git a/step-templates/akeyless-retrieve-static-secrets.json b/step-templates/akeyless-retrieve-static-secrets.json new file mode 100644 index 000000000..d3d0dbd91 --- /dev/null +++ b/step-templates/akeyless-retrieve-static-secrets.json @@ -0,0 +1,115 @@ +{ + "Id": "f8e3a1b2-4c5d-6e7f-8a9b-0c1d2e3f4a02", + "Name": "Akeyless - Retrieve Static Secrets", + "Description": "This step retrieves one or more **static secrets** from Akeyless and creates sensitive [output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for use in later deployment or runbook steps.\n\nThis step template uses the [Akeyless REST API](https://docs.akeyless.io/reference/getsecretvalue), so no other dependencies are needed.\n\n---\n\n**Authentication token**\n\nUse the **Akeyless - Access Key Login** step template first, then bind the Auth Token parameter to:\n\n#{Octopus.Action[].Output.AkeylessAuthToken}\n\n---\n\n**Secret paths**\n\nSpecify secrets one per line in the format:\n\nSecretPath | OutputVariableName\n\nExamples:\n\n/production/database/password\n/production/database/password | DatabasePassword\n\nIf OutputVariableName is omitted, the variable name is derived from the secret path.\n\n---\n\n**Folder retrieval**\n\nChoose retrieval method **Folder** to list static secrets under an Akeyless folder. Enable **Recursive retrieval** to include subfolders.\n\n---\n\n**JSON secrets**\n\nFor generic JSON secrets, optionally choose specific fields in the format:\n\nFieldName | OutputVariableName\n\nIf field names are omitted, all top-level JSON fields are exported as separate output variables.\n\n---\n\n**Required:**\n- Authentication token\n- Secret paths, or a folder path when using folder retrieval", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nfunction Get-AkeylessApiErrorBody {\n param ($RequestError)\n\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $rawResponse = $reader.ReadToEnd()\n try { return ($rawResponse | ConvertFrom-Json) } catch { return $rawResponse }\n }\n return $null\n }\n\n return $RequestError.ErrorDetails.Message\n}\n\nfunction Format-AkeylessApiError {\n param (\n [string]$Action,\n [System.Management.Automation.ErrorRecord]$ErrorRecord\n )\n\n $message = \"An error occurred during $Action`: $($ErrorRecord.Exception.Message)\"\n $body = Get-AkeylessApiErrorBody -RequestError $ErrorRecord\n if ($null -ne $body) {\n if ($body.error) {\n $message += \"`n`tDetail: $($body.error)\"\n }\n elseif ($body.PSObject.Properties.Name -contains 'errors') {\n $message += \"`n`tDetail: $($body.errors -Join ',')\"\n }\n elseif ($body -is [string] -and -not [string]::IsNullOrWhiteSpace($body)) {\n $message += \"`n`tDetail: $body\"\n }\n }\n\n return $message\n}\n\nfunction Invoke-AkeylessApi {\n param (\n [Parameter(Mandatory = $true)][string]$GatewayUrl,\n [Parameter(Mandatory = $true)][string]$Path,\n [Parameter(Mandatory = $true)][hashtable]$Body\n )\n\n $base = $GatewayUrl.TrimEnd('/')\n $apiPath = $Path.TrimStart('/')\n $uri = \"$base/$apiPath\"\n $json = $Body | ConvertTo-Json -Depth 20 -Compress:$false\n\n try {\n return Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType 'application/json'\n }\n catch {\n $detail = Format-AkeylessApiError -Action \"POST $Path\" -ErrorRecord $_\n Write-Error $detail -Category ConnectionError\n }\n}\n\nfunction Normalize-AkeylessPath {\n param ([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) {\n return $Path\n }\n\n $normalized = $Path.Trim()\n if (-not $normalized.StartsWith('/')) {\n $normalized = \"/$normalized\"\n }\n\n return $normalized\n}\n\nfunction ConvertTo-AkeylessOutputVariableName {\n param (\n [string]$SecretPath,\n [string]$FieldName = '',\n [string]$OverrideName = ''\n )\n\n if (-not [string]::IsNullOrWhiteSpace($OverrideName)) {\n return $OverrideName.Trim()\n }\n\n $base = (Normalize-AkeylessPath $SecretPath).Trim('/').Replace('/', '.')\n if ([string]::IsNullOrWhiteSpace($FieldName)) {\n return $base\n }\n\n return \"$base.$($FieldName.Trim())\"\n}\n\nfunction Get-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [int]$Version = 0\n )\n\n $path = Normalize-AkeylessPath $SecretPath\n $body = @{\n token = $Token\n names = @($path)\n }\n\n if ($Version -gt 0) {\n $body.version = $Version\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'get-secret-value' -Body $body\n if ($null -eq $response) {\n throw \"Empty response retrieving secret '$path'\"\n }\n\n if ($response.PSObject.Properties.Name -contains $path) {\n return $response.$path\n }\n\n foreach ($property in $response.PSObject.Properties) {\n if (-not [string]::IsNullOrWhiteSpace([string]$property.Value)) {\n return $property.Value\n }\n }\n\n throw \"Secret '$path' was not found in the API response\"\n}\n\nfunction Set-AkeylessSensitiveOutput {\n param (\n [string]$Name,\n [string]$Value,\n [string]$StepName,\n [bool]$PrintVariableNames\n )\n\n if ([string]::IsNullOrWhiteSpace($Name)) {\n throw 'Output variable name cannot be empty'\n }\n\n Set-OctopusVariable -Name $Name -Value $Value -Sensitive\n if ($PrintVariableNames) {\n Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$Name}\"\n }\n}\n\nfunction Publish-AkeylessSecretValue {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @(),\n [int]$Version = 0\n )\n\n $raw = Get-AkeylessSecretValue -GatewayUrl $GatewayUrl -Token $Token -SecretPath $SecretPath -Version $Version\n $created = 0\n $fieldsSpecified = ($Fields.Count -gt 0)\n\n if ($raw -is [string]) {\n $trimmed = $raw.Trim()\n if ($fieldsSpecified) {\n $parsed = $null\n try { $parsed = $trimmed | ConvertFrom-Json } catch {}\n if ($null -eq $parsed) {\n throw \"Secret '$SecretPath' is not JSON but field names were specified\"\n }\n $raw = $parsed\n }\n }\n\n if ($raw -is [pscustomobject] -or $raw -is [hashtable]) {\n $properties = if ($raw -is [hashtable]) { $raw.Keys } else { $raw.PSObject.Properties.Name }\n\n if ($fieldsSpecified) {\n foreach ($field in $Fields) {\n $fieldName = $field.Name\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n foreach ($fieldName in $properties) {\n $fieldValue = if ($raw -is [hashtable]) { $raw[$fieldName] } else { $raw.$fieldName }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n\n return $created\n }\n\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$raw) -StepName $StepName -PrintVariableNames $PrintVariableNames\n return 1\n}\n\nfunction Invoke-AkeylessListItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @()\n )\n\n $path = Normalize-AkeylessPath $FolderPath\n $items = @()\n $folders = @()\n $paginationToken = ''\n\n do {\n $body = @{\n token = $Token\n path = $path\n 'current-folder' = $true\n }\n\n if ($Types.Count -gt 0) {\n $body.type = $Types\n }\n if (-not [string]::IsNullOrWhiteSpace($paginationToken)) {\n $body.'pagination-token' = $paginationToken\n }\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'list-items' -Body $body\n if ($null -eq $response) {\n break\n }\n\n if ($null -ne $response.items) {\n $items += @($response.items | ForEach-Object { $_.item_name })\n }\n if ($null -ne $response.folders) {\n $folders += @($response.folders)\n }\n\n $paginationToken = ''\n if ($response.PSObject.Properties.Name -contains 'next_page') {\n $paginationToken = [string]$response.next_page\n }\n } while (-not [string]::IsNullOrWhiteSpace($paginationToken))\n\n return [pscustomobject]@{\n Items = $items\n Folders = $folders\n }\n}\n\nfunction Get-AkeylessFolderItems {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [string[]]$Types = @('static-secret')\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath -Types $Types\n return @($listing.Items)\n}\n\nfunction Get-AkeylessFolderChildren {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath\n )\n\n $listing = Invoke-AkeylessListItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $FolderPath\n return @($listing.Folders)\n}\n\nfunction Parse-AkeylessFieldDefinitions {\n param ([string]$RawValue)\n\n $fields = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $fields\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $name = $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($name)) {\n throw \"Unable to establish field name from: '$_'\"\n }\n $fields += [pscustomobject]@{\n Name = $name\n VariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $fields\n}\n\nfunction Parse-AkeylessSecretDefinitions {\n param ([string]$RawValue)\n\n $secrets = @()\n if ([string]::IsNullOrWhiteSpace($RawValue)) {\n return $secrets\n }\n\n @(($RawValue -Split \"`n\").Trim()) | ForEach-Object {\n if ([string]::IsNullOrWhiteSpace($_)) { return }\n $parts = ($_ -Split '\\|', 2)\n $path = Normalize-AkeylessPath $parts[0].Trim()\n if ([string]::IsNullOrWhiteSpace($path)) {\n throw \"Unable to establish secret path from: '$_'\"\n }\n $secrets += [pscustomobject]@{\n Path = $path\n OutputVariableName = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }\n }\n }\n\n return $secrets\n}\n\nfunction Get-AkeylessSecretsRecursively {\n param (\n [string]$GatewayUrl,\n [string]$Token,\n [string]$FolderPath,\n [bool]$Recursive\n )\n\n $results = @()\n $folder = Normalize-AkeylessPath $FolderPath\n\n $items = Get-AkeylessFolderItems -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($item in $items) {\n $results += $item\n }\n\n if ($Recursive) {\n $children = Get-AkeylessFolderChildren -GatewayUrl $GatewayUrl -Token $Token -FolderPath $folder\n foreach ($child in $children) {\n $results += Get-AkeylessSecretsRecursively -GatewayUrl $GatewayUrl -Token $Token -FolderPath $child -Recursive $true\n }\n }\n\n return $results\n}\n\nfunction Complete-AkeylessLogin {\n param (\n [string]$GatewayUrl,\n [hashtable]$AuthBody,\n [string]$StepName\n )\n\n $response = Invoke-AkeylessApi -GatewayUrl $GatewayUrl -Path 'auth' -Body $AuthBody\n if ($null -eq $response -or [string]::IsNullOrWhiteSpace($response.token)) {\n throw 'Authentication succeeded but no token was returned'\n }\n\n Set-AkeylessSensitiveOutput -Name 'AkeylessAuthToken' -Value $response.token -StepName $StepName -PrintVariableNames $true\n Write-Host 'Authenticated to Akeyless successfully'\n}\n\nfunction Publish-AkeylessStructuredSecretResponse {\n param (\n [object]$Response,\n [string]$SecretPath,\n [string]$StepName,\n [bool]$PrintVariableNames,\n [string]$OutputVariableName = '',\n [array]$Fields = @()\n )\n\n $created = 0\n if ($Response -is [pscustomobject] -or $Response -is [hashtable]) {\n $properties = if ($Response -is [hashtable]) { @($Response.Keys) } else { @($Response.PSObject.Properties.Name) }\n if ($Fields.Count -gt 0) {\n foreach ($field in $Fields) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$field.Name] } else { $Response.$($field.Name) }\n if ($null -ne $fieldValue) {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $field.Name -OverrideName $field.VariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n else {\n $useSingleOutputName = (-not [string]::IsNullOrWhiteSpace($OutputVariableName)) -and ($properties.Count -eq 1)\n foreach ($fieldName in $properties) {\n $fieldValue = if ($Response -is [hashtable]) { $Response[$fieldName] } else { $Response.$fieldName }\n if ($null -ne $fieldValue) {\n $overrideName = if ($useSingleOutputName) { $OutputVariableName } else { '' }\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -FieldName $fieldName -OverrideName $overrideName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$fieldValue) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created++\n }\n }\n }\n }\n else {\n $variableName = ConvertTo-AkeylessOutputVariableName -SecretPath $SecretPath -OverrideName $OutputVariableName\n Set-AkeylessSensitiveOutput -Name $variableName -Value ([string]$Response) -StepName $StepName -PrintVariableNames $PrintVariableNames\n $created = 1\n }\n\n return $created\n}\n\nfunction Get-AwsHmacSha256Bytes {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n $hmac = New-Object System.Security.Cryptography.HMACSHA256 (, $Key)\n return $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Message))\n}\n\nfunction Get-AwsHmacSha256Hex {\n param (\n [byte[]]$Key,\n [string]$Message\n )\n\n return ([BitConverter]::ToString((Get-AwsHmacSha256Bytes -Key $Key -Message $Message))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsSigningKey {\n param (\n [string]$SecretKey,\n [string]$DateStamp,\n [string]$Region,\n [string]$Service\n )\n\n $kSecret = [Text.Encoding]::UTF8.GetBytes(\"AWS4$SecretKey\")\n $kDate = Get-AwsHmacSha256Bytes -Key $kSecret -Message $DateStamp\n $kRegion = Get-AwsHmacSha256Bytes -Key $kDate -Message $Region\n $kService = Get-AwsHmacSha256Bytes -Key $kRegion -Message $Service\n return Get-AwsHmacSha256Bytes -Key $kService -Message 'aws4_request'\n}\n\nfunction Get-AwsSha256Hex {\n param ([string]$Text)\n\n $sha = [System.Security.Cryptography.SHA256]::Create()\n return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace('-', '').ToLowerInvariant()\n}\n\nfunction Get-AwsCredentialChain {\n $accessKeyId = $env:AWS_ACCESS_KEY_ID\n $secretAccessKey = $env:AWS_SECRET_ACCESS_KEY\n $sessionToken = $env:AWS_SESSION_TOKEN\n\n if (-not [string]::IsNullOrWhiteSpace($accessKeyId) -and -not [string]::IsNullOrWhiteSpace($secretAccessKey)) {\n return [pscustomobject]@{\n AccessKeyId = $accessKeyId.Trim()\n SecretAccessKey = $secretAccessKey.Trim()\n SessionToken = if ([string]::IsNullOrWhiteSpace($sessionToken)) { '' } else { $sessionToken.Trim() }\n }\n }\n\n try {\n $imdsToken = Invoke-RestMethod -Method Put -Uri 'http://169.254.169.254/latest/api/token' -Headers @{ 'X-aws-ec2-metadata-token-ttl-seconds' = '21600' } -TimeoutSec 2\n $roleName = Invoke-RestMethod -Uri 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n $roleCreds = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$roleName\" -Headers @{ 'X-aws-ec2-metadata-token' = $imdsToken } -TimeoutSec 2\n if ($null -eq $roleCreds -or [string]::IsNullOrWhiteSpace($roleCreds.AccessKeyId)) {\n throw 'EC2 instance metadata returned no IAM credentials'\n }\n\n return [pscustomobject]@{\n AccessKeyId = [string]$roleCreds.AccessKeyId\n SecretAccessKey = [string]$roleCreds.SecretAccessKey\n SessionToken = [string]$roleCreds.Token\n }\n }\n catch {\n throw \"AWS credentials were not found in environment variables or EC2 instance metadata: $($_.Exception.Message)\"\n }\n}\n\nfunction New-AwsIamCloudId {\n param (\n [string]$AccessKeyId,\n [string]$SecretAccessKey,\n [string]$SessionToken = '',\n [string]$Region = 'us-east-1',\n [string]$StsUrl = 'https://sts.amazonaws.com/'\n )\n\n $service = 'sts'\n $method = 'POST'\n $hostName = ([Uri]$StsUrl).Host\n $body = 'Action=GetCallerIdentity&Version=2011-06-15'\n $amzDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')\n $dateStamp = $amzDate.Substring(0, 8)\n $payloadHash = Get-AwsSha256Hex -Text $body\n\n $headers = [ordered]@{\n Host = $hostName\n 'Content-Type' = 'application/x-www-form-urlencoded; charset=utf-8'\n 'Content-Length' = [string]$body.Length\n 'X-Amz-Date' = $amzDate\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $headers['X-Amz-Security-Token'] = $SessionToken\n }\n\n $canonicalHeaders = ($headers.GetEnumerator() | ForEach-Object { \"$($_.Key.ToLowerInvariant()):$($_.Value)\" }) -join \"`n\"\n $signedHeaders = (($headers.Keys | ForEach-Object { $_.ToLowerInvariant() }) | Sort-Object) -join ';'\n $canonicalRequest = @(\n $method\n '/'\n ''\n \"$canonicalHeaders`n\"\n $signedHeaders\n $payloadHash\n ) -join \"`n\"\n\n $credentialScope = \"$dateStamp/$Region/$service/aws4_request\"\n $stringToSign = @(\n 'AWS4-HMAC-SHA256'\n $amzDate\n $credentialScope\n (Get-AwsSha256Hex -Text $canonicalRequest)\n ) -join \"`n\"\n\n $signingKey = Get-AwsSigningKey -SecretKey $SecretAccessKey -DateStamp $dateStamp -Region $Region -Service $service\n $signature = Get-AwsHmacSha256Hex -Key $signingKey -Message $stringToSign\n $authorization = \"AWS4-HMAC-SHA256 Credential=$AccessKeyId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature\"\n\n $requestHeaders = @{\n Authorization = @($authorization)\n 'Content-Length' = @([string]$body.Length)\n Host = @($hostName)\n 'Content-Type' = @('application/x-www-form-urlencoded; charset=utf-8')\n 'X-Amz-Date' = @($amzDate)\n }\n\n if (-not [string]::IsNullOrWhiteSpace($SessionToken)) {\n $requestHeaders['X-Amz-Security-Token'] = @($SessionToken)\n }\n\n $payload = [ordered]@{\n sts_request_method = $method\n sts_request_url = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($StsUrl))\n sts_request_body = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body))\n sts_request_headers = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($requestHeaders | ConvertTo-Json -Compress)))\n }\n\n return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Compress)))\n}\n\nfunction Resolve-AwsIamCloudId {\n param (\n [string]$CloudId,\n [string]$Region,\n [string]$StsUrl\n )\n\n if (-not [string]::IsNullOrWhiteSpace($CloudId)) {\n return $CloudId.Trim()\n }\n\n $credentials = Get-AwsCredentialChain\n return New-AwsIamCloudId -AccessKeyId $credentials.AccessKeyId -SecretAccessKey $credentials.SecretAccessKey -SessionToken $credentials.SessionToken -Region $Region -StsUrl $StsUrl\n}\n\n$GATEWAY_URL = $OctopusParameters['Akeyless.Retrieve.Static.GatewayUrl']\n$AUTH_TOKEN = $OctopusParameters['Akeyless.Retrieve.Static.AuthToken']\n$SECRET_DEFINITIONS = $OctopusParameters['Akeyless.Retrieve.Static.Secrets']\n$FOLDER_PATH = $OctopusParameters['Akeyless.Retrieve.Static.FolderPath']\n$RETRIEVAL_METHOD = $OctopusParameters['Akeyless.Retrieve.Static.RetrievalMethod']\n$RECURSIVE_SEARCH = $OctopusParameters['Akeyless.Retrieve.Static.RecursiveSearch']\n$FIELD_VALUES = $OctopusParameters['Akeyless.Retrieve.Static.FieldValues']\n$PRINT_VARIABLE_NAMES = $OctopusParameters['Akeyless.Retrieve.Static.PrintVariableNames']\n$VERSION_TEXT = $OctopusParameters['Akeyless.Retrieve.Static.Version']\n$StepName = $OctopusParameters['Octopus.Step.Name']\n\nif ([string]::IsNullOrWhiteSpace($GATEWAY_URL)) {\n $GATEWAY_URL = 'https://api.akeyless.io'\n}\nif ([string]::IsNullOrWhiteSpace($AUTH_TOKEN)) {\n throw 'Required parameter Auth Token not specified'\n}\nif ([string]::IsNullOrWhiteSpace($RETRIEVAL_METHOD)) {\n $RETRIEVAL_METHOD = 'Single'\n}\nif ([string]::IsNullOrWhiteSpace($RECURSIVE_SEARCH)) {\n $RECURSIVE_SEARCH = 'False'\n}\nif ([string]::IsNullOrWhiteSpace($PRINT_VARIABLE_NAMES)) {\n $PRINT_VARIABLE_NAMES = 'False'\n}\n\n$version = 0\nif (-not [string]::IsNullOrWhiteSpace($VERSION_TEXT)) {\n if (-not [int]::TryParse($VERSION_TEXT, [ref]$version)) {\n throw \"Version must be a positive integer, got '$VERSION_TEXT'\"\n }\n}\n\n$printNames = ($PRINT_VARIABLE_NAMES -eq 'True')\n$recursive = ($RECURSIVE_SEARCH -eq 'True')\n$fields = Parse-AkeylessFieldDefinitions -RawValue $FIELD_VALUES\n$created = 0\n\nif ($RETRIEVAL_METHOD.ToUpper().Trim() -eq 'FOLDER') {\n if ([string]::IsNullOrWhiteSpace($FOLDER_PATH)) {\n throw 'Folder path is required when retrieval method is Folder'\n }\n\n $secretPaths = Get-AkeylessSecretsRecursively -GatewayUrl $GATEWAY_URL -Token $AUTH_TOKEN -FolderPath $FOLDER_PATH -Recursive $recursive\n foreach ($secretPath in $secretPaths) {\n $created += Publish-AkeylessSecretValue -GatewayUrl $GATEWAY_URL -Token $AUTH_TOKEN -SecretPath $secretPath -StepName $StepName -PrintVariableNames $printNames -Fields $fields -Version $version\n }\n}\nelse {\n $definitions = Parse-AkeylessSecretDefinitions -RawValue $SECRET_DEFINITIONS\n if ($definitions.Count -eq 0) {\n throw 'At least one secret path is required when retrieval method is Single or Multiple'\n }\n\n foreach ($definition in $definitions) {\n $created += Publish-AkeylessSecretValue -GatewayUrl $GATEWAY_URL -Token $AUTH_TOKEN -SecretPath $definition.Path -StepName $StepName -PrintVariableNames $printNames -OutputVariableName $definition.OutputVariableName -Fields $fields -Version $version\n }\n}\n\nWrite-Host \"Created $created output variables\"" + }, + "Parameters": [ + { + "HelpText": "The Akeyless API or Gateway URL. For SaaS, use https://api.akeyless.io.", + "Id": "10002000-0000-0000-0000-100020001001", + "Label": "Gateway URL", + "DefaultValue": "https://api.akeyless.io", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Static.GatewayUrl" + }, + { + "HelpText": "Authentication token from a previous Akeyless login step.", + "Id": "10002000-0000-0000-0000-100020001002", + "Label": "Auth Token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Name": "Akeyless.Retrieve.Static.AuthToken" + }, + { + "HelpText": "One secret per line in the format SecretPath | OutputVariableName.", + "Id": "10002000-0000-0000-0000-100020001003", + "Label": "Secret paths", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Name": "Akeyless.Retrieve.Static.Secrets" + }, + { + "HelpText": "Akeyless folder to enumerate when retrieval method is Folder, e.g. `/production`", + "Id": "10002000-0000-0000-0000-100020001004", + "Label": "Folder path", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Static.FolderPath" + }, + { + "HelpText": "Retrieve explicit secret paths, or enumerate static secrets in a folder.", + "Id": "10002000-0000-0000-0000-100020001005", + "Label": "Retrieval method", + "DefaultValue": "Single", + "DisplaySettings": { + "Octopus.SelectOptions": "Single|Explicit secret paths\nFolder|Enumerate folder", + "Octopus.ControlType": "Select" + }, + "Name": "Akeyless.Retrieve.Static.RetrievalMethod" + }, + { + "HelpText": "When enumerating a folder, also retrieve secrets from subfolders.", + "Id": "10002000-0000-0000-0000-100020001006", + "Label": "Recursive retrieval", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Name": "Akeyless.Retrieve.Static.RecursiveSearch" + }, + { + "HelpText": "For JSON secrets, choose fields in the format FieldName | OutputVariableName.", + "Id": "10002000-0000-0000-0000-100020001007", + "Label": "Field names", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Name": "Akeyless.Retrieve.Static.FieldValues" + }, + { + "HelpText": "Optional static secret version to retrieve. Leave blank for latest.", + "Id": "10002000-0000-0000-0000-100020001008", + "Label": "Secret version", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Akeyless.Retrieve.Static.Version" + }, + { + "HelpText": "Write created output variable names to the task log.", + "Id": "10002000-0000-0000-0000-100020001009", + "Label": "Print output variable names", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Name": "Akeyless.Retrieve.Static.PrintVariableNames" + } + ], + "LastModifiedBy": "akeyless-community", + "LastModifiedAt": "2026-06-17T04:32:01.617Z", + "$Meta": { + "ExportedAt": "2026-06-17T04:32:01.617Z", + "OctopusVersion": "2024.4.0", + "Type": "ActionTemplate" + }, + "Category": "akeyless" +} diff --git a/step-templates/logos/akeyless.png b/step-templates/logos/akeyless.png new file mode 100644 index 000000000..4e4a91c8d Binary files /dev/null and b/step-templates/logos/akeyless.png differ