From 36af5bd89f1e2af213f7be1dbd5f410197319cee Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:32:39 -0800 Subject: [PATCH 01/46] feat: Initial provider code --- TestConsole/Dockerfile | 27 + TestConsole/Program.cs | 80 +++ akeyless-pam/AkeylessPam.cs | 570 +++++++++++++++++++ akeyless-pam/Constants.cs | 12 + akeyless-pam/Models/AkeylessConfiguration.cs | 194 +++++++ akeyless-pam/manifest.json | 16 + docsource/akeyless.md | 100 ++++ docsource/overview.md | 3 + 8 files changed, 1002 insertions(+) create mode 100644 TestConsole/Dockerfile create mode 100644 TestConsole/Program.cs create mode 100644 akeyless-pam/AkeylessPam.cs create mode 100644 akeyless-pam/Constants.cs create mode 100644 akeyless-pam/Models/AkeylessConfiguration.cs create mode 100644 akeyless-pam/manifest.json create mode 100644 docsource/akeyless.md create mode 100644 docsource/overview.md diff --git a/TestConsole/Dockerfile b/TestConsole/Dockerfile new file mode 100644 index 0000000..1430d3e --- /dev/null +++ b/TestConsole/Dockerfile @@ -0,0 +1,27 @@ +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["TestConsole/TestConsole.csproj", "TestConsole/"] +RUN dotnet restore "TestConsole/TestConsole.csproj" +COPY . . +WORKDIR "/src/TestConsole" +RUN dotnet build "TestConsole.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "TestConsole.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +ARG SECRET_SERVER_URL +ARG SECRET_SERVER_USERNAME +ARG SECRET_SERVER_PASSWORD +ARG SECRET_SERVER_SECRET_ID + +ENV SECRET_SERVER_URL=$SECRET_SERVER_URL +ENV SECRET_SERVER_USERNAME=$SECRET_SERVER_USERNAME +ENV SECRET_SERVER_PASSWORD=$SECRET_SERVER_PASSWORD +ENV SECRET_SERVER_SECRET_ID=$SECRET_SERVER_SECRET_ID +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "TestConsole.dll"] diff --git a/TestConsole/Program.cs b/TestConsole/Program.cs new file mode 100644 index 0000000..c325ab4 --- /dev/null +++ b/TestConsole/Program.cs @@ -0,0 +1,80 @@ +// Copyright 2023 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using Keyfactor.Extensions.Pam.Akeyless; + +namespace TestConsole; + +internal class Program +{ + private static void Main(string[] args) + { + var pam = new AkeylessPam(); + var initInfo = new Dictionary(); + + var instanceParams = new Dictionary(); + + //Read Url from environment variable + initInfo.Add("Url", + Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? "https://api.akeyless.io"); + //Read Username from environment variable + initInfo.Add("AuthType", Environment.GetEnvironmentVariable("AKEYLESS_AUTH_TYPE") ?? "access_key"); + + switch (initInfo["AuthType"]) + { + case "access_key": + initInfo.Add("AccessId", Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_ID") ?? "changemeId!"); + initInfo.Add("AccessKey", Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_KEY") ?? "changemeKey!"); + break; + default: + Console.WriteLine("Using implicit authentication."); + break; + } + + Console.WriteLine("Test secret type `static_text`:"); + instanceParams.Add("SecretType", "static_text"); + instanceParams.Add("SecretName", "pam/test/pamStaticTextUsername"); + var username = pam.GetPassword(instanceParams, initInfo); + instanceParams["SecretName"] = "pam/test/pamStaticTextPassword"; + var password = pam.GetPassword(instanceParams, initInfo); + Console.WriteLine($"ServerUsername: {username}"); + Console.WriteLine($"ServerPassword: {password}"); + Console.WriteLine(); + + Console.WriteLine("Test secret type `static_kv`:"); + instanceParams["SecretType"] = "static_kv"; + instanceParams["SecretName"] = "pam/test/pamStaticKV"; + instanceParams["StaticSecretFieldName"] = "username"; + var kvUsername = pam.GetPassword(instanceParams, initInfo); + instanceParams["StaticSecretFieldName"] = "password"; + var kvPassword = pam.GetPassword(instanceParams, initInfo); + + Console.WriteLine($"ServerUsername: {kvUsername}"); + Console.WriteLine($"ServerPassword: {kvPassword}"); + + Console.WriteLine(); + Console.WriteLine("Test secret type `static_json`:"); + instanceParams["SecretType"] = "static_json"; + instanceParams["SecretName"] = "pam/test/pamStaticJSON"; + instanceParams["StaticSecretFieldName"] = "username"; + var jsonUsername = pam.GetPassword(instanceParams, initInfo); + instanceParams["StaticSecretFieldName"] = "password"; + var jsonPassword = pam.GetPassword(instanceParams, initInfo); + Console.WriteLine($"ServerUsername: {jsonUsername}"); + Console.WriteLine($"ServerPassword: {jsonPassword}"); + + Console.WriteLine(); + Console.WriteLine("Test secret type `static_json` raw:"); + instanceParams["SecretType"] = "static_json"; + instanceParams["SecretName"] = "pam/test/k8s-orchestrator"; + instanceParams.Remove("StaticSecretFieldName"); + var jsonRaw = pam.GetPassword(instanceParams, initInfo); + Console.WriteLine($"ServerSecret: {jsonRaw}"); + + Console.WriteLine("Test completed."); + } +} \ No newline at end of file diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs new file mode 100644 index 0000000..8100ffb --- /dev/null +++ b/akeyless-pam/AkeylessPam.cs @@ -0,0 +1,570 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using akeyless.Api; +using akeyless.Client; +using akeyless.Model; +using Keyfactor.Extensions.Pam.Akeyless.Models; +using Keyfactor.Logging; +using Keyfactor.Platform.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.Pam.Akeyless; + +/// +/// Exception thrown when the authentication token for Akeyless is invalid or cannot be obtained. +/// +/// +/// This exception is typically thrown when authentication credentials are incorrect or the server rejects the auth +/// request. +/// +public class InvalidTokenException : Exception +{ + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InvalidTokenException(string message) : base(message) + { + } +} + +public class InvalidClientConfigurationException : Exception +{ + /// + /// Initializes a new instance of the class with a specified error + /// message. + /// + /// The message that describes the error. + public InvalidClientConfigurationException(string message) : base(message) + { + } +} + +public class InvalidSecretConfigurationException : Exception +{ + /// + /// Initializes a new instance of the class with a specified error + /// message. + /// + /// The message that describes the error. + public InvalidSecretConfigurationException(string message) : base(message) + { + } +} + +/// +/// Privileged Access Management (PAM) provider implementation for Akeyless. +/// +/// +/// This class implements the IPAMProvider interface to retrieve secrets from Akeyless. +/// +public class AkeylessPam : IPAMProvider +{ + private ILogger Logger { get; } = LogHandler.GetClassLogger(); + + private string AuthToken { get; set; } = string.Empty; + + /// + /// Gets the name of this PAM provider. + /// + /// The string "Akeyless". + public string Name => "Akeyless"; + + /// + /// Retrieves a password from Akeyless using the provided configuration parameters. + /// + /// Dictionary containing instance-specific parameters like SecretId and SecretFieldName. + /// + /// Dictionary containing connection and authentication parameters such as host URL, + /// username, and password. + /// + /// The password value retrieved from. + /// Thrown when required parameters are missing or invalid. + /// Thrown when authentication with fails. + /// Thrown when communication with fails. + public string GetPassword(Dictionary instanceParameters, + Dictionary serverConfigurationParameters) + { + try + { + Logger.MethodEntry(); + Logger.LogInformation("Starting Akeyless PAM Provider"); + Logger.LogDebug("Getting password from Akeyless"); + Logger.LogTrace("instanceParameters: {@InstanceParameters}", instanceParameters); + // Logger.LogTrace("initializationInfo: {@ServerConfigurationParameters}", + // serverConfigurationParameters); // TODO: Commented out to avoid logging sensitive information + + var config = BuildAkeylessConfiguration(instanceParameters, serverConfigurationParameters); + return GetAkeylessSecretAsync(config).Result; + } + finally + { + Logger.MethodExit(); + } + } + + private V2Api InitClient(AkeylessConfiguration configurationInfo) + { + try + { + Logger.MethodEntry(); + var config = new Configuration + { + BasePath = Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? + configurationInfo.Url ?? "https://api.akeyless.io" + }; + + var api = new V2Api(config); + switch (configurationInfo.AuthType) + { + case "access_key": + Logger.LogDebug("Authenticating with Akeyless using access_key"); + var authRequest = new Auth( + configurationInfo.AccessId, + configurationInfo.AccessKey + ); + // Logger.LogTrace("Auth Request: {@AuthRequest}", + // authRequest); //TODO: COMMENT THIS OUT it exposes secrets + var authResp = api.Auth(authRequest); + + if (string.IsNullOrEmpty(authResp.Token) && string.IsNullOrEmpty(authResp.Creds.Token)) + { + Logger.LogError("Unable to obtain access token from Akeyless server"); + throw new InvalidTokenException("Unable to obtain access token from Akeyless server"); + } + + AuthToken = string.IsNullOrEmpty(authResp.Token) ? authResp.Creds.Token : authResp.Token; + Logger.LogInformation("Successfully authenticated with Akeyless"); + + break; + default: + Logger.LogWarning("No authentication performed for auth type '{AuthType}'", + configurationInfo.AuthType); + break; + } + + return api; + } + catch (ApiException ex) + { + Logger.LogError(ex, "Akeyless API exception during client initialization"); + throw new InvalidClientConfigurationException( + $"Unable to authenticate to Akeyless API. {ex.Message}"); + } + finally + { + Logger.MethodExit(); + } + } + + private static bool LooksLikeJson(string s) + { + s = s.Trim(); + return (s.StartsWith('{') && s.EndsWith('}')) || (s.StartsWith('[') && s.EndsWith(']')); + } + + + private string ParseJsonSecret(string secretValueStr, string fieldName = "") + { + try + { + Logger.MethodEntry(); + var jsonObj = JsonConvert.DeserializeObject>(secretValueStr); + if (string.IsNullOrEmpty(fieldName)) + { + Logger.LogDebug("No field name specified, returning full JSON secret"); + return secretValueStr; + } + if (jsonObj != null && jsonObj.TryGetValue(fieldName, out var fieldValue)) + { + Logger.LogDebug("Returning value for field '{FieldName}'", fieldName); + return fieldValue.ToString() ?? string.Empty; + } + + Logger.LogError("❌ Secret does not contain the specified field '{FieldName}'", fieldName); + throw new InvalidSecretConfigurationException( + $"Secret does not contain the specified field '{fieldName}'"); + } + catch (JsonException ex) + { + Logger.LogError(ex, "❌ Failed to parse secret as JSON"); + throw; + } + finally + { + Logger.MethodExit(); + } + } + + private string ParseKvSecret(string secretValueStr, string fieldName) + { + try + { + Logger.MethodEntry(); + foreach (var line in secretValueStr.Split('\n')) + { + var parts = line.Split('=', 2); + if (parts.Length != 2) + { + Logger.LogWarning("Skipping malformed KV line: {Line}", line); + continue; + } + + var k = parts[0].Trim(); + var val = parts[1].Trim(); + Logger.LogDebug("Key: {Key}, Value: {Value}", k, val); + if (k != fieldName) continue; + Logger.LogDebug("Returning value for field '{FieldName}'", fieldName); + return val; + } + + Logger.LogError("❌ Secret does not contain the specified field '{FieldName}'", fieldName); + throw new InvalidSecretConfigurationException( + $"Secret does not contain the specified field '{fieldName}'"); + } + finally + { + Logger.MethodExit(); + } + } + + private async Task GetStaticSecret(V2Api client, AkeylessConfiguration configurationInfo) + { + try + { + Logger.MethodEntry(); + Logger.LogDebug("Fetching static text secret '{SecretName}'", + configurationInfo.SecretName); + var req = new GetSecretValue( + names: [configurationInfo.SecretName], + token: AuthToken + ); + var secrets = await client.GetSecretValueAsync(req); + + if (!secrets.TryGetValue(configurationInfo.SecretName, out var secretValue)) + { + Logger.LogError("Secret '{SecretName}' not found in Akeyless", + configurationInfo.SecretName); + throw new InvalidSecretConfigurationException( + $"Secret '{configurationInfo.SecretName}' not found in Akeyless"); + } + + var secretValueStr = secretValue.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(secretValueStr)) + { + Logger.LogError("Secret '{SecretName}' has an empty value", + configurationInfo.SecretName); + throw new InvalidSecretConfigurationException( + $"Secret '{configurationInfo.SecretName}' is empty"); + } + + if (LooksLikeJson(secretValueStr)) + { + Logger.LogInformation("✅ Secret '{SecretName}' appears to be JSON", + configurationInfo.SecretName); + // Parse JSON if secret isn't meant to be a full JSON blob else returns the JSON blob + return configurationInfo.SecretType is "static_json" or "static_kv" + ? ParseJsonSecret(secretValueStr, configurationInfo.StaticSecretFieldName) + : secretValueStr; + } + + if (secretValueStr.Contains('=') && secretValueStr.Contains('\n')) + { + Logger.LogInformation("✅ Secret '{SecretName}' appears to be KV formatted", + configurationInfo.SecretName); + return ParseKvSecret(secretValueStr, configurationInfo.StaticSecretFieldName); + } + + + Logger.LogInformation("✅ Secret '{SecretName}' appears to be plain text", + configurationInfo.SecretName); + return secretValueStr; + } + finally + { + Logger.MethodExit(); + } + } + + /// + /// Asynchronously retrieves a secret from Akeyless. + /// + /// The configuration containing connection and request details. + /// The value of the requested secret field. + /// Thrown when the HTTP request to fails. + /// Thrown when deserializing the response fails or the requested secret is not found. + private async Task GetAkeylessSecretAsync(AkeylessConfiguration configurationInfo) + { + try + { + Logger.MethodEntry(); + Logger.LogDebug("Attempting to fetch access token from Akeyless at {Url}", + configurationInfo.Url); + var client = InitClient(configurationInfo); + + switch (configurationInfo.SecretType) + { + case "static_text": + case "static_kv": + case "static_json": + return await GetStaticSecret(client, configurationInfo); + default: + Logger.LogError("Invalid or unsupported secret type '{SecretType}' specified", + configurationInfo.SecretType); + throw new Exception( + $"Invalid secret type '{configurationInfo.SecretType}' specified, please use one of [{string.Join(", ", AkeylessConfiguration.SupportedSecretTypes)}]"); + } + } + finally + { + Logger.MethodExit(); + } + } + + /// + /// Validates the instance parameters provided to the PAM provider. + /// + /// + /// A read-only dictionary containing instance-specific parameters, such as SecretId and SecretFieldName. + /// + /// + /// True if the instance parameters are valid; otherwise, throws an . + /// + /// + /// Thrown if required parameters are missing or cannot be parsed as expected. + /// + private bool ValidateInstanceParams(IReadOnlyDictionary instanceParameters) + { + try + { + Logger.MethodEntry(); + Logger.LogDebug("Validating instance parameters"); + ValidateRequiredParameter(instanceParameters, AkeylessConfiguration.SECRET_NAME, + "instance configuration parameter"); + Logger.LogDebug("Instance parameters are valid"); + return true; + } + catch (MissingFieldException ex) + { + Logger.LogError(ex, "Instance parameter validation failed"); + return false; + } + finally + { + Logger.MethodExit(); + } + } + + /// + /// Validates the server configuration parameters for connecting to Akeyless. + /// + /// + /// A read-only dictionary containing server configuration parameters such as URL, credentials, and grant + /// type. + /// + /// + /// The auth type to use for interacting with the Akeyless API. Supported values are "access_key". + /// Defaults to "access_key". + /// + /// + /// True if the server configuration parameters are valid; otherwise, throws an + /// . + /// + /// + /// Thrown if required parameters are missing or invalid for the specified grant type. + /// + private bool ValidateServerConfigurationParams( + IReadOnlyDictionary connectionConfiguration, + string authType = AkeylessConstants.DefaultAuthMethod) + { + try + { + Logger.MethodEntry(); + Logger.LogDebug("Validating server configuration parameters"); + + // Validate credentials based on grant type + switch (authType) + { + case "implicit": + Logger.LogWarning("No validation performed for 'implicit' auth type"); + break; + + case "access_key": + Logger.LogDebug("Validating credentials for 'access_key' auth type"); + ValidateAuthTypeAccessKey(connectionConfiguration); + break; + default: + Logger.LogError( + "Invalid auth type '{AuthType}'", + authType); + Logger.MethodExit(); + throw new Exception( + $"Invalid auth type '{authType}' specified."); + } + + Logger.LogInformation("Server configuration parameters are valid"); + return true; + } + finally + { + Logger.MethodExit(); + } + } + + /// + /// Validates that a required parameter exists and is not null or empty in the provided configuration dictionary. + /// + /// + /// The configuration dictionary to validate. + /// + /// + /// The name of the parameter to check for existence and non-empty value. + /// + /// + /// A string prefix to include in the error message if validation fails. + /// + /// + /// Thrown if the required parameter is missing or its value is null or empty. + /// + private void ValidateRequiredParameter( + IReadOnlyDictionary config, + string paramName, + string errorPrefix) + { + try + { + Logger.MethodEntry(); + Logger.LogDebug("Validating parameter '{ParamName}'", paramName); + + if (config.ContainsKey(paramName) && !string.IsNullOrEmpty(config[paramName])) return; + Logger.LogError("{ErrorPrefix} '{ParamName}' not provided", errorPrefix, paramName); + throw new MissingFieldException($"{errorPrefix} '{paramName}' not provided"); + } + finally + { + Logger.MethodExit(); + } + } + + /// + /// Validates that the required client ID and client secret parameters exist and are not empty for the client + /// credentials grant type. + /// + /// + /// The configuration dictionary containing client parameters. + /// + /// + /// Thrown if the client ID or client secret parameter is missing or empty. + /// + private void ValidateAuthTypeAccessKey(IReadOnlyDictionary config) + { + try + { + Logger.MethodEntry(); + ValidateRequiredParameter(config, AkeylessConfiguration.ACCESS_KEY, "client configuration parameter"); + ValidateRequiredParameter(config, AkeylessConfiguration.ACCESS_ID, "client configuration parameter"); + } + catch (MissingFieldException ex) + { + Logger.LogError(ex, "Access key authentication parameter validation failed"); + throw new InvalidClientConfigurationException(ex.Message); + } + finally + { + Logger.MethodExit(); + } + } + + /// + /// Creates a AkeylessConfiguration object from the provided parameters. + /// + /// + /// Dictionary containing instance-specific parameters, including the secret name and field + /// name. + /// + /// Dictionary containing connection and authentication parameters for. + /// A fully populated AkeylessConfiguration object. + /// Thrown when required parameters are missing or invalid. + private AkeylessConfiguration BuildAkeylessConfiguration( + IReadOnlyDictionary instanceParameters, + IReadOnlyDictionary connectionConfiguration) + { + try + { + Logger.MethodEntry(); + Logger.LogInformation("Validating Akeyless configuration"); + var validServer = ValidateServerConfigurationParams(connectionConfiguration); + var validInstance = ValidateInstanceParams(instanceParameters); + + if (!validServer || !validInstance) + { + Logger.LogError("Akeyless PAM provider configuration is invalid"); + throw new InvalidClientConfigurationException( + "Akeyless configuration is invalid, please review server logs."); + } + + if (!connectionConfiguration.TryGetValue(AkeylessConfiguration.AUTH_TYPE, out var authType)) + { + Logger.LogWarning( + "\'{AuthType}\' parameter not provided defaulting to 'implicit' auth type which uses environment variables", + AkeylessConfiguration.AUTH_TYPE); + authType = "access_key"; + } + + Logger.LogDebug("Building Akeyless configuration"); + var config = new AkeylessConfiguration + { + Url = connectionConfiguration.GetValueOrDefault(AkeylessConfiguration.AKEYLESS_API_URL, + AkeylessConstants.DefaultAkeylessApiUrl), + AuthType = authType + }; + switch (authType) + { + case "implicit": + Logger.LogInformation("Building Akeyless configuration for implicit auth type"); + break; + case "access_key": + Logger.LogInformation("Building Akeyless configuration for 'access_key' auth type"); + config.AccessId = connectionConfiguration[AkeylessConfiguration.ACCESS_ID]; + config.AccessKey = connectionConfiguration[AkeylessConfiguration.ACCESS_KEY]; + break; + default: + Logger.LogError("Invalid auth type '{AuthType}' specified", authType); + throw new Exception($"Invalid grant type '{authType}' specified"); + } + + config.SecretType = instanceParameters.GetValueOrDefault(AkeylessConfiguration.SECRET_TYPE, ""); + config.SecretName = instanceParameters[AkeylessConfiguration.SECRET_NAME]; + switch (config.SecretType) + { + case "static_kv": + Logger.LogInformation("Configuring static secret field name for secret type '{SecretType}'", + config.SecretType); + config.StaticSecretFieldName = instanceParameters[AkeylessConfiguration.STATIC_SECRET_FIELD_NAME]; + break; + case "static_json": + Logger.LogInformation("Configuring static secret field name for secret type '{SecretType}'", + config.SecretType); + config.StaticSecretFieldName = instanceParameters.GetValueOrDefault( + AkeylessConfiguration.STATIC_SECRET_FIELD_NAME, ""); + break; + } + + return config; + } + finally + { + Logger.MethodExit(); + } + } +} \ No newline at end of file diff --git a/akeyless-pam/Constants.cs b/akeyless-pam/Constants.cs new file mode 100644 index 0000000..383d8c0 --- /dev/null +++ b/akeyless-pam/Constants.cs @@ -0,0 +1,12 @@ +namespace Keyfactor.Extensions.Pam.Akeyless; + +public static class AkeylessConstants +{ + // Compile-time constant (gets inlined into referencing assemblies) + public const string DefaultAuthMethod = "access_key"; + + public const string DefaultAkeylessApiUrl = "https://api.akeyless.io"; + + // Recommended for libraries: avoids inlining so you can change value without recompiling dependents + public static readonly string DefaultAuthMethodReadOnly = "access_key"; +} \ No newline at end of file diff --git a/akeyless-pam/Models/AkeylessConfiguration.cs b/akeyless-pam/Models/AkeylessConfiguration.cs new file mode 100644 index 0000000..f9d139a --- /dev/null +++ b/akeyless-pam/Models/AkeylessConfiguration.cs @@ -0,0 +1,194 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; + +namespace Keyfactor.Extensions.Pam.Akeyless.Models; + +/// +/// Configuration class for connecting to and retrieving secrets from Akeyless. +/// +internal class AkeylessConfiguration : IValidatableObject +{ + public static readonly ImmutableList SupportedAuthMethods = + ImmutableList.Create(AkeylessConstants.DefaultAuthMethod); + + public static readonly ImmutableList SupportedSecretTypes = ImmutableList.Create( + "static_text", "static_json", "static_kv" + ); + + public static readonly ImmutableList UnsupportedAuthMethods = ImmutableList.Create( + "saml" + ); + + /// + /// Initializes a new instance of the class with empty strings. + /// + /// + /// This constructor initializes required string properties with empty strings to satisfy nullable requirements. + /// Validation logic in the Validate method ensures actual values are provided when needed. + /// + public AkeylessConfiguration() + { + // Initialize non-nullable string properties to satisfy compiler + Url = string.Empty; + AccessId = string.Empty; + AccessKey = string.Empty; + StaticSecretFieldName = string.Empty; + AuthType = "access_key"; // Default value is already set in property declaration + } + + /// + /// The configuration for the AKeyless API URL. + /// + public static string AKEYLESS_API_URL => "Url"; + + /// + /// The configuration key for the auth type used for authentication. For more information on auth_types see + /// https://docs.akeyless.io/reference/auth `access-type`. + /// + /// + /// Supported auth types: + /// - `access_key`: Uses AccessId and AccessKey for authentication. + /// + public static string AUTH_TYPE => "AuthType"; + + /// + /// The configuration key for the access ID used in access key authentication. + /// + public static string ACCESS_ID => "AccessId"; + + /// + /// The configuration key for the access key used in access key authentication. + /// + public static string ACCESS_KEY => "AccessKey"; + + /// + /// The configuration key for the type of secret to retrieve from Akeyless. For more information on secret types see + /// https://docs.akeyless.io/docs/manage-your-secrets-overview + /// + /// + /// Supported secret types: + /// - `static_text`: A static text secret. + /// - `static_json`: A static JSON secret. + /// - `static_kv`: A static key-value secret. + /// + public static string SECRET_TYPE => "SecretType"; + + /// + /// The configuration key for the name of the secret to retrieve from Akeyless. + /// + public static string SECRET_NAME => "SecretName"; + + /// + /// The configuration key for the field name within the secret to retrieve. + /// + public static string STATIC_SECRET_FIELD_NAME => "StaticSecretFieldName"; + + /// + /// The base URL of the Akeyless Secret Server. + /// + /// + /// Defaults to the public Akeyless API URL if not specified. + /// + public string Url { get; init; } + + /// + /// The access ID for access_key authentication with Akeyless. + /// For more information see https://docs.akeyless.io/docs/api-key + /// + public string AccessId { get; set; } + + /// + /// The access key for access_key authentication with Akeyless. + /// For more information see https://docs.akeyless.io/docs/api-key + /// + public string AccessKey { get; set; } + + /// + /// The type of the secret to retrieve from Akeyless. + /// + /// + /// Must be one of: + /// - `static_text` + /// - `static_json` + /// - `static_kv`. + /// Defaults to `static_text`. + /// + [Required(ErrorMessage = "The SecretType field is required.")] + [RegularExpression("^(static_text|static_json|static_kv)$", + ErrorMessage = "SecretType must be one of `[static_text,static_json,static_kv]`.")] + public string SecretType { get; set; } = "static_text"; + + /// + /// The identifier of the secret to retrieve from Akeyless. + /// + [Required(ErrorMessage = "The SecretName field is required.")] + public string SecretName { get; set; } + + /// + /// The name of the field within the secret to retrieve. + /// This can be either the field name or slug. + /// + public string StaticSecretFieldName { get; set; } + + /// + /// The auth type to use for authentication to AKeyless. + /// + /// + /// Defaults to `access_key`. + /// Unsupported auth types: + /// - `saml`: Due to requiring a web browser for SAML assertions. + /// + [RegularExpression( + "^(access_key|password|ldap|k8s|azure_ad|oidc|aws_iam|universal_identity|jwt|gcp|cert|oci|kerberos)$", + ErrorMessage = + "AuthType must be one of `[access_key,password,ldap,k8s,azure_ad,oidc,aws_iam,universal_identity,jwt,gcp,cert,oci,kerberos]`.")] + public string AuthType { get; init; } + + /// + /// Validates that the configuration has either username/password or client credentials for authentication. + /// + /// The validation context. + /// A collection of validation results. + public IEnumerable Validate(ValidationContext validationContext) + { + switch (AuthType) + { + case AkeylessConstants.DefaultAuthMethod: + if (string.IsNullOrWhiteSpace(AccessId) || string.IsNullOrWhiteSpace(AccessKey)) + yield return new ValidationResult( + $"AccessId and AccessKey must be provided for '{AkeylessConstants.DefaultAuthMethod}' authentication.", + [nameof(AccessId), nameof(AccessKey)]); + break; + default: + yield return new ValidationResult( + $"Unsupported AuthType. Currently, only '{string.Join(", ", SupportedAuthMethods)}' are supported.", + [nameof(AuthType)]); + break; + } + + if (!SupportedSecretTypes.Contains(SecretType)) + yield return new ValidationResult( + $"Unsupported SecretType. Supported types are: {string.Join(", ", SupportedSecretTypes)}.", + [nameof(SecretType)] + ); + switch (SecretType) + { + case "static_kv": + case "static_json": + if (string.IsNullOrWhiteSpace(StaticSecretFieldName)) + yield return new ValidationResult( + "StaticSecretFieldName must be provided when SecretType is 'static_kv|static_json'.", + [nameof(StaticSecretFieldName)] + ); + break; + } + } +} \ No newline at end of file diff --git a/akeyless-pam/manifest.json b/akeyless-pam/manifest.json new file mode 100644 index 0000000..2e85b5d --- /dev/null +++ b/akeyless-pam/manifest.json @@ -0,0 +1,16 @@ +{ + "extensions": { + "Keyfactor.Platform.Extensions.IPAMProvider": { + "PAMProviders.Akeyless.PAMProvider": { + "assemblyPath": "akeyless-pam.dll", + "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.Pam" + } + } + }, + "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { + "Url": "https://api.akeyless.io", + "AuthType": "access_key", + "AccessId": "", + "AccessKey": "" + } +} \ No newline at end of file diff --git a/docsource/akeyless.md b/docsource/akeyless.md new file mode 100644 index 0000000..739272d --- /dev/null +++ b/docsource/akeyless.md @@ -0,0 +1,100 @@ +## Overview + +The AkeylessPAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. +Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on +these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) + +## Requirements + +- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. + +## Supported Authentication Methods + +### Access Key (API Key) Authentication +This method uses an Access Key and Access ID pair to authenticate to the Akeyless API. These credentials can be created in the Akeyless console. +For more information, see the [Akeyless documentation](https://tutorials.akeyless.io/docs/authentication-methods-and-api-key-authentication). + +#### Example `manifest.json` configuration: + +```json +{ + "extensions": { + "Keyfactor.Platform.Extensions.IPAMProvider": { + "PAMProviders.Akeyless.PAMProvider": { + "assemblyPath": "akeyless-pam.dll", + "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.Pam" + } + } + }, + "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { + "Url": "https://api.akeyless.io", + "AuthType": "access_key", + "AccessId": "", + "AccessKey": "" + } +} +``` + +## Supported Secret Types +Below are the types of Akeyless secret that are supported by this provider. + +### Static Secrets +For full details on static secrets, see the [Akeyless documentation](https://docs.akeyless.io/docs/secret-management/static-secrets). + +| Secret Type | Description | Additional Fields | +|---------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `static_text` | A static secret where the full value will be retrieved unparsed | N/A | +| `static_json` | A static secret where the full value will be retrieved unparsed | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | +| `static_kv` | A static secret where the full value will be retrieved unparsed | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | + + +## Mechanics + +When configuring the Akeyless for use as a PAM Provider with Keyfactor, you will need to ensure that your +instance is configured for API access using the desired auth method. This can be done by an Akeyless administrator. +For more details visit the vendor +docs [here](https://docs.akeyless.io/docs/access-and-authentication-methods). + +Once API access is configured the credential *MUST* be granted access to view secret(s) you'll be using. + +After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from the Akeyless as a PAM Provider. + +### Running the PAM provider on Keyfactor Universal Orchestrator (UO) + +When installing on the Universal Orchestrator (UO), is installed on and run from the UO host. Below is a sequence +diagram +showing the flow of the PAM provider when it is run from the UO. + +```mermaid +sequenceDiagram + KeyfactorCommand->>KeyfactorCommand: New job created. + UO->>KeyfactorCommand: Hello do you have any jobs for me? + KeyfactorCommand->>UO: Yes here's a job. + UO->>Akeyless: Hello here are my client credentials. + Akeyless->>UO: Here's your API token. + UO->>Akeyless: I need secret name 100, here's my API token. + Akeyless->>Akeyless: Check secret ACL. + Akeyless->>UO: This is allowed, here's the secret. + UO->>UO: Running job. + UO->>KeyfactorCommand: Job completed. +``` + +### Running the PAM provider on the Keyfactor Command Host + +When installing the PAM provider on the Keyfactor Command Host, is installed on and run from the Keyfactor Command host. +Below is a sequence diagram showing the flow of the PAM provider when it is run from the Keyfactor Command Host. + +```mermaid +sequenceDiagram + KeyfactorCommand->>KeyfactorCommand: Creating a new job. + KeyfactorCommand->>Akeyless: Hello here are my credentials. + Akeyless->>KeyfactorCommand: Here's your API token. + KeyfactorCommand->>Akeyless: I need secret named `my_secret`, here's my API token. + Akeyless->>Akeyless: Check secret ACL. + Akeyless->>KeyfactorCommand: This is allowed, here's the secret. + UO->>KeyfactorCommand: Hello do you have any jobs for me? + KeyfactorCommand->>UO: Yes here's a job with these credentials I pulled from . + UO->>UO: Running job. + UO->>KeyfactorCommand: Job completed. +``` + diff --git a/docsource/overview.md b/docsource/overview.md new file mode 100644 index 0000000..33dd318 --- /dev/null +++ b/docsource/overview.md @@ -0,0 +1,3 @@ +## Overview + +The Akeyless PAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. \ No newline at end of file From c44645ee40ec31d99dadbf53af27551b744c8271 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:43:06 -0800 Subject: [PATCH 02/46] chore: Reformat and cleanup --- TestConsole/Program.cs | 2 +- akeyless-pam/AkeylessPam.cs | 3 ++- integration-manifest.json | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TestConsole/Program.cs b/TestConsole/Program.cs index c325ab4..5b8521f 100644 --- a/TestConsole/Program.cs +++ b/TestConsole/Program.cs @@ -74,7 +74,7 @@ private static void Main(string[] args) instanceParams.Remove("StaticSecretFieldName"); var jsonRaw = pam.GetPassword(instanceParams, initInfo); Console.WriteLine($"ServerSecret: {jsonRaw}"); - + Console.WriteLine("Test completed."); } } \ No newline at end of file diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 8100ffb..507af21 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -185,6 +185,7 @@ private string ParseJsonSecret(string secretValueStr, string fieldName = "") Logger.LogDebug("No field name specified, returning full JSON secret"); return secretValueStr; } + if (jsonObj != null && jsonObj.TryGetValue(fieldName, out var fieldValue)) { Logger.LogDebug("Returning value for field '{FieldName}'", fieldName); @@ -311,7 +312,7 @@ private async Task GetAkeylessSecretAsync(AkeylessConfiguration configur Logger.LogDebug("Attempting to fetch access token from Akeyless at {Url}", configurationInfo.Url); var client = InitClient(configurationInfo); - + switch (configurationInfo.SecretType) { case "static_text": diff --git a/integration-manifest.json b/integration-manifest.json index 7329288..a272106 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -71,7 +71,6 @@ ] } ] - } } } } From 32bc8c50bae4be26bcf4a0e5287ad09efb90e365 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:59:56 -0800 Subject: [PATCH 03/46] chore(docs): Force docs gen --- docs/akeyless.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/akeyless.md diff --git a/docs/akeyless.md b/docs/akeyless.md new file mode 100644 index 0000000..45789c6 --- /dev/null +++ b/docs/akeyless.md @@ -0,0 +1,101 @@ +## Akeyless + +The AkeylessPAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. +Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on +these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) + +## Requirements + +- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. + + + + +## Supported Authentication Methods + +### Access Key (API Key) Authentication +This method uses an Access Key and Access ID pair to authenticate to the Akeyless API. These credentials can be created in the Akeyless console. +For more information, see the [Akeyless documentation](https://tutorials.akeyless.io/docs/authentication-methods-and-api-key-authentication). + +#### Example `manifest.json` configuration: + +```json +{ + "extensions": { + "Keyfactor.Platform.Extensions.IPAMProvider": { + "PAMProviders.Akeyless.PAMProvider": { + "assemblyPath": "akeyless-pam.dll", + "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.Pam" + } + } + }, + "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { + "Url": "https://api.akeyless.io", + "AuthType": "access_key", + "AccessId": "", + "AccessKey": "" + } +} +``` + +## Supported Secret Types +Below are the types of Akeyless secret that are supported by this provider. + +### Static Secrets +For full details on static secrets, see the [Akeyless documentation](https://docs.akeyless.io/docs/secret-management/static-secrets). + +| Secret Type | Description | Additional Fields | +|---------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `static_text` | A static secret where the full value will be retrieved unparsed | N/A | +| `static_json` | A static secret where the full value will be retrieved unparsed | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | +| `static_kv` | A static secret where the full value will be retrieved unparsed | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | + +## Mechanics + +When configuring the Akeyless for use as a PAM Provider with Keyfactor, you will need to ensure that your +instance is configured for API access using the desired auth method. This can be done by an Akeyless administrator. +For more details visit the vendor +docs [here](https://docs.akeyless.io/docs/access-and-authentication-methods). + +Once API access is configured the credential *MUST* be granted access to view secret(s) you'll be using. + +After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from the Akeyless as a PAM Provider. + +### Running the PAM provider on Keyfactor Universal Orchestrator (UO) + +When installing on the Universal Orchestrator (UO), is installed on and run from the UO host. Below is a sequence +diagram +showing the flow of the PAM provider when it is run from the UO. + +```mermaid +sequenceDiagram + KeyfactorCommand->>KeyfactorCommand: New job created. + UO->>KeyfactorCommand: Hello do you have any jobs for me? + KeyfactorCommand->>UO: Yes here's a job. + UO->>Akeyless: Hello here are my client credentials. + Akeyless->>UO: Here's your API token. + UO->>Akeyless: I need secret name 100, here's my API token. + Akeyless->>Akeyless: Check secret ACL. + Akeyless->>UO: This is allowed, here's the secret. + UO->>UO: Running job. + UO->>KeyfactorCommand: Job completed. +``` + +### Running the PAM provider on the Keyfactor Command Host + +When installing the PAM provider on the Keyfactor Command Host, is installed on and run from the Keyfactor Command host. +Below is a sequence diagram showing the flow of the PAM provider when it is run from the Keyfactor Command Host. + +```mermaid +sequenceDiagram + KeyfactorCommand->>KeyfactorCommand: Creating a new job. + KeyfactorCommand->>Akeyless: Hello here are my credentials. + Akeyless->>KeyfactorCommand: Here's your API token. + KeyfactorCommand->>Akeyless: I need secret named `my_secret`, here's my API token. + Akeyless->>Akeyless: Check secret ACL. + Akeyless->>KeyfactorCommand: This is allowed, here's the secret. + UO->>KeyfactorCommand: Hello do you have any jobs for me? + KeyfactorCommand->>UO: Yes here's a job with these credentials I pulled from . + UO->>UO: Running job. + UO->>KeyfactorCommand: Job completed. +``` From f781ed52e3afb5db0fb138c8173da540fb07ce3c Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Tue, 11 Nov 2025 20:01:43 +0000 Subject: [PATCH 04/46] Update generated docs --- README.md | 355 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d695e19 --- /dev/null +++ b/README.md @@ -0,0 +1,355 @@ +

+ Akeyless PAM Provider +

+ +

+ +Integration Status: production +Release +Issues +GitHub Downloads (all assets, all releases) +

+ +

+ + + Support + + · + + Installation + + · + + License + + · + + Related Integrations + +

+ +## Overview + +The Akeyless PAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. + +## Support +The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. + +> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. + +## Getting Started + +The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. When configured, Command will use the Akeyless PAM Provider to retrieve credentials needed to communicate with the target system. There are two ways to install the Akeyless PAM Provider, and you may elect to use one or both methods: + +1. **Locally on the Keyfactor Command server**: PAM credential resolution via the Akeyless PAM Provider will occur on the Keyfactor Command server each time an elegible credential is needed. +2. **Remotely On Universal Orchestrators**: When Jobs are dispatched to Universal Orchestrators, the associated Certificate Store extension assembly will use the Akeyless PAM Provider to resolve eligible PAM credentials. + +Before proceeding with installation, you should consider which pattern is best for your requirements and use case. + +### Installation + +> [!IMPORTANT] +> For the most up-to-date and complete documentation on how to install a PAM provider extension, please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Preparing%20Third%20Party%20PAM%20Providers%20to%20Work%20with.htm?Highlight=pam%20provider#InstallingCustomPAMProviderExtensions) + + +To install Akeyless PAM Provider, it is recommended you install [kfutil](https://github.com/Keyfactor/kfutil). `kfutil` is a command-line tool that simplifies the process of creating PAM Types in Keyfactor Command. + + + + + + + +#### Requirements + - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. + +#### Create PAM type in Keyfactor Command + + +##### Using `kfutil` +Create the required PAM Types in the connected Command platform. + +```shell +# Akeyless +kfutil pam types-create -r akeyless-pam -n Akeyless +``` + +##### Using the API +For full API docs please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/PAMProvidersPOSTTypes.htm?Highlight=pam%20type) + +Below is the payload to `POST` to the Keyfactor Command API +```json +{ + "Name": "Akeyless", + "Parameters": [ + { + "Name": "Url", + "DisplayName": "Secret Server URL", + "Description": "The URL to the Secret Server instance. Example: https://example.cloud.com/", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "AccessKeyId", + "DisplayName": "Access Key ID", + "Description": "The access key ID used to authenticate to Akeyless using `access_key` authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AccessKey", + "DisplayName": "Access Key", + "Description": "The access key used to authenticate to Akeyless using `access_key` authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AuthType", + "DisplayName": "Auth Type", + "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`.", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "SecretName", + "DisplayName": "Secret Name", + "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "SecretType", + "DisplayName": "Secret Type", + "Description": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "StaticSecretFieldName", + "DisplayName": "Static Secret Field Name", + "Description": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types.", + "DataType": 1, + "InstanceLevel": true + } + ] +} +``` + +#### Install PAM provider on Keyfactor Command Host (Local) + + + +1. On the server that hosts Keyfactor Command, download and unzip the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. + +2. Copy the assemblies to the appropriate directories on the Keyfactor Command server: + +
Keyfactor Command 11+ + + 1. Copy the unzipped assemblies to each of the following directories: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\akeyless-pam` + +
+ +
Keyfactor Command 10 + + 1. Copy the assemblies to each of the following directories: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\akeyless-pam` + + 2. Open a text editor on the Keyfactor Command server as an administrator and open the `web.config` file located in the `WebAgentServices` directory. + + 3. In the `web.config` file, locate the ` ` section and add the following registration: + + ```xml + + ... + + + + + + ``` + + 4. Repeat steps 2 and 3 for each of the directories listed in step 1. The configuration files are located in the following paths by default: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\CMSTimerService.exe.config` + +
+ +3. Restart the Keyfactor Command services (`iisreset`). + + + + +#### Install PAM provider on a Universal Orchestrator Host (Remote) + + +1. Install the Akeyless PAM Provider assemblies. + + * **Using kfutil**: On the server that that hosts the Universal Orchestrator, run the following command: + + ```shell + # Windows Server + kfutil orchestrator extension -e akeyless-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" + + # Linux + kfutil orchestrator extension -e akeyless-pam@latest --out "/opt/keyfactor/orchestrator/extensions" + ``` + + * **Manually**: Download the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. Extract the contents of the archive to: + + * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\akeyless-pam` + * **Linux**: `/opt/keyfactor/orchestrator/extensions/akeyless-pam` + +2. Included in the release is a `manifest.json` file that contains the following object: + ```json + + { + "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { + "Url": "https://api.akeyless.io", + "AuthType": "access_key", + "AccessId": "", + "AccessKey": "" + } + } + + ``` + + Populate the fields in this object with credentials and configuration data collected in the [requirements](docs/akeyless.md#requirements) section. + +3. Restart the Universal Orchestrator service. + + + + + + + + + +### Usage + + + + + + +#### From Keyfactor Command Host (Local) + + + +##### Define a PAM provider in Command +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. + +2. Select the **Add** button to create a new PAM provider. Click the dropdown for **Provider Type** and select **Akeyless**. + +> [!IMPORTANT] +> If you're running Keyfactor Command 11+, make sure `Remote Provider` is unchecked. + +3. Populate the fields with the necessary information collected in the [requirements](docs/akeyless.md#requirements) section: + +| Initialization parameter | Display Name | Description | +| --- | --- | --- | +| Url | Secret Server URL | The URL to the Secret Server instance. Example: https://example.cloud.com/ | +| AccessKeyId | Access Key ID | The access key ID used to authenticate to Akeyless using `access_key` authentication. | +| AccessKey | Access Key | The access key used to authenticate to Akeyless using `access_key` authentication. | +| AuthType | Auth Type | The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`. | + + +4. Click **Save**. The PAM provider is now available for use in Keyfactor Command. + +##### Using the PAM provider + +Now, when defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** will be available as a PAM provider option. When defining new Certificate Stores, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. + +Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: + +| Instance parameter | Display Name | Description | +| --- | --- | --- | +| SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | +| SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | +| StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | + + + + + +#### From a Universal Orchestrator Host (Remote) + + + +
Keyfactor Command 11+ + +##### Define a remote PAM provider in Command + +In Command 11 and greater, before using the Akeyless PAM type, you must define a Remote PAM Provider in the Command portal. + +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. + +2. Select the **Add** button to create a new PAM provider. + +3. Make sure that `Remote Provider` is checked. + +4. Click the dropdown for **Provider Type** and select **Akeyless**. + +5. Give the provider a unique name. + +6. Click "Save". + +##### Using the PAM provider + +When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. When defining a new Certificate Store, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. + +Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: + +| Instance parameter | Display Name | Description | +| --- | --- | --- | +| SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | +| SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | +| StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | + + +
+ +
Keyfactor Command 10 + +When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. + +When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and populate the **Secret Value** field with the following JSON object: + +```json +{"SecretName": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.","SecretType": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.","StaticSecretFieldName": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types."} + +``` + +> We recommend creating this JSON object in a text editor, and copying it into the Secret Value field. + +
+ + + + + + +> [!NOTE] +> Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). + + + +## License + +Apache License 2.0, see [LICENSE](LICENSE) + +## Related Integrations + +See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). \ No newline at end of file From 0b61ecb8cd005234dd3cc0fe2284703b2b2d4c30 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:35:25 -0800 Subject: [PATCH 05/46] chore: Remove .net6 build target --- akeyless-pam/akeyless-pam.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akeyless-pam/akeyless-pam.csproj b/akeyless-pam/akeyless-pam.csproj index d5e085b..d92d7cd 100644 --- a/akeyless-pam/akeyless-pam.csproj +++ b/akeyless-pam/akeyless-pam.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0;net9.0 + net8.0;net9.0 true Keyfactor.Extensions.PAM.Akeyless latest From 29df34c6255cf75e279143a444f37d3d0656660b Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:41:52 -0800 Subject: [PATCH 06/46] chore(docs): Force C# primary language --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5a35c3d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* linguist-language=C# \ No newline at end of file From 877267cf06a66106c1292734696a5626607ec944 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:45:32 -0800 Subject: [PATCH 07/46] chore: Force build workflow --- akeyless-pam/akeyless-pam.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akeyless-pam/akeyless-pam.csproj b/akeyless-pam/akeyless-pam.csproj index d92d7cd..d753998 100644 --- a/akeyless-pam/akeyless-pam.csproj +++ b/akeyless-pam/akeyless-pam.csproj @@ -9,7 +9,7 @@ true Keyfactor.PAM.Akeyless - true + false portable false From 9bbd385c2953523be3732814d0a6b53cb29ad30c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:45:32 -0800 Subject: [PATCH 08/46] chore: Conditionally include debug symbols --- akeyless-pam/akeyless-pam.csproj | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/akeyless-pam/akeyless-pam.csproj b/akeyless-pam/akeyless-pam.csproj index d92d7cd..3f01ff7 100644 --- a/akeyless-pam/akeyless-pam.csproj +++ b/akeyless-pam/akeyless-pam.csproj @@ -8,10 +8,8 @@ true true Keyfactor.PAM.Akeyless - - true - portable - false + + true @@ -19,6 +17,11 @@ false + + portable + true + + From e5538e15c1a608b5f79c8d24fa454a0690f300bb Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:43:21 -0700 Subject: [PATCH 09/46] feat(tests): add unit and integration test suite with IAkeylessApiClient abstraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce IAkeylessApiClient interface and AkeylessApiClient adapter to decouple V2Api SDK from AkeylessPam, enabling unit testing via Moq - Add internal constructor on AkeylessPam accepting a client factory - Call Validator.TryValidateObject after building AkeylessConfiguration, making IValidatableObject.Validate() active in production code - Fix AkeylessConfiguration.Validate() bug: StaticSecretFieldName was incorrectly required for static_json (optional per docs); now only required for static_kv - Add InternalsVisibleTo for both test assemblies and DynamicProxyGenAssembly2 - Add AkeylessPam.Unit.Tests (27 tests, xUnit + Moq, 86% line coverage) - Add AkeylessPam.Integration.Tests (15 tests, skip when credentials absent, DotEnvLoader reads .env for local dev) - Fix docs/docsource: wrong TypeFullName (Pam → AkeylessPam), inaccurate secret type descriptions, broken sequence diagram text, grammar fixes - Add CLAUDE.md and docsource/testing.md --- CLAUDE.md | 90 ++++++ README.md | 36 +-- akeyless-pam.sln | 59 +++- akeyless-pam/AkeylessApiClient.cs | 42 +++ akeyless-pam/AkeylessPam.cs | 71 +++-- akeyless-pam/IAkeylessApiClient.cs | 25 ++ akeyless-pam/Models/AkeylessConfiguration.cs | 17 +- akeyless-pam/akeyless-pam.csproj | 15 +- docs/akeyless.md | 29 +- docsource/akeyless.md | 30 +- docsource/testing.md | 115 +++++++ .../AkeylessApiClientTests.cs | 140 +++++++++ .../AkeylessPam.Integration.Tests.csproj | 28 ++ .../AkeylessPamIntegrationTests.cs | 223 +++++++++++++ .../DotEnvLoader.cs | 49 +++ .../AkeylessConfigurationTests.cs | 169 ++++++++++ .../AkeylessPam.Unit.Tests.csproj | 28 ++ .../AkeylessPamTests.cs | 294 ++++++++++++++++++ 18 files changed, 1367 insertions(+), 93 deletions(-) create mode 100644 CLAUDE.md create mode 100644 akeyless-pam/AkeylessApiClient.cs create mode 100644 akeyless-pam/IAkeylessApiClient.cs create mode 100644 docsource/testing.md create mode 100644 tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs create mode 100644 tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj create mode 100644 tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs create mode 100644 tests/AkeylessPam.Integration.Tests/DotEnvLoader.cs create mode 100644 tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs create mode 100644 tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj create mode 100644 tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..363958f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the **Akeyless PAM Provider** for Keyfactor Command — a C# class library that implements the `IPAMProvider` interface to retrieve secrets from Akeyless and provide them as credentials to Keyfactor Command and Universal Orchestrator extensions. + +## Build Commands + +```shell +# Build the PAM provider library +dotnet build akeyless-pam/akeyless-pam.csproj + +# Build release (no debug symbols) +dotnet build akeyless-pam/akeyless-pam.csproj -c Release + +# Build the test console +dotnet build TestConsole/TestConsole.csproj +``` + +## Tests + +```shell +# Unit tests (no external dependencies, always runnable) +dotnet test tests/AkeylessPam.Unit.Tests/ + +# Integration tests (skip automatically when env vars absent) +dotnet test tests/AkeylessPam.Integration.Tests/ + +# Both test projects +dotnet test +``` + +Integration tests require env vars: +- `AKEYLESS_ACCESS_ID` / `AKEYLESS_ACCESS_KEY` — credentials (required for any integration test) +- `AKEYLESS_API_URL` — defaults to `https://api.akeyless.io` +- `AKEYLESS_AUTH_TYPE` — defaults to `access_key` +- `AKEYLESS_SECRET_STATIC_TEXT` / `AKEYLESS_SECRET_STATIC_KV` / `AKEYLESS_SECRET_STATIC_JSON` / `AKEYLESS_SECRET_STATIC_JSON_RAW` — paths to Akeyless secrets for each secret-type test + +## Running the Test Console + +The `TestConsole` project is a manual integration test harness — there are no automated unit tests. Configure environment variables, then run: + +```shell +export AKEYLESS_API_URL="https://api.akeyless.io" +export AKEYLESS_AUTH_TYPE="access_key" +export AKEYLESS_ACCESS_ID="" +export AKEYLESS_ACCESS_KEY="" + +dotnet run --project TestConsole/TestConsole.csproj +``` + +The test console exercises all three secret types (`static_text`, `static_kv`, `static_json`) against hardcoded secret paths under `pam/test/` in Akeyless. + +## Architecture + +The solution has two projects: + +- **`akeyless-pam/`** — The PAM provider library (targets `net8.0`). This is what gets deployed to Keyfactor Command or Universal Orchestrator hosts. +- **`TestConsole/`** — A console app for manual end-to-end testing against a live Akeyless instance. + +### Key Files + +- `akeyless-pam/AkeylessPam.cs` — Main provider class implementing `IPAMProvider`. Entry point is `GetPassword()`, which builds config, authenticates, and retrieves the secret. +- `akeyless-pam/Models/AkeylessConfiguration.cs` — Configuration model with parameter key constants (used as dictionary keys when Keyfactor calls `GetPassword`), validation attributes, and supported types. +- `akeyless-pam/Constants.cs` — Default values (`access_key` auth, `https://api.akeyless.io`). +- `akeyless-pam/manifest.json` — Copied to output; used by Universal Orchestrators to configure the PAM provider. + +### How It Works + +Keyfactor Command calls `IPAMProvider.GetPassword(instanceParameters, serverConfigurationParameters)` with two dictionaries: + +- **Server (initialization) parameters** — `Url`, `AuthType`, `AccessId`, `AccessKey` — set once per PAM provider instance in Command. +- **Instance parameters** — `SecretName`, `SecretType`, `StaticSecretFieldName` — set per Certificate Store or credential. + +The provider authenticates to Akeyless using the `akeyless` NuGet SDK (`V2Api`), then retrieves the secret via `GetSecretValue`. Secret parsing depends on `SecretType`: +- `static_text` — returns raw string (auto-detects JSON and KV formats) +- `static_kv` — parses `key=value\n` lines, requires `StaticSecretFieldName` +- `static_json` — deserializes JSON, optionally extracts a field by `StaticSecretFieldName` + +The `AKEYLESS_API_URL` environment variable overrides the configured URL at runtime. + +### PAM Provider Registration + +The provider is registered in Keyfactor Command by its fully qualified class name `Keyfactor.Extensions.Pam.Akeyless` and display name `Akeyless`. The `integration-manifest.json` at the repo root drives the Keyfactor CI/CD release pipeline (via the `keyfactor/actions` reusable workflow). + +## Release + +Releases are built from `akeyless-pam/bin/Release` (as defined in `integration-manifest.json`). The GitHub Actions workflow (`keyfactor-starter-workflow.yml`) handles building, signing, and publishing releases automatically on push/PR events. diff --git a/README.md b/README.md index d695e19..0735881 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -72,7 +72,7 @@ Create the required PAM Types in the connected Command platform. ```shell # Akeyless -kfutil pam types-create -r akeyless-pam -n Akeyless +kfutil pam types-create -r delinea-secretserver-pam -n Akeyless ``` ##### Using the API @@ -85,8 +85,8 @@ Below is the payload to `POST` to the Keyfactor Command API "Parameters": [ { "Name": "Url", - "DisplayName": "Secret Server URL", - "Description": "The URL to the Secret Server instance. Example: https://example.cloud.com/", + "DisplayName": "Akeyless URL", + "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", "DataType": 1, "InstanceLevel": false }, @@ -148,9 +148,9 @@ Below is the payload to `POST` to the Keyfactor Command API 1. Copy the unzipped assemblies to each of the following directories: - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\delinea-secretserver-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\delinea-secretserver-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\delinea-secretserver-pam` @@ -158,10 +158,10 @@ Below is the payload to `POST` to the Keyfactor Command API 1. Copy the assemblies to each of the following directories: - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\delinea-secretserver-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\delinea-secretserver-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\delinea-secretserver-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\delinea-secretserver-pam` 2. Open a text editor on the Keyfactor Command server as an administrator and open the `web.config` file located in the `WebAgentServices` directory. @@ -200,16 +200,16 @@ Below is the payload to `POST` to the Keyfactor Command API ```shell # Windows Server - kfutil orchestrator extension -e akeyless-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" + kfutil orchestrator extension -e delinea-secretserver-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" # Linux - kfutil orchestrator extension -e akeyless-pam@latest --out "/opt/keyfactor/orchestrator/extensions" + kfutil orchestrator extension -e delinea-secretserver-pam@latest --out "/opt/keyfactor/orchestrator/extensions" ``` * **Manually**: Download the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. Extract the contents of the archive to: - * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\akeyless-pam` - * **Linux**: `/opt/keyfactor/orchestrator/extensions/akeyless-pam` + * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\delinea-secretserver-pam` + * **Linux**: `/opt/keyfactor/orchestrator/extensions/delinea-secretserver-pam` 2. Included in the release is a `manifest.json` file that contains the following object: ```json @@ -260,7 +260,7 @@ Below is the payload to `POST` to the Keyfactor Command API | Initialization parameter | Display Name | Description | | --- | --- | --- | -| Url | Secret Server URL | The URL to the Secret Server instance. Example: https://example.cloud.com/ | +| Url | Akeyless URL | The URL to the Akeyless instance. Defaults to: https://api.akeyless.io | | AccessKeyId | Access Key ID | The access key ID used to authenticate to Akeyless using `access_key` authentication. | | AccessKey | Access Key | The access key used to authenticate to Akeyless using `access_key` authentication. | | AuthType | Auth Type | The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`. | diff --git a/akeyless-pam.sln b/akeyless-pam.sln index b604762..1f42cae 100644 --- a/akeyless-pam.sln +++ b/akeyless-pam.sln @@ -4,20 +4,77 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "akeyless-pam", "akeyless-pa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsole", "TestConsole\TestConsole.csproj", "{90C4CEE8-44EE-4488-B464-4063432051D8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkeylessPam.Unit.Tests", "tests\AkeylessPam.Unit.Tests\AkeylessPam.Unit.Tests.csproj", "{5A2FD372-A499-40F7-8448-1955FC09F591}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkeylessPam.Integration.Tests", "tests\AkeylessPam.Integration.Tests\AkeylessPam.Integration.Tests.csproj", "{D3F87543-EB4C-4242-8668-255D04A2113D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|x64.Build.0 = Debug|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|x86.Build.0 = Debug|Any CPU {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|Any CPU.ActiveCfg = Release|Any CPU {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|Any CPU.Build.0 = Release|Any CPU - {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|x64.ActiveCfg = Release|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|x64.Build.0 = Release|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|x86.ActiveCfg = Release|Any CPU + {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|x86.Build.0 = Release|Any CPU {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x64.ActiveCfg = Debug|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x64.Build.0 = Debug|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x86.ActiveCfg = Debug|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x86.Build.0 = Debug|Any CPU {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|Any CPU.Build.0 = Release|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x64.ActiveCfg = Release|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x64.Build.0 = Release|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x86.ActiveCfg = Release|Any CPU + {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x86.Build.0 = Release|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|x64.Build.0 = Debug|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|x86.Build.0 = Debug|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Release|Any CPU.Build.0 = Release|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Release|x64.ActiveCfg = Release|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Release|x64.Build.0 = Release|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Release|x86.ActiveCfg = Release|Any CPU + {5A2FD372-A499-40F7-8448-1955FC09F591}.Release|x86.Build.0 = Release|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Debug|x64.Build.0 = Debug|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Debug|x86.Build.0 = Debug|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Release|Any CPU.Build.0 = Release|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Release|x64.ActiveCfg = Release|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Release|x64.Build.0 = Release|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Release|x86.ActiveCfg = Release|Any CPU + {D3F87543-EB4C-4242-8668-255D04A2113D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5A2FD372-A499-40F7-8448-1955FC09F591} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {D3F87543-EB4C-4242-8668-255D04A2113D} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/akeyless-pam/AkeylessApiClient.cs b/akeyless-pam/AkeylessApiClient.cs new file mode 100644 index 0000000..f6d6522 --- /dev/null +++ b/akeyless-pam/AkeylessApiClient.cs @@ -0,0 +1,42 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using akeyless.Api; +using akeyless.Client; +using akeyless.Model; + +namespace Keyfactor.Extensions.Pam.Akeyless; + +internal class AkeylessApiClient : IAkeylessApiClient +{ + private readonly V2Api _api; + + internal AkeylessApiClient(string basePath) + { + var config = new Configuration { BasePath = basePath }; + _api = new V2Api(config); + } + + public string Authenticate(string accessId, string accessKey) + { + var authResp = _api.Auth(new Auth(accessId, accessKey)); + if (string.IsNullOrEmpty(authResp.Token) && string.IsNullOrEmpty(authResp.Creds?.Token)) + return string.Empty; + return string.IsNullOrEmpty(authResp.Token) ? authResp.Creds.Token : authResp.Token; + } + + public async Task> GetSecretValuesAsync(IEnumerable names, string token) + { + var nameList = names.ToList(); + var req = new GetSecretValue(names: nameList, token: token); + var result = await _api.GetSecretValueAsync(req); + return result.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); + } +} diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 507af21..525cca6 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -7,11 +7,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using akeyless.Api; using akeyless.Client; -using akeyless.Model; using Keyfactor.Extensions.Pam.Akeyless.Models; using Keyfactor.Logging; using Keyfactor.Platform.Extensions; @@ -70,10 +70,21 @@ public InvalidSecretConfigurationException(string message) : base(message) /// public class AkeylessPam : IPAMProvider { + private readonly Func _clientFactory; + private ILogger Logger { get; } = LogHandler.GetClassLogger(); private string AuthToken { get; set; } = string.Empty; + public AkeylessPam() : this(basePath => new AkeylessApiClient(basePath)) + { + } + + internal AkeylessPam(Func clientFactory) + { + _clientFactory = clientFactory; + } + ///

/// Gets the name of this PAM provider. /// @@ -113,47 +124,39 @@ public string GetPassword(Dictionary instanceParameters, } } - private V2Api InitClient(AkeylessConfiguration configurationInfo) + private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) { try { Logger.MethodEntry(); - var config = new Configuration - { - BasePath = Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? - configurationInfo.Url ?? "https://api.akeyless.io" - }; + var basePath = Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? + configurationInfo.Url ?? "https://api.akeyless.io"; + + var client = _clientFactory(basePath); - var api = new V2Api(config); switch (configurationInfo.AuthType) { case "access_key": Logger.LogDebug("Authenticating with Akeyless using access_key"); - var authRequest = new Auth( - configurationInfo.AccessId, - configurationInfo.AccessKey - ); - // Logger.LogTrace("Auth Request: {@AuthRequest}", - // authRequest); //TODO: COMMENT THIS OUT it exposes secrets - var authResp = api.Auth(authRequest); - - if (string.IsNullOrEmpty(authResp.Token) && string.IsNullOrEmpty(authResp.Creds.Token)) + var token = client.Authenticate(configurationInfo.AccessId, configurationInfo.AccessKey); + + if (string.IsNullOrEmpty(token)) { Logger.LogError("Unable to obtain access token from Akeyless server"); throw new InvalidTokenException("Unable to obtain access token from Akeyless server"); } - AuthToken = string.IsNullOrEmpty(authResp.Token) ? authResp.Creds.Token : authResp.Token; + AuthToken = token; Logger.LogInformation("Successfully authenticated with Akeyless"); - break; + default: Logger.LogWarning("No authentication performed for auth type '{AuthType}'", configurationInfo.AuthType); break; } - return api; + return client; } catch (ApiException ex) { @@ -239,20 +242,17 @@ private string ParseKvSecret(string secretValueStr, string fieldName) } } - private async Task GetStaticSecret(V2Api client, AkeylessConfiguration configurationInfo) + private async Task GetStaticSecret(IAkeylessApiClient client, AkeylessConfiguration configurationInfo) { try { Logger.MethodEntry(); Logger.LogDebug("Fetching static text secret '{SecretName}'", configurationInfo.SecretName); - var req = new GetSecretValue( - names: [configurationInfo.SecretName], - token: AuthToken - ); - var secrets = await client.GetSecretValueAsync(req); - if (!secrets.TryGetValue(configurationInfo.SecretName, out var secretValue)) + var secrets = await client.GetSecretValuesAsync([configurationInfo.SecretName], AuthToken); + + if (!secrets.TryGetValue(configurationInfo.SecretName, out var secretValueStr)) { Logger.LogError("Secret '{SecretName}' not found in Akeyless", configurationInfo.SecretName); @@ -260,7 +260,6 @@ private async Task GetStaticSecret(V2Api client, AkeylessConfiguration c $"Secret '{configurationInfo.SecretName}' not found in Akeyless"); } - var secretValueStr = secretValue.ToString() ?? string.Empty; if (string.IsNullOrEmpty(secretValueStr)) { Logger.LogError("Secret '{SecretName}' has an empty value", @@ -561,6 +560,16 @@ private AkeylessConfiguration BuildAkeylessConfiguration( break; } + var validationContext = new ValidationContext(config); + var validationResults = new List(); + if (!Validator.TryValidateObject(config, validationContext, validationResults, validateAllProperties: true)) + { + var errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); + Logger.LogError("Akeyless configuration model validation failed: {Errors}", errors); + throw new InvalidClientConfigurationException( + $"Akeyless configuration validation failed: {errors}"); + } + return config; } finally @@ -568,4 +577,4 @@ private AkeylessConfiguration BuildAkeylessConfiguration( Logger.MethodExit(); } } -} \ No newline at end of file +} diff --git a/akeyless-pam/IAkeylessApiClient.cs b/akeyless-pam/IAkeylessApiClient.cs new file mode 100644 index 0000000..4c076e7 --- /dev/null +++ b/akeyless-pam/IAkeylessApiClient.cs @@ -0,0 +1,25 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.Pam.Akeyless; + +public interface IAkeylessApiClient +{ + /// + /// Authenticates to Akeyless using the provided access key credentials. + /// + /// The auth token, or empty string if authentication failed. + string Authenticate(string accessId, string accessKey); + + /// + /// Retrieves secret values by name from Akeyless. + /// + Task> GetSecretValuesAsync(IEnumerable names, string token); +} diff --git a/akeyless-pam/Models/AkeylessConfiguration.cs b/akeyless-pam/Models/AkeylessConfiguration.cs index f9d139a..6dbf64d 100644 --- a/akeyless-pam/Models/AkeylessConfiguration.cs +++ b/akeyless-pam/Models/AkeylessConfiguration.cs @@ -179,16 +179,11 @@ public IEnumerable Validate(ValidationContext validationContex $"Unsupported SecretType. Supported types are: {string.Join(", ", SupportedSecretTypes)}.", [nameof(SecretType)] ); - switch (SecretType) - { - case "static_kv": - case "static_json": - if (string.IsNullOrWhiteSpace(StaticSecretFieldName)) - yield return new ValidationResult( - "StaticSecretFieldName must be provided when SecretType is 'static_kv|static_json'.", - [nameof(StaticSecretFieldName)] - ); - break; - } + // StaticSecretFieldName is required for static_kv, optional for static_json + if (SecretType == "static_kv" && string.IsNullOrWhiteSpace(StaticSecretFieldName)) + yield return new ValidationResult( + "StaticSecretFieldName must be provided when SecretType is 'static_kv'.", + [nameof(StaticSecretFieldName)] + ); } } \ No newline at end of file diff --git a/akeyless-pam/akeyless-pam.csproj b/akeyless-pam/akeyless-pam.csproj index 3f01ff7..eed14e9 100644 --- a/akeyless-pam/akeyless-pam.csproj +++ b/akeyless-pam/akeyless-pam.csproj @@ -1,7 +1,7 @@ - net8.0;net9.0 + net8.0 true Keyfactor.Extensions.PAM.Akeyless latest @@ -55,4 +55,17 @@ + + + <_Parameter1>AkeylessPam.Unit.Tests + + + <_Parameter1>AkeylessPam.Integration.Tests + + + + <_Parameter1>DynamicProxyGenAssembly2 + + + \ No newline at end of file diff --git a/docs/akeyless.md b/docs/akeyless.md index 45789c6..c1e44ea 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -1,6 +1,6 @@ ## Akeyless -The AkeylessPAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. +The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) @@ -25,7 +25,7 @@ For more information, see the [Akeyless documentation](https://tutorials.akeyles "Keyfactor.Platform.Extensions.IPAMProvider": { "PAMProviders.Akeyless.PAMProvider": { "assemblyPath": "akeyless-pam.dll", - "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.Pam" + "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.AkeylessPam" } } }, @@ -44,27 +44,26 @@ Below are the types of Akeyless secret that are supported by this provider. ### Static Secrets For full details on static secrets, see the [Akeyless documentation](https://docs.akeyless.io/docs/secret-management/static-secrets). -| Secret Type | Description | Additional Fields | -|---------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| -| `static_text` | A static secret where the full value will be retrieved unparsed | N/A | -| `static_json` | A static secret where the full value will be retrieved unparsed | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | -| `static_kv` | A static secret where the full value will be retrieved unparsed | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | +| Secret Type | Description | Additional Fields | +|---------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `static_text` | A static secret whose value is returned as a plain string | N/A | +| `static_json` | A static secret containing JSON; a specific field can optionally be extracted | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | +| `static_kv` | A static secret containing key-value pairs; a specific field is extracted by name | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | ## Mechanics -When configuring the Akeyless for use as a PAM Provider with Keyfactor, you will need to ensure that your +When configuring Akeyless for use as a PAM Provider with Keyfactor, you will need to ensure that your instance is configured for API access using the desired auth method. This can be done by an Akeyless administrator. For more details visit the vendor docs [here](https://docs.akeyless.io/docs/access-and-authentication-methods). Once API access is configured the credential *MUST* be granted access to view secret(s) you'll be using. -After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from the Akeyless as a PAM Provider. +After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from Akeyless as a PAM Provider. ### Running the PAM provider on Keyfactor Universal Orchestrator (UO) -When installing on the Universal Orchestrator (UO), is installed on and run from the UO host. Below is a sequence -diagram +When installing on the Universal Orchestrator (UO), the PAM provider is installed on and run from the UO host. Below is a sequence diagram showing the flow of the PAM provider when it is run from the UO. ```mermaid @@ -74,16 +73,16 @@ sequenceDiagram KeyfactorCommand->>UO: Yes here's a job. UO->>Akeyless: Hello here are my client credentials. Akeyless->>UO: Here's your API token. - UO->>Akeyless: I need secret name 100, here's my API token. + UO->>Akeyless: I need secret named `my_secret`, here's my API token. Akeyless->>Akeyless: Check secret ACL. - Akeyless->>UO: This is allowed, here's the secret. + Akeyless->>UO: This is allowed, here's the secret. UO->>UO: Running job. UO->>KeyfactorCommand: Job completed. ``` ### Running the PAM provider on the Keyfactor Command Host -When installing the PAM provider on the Keyfactor Command Host, is installed on and run from the Keyfactor Command host. +When installing the PAM provider on the Keyfactor Command Host, it is installed on and run from the Keyfactor Command host. Below is a sequence diagram showing the flow of the PAM provider when it is run from the Keyfactor Command Host. ```mermaid @@ -95,7 +94,7 @@ sequenceDiagram Akeyless->>Akeyless: Check secret ACL. Akeyless->>KeyfactorCommand: This is allowed, here's the secret. UO->>KeyfactorCommand: Hello do you have any jobs for me? - KeyfactorCommand->>UO: Yes here's a job with these credentials I pulled from . + KeyfactorCommand->>UO: Yes here's a job with these credentials I pulled from Akeyless. UO->>UO: Running job. UO->>KeyfactorCommand: Job completed. ``` diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 739272d..ba5cb3c 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -1,6 +1,6 @@ ## Overview -The AkeylessPAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. +The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) @@ -22,7 +22,7 @@ For more information, see the [Akeyless documentation](https://tutorials.akeyles "Keyfactor.Platform.Extensions.IPAMProvider": { "PAMProviders.Akeyless.PAMProvider": { "assemblyPath": "akeyless-pam.dll", - "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.Pam" + "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.AkeylessPam" } } }, @@ -41,28 +41,27 @@ Below are the types of Akeyless secret that are supported by this provider. ### Static Secrets For full details on static secrets, see the [Akeyless documentation](https://docs.akeyless.io/docs/secret-management/static-secrets). -| Secret Type | Description | Additional Fields | -|---------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| -| `static_text` | A static secret where the full value will be retrieved unparsed | N/A | -| `static_json` | A static secret where the full value will be retrieved unparsed | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | -| `static_kv` | A static secret where the full value will be retrieved unparsed | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | +| Secret Type | Description | Additional Fields | +|---------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `static_text` | A static secret whose value is returned as a plain string | N/A | +| `static_json` | A static secret containing JSON; a specific field can optionally be extracted | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | +| `static_kv` | A static secret containing key-value pairs; a specific field is extracted by name | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | ## Mechanics -When configuring the Akeyless for use as a PAM Provider with Keyfactor, you will need to ensure that your +When configuring Akeyless for use as a PAM Provider with Keyfactor, you will need to ensure that your instance is configured for API access using the desired auth method. This can be done by an Akeyless administrator. For more details visit the vendor docs [here](https://docs.akeyless.io/docs/access-and-authentication-methods). Once API access is configured the credential *MUST* be granted access to view secret(s) you'll be using. -After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from the Akeyless as a PAM Provider. +After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from Akeyless as a PAM Provider. ### Running the PAM provider on Keyfactor Universal Orchestrator (UO) -When installing on the Universal Orchestrator (UO), is installed on and run from the UO host. Below is a sequence -diagram +When installing on the Universal Orchestrator (UO), the PAM provider is installed on and run from the UO host. Below is a sequence diagram showing the flow of the PAM provider when it is run from the UO. ```mermaid @@ -72,16 +71,16 @@ sequenceDiagram KeyfactorCommand->>UO: Yes here's a job. UO->>Akeyless: Hello here are my client credentials. Akeyless->>UO: Here's your API token. - UO->>Akeyless: I need secret name 100, here's my API token. + UO->>Akeyless: I need secret named `my_secret`, here's my API token. Akeyless->>Akeyless: Check secret ACL. - Akeyless->>UO: This is allowed, here's the secret. + Akeyless->>UO: This is allowed, here's the secret. UO->>UO: Running job. UO->>KeyfactorCommand: Job completed. ``` ### Running the PAM provider on the Keyfactor Command Host -When installing the PAM provider on the Keyfactor Command Host, is installed on and run from the Keyfactor Command host. +When installing the PAM provider on the Keyfactor Command Host, it is installed on and run from the Keyfactor Command host. Below is a sequence diagram showing the flow of the PAM provider when it is run from the Keyfactor Command Host. ```mermaid @@ -93,8 +92,7 @@ sequenceDiagram Akeyless->>Akeyless: Check secret ACL. Akeyless->>KeyfactorCommand: This is allowed, here's the secret. UO->>KeyfactorCommand: Hello do you have any jobs for me? - KeyfactorCommand->>UO: Yes here's a job with these credentials I pulled from . + KeyfactorCommand->>UO: Yes here's a job with these credentials I pulled from Akeyless. UO->>UO: Running job. UO->>KeyfactorCommand: Job completed. ``` - diff --git a/docsource/testing.md b/docsource/testing.md new file mode 100644 index 0000000..a8a2d83 --- /dev/null +++ b/docsource/testing.md @@ -0,0 +1,115 @@ +## Testing + +The test suite is split into two projects under `tests/`: + +| Project | Purpose | +|---|---| +| `AkeylessPam.Unit.Tests` | Pure unit tests — no network, always runnable | +| `AkeylessPam.Integration.Tests` | Tests against a live Akeyless instance — skip automatically when credentials are absent | + +### Running Tests + +```shell +# Unit tests only +dotnet test tests/AkeylessPam.Unit.Tests/ + +# Integration tests only +dotnet test tests/AkeylessPam.Integration.Tests/ +``` + +Integration tests load credentials from environment variables. As a convenience for local development, they also read a `.env` file in the repository root if present. Environment variables always take precedence over `.env` values. + +#### Required environment variables + +| Variable | Description | +|---|---| +| `AKEYLESS_ACCESS_ID` | Akeyless access key ID | +| `AKEYLESS_ACCESS_KEY` | Akeyless access key secret | + +#### Optional environment variables + +| Variable | Default | Description | +|---|---|---| +| `AKEYLESS_API_URL` | `https://api.akeyless.io` | Akeyless API base URL | +| `AKEYLESS_AUTH_TYPE` | `access_key` | Auth type | +| `AKEYLESS_SECRET_STATIC_TEXT` | `pam/test/pamStaticTextUsername` | Path to a `static_text` secret | +| `AKEYLESS_SECRET_STATIC_TEXT_2` | `pam/test/pamStaticTextPassword` | Path to a second `static_text` secret (used in multi-secret test) | +| `AKEYLESS_SECRET_STATIC_KV` | `pam/test/pamStaticKV` | Path to a `static_kv` secret with `username` and `password` fields | +| `AKEYLESS_SECRET_STATIC_JSON` | `pam/test/pamStaticJSON` | Path to a `static_json` secret with `username` and `password` fields | +| `AKEYLESS_SECRET_STATIC_JSON_RAW` | — | Path to a `static_json` secret to retrieve as a raw blob (no field extraction) | + +### Unit Test Cases + +#### Validation (`ValidationTests`) + +| Test | What it verifies | +|---|---| +| `GetPassword_MissingSecretName_ThrowsInvalidClientConfigurationException` | `SecretName` absent in instance parameters → exception | +| `GetPassword_MissingAccessId_ThrowsInvalidClientConfigurationException` | `AccessId` absent in server parameters → exception | +| `GetPassword_MissingAccessKey_ThrowsInvalidClientConfigurationException` | `AccessKey` absent in server parameters → exception | +| `GetPassword_InvalidAuthType_Throws` | Unknown `AuthType` value → exception | +| `GetPassword_InvalidSecretType_ThrowsInvalidClientConfigurationException` | Unknown `SecretType` value → `InvalidClientConfigurationException` (caught at model validation before the async path) | + +#### Authentication (`AuthenticationTests`) + +| Test | What it verifies | +|---|---| +| `GetPassword_AuthenticateReturnsEmptyToken_ThrowsInvalidTokenException` | Mock returns empty token → `InvalidTokenException` | +| `GetPassword_UsesConfiguredUrl_WhenNoEnvVar` | The `Url` server parameter is forwarded to the client factory as `basePath` | + +#### Secret Retrieval (`SecretRetrievalTests`) + +| Test | What it verifies | +|---|---| +| `GetPassword_StaticText_PlainString_ReturnsAsIs` | Plain text secret returned unchanged | +| `GetPassword_StaticText_JsonContent_ReturnsFullJsonBlob` | JSON-shaped content with `SecretType=static_text` returned as raw string | +| `GetPassword_StaticKv_ReturnsMatchingFieldValue` | KV-formatted content, field found → correct value | +| `GetPassword_StaticKv_MissingField_ThrowsInvalidSecretConfigurationException` | KV content, requested field absent → exception | +| `GetPassword_StaticKv_JsonStoredAsKv_ParsesViaJson` | JSON-shaped content with `SecretType=static_kv` is parsed as JSON (auto-detection) | +| `GetPassword_StaticJson_ReturnsSpecifiedField` | JSON content, field name provided → field value | +| `GetPassword_StaticJson_NoFieldName_ReturnsFullBlob` | JSON content, no `StaticSecretFieldName` → full JSON blob | +| `GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException` | JSON content, requested field absent → exception | +| `GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException` | API response does not contain the requested secret name → exception | +| `GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException` | Secret found but value is empty → exception | + +#### Configuration Model (`AkeylessConfigurationTests`) + +| Test | What it verifies | +|---|---| +| `Validate_ValidAccessKeyConfig_NoErrors` | A fully populated valid config produces no validation errors | +| `Validate_MissingAccessId_ReturnsError` | Empty `AccessId` with `access_key` auth → validation error | +| `Validate_MissingAccessKey_ReturnsError` | Empty `AccessKey` with `access_key` auth → validation error | +| `Validate_UnsupportedAuthType_ReturnsError` | Auth type not in the supported list → validation error | +| `Validate_UnsupportedSecretType_ReturnsError` | `SecretType` not in `[static_text, static_kv, static_json]` → validation error | +| `Validate_StaticKvMissingFieldName_ReturnsError` | `static_kv` with empty `StaticSecretFieldName` → validation error | +| `Validate_StaticJsonMissingFieldName_NoError` | `static_json` with empty `StaticSecretFieldName` is valid (field is optional — returns full blob) | +| `SupportedSecretTypes_ContainsExpectedValues` | Static list contains all three supported types | +| `Constants_DefaultAuthMethod_IsAccessKey` | Default auth method constants are `access_key` | +| `Constants_DefaultApiUrl_IsCorrect` | Default API URL is `https://api.akeyless.io` | + +### Integration Test Cases + +#### `AkeylessApiClientTests` — exercises `AkeylessApiClient` directly + +| Test | Requires | +|---|---| +| `Authenticate_ValidCredentials_ReturnsNonEmptyToken` | Credentials | +| `Authenticate_InvalidCredentials_ThrowsApiException` | Credentials | +| `GetSecretValuesAsync_StaticTextSecret_ReturnsDictWithValue` | Credentials, `AKEYLESS_SECRET_STATIC_TEXT` | +| `GetSecretValuesAsync_StaticKvSecret_ReturnsDictWithValue` | Credentials, `AKEYLESS_SECRET_STATIC_KV` | +| `GetSecretValuesAsync_StaticJsonSecret_ReturnsDictWithValue` | Credentials, `AKEYLESS_SECRET_STATIC_JSON` | +| `GetSecretValuesAsync_MultipleSecrets_ReturnsAllRequested` | Credentials, `AKEYLESS_SECRET_STATIC_TEXT` + `AKEYLESS_SECRET_STATIC_TEXT_2` | +| `GetSecretValuesAsync_InvalidToken_ThrowsApiException` | Credentials | + +#### `AkeylessPamIntegrationTests` — exercises the full `AkeylessPam.GetPassword()` stack + +| Test | Requires | +|---|---| +| `GetPassword_StaticText_ReturnsNonEmptyValue` | Credentials, `AKEYLESS_SECRET_STATIC_TEXT` | +| `GetPassword_StaticKv_UsernameField_ReturnsValue` | Credentials, `AKEYLESS_SECRET_STATIC_KV` | +| `GetPassword_StaticKv_PasswordField_ReturnsValue` | Credentials, `AKEYLESS_SECRET_STATIC_KV` | +| `GetPassword_StaticJson_UsernameField_ReturnsValue` | Credentials, `AKEYLESS_SECRET_STATIC_JSON` | +| `GetPassword_StaticJson_PasswordField_ReturnsValue` | Credentials, `AKEYLESS_SECRET_STATIC_JSON` | +| `GetPassword_StaticJson_NoFieldName_ReturnsRawJsonBlob` | Credentials, `AKEYLESS_SECRET_STATIC_JSON_RAW` | +| `GetPassword_BadCredentials_ThrowsInvalidClientConfigurationException` | `AKEYLESS_SECRET_STATIC_TEXT` (uses deliberately wrong credentials) | +| `GetPassword_NonexistentSecret_ThrowsException` | Credentials (uses hardcoded nonexistent path) | diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs new file mode 100644 index 0000000..d6637c1 --- /dev/null +++ b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs @@ -0,0 +1,140 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using akeyless.Client; +using Keyfactor.Extensions.Pam.Akeyless; +using Xunit; + +namespace Keyfactor.Tests.Integration; + +/// +/// Integration tests for exercising the real Akeyless API. +/// Credentials are loaded from environment variables or a .env file in the repo root. +/// +/// Secret paths used by retrieval tests default to the same paths as TestConsole and can be +/// overridden with environment variables: +/// AKEYLESS_SECRET_STATIC_TEXT (default: pam/test/pamStaticTextUsername) +/// AKEYLESS_SECRET_STATIC_KV (default: pam/test/pamStaticKV) +/// AKEYLESS_SECRET_STATIC_JSON (default: pam/test/pamStaticJSON) +/// +public class AkeylessApiClientTests +{ + static AkeylessApiClientTests() => DotEnvLoader.Load(); + + private static string AccessId => + Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_ID") ?? string.Empty; + + private static string AccessKey => + Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_KEY") ?? string.Empty; + + private static string ApiUrl => + Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? "https://api.akeyless.io"; + + private static AkeylessApiClient Client => new(ApiUrl); + + private static void SkipIfMissingCredentials() + { + Skip.If(string.IsNullOrEmpty(AccessId) || string.IsNullOrEmpty(AccessKey), + "AKEYLESS_ACCESS_ID and AKEYLESS_ACCESS_KEY not set; skipping API client integration tests."); + } + + // ── Authentication ────────────────────────────────────────────────────── + + [SkippableFact] + public void Authenticate_ValidCredentials_ReturnsNonEmptyToken() + { + SkipIfMissingCredentials(); + + var token = Client.Authenticate(AccessId, AccessKey); + + Assert.NotNull(token); + Assert.NotEmpty(token); + } + + [SkippableFact] + public void Authenticate_InvalidCredentials_ThrowsApiException() + { + SkipIfMissingCredentials(); + + Assert.Throws(() => + Client.Authenticate("p-bad-id", "bad-key")); + } + + // ── Secret retrieval ───────────────────────────────────────────────────── + + [SkippableFact] + public async Task GetSecretValuesAsync_StaticTextSecret_ReturnsDictWithValue() + { + SkipIfMissingCredentials(); + var secretName = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_TEXT") + ?? "pam/test/pamStaticTextUsername"; + + var client = Client; + var token = client.Authenticate(AccessId, AccessKey); + var result = await client.GetSecretValuesAsync([secretName], token); + + Assert.True(result.ContainsKey(secretName), $"Response did not contain key '{secretName}'"); + Assert.NotEmpty(result[secretName]); + } + + [SkippableFact] + public async Task GetSecretValuesAsync_StaticKvSecret_ReturnsDictWithValue() + { + SkipIfMissingCredentials(); + var secretName = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_KV") + ?? "pam/test/pamStaticKV"; + + var client = Client; + var token = client.Authenticate(AccessId, AccessKey); + var result = await client.GetSecretValuesAsync([secretName], token); + + Assert.True(result.ContainsKey(secretName), $"Response did not contain key '{secretName}'"); + Assert.NotEmpty(result[secretName]); + } + + [SkippableFact] + public async Task GetSecretValuesAsync_StaticJsonSecret_ReturnsDictWithValue() + { + SkipIfMissingCredentials(); + var secretName = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_JSON") + ?? "pam/test/pamStaticJSON"; + + var client = Client; + var token = client.Authenticate(AccessId, AccessKey); + var result = await client.GetSecretValuesAsync([secretName], token); + + Assert.True(result.ContainsKey(secretName), $"Response did not contain key '{secretName}'"); + Assert.NotEmpty(result[secretName]); + } + + [SkippableFact] + public async Task GetSecretValuesAsync_MultipleSecrets_ReturnsAllRequested() + { + SkipIfMissingCredentials(); + var secret1 = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_TEXT") + ?? "pam/test/pamStaticTextUsername"; + var secret2 = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_TEXT_2") + ?? "pam/test/pamStaticTextPassword"; + + var client = Client; + var token = client.Authenticate(AccessId, AccessKey); + var result = await client.GetSecretValuesAsync([secret1, secret2], token); + + Assert.True(result.ContainsKey(secret1), $"Response did not contain key '{secret1}'"); + Assert.True(result.ContainsKey(secret2), $"Response did not contain key '{secret2}'"); + } + + [SkippableFact] + public async Task GetSecretValuesAsync_InvalidToken_ThrowsApiException() + { + SkipIfMissingCredentials(); + + var client = Client; + await Assert.ThrowsAsync(() => + client.GetSecretValuesAsync(["pam/test/any"], "invalid-token")); + } +} diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj b/tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj new file mode 100644 index 0000000..bc95379 --- /dev/null +++ b/tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + false + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs new file mode 100644 index 0000000..8fd4dec --- /dev/null +++ b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs @@ -0,0 +1,223 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using Keyfactor.Extensions.Pam.Akeyless; +using Xunit; + +namespace Keyfactor.Tests.Integration; + +/// +/// Integration tests that connect to a live Akeyless instance. +/// All tests are skipped when the required environment variables are not set. +/// +/// +/// Required env vars for all tests: +/// AKEYLESS_ACCESS_ID — Akeyless access key ID +/// AKEYLESS_ACCESS_KEY — Akeyless access key secret +/// +/// Optional env vars: +/// AKEYLESS_API_URL — Akeyless API URL (defaults to https://api.akeyless.io) +/// AKEYLESS_AUTH_TYPE — Auth type (defaults to access_key) +/// +/// Per-test secret path env vars: +/// AKEYLESS_SECRET_STATIC_TEXT — path to a static_text secret +/// AKEYLESS_SECRET_STATIC_KV — path to a static_kv secret with "username" and "password" fields +/// AKEYLESS_SECRET_STATIC_JSON — path to a static_json secret with "username" and "password" fields +/// AKEYLESS_SECRET_STATIC_JSON_RAW — path to a static_json secret to retrieve as a raw blob +/// +public class AkeylessPamIntegrationTests +{ + private static Dictionary BuildServerParams() + { + return new Dictionary + { + ["Url"] = Env("AKEYLESS_API_URL", "https://api.akeyless.io"), + ["AuthType"] = Env("AKEYLESS_AUTH_TYPE", "access_key"), + ["AccessId"] = Env("AKEYLESS_ACCESS_ID"), + ["AccessKey"] = Env("AKEYLESS_ACCESS_KEY") + }; + } + + private static string Env(string key, string? fallback = null) + => Environment.GetEnvironmentVariable(key) ?? fallback ?? string.Empty; + + private static void SkipIfMissingCredentials() + { + var id = Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_ID"); + var key = Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_KEY"); + Skip.If(string.IsNullOrEmpty(id) || string.IsNullOrEmpty(key), + "AKEYLESS_ACCESS_ID and AKEYLESS_ACCESS_KEY env vars not set; skipping integration tests."); + } + + private static void SkipIfMissingSecretPath(string envVar) + { + Skip.If(string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envVar)), + $"{envVar} env var not set; skipping this integration test."); + } + + [SkippableFact] + public void GetPassword_StaticText_ReturnsNonEmptyValue() + { + SkipIfMissingCredentials(); + SkipIfMissingSecretPath("AKEYLESS_SECRET_STATIC_TEXT"); + + var pam = new AkeylessPam(); + var instance = new Dictionary + { + ["SecretType"] = "static_text", + ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_TEXT") + }; + + var result = pam.GetPassword(instance, BuildServerParams()); + + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [SkippableFact] + public void GetPassword_StaticKv_UsernameField_ReturnsValue() + { + SkipIfMissingCredentials(); + SkipIfMissingSecretPath("AKEYLESS_SECRET_STATIC_KV"); + + var pam = new AkeylessPam(); + var instance = new Dictionary + { + ["SecretType"] = "static_kv", + ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_KV"), + ["StaticSecretFieldName"] = "username" + }; + + var result = pam.GetPassword(instance, BuildServerParams()); + + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [SkippableFact] + public void GetPassword_StaticKv_PasswordField_ReturnsValue() + { + SkipIfMissingCredentials(); + SkipIfMissingSecretPath("AKEYLESS_SECRET_STATIC_KV"); + + var pam = new AkeylessPam(); + var instance = new Dictionary + { + ["SecretType"] = "static_kv", + ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_KV"), + ["StaticSecretFieldName"] = "password" + }; + + var result = pam.GetPassword(instance, BuildServerParams()); + + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [SkippableFact] + public void GetPassword_StaticJson_UsernameField_ReturnsValue() + { + SkipIfMissingCredentials(); + SkipIfMissingSecretPath("AKEYLESS_SECRET_STATIC_JSON"); + + var pam = new AkeylessPam(); + var instance = new Dictionary + { + ["SecretType"] = "static_json", + ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_JSON"), + ["StaticSecretFieldName"] = "username" + }; + + var result = pam.GetPassword(instance, BuildServerParams()); + + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [SkippableFact] + public void GetPassword_StaticJson_PasswordField_ReturnsValue() + { + SkipIfMissingCredentials(); + SkipIfMissingSecretPath("AKEYLESS_SECRET_STATIC_JSON"); + + var pam = new AkeylessPam(); + var instance = new Dictionary + { + ["SecretType"] = "static_json", + ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_JSON"), + ["StaticSecretFieldName"] = "password" + }; + + var result = pam.GetPassword(instance, BuildServerParams()); + + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [SkippableFact] + public void GetPassword_StaticJson_NoFieldName_ReturnsRawJsonBlob() + { + SkipIfMissingCredentials(); + SkipIfMissingSecretPath("AKEYLESS_SECRET_STATIC_JSON_RAW"); + + var pam = new AkeylessPam(); + var instance = new Dictionary + { + ["SecretType"] = "static_json", + ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_JSON_RAW") + // no StaticSecretFieldName — expect full JSON blob back + }; + + var result = pam.GetPassword(instance, BuildServerParams()); + + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.True(result.TrimStart().StartsWith('{') || result.TrimStart().StartsWith('['), + "Expected raw JSON blob but got: " + result); + } + + [SkippableFact] + public void GetPassword_BadCredentials_ThrowsInvalidClientConfigurationException() + { + SkipIfMissingSecretPath("AKEYLESS_SECRET_STATIC_TEXT"); // need a valid secret path + + var server = new Dictionary + { + ["Url"] = Env("AKEYLESS_API_URL", "https://api.akeyless.io"), + ["AuthType"] = "access_key", + ["AccessId"] = "p-bad-id", + ["AccessKey"] = "bad-key" + }; + var instance = new Dictionary + { + ["SecretType"] = "static_text", + ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_TEXT") + }; + + var pam = new AkeylessPam(); + var ex = Assert.Throws(() => pam.GetPassword(instance, server)); + Assert.IsType(ex.InnerException); + } + + [SkippableFact] + public void GetPassword_NonexistentSecret_ThrowsException() + { + SkipIfMissingCredentials(); + + var pam = new AkeylessPam(); + var instance = new Dictionary + { + ["SecretType"] = "static_text", + ["SecretName"] = "/pam/does/not/exist/at/all" + }; + + // Akeyless returns an ApiException (404-like) for nonexistent secrets rather than + // an empty response, so it propagates as-is. Both domain exceptions and ApiException + // are acceptable here until the adapter normalizes SDK errors into domain exceptions. + Assert.ThrowsAny(() => pam.GetPassword(instance, BuildServerParams())); + } +} diff --git a/tests/AkeylessPam.Integration.Tests/DotEnvLoader.cs b/tests/AkeylessPam.Integration.Tests/DotEnvLoader.cs new file mode 100644 index 0000000..1ad2fbf --- /dev/null +++ b/tests/AkeylessPam.Integration.Tests/DotEnvLoader.cs @@ -0,0 +1,49 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +namespace Keyfactor.Tests.Integration; + +/// +/// Loads variables from a .env file into the process environment, without overwriting +/// variables that are already set. Walks up the directory tree from the executing assembly +/// to find the repo-root .env file. +/// +internal static class DotEnvLoader +{ + internal static void Load() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, ".env"); + if (File.Exists(candidate)) + { + foreach (var line in File.ReadAllLines(candidate)) + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) continue; + + // Strip optional leading "export " + if (trimmed.StartsWith("export ", StringComparison.OrdinalIgnoreCase)) + trimmed = trimmed["export ".Length..].TrimStart(); + + var eq = trimmed.IndexOf('='); + if (eq <= 0) continue; + + var key = trimmed[..eq].Trim(); + var value = trimmed[(eq + 1)..].Trim().Trim('"'); + + // Only set if not already present in the environment + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(key))) + Environment.SetEnvironmentVariable(key, value); + } + return; + } + dir = dir.Parent; + } + } +} diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs new file mode 100644 index 0000000..17a153b --- /dev/null +++ b/tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs @@ -0,0 +1,169 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System.ComponentModel.DataAnnotations; +using Keyfactor.Extensions.Pam.Akeyless; +using Keyfactor.Extensions.Pam.Akeyless.Models; +using Xunit; + +namespace Keyfactor.Tests.Unit; + +public class AkeylessConfigurationTests +{ + private static IList Validate(AkeylessConfiguration config) + { + var ctx = new ValidationContext(config); + var results = new List(); + Validator.TryValidateObject(config, ctx, results, validateAllProperties: true); + // Also invoke IValidatableObject.Validate explicitly since TryValidateObject may not call it + results.AddRange(config.Validate(ctx)); + return results; + } + + [Fact] + public void Validate_ValidAccessKeyConfig_NoErrors() + { + var config = new AkeylessConfiguration + { + AccessId = "p-abc123", + AccessKey = "super-secret", + SecretType = "static_text", + SecretName = "pam/test/secret", + AuthType = "access_key" + }; + + var errors = Validate(config); + + Assert.Empty(errors); + } + + [Fact] + public void Validate_MissingAccessId_ReturnsError() + { + var config = new AkeylessConfiguration + { + AccessId = "", + AccessKey = "super-secret", + SecretType = "static_text", + SecretName = "pam/test/secret", + AuthType = "access_key" + }; + + var errors = Validate(config); + + Assert.Contains(errors, e => e.MemberNames.Contains(nameof(AkeylessConfiguration.AccessId))); + } + + [Fact] + public void Validate_MissingAccessKey_ReturnsError() + { + var config = new AkeylessConfiguration + { + AccessId = "p-abc123", + AccessKey = "", + SecretType = "static_text", + SecretName = "pam/test/secret", + AuthType = "access_key" + }; + + var errors = Validate(config); + + Assert.Contains(errors, e => e.MemberNames.Contains(nameof(AkeylessConfiguration.AccessKey))); + } + + [Fact] + public void Validate_UnsupportedAuthType_ReturnsError() + { + var config = new AkeylessConfiguration + { + AccessId = "p-abc123", + AccessKey = "super-secret", + SecretType = "static_text", + SecretName = "pam/test/secret", + AuthType = "saml" // unsupported + }; + + var errors = Validate(config); + + Assert.NotEmpty(errors); + } + + [Fact] + public void Validate_UnsupportedSecretType_ReturnsError() + { + var config = new AkeylessConfiguration + { + AccessId = "p-abc123", + AccessKey = "super-secret", + SecretType = "dynamic_secret", + SecretName = "pam/test/secret", + AuthType = "access_key" + }; + + var errors = Validate(config); + + Assert.Contains(errors, e => e.MemberNames.Contains(nameof(AkeylessConfiguration.SecretType))); + } + + [Fact] + public void Validate_StaticKvMissingFieldName_ReturnsError() + { + var config = new AkeylessConfiguration + { + AccessId = "p-abc123", + AccessKey = "super-secret", + SecretType = "static_kv", + SecretName = "pam/test/secret", + StaticSecretFieldName = "", + AuthType = "access_key" + }; + + var errors = Validate(config); + + Assert.Contains(errors, e => e.MemberNames.Contains(nameof(AkeylessConfiguration.StaticSecretFieldName))); + } + + [Fact] + public void Validate_StaticJsonMissingFieldName_NoError() + { + // StaticSecretFieldName is optional for static_json (returns full blob when omitted) + var config = new AkeylessConfiguration + { + AccessId = "p-abc123", + AccessKey = "super-secret", + SecretType = "static_json", + SecretName = "pam/test/secret", + StaticSecretFieldName = "", + AuthType = "access_key" + }; + + var errors = Validate(config); + + Assert.DoesNotContain(errors, e => e.MemberNames.Contains(nameof(AkeylessConfiguration.StaticSecretFieldName))); + } + + [Fact] + public void SupportedSecretTypes_ContainsExpectedValues() + { + Assert.Contains("static_text", AkeylessConfiguration.SupportedSecretTypes); + Assert.Contains("static_kv", AkeylessConfiguration.SupportedSecretTypes); + Assert.Contains("static_json", AkeylessConfiguration.SupportedSecretTypes); + } + + [Fact] + public void Constants_DefaultAuthMethod_IsAccessKey() + { + Assert.Equal("access_key", AkeylessConstants.DefaultAuthMethod); + Assert.Equal("access_key", AkeylessConstants.DefaultAuthMethodReadOnly); + } + + [Fact] + public void Constants_DefaultApiUrl_IsCorrect() + { + Assert.Equal("https://api.akeyless.io", AkeylessConstants.DefaultAkeylessApiUrl); + } +} diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj b/tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj new file mode 100644 index 0000000..aa5a286 --- /dev/null +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + false + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs new file mode 100644 index 0000000..5bc6103 --- /dev/null +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -0,0 +1,294 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using Keyfactor.Extensions.Pam.Akeyless; +using Moq; +using Xunit; + +namespace Keyfactor.Tests.Unit; + +/// +/// Helper to build the standard valid server config dictionary. +/// +internal static class Params +{ + internal static Dictionary ValidServer( + string authType = "access_key", + string accessId = "test-id", + string accessKey = "test-key", + string url = "https://api.akeyless.io") => new() + { + ["AuthType"] = authType, + ["AccessId"] = accessId, + ["AccessKey"] = accessKey, + ["Url"] = url + }; + + internal static Dictionary Instance( + string secretName = "pam/test/secret", + string secretType = "static_text", + string? fieldName = null) + { + var d = new Dictionary + { + ["SecretName"] = secretName, + ["SecretType"] = secretType + }; + if (fieldName != null) d["StaticSecretFieldName"] = fieldName; + return d; + } +} + +public class ValidationTests +{ + [Fact] + public void GetPassword_MissingSecretName_ThrowsInvalidClientConfigurationException() + { + var pam = new AkeylessPam(_ => Mock.Of()); + var instance = new Dictionary { ["SecretType"] = "static_text" }; // no SecretName + + Assert.Throws(() => + pam.GetPassword(instance, Params.ValidServer())); + } + + [Fact] + public void GetPassword_MissingAccessId_ThrowsInvalidClientConfigurationException() + { + var pam = new AkeylessPam(_ => Mock.Of()); + var server = new Dictionary + { + ["AuthType"] = "access_key", + ["AccessKey"] = "test-key" + // no AccessId + }; + + Assert.Throws(() => + pam.GetPassword(Params.Instance(), server)); + } + + [Fact] + public void GetPassword_MissingAccessKey_ThrowsInvalidClientConfigurationException() + { + var pam = new AkeylessPam(_ => Mock.Of()); + var server = new Dictionary + { + ["AuthType"] = "access_key", + ["AccessId"] = "test-id" + // no AccessKey + }; + + Assert.Throws(() => + pam.GetPassword(Params.Instance(), server)); + } + + [Fact] + public void GetPassword_InvalidAuthType_Throws() + { + var pam = new AkeylessPam(_ => Mock.Of()); + var server = new Dictionary + { + ["AuthType"] = "unsupported_auth", + ["AccessId"] = "test-id", + ["AccessKey"] = "test-key" + }; + + Assert.Throws(() => + pam.GetPassword(Params.Instance(), server)); + } + + [Fact] + public void GetPassword_InvalidSecretType_ThrowsInvalidClientConfigurationException() + { + var pam = new AkeylessPam(_ => Mock.Of()); + var instance = Params.Instance(secretType: "dynamic_secret"); + + Assert.Throws(() => + pam.GetPassword(instance, Params.ValidServer())); + } +} + +public class AuthenticationTests +{ + [Fact] + public void GetPassword_AuthenticateReturnsEmptyToken_ThrowsInvalidTokenException() + { + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Returns(string.Empty); + + var pam = new AkeylessPam(_ => mock.Object); + + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer())); + + Assert.IsType(ex.InnerException); + } + + [Fact] + public void GetPassword_UsesConfiguredUrl_WhenNoEnvVar() + { + string? capturedBasePath = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(basePath => + { + capturedBasePath = basePath; + return mock.Object; + }); + + pam.GetPassword(Params.Instance(), Params.ValidServer(url: "https://custom.akeyless.io")); + + Assert.Equal("https://custom.akeyless.io", capturedBasePath); + } +} + +public class SecretRetrievalTests +{ + private static AkeylessPam PamWithMockReturning(string secretName, string secretValue) + { + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { [secretName] = secretValue }); + return new AkeylessPam(_ => mock.Object); + } + + [Fact] + public void GetPassword_StaticText_PlainString_ReturnsAsIs() + { + var pam = PamWithMockReturning("pam/test/secret", "my-password"); + + var result = pam.GetPassword(Params.Instance(), Params.ValidServer()); + + Assert.Equal("my-password", result); + } + + [Fact] + public void GetPassword_StaticText_JsonContent_ReturnsFullJsonBlob() + { + const string json = "{\"username\":\"admin\",\"password\":\"s3cr3t\"}"; + var pam = PamWithMockReturning("pam/test/secret", json); + + var result = pam.GetPassword(Params.Instance(secretType: "static_text"), Params.ValidServer()); + + Assert.Equal(json, result); + } + + [Fact] + public void GetPassword_StaticKv_ReturnsMatchingFieldValue() + { + const string kvContent = "username=admin\npassword=s3cr3t\n"; + var pam = PamWithMockReturning("pam/test/secret", kvContent); + + var result = pam.GetPassword( + Params.Instance(secretType: "static_kv", fieldName: "password"), + Params.ValidServer()); + + Assert.Equal("s3cr3t", result); + } + + [Fact] + public void GetPassword_StaticKv_MissingField_ThrowsInvalidSecretConfigurationException() + { + const string kvContent = "username=admin\npassword=s3cr3t\n"; + var pam = PamWithMockReturning("pam/test/secret", kvContent); + + var ex = Assert.Throws(() => + pam.GetPassword( + Params.Instance(secretType: "static_kv", fieldName: "nonexistent"), + Params.ValidServer())); + + Assert.IsType(ex.InnerException); + } + + [Fact] + public void GetPassword_StaticJson_ReturnsSpecifiedField() + { + const string json = "{\"username\":\"admin\",\"password\":\"s3cr3t\"}"; + var pam = PamWithMockReturning("pam/test/secret", json); + + var result = pam.GetPassword( + Params.Instance(secretType: "static_json", fieldName: "username"), + Params.ValidServer()); + + Assert.Equal("admin", result); + } + + [Fact] + public void GetPassword_StaticJson_NoFieldName_ReturnsFullBlob() + { + const string json = "{\"username\":\"admin\",\"password\":\"s3cr3t\"}"; + var pam = PamWithMockReturning("pam/test/secret", json); + + var result = pam.GetPassword( + Params.Instance(secretType: "static_json"), // no fieldName + Params.ValidServer()); + + Assert.Equal(json, result); + } + + [Fact] + public void GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException() + { + const string json = "{\"username\":\"admin\"}"; + var pam = PamWithMockReturning("pam/test/secret", json); + + var ex = Assert.Throws(() => + pam.GetPassword( + Params.Instance(secretType: "static_json", fieldName: "nonexistent"), + Params.ValidServer())); + + Assert.IsType(ex.InnerException); + } + + [Fact] + public void GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException() + { + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary()); // empty — secret not found + + var pam = new AkeylessPam(_ => mock.Object); + + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer())); + + Assert.IsType(ex.InnerException); + } + + [Fact] + public void GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException() + { + var pam = PamWithMockReturning("pam/test/secret", string.Empty); + + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer())); + + Assert.IsType(ex.InnerException); + } + + [Fact] + public void GetPassword_StaticKv_JsonStoredAsKv_ParsesViaJson() + { + // When SecretType is static_kv but content is JSON, ParseJsonSecret is used + const string json = "{\"username\":\"admin\",\"password\":\"s3cr3t\"}"; + var pam = PamWithMockReturning("pam/test/secret", json); + + var result = pam.GetPassword( + Params.Instance(secretType: "static_kv", fieldName: "password"), + Params.ValidServer()); + + Assert.Equal("s3cr3t", result); + } +} From 4fa98931bf14a344f8d1fead348b22ce5b3286c9 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:44:53 -0700 Subject: [PATCH 10/46] ci: add unit and integration test workflow --- .github/workflows/tests.yml | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2de010a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,56 @@ +name: Tests + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj + + - name: Run unit tests + run: dotnet test tests/AkeylessPam.Unit.Tests/ --no-restore --logger "github" --collect:"XPlat Code Coverage" --results-directory ./coverage + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage-unit + path: coverage/**/coverage.cobertura.xml + + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj + + - name: Run integration tests + env: + AKEYLESS_ACCESS_ID: ${{ secrets.AKEYLESS_ACCESS_ID }} + AKEYLESS_ACCESS_KEY: ${{ secrets.AKEYLESS_ACCESS_KEY }} + AKEYLESS_API_URL: ${{ vars.AKEYLESS_API_URL }} + AKEYLESS_SECRET_STATIC_TEXT: ${{ vars.AKEYLESS_SECRET_STATIC_TEXT }} + AKEYLESS_SECRET_STATIC_TEXT_2: ${{ vars.AKEYLESS_SECRET_STATIC_TEXT_2 }} + AKEYLESS_SECRET_STATIC_KV: ${{ vars.AKEYLESS_SECRET_STATIC_KV }} + AKEYLESS_SECRET_STATIC_JSON: ${{ vars.AKEYLESS_SECRET_STATIC_JSON }} + AKEYLESS_SECRET_STATIC_JSON_RAW: ${{ vars.AKEYLESS_SECRET_STATIC_JSON_RAW }} + run: dotnet test tests/AkeylessPam.Integration.Tests/ --no-restore --logger "github" From c3f5d72fc4ad8845b4deb54af1ecf08890074aa8 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:56:44 -0700 Subject: [PATCH 11/46] fix(logging): remove secret value leak, fix misleading messages, normalise log levels - Remove Value from ParseKvSecret debug log (was logging actual secret values) - Replace malformed KV line content log with line index only - Fix warning message that incorrectly said 'defaulting to implicit' when code actually defaults to access_key - Fix GetAkeylessSecretAsync debug message that said 'fetch access token' instead of 'connecting to Akeyless' - Add AccessId (not key) to auth success/failure log entries for audit trail - Add structured success log after secret retrieval completes - Remove emoji from all log messages - Demote noisy Info logs to Debug: format detection, validation steps, config build steps, server config valid message - Add URL and auth type to configuration debug log for traceability - Add inline comments explaining intentional omissions of sensitive values --- akeyless-pam/AkeylessPam.cs | 146 +++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 67 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 525cca6..f971508 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -99,21 +99,19 @@ internal AkeylessPam(Func clientFactory) /// Dictionary containing connection and authentication parameters such as host URL, /// username, and password. /// - /// The password value retrieved from. + /// The password value retrieved from Akeyless. /// Thrown when required parameters are missing or invalid. - /// Thrown when authentication with fails. - /// Thrown when communication with fails. + /// Thrown when authentication with Akeyless fails. + /// Thrown when communication with Akeyless fails. public string GetPassword(Dictionary instanceParameters, Dictionary serverConfigurationParameters) { try { Logger.MethodEntry(); - Logger.LogInformation("Starting Akeyless PAM Provider"); - Logger.LogDebug("Getting password from Akeyless"); + Logger.LogDebug("Akeyless PAM Provider invoked"); + // NOTE: serverConfigurationParameters intentionally not logged — contains AccessId/AccessKey. Logger.LogTrace("instanceParameters: {@InstanceParameters}", instanceParameters); - // Logger.LogTrace("initializationInfo: {@ServerConfigurationParameters}", - // serverConfigurationParameters); // TODO: Commented out to avoid logging sensitive information var config = BuildAkeylessConfiguration(instanceParameters, serverConfigurationParameters); return GetAkeylessSecretAsync(config).Result; @@ -137,21 +135,27 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) switch (configurationInfo.AuthType) { case "access_key": - Logger.LogDebug("Authenticating with Akeyless using access_key"); + Logger.LogDebug("Authenticating with Akeyless using access_key auth, AccessId: '{AccessId}'", + configurationInfo.AccessId); var token = client.Authenticate(configurationInfo.AccessId, configurationInfo.AccessKey); if (string.IsNullOrEmpty(token)) { - Logger.LogError("Unable to obtain access token from Akeyless server"); + Logger.LogError( + "Authentication failed: unable to obtain access token from Akeyless for AccessId '{AccessId}'", + configurationInfo.AccessId); throw new InvalidTokenException("Unable to obtain access token from Akeyless server"); } AuthToken = token; - Logger.LogInformation("Successfully authenticated with Akeyless"); + Logger.LogInformation( + "Successfully authenticated with Akeyless using AccessId '{AccessId}'", + configurationInfo.AccessId); break; default: - Logger.LogWarning("No authentication performed for auth type '{AuthType}'", + Logger.LogWarning( + "No authentication performed for unrecognised auth type '{AuthType}'", configurationInfo.AuthType); break; } @@ -160,7 +164,7 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) } catch (ApiException ex) { - Logger.LogError(ex, "Akeyless API exception during client initialization"); + Logger.LogError(ex, "Akeyless API exception during authentication"); throw new InvalidClientConfigurationException( $"Unable to authenticate to Akeyless API. {ex.Message}"); } @@ -176,7 +180,6 @@ private static bool LooksLikeJson(string s) return (s.StartsWith('{') && s.EndsWith('}')) || (s.StartsWith('[') && s.EndsWith(']')); } - private string ParseJsonSecret(string secretValueStr, string fieldName = "") { try @@ -185,23 +188,23 @@ private string ParseJsonSecret(string secretValueStr, string fieldName = "") var jsonObj = JsonConvert.DeserializeObject>(secretValueStr); if (string.IsNullOrEmpty(fieldName)) { - Logger.LogDebug("No field name specified, returning full JSON secret"); + Logger.LogDebug("No field name specified; returning full JSON blob"); return secretValueStr; } if (jsonObj != null && jsonObj.TryGetValue(fieldName, out var fieldValue)) { - Logger.LogDebug("Returning value for field '{FieldName}'", fieldName); + Logger.LogDebug("Successfully extracted field '{FieldName}' from JSON secret", fieldName); return fieldValue.ToString() ?? string.Empty; } - Logger.LogError("❌ Secret does not contain the specified field '{FieldName}'", fieldName); + Logger.LogError("JSON secret does not contain the specified field '{FieldName}'", fieldName); throw new InvalidSecretConfigurationException( $"Secret does not contain the specified field '{fieldName}'"); } catch (JsonException ex) { - Logger.LogError(ex, "❌ Failed to parse secret as JSON"); + Logger.LogError(ex, "Failed to parse secret value as JSON"); throw; } finally @@ -215,24 +218,27 @@ private string ParseKvSecret(string secretValueStr, string fieldName) try { Logger.MethodEntry(); + var lineIndex = 0; foreach (var line in secretValueStr.Split('\n')) { + lineIndex++; var parts = line.Split('=', 2); if (parts.Length != 2) { - Logger.LogWarning("Skipping malformed KV line: {Line}", line); + Logger.LogWarning("Skipping malformed KV entry at line {LineIndex}", lineIndex); continue; } var k = parts[0].Trim(); - var val = parts[1].Trim(); - Logger.LogDebug("Key: {Key}, Value: {Value}", k, val); + // NOTE: value is intentionally not logged to prevent secret exposure. + Logger.LogDebug("Evaluating KV key '{Key}' at line {LineIndex}", k, lineIndex); if (k != fieldName) continue; - Logger.LogDebug("Returning value for field '{FieldName}'", fieldName); - return val; + + Logger.LogDebug("Successfully extracted field '{FieldName}' from KV secret", fieldName); + return parts[1].Trim(); } - Logger.LogError("❌ Secret does not contain the specified field '{FieldName}'", fieldName); + Logger.LogError("KV secret does not contain the specified field '{FieldName}'", fieldName); throw new InvalidSecretConfigurationException( $"Secret does not contain the specified field '{fieldName}'"); } @@ -247,14 +253,14 @@ private async Task GetStaticSecret(IAkeylessApiClient client, AkeylessCo try { Logger.MethodEntry(); - Logger.LogDebug("Fetching static text secret '{SecretName}'", - configurationInfo.SecretName); + Logger.LogDebug("Fetching secret '{SecretName}' (type: {SecretType}) from Akeyless", + configurationInfo.SecretName, configurationInfo.SecretType); var secrets = await client.GetSecretValuesAsync([configurationInfo.SecretName], AuthToken); if (!secrets.TryGetValue(configurationInfo.SecretName, out var secretValueStr)) { - Logger.LogError("Secret '{SecretName}' not found in Akeyless", + Logger.LogError("Secret '{SecretName}' was not found in Akeyless", configurationInfo.SecretName); throw new InvalidSecretConfigurationException( $"Secret '{configurationInfo.SecretName}' not found in Akeyless"); @@ -262,33 +268,35 @@ private async Task GetStaticSecret(IAkeylessApiClient client, AkeylessCo if (string.IsNullOrEmpty(secretValueStr)) { - Logger.LogError("Secret '{SecretName}' has an empty value", + Logger.LogError("Secret '{SecretName}' exists in Akeyless but has an empty value", configurationInfo.SecretName); throw new InvalidSecretConfigurationException( $"Secret '{configurationInfo.SecretName}' is empty"); } + string result; if (LooksLikeJson(secretValueStr)) { - Logger.LogInformation("✅ Secret '{SecretName}' appears to be JSON", - configurationInfo.SecretName); - // Parse JSON if secret isn't meant to be a full JSON blob else returns the JSON blob - return configurationInfo.SecretType is "static_json" or "static_kv" + Logger.LogDebug("Secret '{SecretName}' value is JSON-formatted", configurationInfo.SecretName); + result = configurationInfo.SecretType is "static_json" or "static_kv" ? ParseJsonSecret(secretValueStr, configurationInfo.StaticSecretFieldName) : secretValueStr; } - - if (secretValueStr.Contains('=') && secretValueStr.Contains('\n')) + else if (secretValueStr.Contains('=') && secretValueStr.Contains('\n')) { - Logger.LogInformation("✅ Secret '{SecretName}' appears to be KV formatted", - configurationInfo.SecretName); - return ParseKvSecret(secretValueStr, configurationInfo.StaticSecretFieldName); + Logger.LogDebug("Secret '{SecretName}' value is KV-formatted", configurationInfo.SecretName); + result = ParseKvSecret(secretValueStr, configurationInfo.StaticSecretFieldName); + } + else + { + Logger.LogDebug("Secret '{SecretName}' value is plain text", configurationInfo.SecretName); + result = secretValueStr; } - - Logger.LogInformation("✅ Secret '{SecretName}' appears to be plain text", - configurationInfo.SecretName); - return secretValueStr; + Logger.LogInformation( + "Successfully retrieved secret '{SecretName}' (type: {SecretType}) from Akeyless", + configurationInfo.SecretName, configurationInfo.SecretType); + return result; } finally { @@ -301,15 +309,14 @@ private async Task GetStaticSecret(IAkeylessApiClient client, AkeylessCo /// /// The configuration containing connection and request details. /// The value of the requested secret field. - /// Thrown when the HTTP request to fails. + /// Thrown when the HTTP request to Akeyless fails. /// Thrown when deserializing the response fails or the requested secret is not found. private async Task GetAkeylessSecretAsync(AkeylessConfiguration configurationInfo) { try { Logger.MethodEntry(); - Logger.LogDebug("Attempting to fetch access token from Akeyless at {Url}", - configurationInfo.Url); + Logger.LogDebug("Connecting to Akeyless at '{Url}'", configurationInfo.Url); var client = InitClient(configurationInfo); switch (configurationInfo.SecretType) @@ -319,8 +326,9 @@ private async Task GetAkeylessSecretAsync(AkeylessConfiguration configur case "static_json": return await GetStaticSecret(client, configurationInfo); default: - Logger.LogError("Invalid or unsupported secret type '{SecretType}' specified", - configurationInfo.SecretType); + Logger.LogError("Unsupported secret type '{SecretType}' — valid types are: {ValidTypes}", + configurationInfo.SecretType, + string.Join(", ", AkeylessConfiguration.SupportedSecretTypes)); throw new Exception( $"Invalid secret type '{configurationInfo.SecretType}' specified, please use one of [{string.Join(", ", AkeylessConfiguration.SupportedSecretTypes)}]"); } @@ -390,29 +398,26 @@ private bool ValidateServerConfigurationParams( try { Logger.MethodEntry(); - Logger.LogDebug("Validating server configuration parameters"); + Logger.LogDebug("Validating server configuration parameters for auth type '{AuthType}'", authType); - // Validate credentials based on grant type switch (authType) { case "implicit": - Logger.LogWarning("No validation performed for 'implicit' auth type"); + Logger.LogWarning("No credential validation performed for 'implicit' auth type"); break; case "access_key": - Logger.LogDebug("Validating credentials for 'access_key' auth type"); + Logger.LogDebug("Validating access_key credentials"); ValidateAuthTypeAccessKey(connectionConfiguration); break; default: - Logger.LogError( - "Invalid auth type '{AuthType}'", - authType); + Logger.LogError("Unsupported auth type '{AuthType}' specified in server configuration", authType); Logger.MethodExit(); throw new Exception( $"Invalid auth type '{authType}' specified."); } - Logger.LogInformation("Server configuration parameters are valid"); + Logger.LogDebug("Server configuration parameters are valid"); return true; } finally @@ -444,10 +449,10 @@ private void ValidateRequiredParameter( try { Logger.MethodEntry(); - Logger.LogDebug("Validating parameter '{ParamName}'", paramName); + Logger.LogDebug("Validating required parameter '{ParamName}'", paramName); if (config.ContainsKey(paramName) && !string.IsNullOrEmpty(config[paramName])) return; - Logger.LogError("{ErrorPrefix} '{ParamName}' not provided", errorPrefix, paramName); + Logger.LogError("{ErrorPrefix} '{ParamName}' is required but was not provided", errorPrefix, paramName); throw new MissingFieldException($"{errorPrefix} '{paramName}' not provided"); } finally @@ -492,7 +497,7 @@ private void ValidateAuthTypeAccessKey(IReadOnlyDictionary confi /// Dictionary containing instance-specific parameters, including the secret name and field /// name. /// - /// Dictionary containing connection and authentication parameters for. + /// Dictionary containing connection and authentication parameters for Akeyless. /// A fully populated AkeylessConfiguration object. /// Thrown when required parameters are missing or invalid. private AkeylessConfiguration BuildAkeylessConfiguration( @@ -502,13 +507,13 @@ private AkeylessConfiguration BuildAkeylessConfiguration( try { Logger.MethodEntry(); - Logger.LogInformation("Validating Akeyless configuration"); + Logger.LogDebug("Building and validating Akeyless configuration"); var validServer = ValidateServerConfigurationParams(connectionConfiguration); var validInstance = ValidateInstanceParams(instanceParameters); if (!validServer || !validInstance) { - Logger.LogError("Akeyless PAM provider configuration is invalid"); + Logger.LogError("Akeyless PAM provider configuration is invalid; see preceding log entries for details"); throw new InvalidClientConfigurationException( "Akeyless configuration is invalid, please review server logs."); } @@ -516,47 +521,53 @@ private AkeylessConfiguration BuildAkeylessConfiguration( if (!connectionConfiguration.TryGetValue(AkeylessConfiguration.AUTH_TYPE, out var authType)) { Logger.LogWarning( - "\'{AuthType}\' parameter not provided defaulting to 'implicit' auth type which uses environment variables", + "'{AuthType}' parameter not provided; defaulting to 'access_key'", AkeylessConfiguration.AUTH_TYPE); authType = "access_key"; } - Logger.LogDebug("Building Akeyless configuration"); var config = new AkeylessConfiguration { Url = connectionConfiguration.GetValueOrDefault(AkeylessConfiguration.AKEYLESS_API_URL, AkeylessConstants.DefaultAkeylessApiUrl), AuthType = authType }; + Logger.LogDebug("Using Akeyless URL '{Url}', auth type '{AuthType}'", config.Url, config.AuthType); + switch (authType) { case "implicit": - Logger.LogInformation("Building Akeyless configuration for implicit auth type"); + Logger.LogDebug("Implicit auth type configured; credentials expected via environment variables"); break; case "access_key": - Logger.LogInformation("Building Akeyless configuration for 'access_key' auth type"); config.AccessId = connectionConfiguration[AkeylessConfiguration.ACCESS_ID]; config.AccessKey = connectionConfiguration[AkeylessConfiguration.ACCESS_KEY]; + // NOTE: AccessId logged (not secret), AccessKey intentionally omitted. + Logger.LogDebug("Access key auth configured with AccessId '{AccessId}'", config.AccessId); break; default: - Logger.LogError("Invalid auth type '{AuthType}' specified", authType); + Logger.LogError("Unsupported auth type '{AuthType}' encountered during configuration build", authType); throw new Exception($"Invalid grant type '{authType}' specified"); } config.SecretType = instanceParameters.GetValueOrDefault(AkeylessConfiguration.SECRET_TYPE, ""); config.SecretName = instanceParameters[AkeylessConfiguration.SECRET_NAME]; + Logger.LogDebug("Configured to retrieve secret '{SecretName}' (type: '{SecretType}')", + config.SecretName, config.SecretType); + switch (config.SecretType) { case "static_kv": - Logger.LogInformation("Configuring static secret field name for secret type '{SecretType}'", - config.SecretType); config.StaticSecretFieldName = instanceParameters[AkeylessConfiguration.STATIC_SECRET_FIELD_NAME]; + Logger.LogDebug("KV field name set to '{FieldName}'", config.StaticSecretFieldName); break; case "static_json": - Logger.LogInformation("Configuring static secret field name for secret type '{SecretType}'", - config.SecretType); config.StaticSecretFieldName = instanceParameters.GetValueOrDefault( AkeylessConfiguration.STATIC_SECRET_FIELD_NAME, ""); + if (!string.IsNullOrEmpty(config.StaticSecretFieldName)) + Logger.LogDebug("JSON field name set to '{FieldName}'", config.StaticSecretFieldName); + else + Logger.LogDebug("No JSON field name specified; full JSON blob will be returned"); break; } @@ -570,6 +581,7 @@ private AkeylessConfiguration BuildAkeylessConfiguration( $"Akeyless configuration validation failed: {errors}"); } + Logger.LogDebug("Akeyless configuration built successfully"); return config; } finally From dce444f299e75fa0a88d060e4ea7f8cd9c7e3f0c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:58:24 -0700 Subject: [PATCH 12/46] fix: initialise SecretName in AkeylessConfiguration constructor (CS8618) --- akeyless-pam/Models/AkeylessConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/akeyless-pam/Models/AkeylessConfiguration.cs b/akeyless-pam/Models/AkeylessConfiguration.cs index 6dbf64d..a8b035c 100644 --- a/akeyless-pam/Models/AkeylessConfiguration.cs +++ b/akeyless-pam/Models/AkeylessConfiguration.cs @@ -40,6 +40,7 @@ public AkeylessConfiguration() Url = string.Empty; AccessId = string.Empty; AccessKey = string.Empty; + SecretName = string.Empty; StaticSecretFieldName = string.Empty; AuthType = "access_key"; // Default value is already set in property declaration } From e739e33f10ba78c63c0b0f881b6be557f1b140e9 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:04:30 -0700 Subject: [PATCH 13/46] chore: add net10.0 target framework support - Add net10.0 to TargetFrameworks alongside net8.0 - Add conditional Keyfactor.Logging reference for net10.0 - Update global.json SDK version to 10.0 (latestFeature rollforward) - Update CI workflows to use .NET 10 SDK - Fix integration-manifest.json schema URL and Url field description --- .github/workflows/tests.yml | 4 ++-- akeyless-pam/akeyless-pam.csproj | 6 +++++- global.json | 2 +- integration-manifest.json | 6 +++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2de010a..654e9f7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Restore run: dotnet restore tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj @@ -38,7 +38,7 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Restore run: dotnet restore tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj diff --git a/akeyless-pam/akeyless-pam.csproj b/akeyless-pam/akeyless-pam.csproj index eed14e9..3da62bc 100644 --- a/akeyless-pam/akeyless-pam.csproj +++ b/akeyless-pam/akeyless-pam.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net10.0 true Keyfactor.Extensions.PAM.Akeyless latest @@ -42,6 +42,10 @@ + + + + diff --git a/global.json b/global.json index 90b3042..35bdbc7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestFeature", "allowPrerelease": false } diff --git a/integration-manifest.json b/integration-manifest.json index a272106..21a5536 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,5 +1,5 @@ { - "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json", "integration_type": "pam", "name": "Akeyless PAM Provider", "status": "production", @@ -21,8 +21,8 @@ "Parameters": [ { "Name": "Url", - "DisplayName": "Secret Server URL", - "Description": "The URL to the Secret Server instance. Example: https://example.cloud.com/", + "DisplayName": "Akeyless URL", + "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", "DataType": 1, "InstanceLevel": false }, From fddc55edf2514d3be8a834d0bd321d9eab049faf Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:19:20 -0700 Subject: [PATCH 14/46] docs: expand v1.0.0 changelog entry --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd04a1..43cab5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ # v1.0.0 -- Initial release of Akeyless PAM Provider \ No newline at end of file +Initial release of the Akeyless PAM Provider for Keyfactor Command and Universal Orchestrator. + +### Features + +- Retrieve secrets from Akeyless and surface them as credentials to Keyfactor Command certificate stores and orchestrator jobs +- **Access Key (API Key) authentication** — authenticates to the Akeyless API using an Access ID and Access Key pair +- **Static Text secrets** (`static_text`) — returns the secret value as a plain string +- **Static JSON secrets** (`static_json`) — returns the full JSON blob, or extracts a single field by name via `StaticSecretFieldName` +- **Static Key-Value secrets** (`static_kv`) — extracts a single field from a key-value secret by name via `StaticSecretFieldName` +- Configurable Akeyless API URL (defaults to `https://api.akeyless.io`); can be overridden at runtime via the `AKEYLESS_API_URL` environment variable +- Targets .NET 8.0 and .NET 10.0 \ No newline at end of file From 41210382f90e02791210accf5484372f23664156 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:16:31 -0700 Subject: [PATCH 15/46] docs: add README for each test project; remove TestConsole - Add README.md to unit and integration test projects documenting all test cases with descriptions - Remove TestConsole project and solution reference (replaced by the integration test suite) --- TestConsole/Dockerfile | 27 ------- TestConsole/Program.cs | 80 ------------------- TestConsole/TestConsole.csproj | 26 ------ akeyless-pam.sln | 14 ---- tests/AkeylessPam.Integration.Tests/README.md | 58 ++++++++++++++ tests/AkeylessPam.Unit.Tests/README.md | 68 ++++++++++++++++ 6 files changed, 126 insertions(+), 147 deletions(-) delete mode 100644 TestConsole/Dockerfile delete mode 100644 TestConsole/Program.cs delete mode 100644 TestConsole/TestConsole.csproj create mode 100644 tests/AkeylessPam.Integration.Tests/README.md create mode 100644 tests/AkeylessPam.Unit.Tests/README.md diff --git a/TestConsole/Dockerfile b/TestConsole/Dockerfile deleted file mode 100644 index 1430d3e..0000000 --- a/TestConsole/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build -WORKDIR /src -COPY ["TestConsole/TestConsole.csproj", "TestConsole/"] -RUN dotnet restore "TestConsole/TestConsole.csproj" -COPY . . -WORKDIR "/src/TestConsole" -RUN dotnet build "TestConsole.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "TestConsole.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -ARG SECRET_SERVER_URL -ARG SECRET_SERVER_USERNAME -ARG SECRET_SERVER_PASSWORD -ARG SECRET_SERVER_SECRET_ID - -ENV SECRET_SERVER_URL=$SECRET_SERVER_URL -ENV SECRET_SERVER_USERNAME=$SECRET_SERVER_USERNAME -ENV SECRET_SERVER_PASSWORD=$SECRET_SERVER_PASSWORD -ENV SECRET_SERVER_SECRET_ID=$SECRET_SERVER_SECRET_ID -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "TestConsole.dll"] diff --git a/TestConsole/Program.cs b/TestConsole/Program.cs deleted file mode 100644 index 5b8521f..0000000 --- a/TestConsole/Program.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. - -using Keyfactor.Extensions.Pam.Akeyless; - -namespace TestConsole; - -internal class Program -{ - private static void Main(string[] args) - { - var pam = new AkeylessPam(); - var initInfo = new Dictionary(); - - var instanceParams = new Dictionary(); - - //Read Url from environment variable - initInfo.Add("Url", - Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? "https://api.akeyless.io"); - //Read Username from environment variable - initInfo.Add("AuthType", Environment.GetEnvironmentVariable("AKEYLESS_AUTH_TYPE") ?? "access_key"); - - switch (initInfo["AuthType"]) - { - case "access_key": - initInfo.Add("AccessId", Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_ID") ?? "changemeId!"); - initInfo.Add("AccessKey", Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_KEY") ?? "changemeKey!"); - break; - default: - Console.WriteLine("Using implicit authentication."); - break; - } - - Console.WriteLine("Test secret type `static_text`:"); - instanceParams.Add("SecretType", "static_text"); - instanceParams.Add("SecretName", "pam/test/pamStaticTextUsername"); - var username = pam.GetPassword(instanceParams, initInfo); - instanceParams["SecretName"] = "pam/test/pamStaticTextPassword"; - var password = pam.GetPassword(instanceParams, initInfo); - Console.WriteLine($"ServerUsername: {username}"); - Console.WriteLine($"ServerPassword: {password}"); - Console.WriteLine(); - - Console.WriteLine("Test secret type `static_kv`:"); - instanceParams["SecretType"] = "static_kv"; - instanceParams["SecretName"] = "pam/test/pamStaticKV"; - instanceParams["StaticSecretFieldName"] = "username"; - var kvUsername = pam.GetPassword(instanceParams, initInfo); - instanceParams["StaticSecretFieldName"] = "password"; - var kvPassword = pam.GetPassword(instanceParams, initInfo); - - Console.WriteLine($"ServerUsername: {kvUsername}"); - Console.WriteLine($"ServerPassword: {kvPassword}"); - - Console.WriteLine(); - Console.WriteLine("Test secret type `static_json`:"); - instanceParams["SecretType"] = "static_json"; - instanceParams["SecretName"] = "pam/test/pamStaticJSON"; - instanceParams["StaticSecretFieldName"] = "username"; - var jsonUsername = pam.GetPassword(instanceParams, initInfo); - instanceParams["StaticSecretFieldName"] = "password"; - var jsonPassword = pam.GetPassword(instanceParams, initInfo); - Console.WriteLine($"ServerUsername: {jsonUsername}"); - Console.WriteLine($"ServerPassword: {jsonPassword}"); - - Console.WriteLine(); - Console.WriteLine("Test secret type `static_json` raw:"); - instanceParams["SecretType"] = "static_json"; - instanceParams["SecretName"] = "pam/test/k8s-orchestrator"; - instanceParams.Remove("StaticSecretFieldName"); - var jsonRaw = pam.GetPassword(instanceParams, initInfo); - Console.WriteLine($"ServerSecret: {jsonRaw}"); - - Console.WriteLine("Test completed."); - } -} \ No newline at end of file diff --git a/TestConsole/TestConsole.csproj b/TestConsole/TestConsole.csproj deleted file mode 100644 index d9622e2..0000000 --- a/TestConsole/TestConsole.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - Exe - net8.0 - enable - enable - Linux - - - - - .dockerignore - - - - - - - - - - - - - diff --git a/akeyless-pam.sln b/akeyless-pam.sln index 1f42cae..c86f9a2 100644 --- a/akeyless-pam.sln +++ b/akeyless-pam.sln @@ -2,8 +2,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "akeyless-pam", "akeyless-pam\akeyless-pam.csproj", "{6DEC0EF0-9D07-44CF-868C-82764C83D285}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsole", "TestConsole\TestConsole.csproj", "{90C4CEE8-44EE-4488-B464-4063432051D8}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkeylessPam.Unit.Tests", "tests\AkeylessPam.Unit.Tests\AkeylessPam.Unit.Tests.csproj", "{5A2FD372-A499-40F7-8448-1955FC09F591}" @@ -33,18 +31,6 @@ Global {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|x64.Build.0 = Release|Any CPU {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|x86.ActiveCfg = Release|Any CPU {6DEC0EF0-9D07-44CF-868C-82764C83D285}.Release|x86.Build.0 = Release|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x64.ActiveCfg = Debug|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x64.Build.0 = Debug|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x86.ActiveCfg = Debug|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Debug|x86.Build.0 = Debug|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|Any CPU.Build.0 = Release|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x64.ActiveCfg = Release|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x64.Build.0 = Release|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x86.ActiveCfg = Release|Any CPU - {90C4CEE8-44EE-4488-B464-4063432051D8}.Release|x86.Build.0 = Release|Any CPU {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A2FD372-A499-40F7-8448-1955FC09F591}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/tests/AkeylessPam.Integration.Tests/README.md b/tests/AkeylessPam.Integration.Tests/README.md new file mode 100644 index 0000000..86bb980 --- /dev/null +++ b/tests/AkeylessPam.Integration.Tests/README.md @@ -0,0 +1,58 @@ +# AkeylessPam.Integration.Tests + +Integration tests for the Akeyless PAM Provider that connect to a live Akeyless instance. All tests are marked `[SkippableFact]` and skip automatically when the required environment variables are not set, making them safe to run in CI without credentials configured. + +## Running + +```shell +dotnet test tests/AkeylessPam.Integration.Tests/ +``` + +Credentials can be provided via environment variables or a `.env` file in the repo root. The `.env` file is loaded automatically by the test setup and does not override variables already set in the environment (safe for CI use). + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `AKEYLESS_ACCESS_ID` | Yes | — | Akeyless Access ID (API key) | +| `AKEYLESS_ACCESS_KEY` | Yes | — | Akeyless Access Key secret | +| `AKEYLESS_API_URL` | No | `https://api.akeyless.io` | Akeyless API endpoint | +| `AKEYLESS_AUTH_TYPE` | No | `access_key` | Auth type passed to the provider | +| `AKEYLESS_SECRET_STATIC_TEXT` | Per-test | `pam/test/pamStaticTextUsername` | Path to a `static_text` secret | +| `AKEYLESS_SECRET_STATIC_TEXT_2` | Per-test | `pam/test/pamStaticTextPassword` | Path to a second `static_text` secret (used in multi-secret test) | +| `AKEYLESS_SECRET_STATIC_KV` | Per-test | `pam/test/pamStaticKV` | Path to a `static_kv` secret with `username` and `password` fields | +| `AKEYLESS_SECRET_STATIC_JSON` | Per-test | `pam/test/pamStaticJSON` | Path to a `static_json` secret with `username` and `password` fields | +| `AKEYLESS_SECRET_STATIC_JSON_RAW` | Per-test | — | Path to a `static_json` secret to retrieve as a raw blob (no field extraction) | + +## Test Files + +### `AkeylessPamIntegrationTests.cs` + +End-to-end tests for `AkeylessPam.GetPassword()` against a live Akeyless instance. Each test constructs server and instance parameter dictionaries the same way Keyfactor Command would, then calls the provider. + +| Test | Requires | Description | +|------|----------|-------------| +| `GetPassword_StaticText_ReturnsNonEmptyValue` | credentials + `AKEYLESS_SECRET_STATIC_TEXT` | Retrieves a `static_text` secret and asserts a non-empty value is returned | +| `GetPassword_StaticKv_UsernameField_ReturnsValue` | credentials + `AKEYLESS_SECRET_STATIC_KV` | Retrieves the `username` field from a `static_kv` secret | +| `GetPassword_StaticKv_PasswordField_ReturnsValue` | credentials + `AKEYLESS_SECRET_STATIC_KV` | Retrieves the `password` field from a `static_kv` secret | +| `GetPassword_StaticJson_UsernameField_ReturnsValue` | credentials + `AKEYLESS_SECRET_STATIC_JSON` | Retrieves the `username` field from a `static_json` secret | +| `GetPassword_StaticJson_PasswordField_ReturnsValue` | credentials + `AKEYLESS_SECRET_STATIC_JSON` | Retrieves the `password` field from a `static_json` secret | +| `GetPassword_StaticJson_NoFieldName_ReturnsRawJsonBlob` | credentials + `AKEYLESS_SECRET_STATIC_JSON_RAW` | Retrieves a `static_json` secret without specifying a field, asserts result is a JSON object or array | +| `GetPassword_BadCredentials_ThrowsInvalidClientConfigurationException` | `AKEYLESS_SECRET_STATIC_TEXT` (no credentials needed) | Intentionally uses invalid credentials and asserts `InvalidClientConfigurationException` is thrown | +| `GetPassword_NonexistentSecret_ThrowsException` | credentials | Requests a secret path that does not exist and asserts an exception is thrown | + +--- + +### `AkeylessApiClientTests.cs` + +Lower-level tests for `AkeylessApiClient` — the adapter that wraps the Akeyless SDK `V2Api`. These tests exercise authentication and secret retrieval directly without going through the PAM provider layer. + +| Test | Description | +|------|-------------| +| `Authenticate_ValidCredentials_ReturnsNonEmptyToken` | Authenticates with valid credentials and asserts a non-empty API token is returned | +| `Authenticate_InvalidCredentials_ThrowsApiException` | Authenticates with bad credentials and asserts an `ApiException` is thrown | +| `GetSecretValuesAsync_StaticTextSecret_ReturnsDictWithValue` | Retrieves a `static_text` secret and asserts the response dictionary contains the secret path as a key with a non-empty value | +| `GetSecretValuesAsync_StaticKvSecret_ReturnsDictWithValue` | Retrieves a `static_kv` secret and asserts the response dictionary contains a non-empty value | +| `GetSecretValuesAsync_StaticJsonSecret_ReturnsDictWithValue` | Retrieves a `static_json` secret and asserts the response dictionary contains a non-empty value | +| `GetSecretValuesAsync_MultipleSecrets_ReturnsAllRequested` | Requests two secrets in a single API call and asserts both keys are present in the response | +| `GetSecretValuesAsync_InvalidToken_ThrowsApiException` | Calls `GetSecretValuesAsync` with an invalid token and asserts an `ApiException` is thrown | diff --git a/tests/AkeylessPam.Unit.Tests/README.md b/tests/AkeylessPam.Unit.Tests/README.md new file mode 100644 index 0000000..ccf7340 --- /dev/null +++ b/tests/AkeylessPam.Unit.Tests/README.md @@ -0,0 +1,68 @@ +# AkeylessPam.Unit.Tests + +Unit tests for the Akeyless PAM Provider. Tests run entirely in-process with no external dependencies — the Akeyless API is replaced by a Moq mock of `IAkeylessApiClient`. + +## Running + +```shell +dotnet test tests/AkeylessPam.Unit.Tests/ +``` + +No environment variables or credentials required. + +## Test Files + +### `AkeylessPamTests.cs` + +Tests for `AkeylessPam.GetPassword()` covering configuration validation, authentication behavior, and secret parsing logic. The Akeyless API client is mocked so all tests are deterministic and offline. + +#### ValidationTests + +| Test | Description | +|------|-------------| +| `GetPassword_MissingSecretName_ThrowsInvalidClientConfigurationException` | Throws when `SecretName` is absent from instance parameters | +| `GetPassword_MissingAccessId_ThrowsInvalidClientConfigurationException` | Throws when `AccessId` is absent from server parameters | +| `GetPassword_MissingAccessKey_ThrowsInvalidClientConfigurationException` | Throws when `AccessKey` is absent from server parameters | +| `GetPassword_InvalidAuthType_Throws` | Throws when `AuthType` is not a recognized value | +| `GetPassword_InvalidSecretType_ThrowsInvalidClientConfigurationException` | Throws when `SecretType` is not one of `static_text`, `static_json`, `static_kv` | + +#### AuthenticationTests + +| Test | Description | +|------|-------------| +| `GetPassword_AuthenticateReturnsEmptyToken_ThrowsInvalidTokenException` | When the API client returns an empty token, throws `InvalidTokenException` wrapped in `AggregateException` | +| `GetPassword_UsesConfiguredUrl_WhenNoEnvVar` | The URL passed to the API client factory matches the `Url` server parameter | + +#### SecretRetrievalTests + +| Test | Description | +|------|-------------| +| `GetPassword_StaticText_PlainString_ReturnsAsIs` | A plain-text `static_text` secret is returned unchanged | +| `GetPassword_StaticText_JsonContent_ReturnsFullJsonBlob` | A JSON-formatted value stored as `static_text` is returned as the full JSON string | +| `GetPassword_StaticKv_ReturnsMatchingFieldValue` | A `static_kv` secret returns the value of the field named by `StaticSecretFieldName` | +| `GetPassword_StaticKv_MissingField_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when `StaticSecretFieldName` does not exist in the KV secret | +| `GetPassword_StaticJson_ReturnsSpecifiedField` | A `static_json` secret returns the value of the field named by `StaticSecretFieldName` | +| `GetPassword_StaticJson_NoFieldName_ReturnsFullBlob` | A `static_json` secret without `StaticSecretFieldName` returns the full JSON blob | +| `GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when `StaticSecretFieldName` is not present in the JSON | +| `GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty dictionary (secret not found) | +| `GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty string for the secret value | +| `GetPassword_StaticKv_JsonStoredAsKv_ParsesViaJson` | When a `static_kv` secret contains JSON instead of `key=value` lines, falls back to JSON parsing | + +--- + +### `AkeylessConfigurationTests.cs` + +Tests for `AkeylessConfiguration` — validating configuration model constraints, supported type lists, and constant values. + +| Test | Description | +|------|-------------| +| `Validate_ValidAccessKeyConfig_NoErrors` | A fully populated `access_key` configuration produces no validation errors | +| `Validate_MissingAccessId_ReturnsError` | Empty `AccessId` produces a validation error referencing the `AccessId` field | +| `Validate_MissingAccessKey_ReturnsError` | Empty `AccessKey` produces a validation error referencing the `AccessKey` field | +| `Validate_UnsupportedAuthType_ReturnsError` | An unsupported `AuthType` (e.g. `saml`) produces a validation error | +| `Validate_UnsupportedSecretType_ReturnsError` | An unsupported `SecretType` (e.g. `dynamic_secret`) produces a validation error referencing the `SecretType` field | +| `Validate_StaticKvMissingFieldName_ReturnsError` | Empty `StaticSecretFieldName` with `SecretType = static_kv` produces a validation error | +| `Validate_StaticJsonMissingFieldName_NoError` | Empty `StaticSecretFieldName` with `SecretType = static_json` produces no validation error (field is optional for JSON) | +| `SupportedSecretTypes_ContainsExpectedValues` | `AkeylessConfiguration.SupportedSecretTypes` contains `static_text`, `static_kv`, and `static_json` | +| `Constants_DefaultAuthMethod_IsAccessKey` | `AkeylessConstants.DefaultAuthMethod` and `DefaultAuthMethodReadOnly` are both `access_key` | +| `Constants_DefaultApiUrl_IsCorrect` | `AkeylessConstants.DefaultAkeylessApiUrl` is `https://api.akeyless.io` | From ac6365ef3015dabcf5f4e4b15107e8d970143576 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:21:09 -0700 Subject: [PATCH 16/46] chore: trigger CI From 8645b534245bf5ef67abf3baf04419312d6fcc0f Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:28:52 -0700 Subject: [PATCH 17/46] chore: add Keyfactor NuGet feed for CI restore --- .github/workflows/tests.yml | 6 ++++++ nuget.config | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 nuget.config diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 654e9f7..a210472 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,9 @@ jobs: with: dotnet-version: '10.0.x' + - name: Authenticate to Keyfactor NuGet feed + run: dotnet nuget update source keyfactor --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text + - name: Restore run: dotnet restore tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj @@ -40,6 +43,9 @@ jobs: with: dotnet-version: '10.0.x' + - name: Authenticate to Keyfactor NuGet feed + run: dotnet nuget update source keyfactor --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text + - name: Restore run: dotnet restore tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..b0d28cb --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + From 1986c6c73521bab67052f8111c3944bebfe66ef3 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:30:55 -0700 Subject: [PATCH 18/46] chore: remove unsupported github test logger from workflow --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a210472..a2dc941 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: run: dotnet restore tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj - name: Run unit tests - run: dotnet test tests/AkeylessPam.Unit.Tests/ --no-restore --logger "github" --collect:"XPlat Code Coverage" --results-directory ./coverage + run: dotnet test tests/AkeylessPam.Unit.Tests/ --no-restore --collect:"XPlat Code Coverage" --results-directory ./coverage - name: Upload coverage uses: actions/upload-artifact@v4 @@ -59,4 +59,4 @@ jobs: AKEYLESS_SECRET_STATIC_KV: ${{ vars.AKEYLESS_SECRET_STATIC_KV }} AKEYLESS_SECRET_STATIC_JSON: ${{ vars.AKEYLESS_SECRET_STATIC_JSON }} AKEYLESS_SECRET_STATIC_JSON_RAW: ${{ vars.AKEYLESS_SECRET_STATIC_JSON_RAW }} - run: dotnet test tests/AkeylessPam.Integration.Tests/ --no-restore --logger "github" + run: dotnet test tests/AkeylessPam.Integration.Tests/ --no-restore From 17de64cda0046142df9b77fc82572c35db65db70 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:34:07 -0700 Subject: [PATCH 19/46] fix: handle empty string env vars for secret paths in API client tests --- .../AkeylessApiClientTests.cs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs index d6637c1..57e1e4f 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs +++ b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs @@ -31,8 +31,14 @@ public class AkeylessApiClientTests private static string AccessKey => Environment.GetEnvironmentVariable("AKEYLESS_ACCESS_KEY") ?? string.Empty; - private static string ApiUrl => - Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? "https://api.akeyless.io"; + private static string ApiUrl + { + get + { + var url = Environment.GetEnvironmentVariable("AKEYLESS_API_URL"); + return string.IsNullOrEmpty(url) ? "https://api.akeyless.io" : url; + } + } private static AkeylessApiClient Client => new(ApiUrl); @@ -42,6 +48,12 @@ private static void SkipIfMissingCredentials() "AKEYLESS_ACCESS_ID and AKEYLESS_ACCESS_KEY not set; skipping API client integration tests."); } + private static string RequireSecretPath(string envVar, string defaultPath) + { + var val = Environment.GetEnvironmentVariable(envVar); + return string.IsNullOrEmpty(val) ? defaultPath : val; + } + // ── Authentication ────────────────────────────────────────────────────── [SkippableFact] @@ -70,8 +82,7 @@ public void Authenticate_InvalidCredentials_ThrowsApiException() public async Task GetSecretValuesAsync_StaticTextSecret_ReturnsDictWithValue() { SkipIfMissingCredentials(); - var secretName = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_TEXT") - ?? "pam/test/pamStaticTextUsername"; + var secretName = RequireSecretPath("AKEYLESS_SECRET_STATIC_TEXT", "pam/test/pamStaticTextUsername"); var client = Client; var token = client.Authenticate(AccessId, AccessKey); @@ -85,8 +96,7 @@ public async Task GetSecretValuesAsync_StaticTextSecret_ReturnsDictWithValue() public async Task GetSecretValuesAsync_StaticKvSecret_ReturnsDictWithValue() { SkipIfMissingCredentials(); - var secretName = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_KV") - ?? "pam/test/pamStaticKV"; + var secretName = RequireSecretPath("AKEYLESS_SECRET_STATIC_KV", "pam/test/pamStaticKV"); var client = Client; var token = client.Authenticate(AccessId, AccessKey); @@ -100,8 +110,7 @@ public async Task GetSecretValuesAsync_StaticKvSecret_ReturnsDictWithValue() public async Task GetSecretValuesAsync_StaticJsonSecret_ReturnsDictWithValue() { SkipIfMissingCredentials(); - var secretName = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_JSON") - ?? "pam/test/pamStaticJSON"; + var secretName = RequireSecretPath("AKEYLESS_SECRET_STATIC_JSON", "pam/test/pamStaticJSON"); var client = Client; var token = client.Authenticate(AccessId, AccessKey); @@ -115,10 +124,8 @@ public async Task GetSecretValuesAsync_StaticJsonSecret_ReturnsDictWithValue() public async Task GetSecretValuesAsync_MultipleSecrets_ReturnsAllRequested() { SkipIfMissingCredentials(); - var secret1 = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_TEXT") - ?? "pam/test/pamStaticTextUsername"; - var secret2 = Environment.GetEnvironmentVariable("AKEYLESS_SECRET_STATIC_TEXT_2") - ?? "pam/test/pamStaticTextPassword"; + var secret1 = RequireSecretPath("AKEYLESS_SECRET_STATIC_TEXT", "pam/test/pamStaticTextUsername"); + var secret2 = RequireSecretPath("AKEYLESS_SECRET_STATIC_TEXT_2", "pam/test/pamStaticTextPassword"); var client = Client; var token = client.Authenticate(AccessId, AccessKey); From a82dd049e95ee5c604674a2b1873d2998a1b3d07 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:39:16 -0700 Subject: [PATCH 20/46] fix: address Copilot PR review feedback - Add class-level XML doc comments to InvalidClientConfigurationException and InvalidSecretConfigurationException - Replace ContainsKey+indexer with TryGetValue in ValidateRequiredParameter - Fix 'a Akeyless' -> 'an Akeyless' in overview.md and README.md - Fix 'Priviledged' -> 'Privileged' in README.md (x2) - Fix 'Creates a' -> 'Creates an' in BuildAkeylessConfiguration XML doc - Remove unused DefaultAuthMethodReadOnly from AkeylessConstants; update unit test to use DefaultAuthMethod --- README.md | 6 +++--- akeyless-pam/AkeylessPam.cs | 16 ++++++++++++++-- akeyless-pam/Constants.cs | 2 -- docsource/overview.md | 2 +- .../AkeylessConfigurationTests.cs | 1 - 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0735881..c77e312 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ ## Overview -The Akeyless PAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. +The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. ## Support The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. @@ -249,7 +249,7 @@ Below is the payload to `POST` to the Keyfactor Command API ##### Define a PAM provider in Command -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Privileged Access Management**. 2. Select the **Add** button to create a new PAM provider. Click the dropdown for **Provider Type** and select **Akeyless**. @@ -294,7 +294,7 @@ Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from In Command 11 and greater, before using the Akeyless PAM type, you must define a Remote PAM Provider in the Command portal. -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Privileged Access Management**. 2. Select the **Add** button to create a new PAM provider. diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index f971508..4a6b14f 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -38,6 +38,12 @@ public InvalidTokenException(string message) : base(message) } } +/// +/// Exception thrown when the client configuration for connecting to Akeyless is invalid or incomplete. +/// +/// +/// This exception is typically thrown when required connection or authentication parameters are missing or incorrect. +/// public class InvalidClientConfigurationException : Exception { /// @@ -50,6 +56,12 @@ public InvalidClientConfigurationException(string message) : base(message) } } +/// +/// Exception thrown when the secret configuration for Akeyless is invalid or incomplete. +/// +/// +/// This exception is typically thrown when required secret parameters are missing or improperly configured. +/// public class InvalidSecretConfigurationException : Exception { /// @@ -451,7 +463,7 @@ private void ValidateRequiredParameter( Logger.MethodEntry(); Logger.LogDebug("Validating required parameter '{ParamName}'", paramName); - if (config.ContainsKey(paramName) && !string.IsNullOrEmpty(config[paramName])) return; + if (config.TryGetValue(paramName, out var value) && !string.IsNullOrEmpty(value)) return; Logger.LogError("{ErrorPrefix} '{ParamName}' is required but was not provided", errorPrefix, paramName); throw new MissingFieldException($"{errorPrefix} '{paramName}' not provided"); } @@ -491,7 +503,7 @@ private void ValidateAuthTypeAccessKey(IReadOnlyDictionary confi } /// - /// Creates a AkeylessConfiguration object from the provided parameters. + /// Creates an AkeylessConfiguration object from the provided parameters. /// /// /// Dictionary containing instance-specific parameters, including the secret name and field diff --git a/akeyless-pam/Constants.cs b/akeyless-pam/Constants.cs index 383d8c0..a16a22b 100644 --- a/akeyless-pam/Constants.cs +++ b/akeyless-pam/Constants.cs @@ -7,6 +7,4 @@ public static class AkeylessConstants public const string DefaultAkeylessApiUrl = "https://api.akeyless.io"; - // Recommended for libraries: avoids inlining so you can change value without recompiling dependents - public static readonly string DefaultAuthMethodReadOnly = "access_key"; } \ No newline at end of file diff --git a/docsource/overview.md b/docsource/overview.md index 33dd318..12b4aff 100644 --- a/docsource/overview.md +++ b/docsource/overview.md @@ -1,3 +1,3 @@ ## Overview -The Akeyless PAM Provider allows for the retrieval of stored account credentials from a Akeyless secret. \ No newline at end of file +The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. \ No newline at end of file diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs index 17a153b..ad8e0b5 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessConfigurationTests.cs @@ -158,7 +158,6 @@ public void SupportedSecretTypes_ContainsExpectedValues() public void Constants_DefaultAuthMethod_IsAccessKey() { Assert.Equal("access_key", AkeylessConstants.DefaultAuthMethod); - Assert.Equal("access_key", AkeylessConstants.DefaultAuthMethodReadOnly); } [Fact] From b3a87279cc67cbe9560015f28ff0a0a2081223c7 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:43:28 -0700 Subject: [PATCH 21/46] fix: use latestMajor rollforward so bootstrap runner works with .NET 9 SDK --- global.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 35bdbc7..a27a2b8 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "10.0.0", - "rollForward": "latestFeature", + "version": "9.0.0", + "rollForward": "latestMajor", "allowPrerelease": false } } \ No newline at end of file From a4e980d359e75284f39065d8f04e78015a28456e Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:53:47 -0700 Subject: [PATCH 22/46] chore: test dotnet10-support branch of keyfactor/actions --- .github/workflows/keyfactor-starter-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index bd5f384..205ad8e 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,7 +11,7 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v4 + uses: keyfactor/actions/.github/workflows/starter.yml@dotnet10-support with: command_token_url: ${{ vars.COMMAND_TOKEN_URL }} command_hostname: ${{ vars.COMMAND_HOSTNAME }} From b9ff6765468b7f8d5c445ad61e6a97c547e19ace Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:01:41 -0700 Subject: [PATCH 23/46] chore: remove nuget.config; use dotnet nuget add source in tests workflow --- .github/workflows/tests.yml | 4 ++-- nuget.config | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 nuget.config diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a2dc941..afc9fae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: dotnet-version: '10.0.x' - name: Authenticate to Keyfactor NuGet feed - run: dotnet nuget update source keyfactor --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text + run: dotnet nuget add source https://nuget.pkg.github.com/Keyfactor/index.json -n keyfactor -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text - name: Restore run: dotnet restore tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj @@ -44,7 +44,7 @@ jobs: dotnet-version: '10.0.x' - name: Authenticate to Keyfactor NuGet feed - run: dotnet nuget update source keyfactor --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text + run: dotnet nuget add source https://nuget.pkg.github.com/Keyfactor/index.json -n keyfactor -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text - name: Restore run: dotnet restore tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj diff --git a/nuget.config b/nuget.config deleted file mode 100644 index b0d28cb..0000000 --- a/nuget.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - From 5de78aa3b6004d415677e6223303e0b94733b810 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:43:09 -0700 Subject: [PATCH 24/46] docs: add per-type descriptions with examples for each static secret type --- docs/akeyless.md | 64 +++++++++++++++++++++++++++++++++++++++++++ docsource/akeyless.md | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/docs/akeyless.md b/docs/akeyless.md index c1e44ea..8c3ed6e 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -50,6 +50,70 @@ For full details on static secrets, see the [Akeyless documentation](https://doc | `static_json` | A static secret containing JSON; a specific field can optionally be extracted | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | | `static_kv` | A static secret containing key-value pairs; a specific field is extracted by name | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | +--- + +#### `static_text` + +A static secret whose entire value is a plain string. The value is returned as-is with no parsing. + +**Example secret value in Akeyless:** +``` +s3cr3tP@ssword! +``` + +**Example instance parameter configuration:** + +| Parameter | Value | +|-----------|-------| +| `SecretName` | `/my-org/my-app/db-password` | +| `SecretType` | `static_text` | + +--- + +#### `static_json` + +A static secret whose value is a JSON object. The provider can return either the full JSON blob or a single extracted field. + +- If `StaticSecretFieldName` is **omitted**, the full JSON string is returned. +- If `StaticSecretFieldName` is **provided**, only the value of that field is returned. + +**Example secret value in Akeyless:** +```json +{ + "username": "db_user", + "password": "s3cr3tP@ssword!" +} +``` + +**Example instance parameter configuration (extract a single field):** + +| Parameter | Value | +|-----------|-------| +| `SecretName` | `/my-org/my-app/db-credentials` | +| `SecretType` | `static_json` | +| `StaticSecretFieldName` | `password` | + +--- + +#### `static_kv` + +A static secret whose value is a set of key-value pairs, one per line in `key=value` format. A specific field must be named via `StaticSecretFieldName`. + +**Example secret value in Akeyless:** +``` +username=db_user +password=s3cr3tP@ssword! +host=db.example.com +``` + +**Example instance parameter configuration:** + +| Parameter | Value | +|-----------|-------| +| `SecretName` | `/my-org/my-app/db-credentials` | +| `SecretType` | `static_kv` | +| `StaticSecretFieldName` | `password` | + ## Mechanics When configuring Akeyless for use as a PAM Provider with Keyfactor, you will need to ensure that your diff --git a/docsource/akeyless.md b/docsource/akeyless.md index ba5cb3c..9912d86 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -47,6 +47,70 @@ For full details on static secrets, see the [Akeyless documentation](https://doc | `static_json` | A static secret containing JSON; a specific field can optionally be extracted | *Optional*: `StaticSecretFieldName`. Use this to parse a specific field value from a JSON secret, else the full JSON blob will be returned | | `static_kv` | A static secret containing key-value pairs; a specific field is extracted by name | *Required*: `StaticSecretFieldName`. Use this to parse a specific field value from a key-value secret. For example `password`. | +--- + +#### `static_text` + +A static secret whose entire value is a plain string. The value is returned as-is with no parsing. + +**Example secret value in Akeyless:** +``` +s3cr3tP@ssword! +``` + +**Example instance parameter configuration:** + +| Parameter | Value | +|-----------|-------| +| `SecretName` | `/my-org/my-app/db-password` | +| `SecretType` | `static_text` | + +--- + +#### `static_json` + +A static secret whose value is a JSON object. The provider can return either the full JSON blob or a single extracted field. + +- If `StaticSecretFieldName` is **omitted**, the full JSON string is returned. +- If `StaticSecretFieldName` is **provided**, only the value of that field is returned. + +**Example secret value in Akeyless:** +```json +{ + "username": "db_user", + "password": "s3cr3tP@ssword!" +} +``` + +**Example instance parameter configuration (extract a single field):** + +| Parameter | Value | +|-----------|-------| +| `SecretName` | `/my-org/my-app/db-credentials` | +| `SecretType` | `static_json` | +| `StaticSecretFieldName` | `password` | + +--- + +#### `static_kv` + +A static secret whose value is a set of key-value pairs, one per line in `key=value` format. A specific field must be named via `StaticSecretFieldName`. + +**Example secret value in Akeyless:** +``` +username=db_user +password=s3cr3tP@ssword! +host=db.example.com +``` + +**Example instance parameter configuration:** + +| Parameter | Value | +|-----------|-------| +| `SecretName` | `/my-org/my-app/db-credentials` | +| `SecretType` | `static_kv` | +| `StaticSecretFieldName` | `password` | + ## Mechanics From 89ebe95fa3fc34dfc311697c343d2a2df411e5ed Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:47:12 -0700 Subject: [PATCH 25/46] fix: treat whitespace-only StaticSecretFieldName as empty The Keyfactor Command UI can send a space character instead of an empty string for optional fields. Trim StaticSecretFieldName on assignment so whitespace-only values behave identically to empty (no field extraction). --- akeyless-pam/AkeylessPam.cs | 4 ++-- tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 4a6b14f..b8126a6 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -570,12 +570,12 @@ private AkeylessConfiguration BuildAkeylessConfiguration( switch (config.SecretType) { case "static_kv": - config.StaticSecretFieldName = instanceParameters[AkeylessConfiguration.STATIC_SECRET_FIELD_NAME]; + config.StaticSecretFieldName = instanceParameters[AkeylessConfiguration.STATIC_SECRET_FIELD_NAME].Trim(); Logger.LogDebug("KV field name set to '{FieldName}'", config.StaticSecretFieldName); break; case "static_json": config.StaticSecretFieldName = instanceParameters.GetValueOrDefault( - AkeylessConfiguration.STATIC_SECRET_FIELD_NAME, ""); + AkeylessConfiguration.STATIC_SECRET_FIELD_NAME, "").Trim(); if (!string.IsNullOrEmpty(config.StaticSecretFieldName)) Logger.LogDebug("JSON field name set to '{FieldName}'", config.StaticSecretFieldName); else diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 5bc6103..9baf37b 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -278,6 +278,20 @@ public void GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationExcepti Assert.IsType(ex.InnerException); } + [Fact] + public void GetPassword_StaticJson_WhitespaceFieldName_ReturnsFullBlob() + { + // Command UI may send whitespace instead of empty string — should be treated as no field name + const string json = "{\"username\":\"admin\",\"password\":\"s3cr3t\"}"; + var pam = PamWithMockReturning("pam/test/secret", json); + + var result = pam.GetPassword( + Params.Instance(secretType: "static_json", fieldName: " "), + Params.ValidServer()); + + Assert.Equal(json, result); + } + [Fact] public void GetPassword_StaticKv_JsonStoredAsKv_ParsesViaJson() { From c10ddb219dd5e06bb048a273a7c7182dc091e01f Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:27:23 -0700 Subject: [PATCH 26/46] fix: redact debug tests, document StaticSecretFieldName portal note - Replace temporary debug/print tests with proper assertions for the k8s-orchestrator secret (no secret values printed in output) - Add note to docsource/docs that the Command portal may show StaticSecretFieldName as required; enter a space to return the full JSON blob --- docs/akeyless.md | 2 ++ docsource/akeyless.md | 2 ++ .../AkeylessApiClientTests.cs | 21 ++++++++++++ .../AkeylessPamIntegrationTests.cs | 32 +++++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/docs/akeyless.md b/docs/akeyless.md index 8c3ed6e..df4310f 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -77,6 +77,8 @@ A static secret whose value is a JSON object. The provider can return either the - If `StaticSecretFieldName` is **omitted**, the full JSON string is returned. - If `StaticSecretFieldName` is **provided**, only the value of that field is returned. +> **Note:** The Keyfactor Command portal may display `StaticSecretFieldName` as a required field. If you want the full JSON blob returned (no field extraction), enter a single space (` `) in the field — the provider treats whitespace-only values as empty. + **Example secret value in Akeyless:** ```json { diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 9912d86..71d6b7d 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -74,6 +74,8 @@ A static secret whose value is a JSON object. The provider can return either the - If `StaticSecretFieldName` is **omitted**, the full JSON string is returned. - If `StaticSecretFieldName` is **provided**, only the value of that field is returned. +> **Note:** The Keyfactor Command portal may display `StaticSecretFieldName` as a required field. If you want the full JSON blob returned (no field extraction), enter a single space (` `) in the field — the provider treats whitespace-only values as empty. + **Example secret value in Akeyless:** ```json { diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs index 57e1e4f..161e949 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs +++ b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs @@ -144,4 +144,25 @@ public async Task GetSecretValuesAsync_InvalidToken_ThrowsApiException() await Assert.ThrowsAsync(() => client.GetSecretValuesAsync(["pam/test/any"], "invalid-token")); } + + // ── Debug ───────────────────────────────────────────────────────────────── + + [SkippableFact] + public async Task Debug_K8sOrchestratorSecret_PrintsRawValue() + { + SkipIfMissingCredentials(); + const string secretName = "/pam/test/k8s-orchestrator"; + + var client = Client; + var token = client.Authenticate(AccessId, AccessKey); + var result = await client.GetSecretValuesAsync([secretName], token); + + Console.WriteLine($"Keys in response: [{string.Join(", ", result.Keys)}]"); + if (result.TryGetValue(secretName, out var value)) + Console.WriteLine($"Raw value:\n{value}"); + else + Console.WriteLine("Secret key not found in response."); + + Assert.True(result.ContainsKey(secretName), $"Response did not contain key '{secretName}'"); + } } diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs index 8fd4dec..a47c7c1 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs +++ b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs @@ -220,4 +220,36 @@ public void GetPassword_NonexistentSecret_ThrowsException() // are acceptable here until the adapter normalizes SDK errors into domain exceptions. Assert.ThrowsAny(() => pam.GetPassword(instance, BuildServerParams())); } + + [SkippableFact] + public void GetPassword_StaticJson_NoFieldName_ReturnsRawJsonBlob_K8sOrchestratorSecret() + { + SkipIfMissingCredentials(); + const string secretName = "/pam/test/k8s-orchestrator"; + + var pam = new AkeylessPam(); + var result = pam.GetPassword( + new Dictionary { ["SecretType"] = "static_json", ["SecretName"] = secretName }, + BuildServerParams()); + + Assert.NotEmpty(result); + Assert.True(result.TrimStart().StartsWith('{') || result.TrimStart().StartsWith('['), + "Expected raw JSON blob"); + } + + [SkippableFact] + public void GetPassword_StaticJson_WhitespaceFieldName_ReturnsRawJsonBlob_K8sOrchestratorSecret() + { + SkipIfMissingCredentials(); + const string secretName = "/pam/test/k8s-orchestrator"; + + var pam = new AkeylessPam(); + var result = pam.GetPassword( + new Dictionary { ["SecretType"] = "static_json", ["SecretName"] = secretName, ["StaticSecretFieldName"] = " " }, + BuildServerParams()); + + Assert.NotEmpty(result); + Assert.True(result.TrimStart().StartsWith('{') || result.TrimStart().StartsWith('['), + "Expected raw JSON blob even when StaticSecretFieldName is whitespace-only"); + } } From 82153495fa6a70b385f41c74d2b7619044537597 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:29:09 -0700 Subject: [PATCH 27/46] fix: correct TypeFullName and integration-manifest trailing comma - akeyless-pam/manifest.json: fix TypeFullName to AkeylessPam (was Pam) - integration-manifest.json: remove trailing comma from StaticSecretFieldName entry --- akeyless-pam/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akeyless-pam/manifest.json b/akeyless-pam/manifest.json index 2e85b5d..8a2de1b 100644 --- a/akeyless-pam/manifest.json +++ b/akeyless-pam/manifest.json @@ -3,7 +3,7 @@ "Keyfactor.Platform.Extensions.IPAMProvider": { "PAMProviders.Akeyless.PAMProvider": { "assemblyPath": "akeyless-pam.dll", - "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.Pam" + "TypeFullName": "Keyfactor.Extensions.Pam.Akeyless.AkeylessPam" } } }, From 9456f25430e464e3910a7aca8a804b6766f23745 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:59:06 -0700 Subject: [PATCH 28/46] chore: add Makefile and document make targets in test READMEs --- Makefile | 47 +++++++++++++++++++ docsource/testing.md | 10 ++++ tests/AkeylessPam.Integration.Tests/README.md | 6 +++ tests/AkeylessPam.Unit.Tests/README.md | 5 ++ 4 files changed, 68 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9360df0 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +SLN := akeyless-pam.sln +LIB := akeyless-pam/akeyless-pam.csproj +UNIT := tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj +INT := tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj +CONSOLE := TestConsole/TestConsole.csproj + +.PHONY: all build build-release clean test test-unit test-integration console restore + +all: build + +## Build (debug) +build: + dotnet build $(LIB) + +## Build (release) +build-release: + dotnet build $(LIB) -c Release + +## Restore NuGet packages +restore: + dotnet restore $(SLN) + +## Clean all projects +clean: + dotnet clean $(SLN) + +## Run all tests +test: + dotnet test $(SLN) + +## Run unit tests only +test-unit: + dotnet test $(UNIT) + +## Run integration tests only +test-integration: + dotnet test $(INT) + +## Run the test console +console: + dotnet run --project $(CONSOLE) + +## Show available targets +help: + @grep -E '^## ' Makefile | sed 's/## / /' + @echo "" + @echo "Targets: all build build-release restore clean test test-unit test-integration console" diff --git a/docsource/testing.md b/docsource/testing.md index a8a2d83..2e8a585 100644 --- a/docsource/testing.md +++ b/docsource/testing.md @@ -9,6 +9,16 @@ The test suite is split into two projects under `tests/`: ### Running Tests +A `Makefile` at the repo root provides shortcuts for common tasks: + +```shell +make test-unit # unit tests only +make test-integration # integration tests only +make test # both test projects +``` + +Or use `dotnet` directly: + ```shell # Unit tests only dotnet test tests/AkeylessPam.Unit.Tests/ diff --git a/tests/AkeylessPam.Integration.Tests/README.md b/tests/AkeylessPam.Integration.Tests/README.md index 86bb980..c4ace5d 100644 --- a/tests/AkeylessPam.Integration.Tests/README.md +++ b/tests/AkeylessPam.Integration.Tests/README.md @@ -5,6 +5,10 @@ Integration tests for the Akeyless PAM Provider that connect to a live Akeyless ## Running ```shell +# Via Makefile +make test-integration + +# Or directly dotnet test tests/AkeylessPam.Integration.Tests/ ``` @@ -40,6 +44,8 @@ End-to-end tests for `AkeylessPam.GetPassword()` against a live Akeyless instanc | `GetPassword_StaticJson_NoFieldName_ReturnsRawJsonBlob` | credentials + `AKEYLESS_SECRET_STATIC_JSON_RAW` | Retrieves a `static_json` secret without specifying a field, asserts result is a JSON object or array | | `GetPassword_BadCredentials_ThrowsInvalidClientConfigurationException` | `AKEYLESS_SECRET_STATIC_TEXT` (no credentials needed) | Intentionally uses invalid credentials and asserts `InvalidClientConfigurationException` is thrown | | `GetPassword_NonexistentSecret_ThrowsException` | credentials | Requests a secret path that does not exist and asserts an exception is thrown | +| `GetPassword_StaticJson_NoFieldName_ReturnsRawJsonBlob_K8sOrchestratorSecret` | credentials | Retrieves `/pam/test/k8s-orchestrator` as `static_json` with no field name and asserts the full JSON blob is returned | +| `GetPassword_StaticJson_WhitespaceFieldName_ReturnsRawJsonBlob_K8sOrchestratorSecret` | credentials | Same as above but passes a whitespace-only `StaticSecretFieldName` (simulating the Keyfactor Command portal behavior); asserts the full JSON blob is returned | --- diff --git a/tests/AkeylessPam.Unit.Tests/README.md b/tests/AkeylessPam.Unit.Tests/README.md index ccf7340..74f41a1 100644 --- a/tests/AkeylessPam.Unit.Tests/README.md +++ b/tests/AkeylessPam.Unit.Tests/README.md @@ -5,6 +5,10 @@ Unit tests for the Akeyless PAM Provider. Tests run entirely in-process with no ## Running ```shell +# Via Makefile +make test-unit + +# Or directly dotnet test tests/AkeylessPam.Unit.Tests/ ``` @@ -46,6 +50,7 @@ Tests for `AkeylessPam.GetPassword()` covering configuration validation, authent | `GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when `StaticSecretFieldName` is not present in the JSON | | `GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty dictionary (secret not found) | | `GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty string for the secret value | +| `GetPassword_StaticJson_WhitespaceFieldName_ReturnsFullBlob` | A `static_json` secret with a whitespace-only `StaticSecretFieldName` (e.g. a space from the Command UI) returns the full JSON blob | | `GetPassword_StaticKv_JsonStoredAsKv_ParsesViaJson` | When a `static_kv` secret contains JSON instead of `key=value` lines, falls back to JSON parsing | --- From 26355d629d54a6729a6bc03ada0e549cd8b526b7 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:16:59 -0700 Subject: [PATCH 29/46] docs: add CLI service account setup; copy manifest.json to build output - docsource/akeyless.md: add 'Granting an Auth Method Access to a Secret (CLI)' subsection with full akeyless CLI setup (create auth method, role, association, and access rule) - Makefile: copy manifest.json into each net*/ build output dir after build and build-release targets - .gitignore: ignore manifest.json copies produced in bin/ directories --- .gitignore | 4 +++- Makefile | 14 +++++++---- docsource/akeyless.md | 54 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 649503b..4cad8be 100644 --- a/.gitignore +++ b/.gitignore @@ -351,4 +351,6 @@ MigrationBackup/ *.env -.idea/* \ No newline at end of file +.idea/* + +manifest.json \ No newline at end of file diff --git a/Makefile b/Makefile index 9360df0..200f45a 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -SLN := akeyless-pam.sln -LIB := akeyless-pam/akeyless-pam.csproj -UNIT := tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj -INT := tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj -CONSOLE := TestConsole/TestConsole.csproj +SLN := akeyless-pam.sln +LIB := akeyless-pam/akeyless-pam.csproj +UNIT := tests/AkeylessPam.Unit.Tests/AkeylessPam.Unit.Tests.csproj +INT := tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj +CONSOLE := TestConsole/TestConsole.csproj +MANIFEST := akeyless-pam/manifest.json +LIB_BIN := akeyless-pam/bin .PHONY: all build build-release clean test test-unit test-integration console restore @@ -11,10 +13,12 @@ all: build ## Build (debug) build: dotnet build $(LIB) + @for d in $(LIB_BIN)/Debug/net*/; do cp $(MANIFEST) $$d; done ## Build (release) build-release: dotnet build $(LIB) -c Release + @for d in $(LIB_BIN)/Release/net*/; do cp $(MANIFEST) $$d; done ## Restore NuGet packages restore: diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 71d6b7d..b51bd89 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -123,6 +123,60 @@ docs [here](https://docs.akeyless.io/docs/access-and-authentication-methods). Once API access is configured the credential *MUST* be granted access to view secret(s) you'll be using. +### Granting an Auth Method Access to a Secret + +In Akeyless, access is controlled through **Access Roles**. A role ties one or more auth methods to a set of permitted item paths. The steps below show how to grant an API Key auth method read access to a secret using the Akeyless console. + +**1. Create an Access Role** (if one doesn't exist already) + +Navigate to **Access Roles** → **New Role**, give it a name (e.g. `keyfactor-pam`), and save. + +**2. Associate the Auth Method with the Role** + +Open the role, go to the **Auth Methods** tab, and click **Associate**. Select the API Key auth method whose Access ID and Access Key you'll be configuring in Keyfactor. + +**3. Add a secret access rule to the Role** + +Still in the role, go to the **Access Rules** (or **Items**) tab and click **Add Rule**: + +| Field | Value | +|---|---| +| Item path | The full path to your secret, e.g. `/my-org/my-app/db-password`. Wildcards are supported, e.g. `/my-org/my-app/*` | +| Access type | `read` | + +Save the rule. + +Once the rule is in place, the auth method can authenticate and retrieve any secret that matches the configured path. You can verify access using the Akeyless CLI: + +```shell +akeyless auth --access-id --access-key +akeyless get-secret-value --name /my-org/my-app/db-password --token +``` + +### Granting an Auth Method Access to a Secret (CLI) + +The full service account setup can be scripted using the Akeyless CLI. The `create-auth-method-api-key` command returns the Access ID and Access Key you'll need for the Keyfactor configuration. + +```shell +# 1. Create the API Key auth method +# The response includes the Access ID and Access Key — save these. +akeyless create-auth-method-api-key --name /keyfactor/pam-auth-method + +# 2. Create an access role +akeyless create-role --name keyfactor-pam + +# 3. Associate the auth method with the role +akeyless assoc-role-auth-method \ + --role-name keyfactor-pam \ + --am-name /keyfactor/pam-auth-method + +# 4. Grant the role read access to a secret path (wildcards supported) +akeyless set-role-rule \ + --role-name keyfactor-pam \ + --path "/my-org/my-app/*" \ + --capability read +``` + After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from Akeyless as a PAM Provider. ### Running the PAM provider on Keyfactor Universal Orchestrator (UO) From 7be9c375ba257d08e23b048736010455ee5c376a Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:21:06 -0700 Subject: [PATCH 30/46] fix(ci): Revert to `v4` starter workflow --- .github/workflows/keyfactor-starter-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 205ad8e..bd5f384 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,7 +11,7 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@dotnet10-support + uses: keyfactor/actions/.github/workflows/starter.yml@v4 with: command_token_url: ${{ vars.COMMAND_TOKEN_URL }} command_hostname: ${{ vars.COMMAND_HOSTNAME }} From 92bc1215505dac3fc70e57e7feacc68a73f3ae87 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:23:20 -0700 Subject: [PATCH 31/46] chore: Ignore claude --- .gitignore | 5 ++- CLAUDE.md | 90 ------------------------------------------------------ 2 files changed, 4 insertions(+), 91 deletions(-) delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 4cad8be..91a3277 100644 --- a/.gitignore +++ b/.gitignore @@ -353,4 +353,7 @@ MigrationBackup/ .idea/* -manifest.json \ No newline at end of file +manifest.json + +.claude/* +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 363958f..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,90 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is the **Akeyless PAM Provider** for Keyfactor Command — a C# class library that implements the `IPAMProvider` interface to retrieve secrets from Akeyless and provide them as credentials to Keyfactor Command and Universal Orchestrator extensions. - -## Build Commands - -```shell -# Build the PAM provider library -dotnet build akeyless-pam/akeyless-pam.csproj - -# Build release (no debug symbols) -dotnet build akeyless-pam/akeyless-pam.csproj -c Release - -# Build the test console -dotnet build TestConsole/TestConsole.csproj -``` - -## Tests - -```shell -# Unit tests (no external dependencies, always runnable) -dotnet test tests/AkeylessPam.Unit.Tests/ - -# Integration tests (skip automatically when env vars absent) -dotnet test tests/AkeylessPam.Integration.Tests/ - -# Both test projects -dotnet test -``` - -Integration tests require env vars: -- `AKEYLESS_ACCESS_ID` / `AKEYLESS_ACCESS_KEY` — credentials (required for any integration test) -- `AKEYLESS_API_URL` — defaults to `https://api.akeyless.io` -- `AKEYLESS_AUTH_TYPE` — defaults to `access_key` -- `AKEYLESS_SECRET_STATIC_TEXT` / `AKEYLESS_SECRET_STATIC_KV` / `AKEYLESS_SECRET_STATIC_JSON` / `AKEYLESS_SECRET_STATIC_JSON_RAW` — paths to Akeyless secrets for each secret-type test - -## Running the Test Console - -The `TestConsole` project is a manual integration test harness — there are no automated unit tests. Configure environment variables, then run: - -```shell -export AKEYLESS_API_URL="https://api.akeyless.io" -export AKEYLESS_AUTH_TYPE="access_key" -export AKEYLESS_ACCESS_ID="" -export AKEYLESS_ACCESS_KEY="" - -dotnet run --project TestConsole/TestConsole.csproj -``` - -The test console exercises all three secret types (`static_text`, `static_kv`, `static_json`) against hardcoded secret paths under `pam/test/` in Akeyless. - -## Architecture - -The solution has two projects: - -- **`akeyless-pam/`** — The PAM provider library (targets `net8.0`). This is what gets deployed to Keyfactor Command or Universal Orchestrator hosts. -- **`TestConsole/`** — A console app for manual end-to-end testing against a live Akeyless instance. - -### Key Files - -- `akeyless-pam/AkeylessPam.cs` — Main provider class implementing `IPAMProvider`. Entry point is `GetPassword()`, which builds config, authenticates, and retrieves the secret. -- `akeyless-pam/Models/AkeylessConfiguration.cs` — Configuration model with parameter key constants (used as dictionary keys when Keyfactor calls `GetPassword`), validation attributes, and supported types. -- `akeyless-pam/Constants.cs` — Default values (`access_key` auth, `https://api.akeyless.io`). -- `akeyless-pam/manifest.json` — Copied to output; used by Universal Orchestrators to configure the PAM provider. - -### How It Works - -Keyfactor Command calls `IPAMProvider.GetPassword(instanceParameters, serverConfigurationParameters)` with two dictionaries: - -- **Server (initialization) parameters** — `Url`, `AuthType`, `AccessId`, `AccessKey` — set once per PAM provider instance in Command. -- **Instance parameters** — `SecretName`, `SecretType`, `StaticSecretFieldName` — set per Certificate Store or credential. - -The provider authenticates to Akeyless using the `akeyless` NuGet SDK (`V2Api`), then retrieves the secret via `GetSecretValue`. Secret parsing depends on `SecretType`: -- `static_text` — returns raw string (auto-detects JSON and KV formats) -- `static_kv` — parses `key=value\n` lines, requires `StaticSecretFieldName` -- `static_json` — deserializes JSON, optionally extracts a field by `StaticSecretFieldName` - -The `AKEYLESS_API_URL` environment variable overrides the configured URL at runtime. - -### PAM Provider Registration - -The provider is registered in Keyfactor Command by its fully qualified class name `Keyfactor.Extensions.Pam.Akeyless` and display name `Akeyless`. The `integration-manifest.json` at the repo root drives the Keyfactor CI/CD release pipeline (via the `keyfactor/actions` reusable workflow). - -## Release - -Releases are built from `akeyless-pam/bin/Release` (as defined in `integration-manifest.json`). The GitHub Actions workflow (`keyfactor-starter-workflow.yml`) handles building, signing, and publishing releases automatically on push/PR events. From b7cebf0745641cd456534ed2ea22c4adc93ec748 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:53:23 -0700 Subject: [PATCH 32/46] fix(ci): use fix/chromedriver-version-lookup branch of keyfactor/actions --- .github/workflows/keyfactor-starter-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index bd5f384..59fc47c 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,7 +11,7 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v4 + uses: keyfactor/actions/.github/workflows/starter.yml@fix/chromedriver-version-lookup with: command_token_url: ${{ vars.COMMAND_TOKEN_URL }} command_hostname: ${{ vars.COMMAND_HOSTNAME }} From d2bb7479148c0b661ee084eb6f71948dccb781e8 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 26 Mar 2026 07:49:39 -0700 Subject: [PATCH 33/46] ci: trigger workflow to test chromedriver fix From 84869d98fb091c9268a589a80a2e46447e5b9acd Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Thu, 26 Mar 2026 14:51:01 +0000 Subject: [PATCH 34/46] Update generated docs --- README.md | 34 +++++++++++------------ docs/akeyless.md | 70 +++++++++++++++++++++++------------------------- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index c77e312..2205eb5 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -72,7 +72,7 @@ Create the required PAM Types in the connected Command platform. ```shell # Akeyless -kfutil pam types-create -r delinea-secretserver-pam -n Akeyless +kfutil pam types-create -r akeyless-pam -n Akeyless ``` ##### Using the API @@ -148,9 +148,9 @@ Below is the payload to `POST` to the Keyfactor Command API 1. Copy the unzipped assemblies to each of the following directories: - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\delinea-secretserver-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\delinea-secretserver-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\delinea-secretserver-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\akeyless-pam` @@ -158,10 +158,10 @@ Below is the payload to `POST` to the Keyfactor Command API 1. Copy the assemblies to each of the following directories: - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\delinea-secretserver-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\delinea-secretserver-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\delinea-secretserver-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\delinea-secretserver-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\akeyless-pam` 2. Open a text editor on the Keyfactor Command server as an administrator and open the `web.config` file located in the `WebAgentServices` directory. @@ -200,16 +200,16 @@ Below is the payload to `POST` to the Keyfactor Command API ```shell # Windows Server - kfutil orchestrator extension -e delinea-secretserver-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" + kfutil orchestrator extension -e akeyless-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" # Linux - kfutil orchestrator extension -e delinea-secretserver-pam@latest --out "/opt/keyfactor/orchestrator/extensions" + kfutil orchestrator extension -e akeyless-pam@latest --out "/opt/keyfactor/orchestrator/extensions" ``` * **Manually**: Download the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. Extract the contents of the archive to: - * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\delinea-secretserver-pam` - * **Linux**: `/opt/keyfactor/orchestrator/extensions/delinea-secretserver-pam` + * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\akeyless-pam` + * **Linux**: `/opt/keyfactor/orchestrator/extensions/akeyless-pam` 2. Included in the release is a `manifest.json` file that contains the following object: ```json @@ -249,7 +249,7 @@ Below is the payload to `POST` to the Keyfactor Command API ##### Define a PAM provider in Command -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Privileged Access Management**. +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. 2. Select the **Add** button to create a new PAM provider. Click the dropdown for **Provider Type** and select **Akeyless**. @@ -294,7 +294,7 @@ Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from In Command 11 and greater, before using the Akeyless PAM type, you must define a Remote PAM Provider in the Command portal. -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Privileged Access Management**. +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. 2. Select the **Add** button to create a new PAM provider. diff --git a/docs/akeyless.md b/docs/akeyless.md index df4310f..e7be682 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -125,42 +125,38 @@ docs [here](https://docs.akeyless.io/docs/access-and-authentication-methods). Once API access is configured the credential *MUST* be granted access to view secret(s) you'll be using. -After adding and sharing a secret, you can use the secret's name (the "Secret name") to retrieve credentials from Akeyless as a PAM Provider. - -### Running the PAM provider on Keyfactor Universal Orchestrator (UO) - -When installing on the Universal Orchestrator (UO), the PAM provider is installed on and run from the UO host. Below is a sequence diagram -showing the flow of the PAM provider when it is run from the UO. - -```mermaid -sequenceDiagram - KeyfactorCommand->>KeyfactorCommand: New job created. - UO->>KeyfactorCommand: Hello do you have any jobs for me? - KeyfactorCommand->>UO: Yes here's a job. - UO->>Akeyless: Hello here are my client credentials. - Akeyless->>UO: Here's your API token. - UO->>Akeyless: I need secret named `my_secret`, here's my API token. - Akeyless->>Akeyless: Check secret ACL. - Akeyless->>UO: This is allowed, here's the secret. - UO->>UO: Running job. - UO->>KeyfactorCommand: Job completed. -``` +### Granting an Auth Method Access to a Secret + +In Akeyless, access is controlled through **Access Roles**. A role ties one or more auth methods to a set of permitted item paths. The steps below show how to grant an API Key auth method read access to a secret using the Akeyless console. + +**1. Create an Access Role** (if one doesn't exist already) + +Navigate to **Access Roles** → **New Role**, give it a name (e.g. `keyfactor-pam`), and save. + +**2. Associate the Auth Method with the Role** + +Open the role, go to the **Auth Methods** tab, and click **Associate**. Select the API Key auth method whose Access ID and Access Key you'll be configuring in Keyfactor. + +**3. Add a secret access rule to the Role** -### Running the PAM provider on the Keyfactor Command Host - -When installing the PAM provider on the Keyfactor Command Host, it is installed on and run from the Keyfactor Command host. -Below is a sequence diagram showing the flow of the PAM provider when it is run from the Keyfactor Command Host. - -```mermaid -sequenceDiagram - KeyfactorCommand->>KeyfactorCommand: Creating a new job. - KeyfactorCommand->>Akeyless: Hello here are my credentials. - Akeyless->>KeyfactorCommand: Here's your API token. - KeyfactorCommand->>Akeyless: I need secret named `my_secret`, here's my API token. - Akeyless->>Akeyless: Check secret ACL. - Akeyless->>KeyfactorCommand: This is allowed, here's the secret. - UO->>KeyfactorCommand: Hello do you have any jobs for me? - KeyfactorCommand->>UO: Yes here's a job with these credentials I pulled from Akeyless. - UO->>UO: Running job. - UO->>KeyfactorCommand: Job completed. +Still in the role, go to the **Access Rules** (or **Items**) tab and click **Add Rule**: + +| Field | Value | +|---|---| +| Item path | The full path to your secret, e.g. `/my-org/my-app/db-password`. Wildcards are supported, e.g. `/my-org/my-app/*` | +| Access type | `read` | + +Save the rule. + +Once the rule is in place, the auth method can authenticate and retrieve any secret that matches the configured path. You can verify access using the Akeyless CLI: + +```shell +akeyless auth --access-id --access-key +akeyless get-secret-value --name /my-org/my-app/db-password --token ``` + +### Granting an Auth Method Access to a Secret (CLI) + +The full service account setup can be scripted using the Akeyless CLI. The `create-auth-method-api-key` command returns the Access ID and Access Key you'll need for the Keyfactor configuration. + +```shell From 426f9c875fda38f8b9cd79c70c71263ed0defd07 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 26 Mar 2026 07:54:24 -0700 Subject: [PATCH 35/46] chore(docs): Update docs --- README.md | 331 +++++++++-------------------------------------- docs/akeyless.md | 6 +- 2 files changed, 69 insertions(+), 268 deletions(-) diff --git a/README.md b/README.md index 2205eb5..c44a973 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,10 @@

- - - Support - - · - - Installation - - · - - License - - · - - Related Integrations - + Support · + Installation · + License · + Related Integrations

## Overview @@ -34,230 +22,109 @@ The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. ## Support -The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. +The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. > To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. ## Getting Started -The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. When configured, Command will use the Akeyless PAM Provider to retrieve credentials needed to communicate with the target system. There are two ways to install the Akeyless PAM Provider, and you may elect to use one or both methods: - -1. **Locally on the Keyfactor Command server**: PAM credential resolution via the Akeyless PAM Provider will occur on the Keyfactor Command server each time an elegible credential is needed. -2. **Remotely On Universal Orchestrators**: When Jobs are dispatched to Universal Orchestrators, the associated Certificate Store extension assembly will use the Akeyless PAM Provider to resolve eligible PAM credentials. - -Before proceeding with installation, you should consider which pattern is best for your requirements and use case. +The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. ### Installation -> [!IMPORTANT] -> For the most up-to-date and complete documentation on how to install a PAM provider extension, please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Preparing%20Third%20Party%20PAM%20Providers%20to%20Work%20with.htm?Highlight=pam%20provider#InstallingCustomPAMProviderExtensions) - - -To install Akeyless PAM Provider, it is recommended you install [kfutil](https://github.com/Keyfactor/kfutil). `kfutil` is a command-line tool that simplifies the process of creating PAM Types in Keyfactor Command. - - - - - - #### Requirements - - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. -#### Create PAM type in Keyfactor Command +- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. +#### Create PAM type in Keyfactor Command ##### Using `kfutil` -Create the required PAM Types in the connected Command platform. - ```shell # Akeyless kfutil pam types-create -r akeyless-pam -n Akeyless ``` ##### Using the API -For full API docs please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/PAMProvidersPOSTTypes.htm?Highlight=pam%20type) - -Below is the payload to `POST` to the Keyfactor Command API ```json { - "Name": "Akeyless", - "Parameters": [ - { - "Name": "Url", - "DisplayName": "Akeyless URL", - "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "AccessKeyId", - "DisplayName": "Access Key ID", - "Description": "The access key ID used to authenticate to Akeyless using `access_key` authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AccessKey", - "DisplayName": "Access Key", - "Description": "The access key used to authenticate to Akeyless using `access_key` authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AuthType", - "DisplayName": "Auth Type", - "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`.", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "SecretName", - "DisplayName": "Secret Name", - "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "SecretType", - "DisplayName": "Secret Type", - "Description": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "StaticSecretFieldName", - "DisplayName": "Static Secret Field Name", - "Description": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types.", - "DataType": 1, - "InstanceLevel": true - } - ] + "Name": "Akeyless", + "Parameters": [ + { + "Name": "Url", + "DisplayName": "Akeyless URL", + "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "AccessKeyId", + "DisplayName": "Access Key ID", + "Description": "The access key ID used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AccessKey", + "DisplayName": "Access Key", + "Description": "The access key used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AuthType", + "DisplayName": "Auth Type", + "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are \u0060access_key\u0060.", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "SecretName", + "DisplayName": "Secret Name", + "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "SecretType", + "DisplayName": "Secret Type", + "Description": "The type of secret stored in Akeyless. Supported types are \u0060static_kv,static_text,static_json\u0060.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "StaticSecretFieldName", + "DisplayName": "Static Secret Field Name", + "Description": "The field name within a static secret to retrieve the credential from. Required for \u0060static_kv\u0060 and optional for \u0060static_json\u0060 secret types.", + "DataType": 1, + "InstanceLevel": true + } + ] } ``` #### Install PAM provider on Keyfactor Command Host (Local) - - 1. On the server that hosts Keyfactor Command, download and unzip the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. -2. Copy the assemblies to the appropriate directories on the Keyfactor Command server: - -
Keyfactor Command 11+ - - 1. Copy the unzipped assemblies to each of the following directories: - - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\akeyless-pam` - -
- -
Keyfactor Command 10 - - 1. Copy the assemblies to each of the following directories: - - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\akeyless-pam` - - 2. Open a text editor on the Keyfactor Command server as an administrator and open the `web.config` file located in the `WebAgentServices` directory. - - 3. In the `web.config` file, locate the ` ` section and add the following registration: - - ```xml - - ... - - - - - - ``` - - 4. Repeat steps 2 and 3 for each of the directories listed in step 1. The configuration files are located in the following paths by default: - - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\web.config` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\web.config` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\web.config` - * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\CMSTimerService.exe.config` - -
+2. Copy the assemblies to the appropriate directories on the Keyfactor Command server. 3. Restart the Keyfactor Command services (`iisreset`). - - - #### Install PAM provider on a Universal Orchestrator Host (Remote) +1. Install the Akeyless PAM Provider assemblies using kfutil or manually from the [Releases](../../releases) page. -1. Install the Akeyless PAM Provider assemblies. - - * **Using kfutil**: On the server that that hosts the Universal Orchestrator, run the following command: - - ```shell - # Windows Server - kfutil orchestrator extension -e akeyless-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" - - # Linux - kfutil orchestrator extension -e akeyless-pam@latest --out "/opt/keyfactor/orchestrator/extensions" - ``` - - * **Manually**: Download the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. Extract the contents of the archive to: - - * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\akeyless-pam` - * **Linux**: `/opt/keyfactor/orchestrator/extensions/akeyless-pam` - -2. Included in the release is a `manifest.json` file that contains the following object: - ```json - - { - "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { - "Url": "https://api.akeyless.io", - "AuthType": "access_key", - "AccessId": "", - "AccessKey": "" - } - } - - ``` - - Populate the fields in this object with credentials and configuration data collected in the [requirements](docs/akeyless.md#requirements) section. +2. Included in the release is a `manifest.json` file. Populate with credentials from the [requirements](docs/akeyless.md#requirements) section. 3. Restart the Universal Orchestrator service. - - - - - - - ### Usage - - - - #### From Keyfactor Command Host (Local) - - -##### Define a PAM provider in Command -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. - -2. Select the **Add** button to create a new PAM provider. Click the dropdown for **Provider Type** and select **Akeyless**. - -> [!IMPORTANT] -> If you're running Keyfactor Command 11+, make sure `Remote Provider` is unchecked. - -3. Populate the fields with the necessary information collected in the [requirements](docs/akeyless.md#requirements) section: - | Initialization parameter | Display Name | Description | | --- | --- | --- | | Url | Akeyless URL | The URL to the Akeyless instance. Defaults to: https://api.akeyless.io | @@ -265,91 +132,21 @@ Below is the payload to `POST` to the Keyfactor Command API | AccessKey | Access Key | The access key used to authenticate to Akeyless using `access_key` authentication. | | AuthType | Auth Type | The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`. | - -4. Click **Save**. The PAM provider is now available for use in Keyfactor Command. - -##### Using the PAM provider - -Now, when defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** will be available as a PAM provider option. When defining new Certificate Stores, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. - -Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: - -| Instance parameter | Display Name | Description | -| --- | --- | --- | -| SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | -| SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | -| StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | - - - - - #### From a Universal Orchestrator Host (Remote) - - -
Keyfactor Command 11+ - -##### Define a remote PAM provider in Command - -In Command 11 and greater, before using the Akeyless PAM type, you must define a Remote PAM Provider in the Command portal. - -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. - -2. Select the **Add** button to create a new PAM provider. - -3. Make sure that `Remote Provider` is checked. - -4. Click the dropdown for **Provider Type** and select **Akeyless**. - -5. Give the provider a unique name. - -6. Click "Save". - -##### Using the PAM provider - -When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. When defining a new Certificate Store, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. - -Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: - | Instance parameter | Display Name | Description | | --- | --- | --- | | SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | | SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | | StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | - -
- -
Keyfactor Command 10 - -When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. - -When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and populate the **Secret Value** field with the following JSON object: - -```json -{"SecretName": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.","SecretType": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.","StaticSecretFieldName": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types."} - -``` - -> We recommend creating this JSON object in a text editor, and copying it into the Secret Value field. - -
- - - - - - > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). - - ## License Apache License 2.0, see [LICENSE](LICENSE) ## Related Integrations -See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). \ No newline at end of file +See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). diff --git a/docs/akeyless.md b/docs/akeyless.md index e7be682..0a3d63b 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -8,8 +8,11 @@ these authentication methods, see the [Akeyless documentation](https://docs.akey - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. +The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. +Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on +these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) - +- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. ## Supported Authentication Methods @@ -160,3 +163,4 @@ akeyless get-secret-value --name /my-org/my-app/db-password --token The full service account setup can be scripted using the Akeyless CLI. The `create-auth-method-api-key` command returns the Access ID and Access Key you'll need for the Keyfactor configuration. ```shell + From 2b0806456ae78749819f0928d734d95dc814f3e8 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Thu, 26 Mar 2026 14:56:26 +0000 Subject: [PATCH 36/46] Update generated docs --- README.md | 331 ++++++++++++++++++++++++++++++++++++++--------- docs/akeyless.md | 6 +- 2 files changed, 268 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index c44a973..2205eb5 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,22 @@

- Support · - Installation · - License · - Related Integrations + + + Support + + · + + Installation + + · + + License + + · + + Related Integrations +

## Overview @@ -22,109 +34,230 @@ The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. ## Support -The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. +The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. > To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. ## Getting Started -The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. +The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. When configured, Command will use the Akeyless PAM Provider to retrieve credentials needed to communicate with the target system. There are two ways to install the Akeyless PAM Provider, and you may elect to use one or both methods: + +1. **Locally on the Keyfactor Command server**: PAM credential resolution via the Akeyless PAM Provider will occur on the Keyfactor Command server each time an elegible credential is needed. +2. **Remotely On Universal Orchestrators**: When Jobs are dispatched to Universal Orchestrators, the associated Certificate Store extension assembly will use the Akeyless PAM Provider to resolve eligible PAM credentials. + +Before proceeding with installation, you should consider which pattern is best for your requirements and use case. ### Installation +> [!IMPORTANT] +> For the most up-to-date and complete documentation on how to install a PAM provider extension, please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Preparing%20Third%20Party%20PAM%20Providers%20to%20Work%20with.htm?Highlight=pam%20provider#InstallingCustomPAMProviderExtensions) + + +To install Akeyless PAM Provider, it is recommended you install [kfutil](https://github.com/Keyfactor/kfutil). `kfutil` is a command-line tool that simplifies the process of creating PAM Types in Keyfactor Command. + + + -#### Requirements -- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. + + +#### Requirements + - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. #### Create PAM type in Keyfactor Command + ##### Using `kfutil` +Create the required PAM Types in the connected Command platform. + ```shell # Akeyless kfutil pam types-create -r akeyless-pam -n Akeyless ``` ##### Using the API +For full API docs please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/PAMProvidersPOSTTypes.htm?Highlight=pam%20type) + +Below is the payload to `POST` to the Keyfactor Command API ```json { - "Name": "Akeyless", - "Parameters": [ - { - "Name": "Url", - "DisplayName": "Akeyless URL", - "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "AccessKeyId", - "DisplayName": "Access Key ID", - "Description": "The access key ID used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AccessKey", - "DisplayName": "Access Key", - "Description": "The access key used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AuthType", - "DisplayName": "Auth Type", - "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are \u0060access_key\u0060.", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "SecretName", - "DisplayName": "Secret Name", - "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "SecretType", - "DisplayName": "Secret Type", - "Description": "The type of secret stored in Akeyless. Supported types are \u0060static_kv,static_text,static_json\u0060.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "StaticSecretFieldName", - "DisplayName": "Static Secret Field Name", - "Description": "The field name within a static secret to retrieve the credential from. Required for \u0060static_kv\u0060 and optional for \u0060static_json\u0060 secret types.", - "DataType": 1, - "InstanceLevel": true - } - ] + "Name": "Akeyless", + "Parameters": [ + { + "Name": "Url", + "DisplayName": "Akeyless URL", + "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "AccessKeyId", + "DisplayName": "Access Key ID", + "Description": "The access key ID used to authenticate to Akeyless using `access_key` authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AccessKey", + "DisplayName": "Access Key", + "Description": "The access key used to authenticate to Akeyless using `access_key` authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AuthType", + "DisplayName": "Auth Type", + "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`.", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "SecretName", + "DisplayName": "Secret Name", + "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "SecretType", + "DisplayName": "Secret Type", + "Description": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "StaticSecretFieldName", + "DisplayName": "Static Secret Field Name", + "Description": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types.", + "DataType": 1, + "InstanceLevel": true + } + ] } ``` #### Install PAM provider on Keyfactor Command Host (Local) + + 1. On the server that hosts Keyfactor Command, download and unzip the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. -2. Copy the assemblies to the appropriate directories on the Keyfactor Command server. +2. Copy the assemblies to the appropriate directories on the Keyfactor Command server: + +
Keyfactor Command 11+ + + 1. Copy the unzipped assemblies to each of the following directories: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\akeyless-pam` + +
+ +
Keyfactor Command 10 + + 1. Copy the assemblies to each of the following directories: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\akeyless-pam` + + 2. Open a text editor on the Keyfactor Command server as an administrator and open the `web.config` file located in the `WebAgentServices` directory. + + 3. In the `web.config` file, locate the ` ` section and add the following registration: + + ```xml + + ... + + + + + + ``` + + 4. Repeat steps 2 and 3 for each of the directories listed in step 1. The configuration files are located in the following paths by default: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\CMSTimerService.exe.config` + +
3. Restart the Keyfactor Command services (`iisreset`). + + + #### Install PAM provider on a Universal Orchestrator Host (Remote) -1. Install the Akeyless PAM Provider assemblies using kfutil or manually from the [Releases](../../releases) page. -2. Included in the release is a `manifest.json` file. Populate with credentials from the [requirements](docs/akeyless.md#requirements) section. +1. Install the Akeyless PAM Provider assemblies. + + * **Using kfutil**: On the server that that hosts the Universal Orchestrator, run the following command: + + ```shell + # Windows Server + kfutil orchestrator extension -e akeyless-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" + + # Linux + kfutil orchestrator extension -e akeyless-pam@latest --out "/opt/keyfactor/orchestrator/extensions" + ``` + + * **Manually**: Download the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. Extract the contents of the archive to: + + * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\akeyless-pam` + * **Linux**: `/opt/keyfactor/orchestrator/extensions/akeyless-pam` + +2. Included in the release is a `manifest.json` file that contains the following object: + ```json + + { + "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { + "Url": "https://api.akeyless.io", + "AuthType": "access_key", + "AccessId": "", + "AccessKey": "" + } + } + + ``` + + Populate the fields in this object with credentials and configuration data collected in the [requirements](docs/akeyless.md#requirements) section. 3. Restart the Universal Orchestrator service. + + + + + + + ### Usage + + + + #### From Keyfactor Command Host (Local) + + +##### Define a PAM provider in Command +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. + +2. Select the **Add** button to create a new PAM provider. Click the dropdown for **Provider Type** and select **Akeyless**. + +> [!IMPORTANT] +> If you're running Keyfactor Command 11+, make sure `Remote Provider` is unchecked. + +3. Populate the fields with the necessary information collected in the [requirements](docs/akeyless.md#requirements) section: + | Initialization parameter | Display Name | Description | | --- | --- | --- | | Url | Akeyless URL | The URL to the Akeyless instance. Defaults to: https://api.akeyless.io | @@ -132,21 +265,91 @@ kfutil pam types-create -r akeyless-pam -n Akeyless | AccessKey | Access Key | The access key used to authenticate to Akeyless using `access_key` authentication. | | AuthType | Auth Type | The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`. | + +4. Click **Save**. The PAM provider is now available for use in Keyfactor Command. + +##### Using the PAM provider + +Now, when defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** will be available as a PAM provider option. When defining new Certificate Stores, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. + +Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: + +| Instance parameter | Display Name | Description | +| --- | --- | --- | +| SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | +| SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | +| StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | + + + + + #### From a Universal Orchestrator Host (Remote) + + +
Keyfactor Command 11+ + +##### Define a remote PAM provider in Command + +In Command 11 and greater, before using the Akeyless PAM type, you must define a Remote PAM Provider in the Command portal. + +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. + +2. Select the **Add** button to create a new PAM provider. + +3. Make sure that `Remote Provider` is checked. + +4. Click the dropdown for **Provider Type** and select **Akeyless**. + +5. Give the provider a unique name. + +6. Click "Save". + +##### Using the PAM provider + +When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. When defining a new Certificate Store, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. + +Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: + | Instance parameter | Display Name | Description | | --- | --- | --- | | SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | | SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | | StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | + +
+ +
Keyfactor Command 10 + +When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. + +When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and populate the **Secret Value** field with the following JSON object: + +```json +{"SecretName": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.","SecretType": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.","StaticSecretFieldName": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types."} + +``` + +> We recommend creating this JSON object in a text editor, and copying it into the Secret Value field. + +
+ + + + + + > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). + + ## License Apache License 2.0, see [LICENSE](LICENSE) ## Related Integrations -See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). +See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). \ No newline at end of file diff --git a/docs/akeyless.md b/docs/akeyless.md index 0a3d63b..e7be682 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -8,11 +8,8 @@ these authentication methods, see the [Akeyless documentation](https://docs.akey - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. -The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. -Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on -these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) -- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. + ## Supported Authentication Methods @@ -163,4 +160,3 @@ akeyless get-secret-value --name /my-org/my-app/db-password --token The full service account setup can be scripted using the Akeyless CLI. The `create-auth-method-api-key` command returns the Access ID and Access Key you'll need for the Keyfactor configuration. ```shell - From 346e0ff2515c7ade9893112a160ad152350b21c7 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:07:29 -0700 Subject: [PATCH 37/46] chore(ci): Revert to latest stable starter wofklow. --- .github/workflows/keyfactor-starter-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 59fc47c..bd6be55 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,13 +11,13 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@fix/chromedriver-version-lookup + uses: keyfactor/actions/.github/workflows/starter.yml@v4 with: command_token_url: ${{ vars.COMMAND_TOKEN_URL }} command_hostname: ${{ vars.COMMAND_HOSTNAME }} command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: - token: ${{ secrets.V2BUILDTOKEN}} + token: ${{ secrets.V2BUILDTOKEN}} gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} scan_token: ${{ secrets.SAST_TOKEN }} From bd33c4b94cc92b6ffe846e7790f64bf163487eba Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:09:52 -0700 Subject: [PATCH 38/46] chore(ci): Revert to latest stable starter wofklow. --- .github/workflows/keyfactor-starter-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index bd6be55..bd5f384 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -17,7 +17,7 @@ jobs: command_hostname: ${{ vars.COMMAND_HOSTNAME }} command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: - token: ${{ secrets.V2BUILDTOKEN}} + token: ${{ secrets.V2BUILDTOKEN}} gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} scan_token: ${{ secrets.SAST_TOKEN }} From e99fa5eec12e58d74fb61f3da69c7d10565ef300 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:28:59 -0700 Subject: [PATCH 39/46] fix(security): prevent secrets from leaking to logs and test output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Log only instanceParameters key names (not values) at Trace level to prevent future credential-bearing keys from being auto-logged - Remove ex.Message from ApiException re-throw — SDK error bodies may echo back auth request content including access_key; use HTTP status code only - Remove Console.WriteLine calls in Debug_K8sOrchestratorSecret test that printed raw live secret values to stdout (visible in CI logs) - Remove raw secret value from assertion failure message in integration test to prevent exposure in test runner output Co-Authored-By: Claude Sonnet 4.6 --- akeyless-pam/AkeylessPam.cs | 11 +++++++---- .../AkeylessApiClientTests.cs | 6 +----- .../AkeylessPamIntegrationTests.cs | 3 ++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index b8126a6..bd0ab7f 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -122,8 +122,9 @@ public string GetPassword(Dictionary instanceParameters, { Logger.MethodEntry(); Logger.LogDebug("Akeyless PAM Provider invoked"); - // NOTE: serverConfigurationParameters intentionally not logged — contains AccessId/AccessKey. - Logger.LogTrace("instanceParameters: {@InstanceParameters}", instanceParameters); + // NOTE: Neither parameter dictionary is logged here — serverConfigurationParameters contains + // AccessId/AccessKey, and instanceParameters may gain credential-bearing keys in future versions. + Logger.LogTrace("instanceParameters keys: [{Keys}]", string.Join(", ", instanceParameters.Keys)); var config = BuildAkeylessConfiguration(instanceParameters, serverConfigurationParameters); return GetAkeylessSecretAsync(config).Result; @@ -176,9 +177,11 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) } catch (ApiException ex) { - Logger.LogError(ex, "Akeyless API exception during authentication"); + // NOTE: ex.Message is intentionally excluded — ApiException error content may echo back + // portions of the auth request body, including credentials. + Logger.LogError(ex, "Akeyless API exception during authentication (HTTP {StatusCode})", ex.ErrorCode); throw new InvalidClientConfigurationException( - $"Unable to authenticate to Akeyless API. {ex.Message}"); + $"Unable to authenticate to Akeyless API (HTTP {ex.ErrorCode}). Check AccessId and AccessKey configuration."); } finally { diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs index 161e949..f1db4cc 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs +++ b/tests/AkeylessPam.Integration.Tests/AkeylessApiClientTests.cs @@ -157,11 +157,7 @@ public async Task Debug_K8sOrchestratorSecret_PrintsRawValue() var token = client.Authenticate(AccessId, AccessKey); var result = await client.GetSecretValuesAsync([secretName], token); - Console.WriteLine($"Keys in response: [{string.Join(", ", result.Keys)}]"); - if (result.TryGetValue(secretName, out var value)) - Console.WriteLine($"Raw value:\n{value}"); - else - Console.WriteLine("Secret key not found in response."); + // NOTE: secret value is intentionally not written to Console/output to prevent secret exposure in CI logs. Assert.True(result.ContainsKey(secretName), $"Response did not contain key '{secretName}'"); } diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs index a47c7c1..bad91ab 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs +++ b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs @@ -176,8 +176,9 @@ public void GetPassword_StaticJson_NoFieldName_ReturnsRawJsonBlob() Assert.NotNull(result); Assert.NotEmpty(result); + // NOTE: result value is intentionally excluded from the assertion message to prevent secret exposure. Assert.True(result.TrimStart().StartsWith('{') || result.TrimStart().StartsWith('['), - "Expected raw JSON blob but got: " + result); + "Expected raw JSON blob but result did not start with '{' or '['"); } [SkippableFact] From 1658b9642631ecc355c1fe98becf8b4d7da994f8 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:59:46 -0700 Subject: [PATCH 40/46] chore: Update integration manifest to update catalog and link github. --- integration-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-manifest.json b/integration-manifest.json index 21a5536..58ee018 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -4,8 +4,8 @@ "name": "Akeyless PAM Provider", "status": "production", "support_level": "kf-supported", - "link_github": false, - "update_catalog": false, + "link_github": true, + "update_catalog": true, "release_dir": "akeyless-pam/bin/Release", "release_project": "akeyless-pam/akeyless-pam.csproj", "description": "The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret.", From 07239a0a3daeb998f674819dd22a08388e534341 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:59:32 -0700 Subject: [PATCH 41/46] chore(ci): Update to v5 starter. --- .github/workflows/keyfactor-starter-workflow.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index bd5f384..bd05b07 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,17 +11,9 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v4 - with: - command_token_url: ${{ vars.COMMAND_TOKEN_URL }} - command_hostname: ${{ vars.COMMAND_HOSTNAME }} - command_base_api_path: ${{ vars.COMMAND_API_PATH }} + uses: keyfactor/actions/.github/workflows/starter.yml@v5 secrets: token: ${{ secrets.V2BUILDTOKEN}} gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} - scan_token: ${{ secrets.SAST_TOKEN }} - entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} - entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} - command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} - command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} \ No newline at end of file + scan_token: ${{ secrets.SAST_TOKEN }} \ No newline at end of file From 1f824b2c9efed4acc7cd2c0a3673ab9e2a7ae556 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Mar 2026 21:00:15 +0000 Subject: [PATCH 42/46] docs: auto-generate README and documentation [skip ci] --- README.md | 339 ++++++++++------------------------------------- docs/akeyless.md | 6 +- 2 files changed, 73 insertions(+), 272 deletions(-) diff --git a/README.md b/README.md index 2205eb5..2106fe5 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,16 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

- - - Support - - · - - Installation - - · - - License - - · - - Related Integrations - + Support · + Installation · + License · + Related Integrations

## Overview @@ -34,230 +22,109 @@ The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. ## Support -The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. +The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. > To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. ## Getting Started -The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. When configured, Command will use the Akeyless PAM Provider to retrieve credentials needed to communicate with the target system. There are two ways to install the Akeyless PAM Provider, and you may elect to use one or both methods: - -1. **Locally on the Keyfactor Command server**: PAM credential resolution via the Akeyless PAM Provider will occur on the Keyfactor Command server each time an elegible credential is needed. -2. **Remotely On Universal Orchestrators**: When Jobs are dispatched to Universal Orchestrators, the associated Certificate Store extension assembly will use the Akeyless PAM Provider to resolve eligible PAM credentials. - -Before proceeding with installation, you should consider which pattern is best for your requirements and use case. +The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. ### Installation -> [!IMPORTANT] -> For the most up-to-date and complete documentation on how to install a PAM provider extension, please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Preparing%20Third%20Party%20PAM%20Providers%20to%20Work%20with.htm?Highlight=pam%20provider#InstallingCustomPAMProviderExtensions) - - -To install Akeyless PAM Provider, it is recommended you install [kfutil](https://github.com/Keyfactor/kfutil). `kfutil` is a command-line tool that simplifies the process of creating PAM Types in Keyfactor Command. - - - - - - #### Requirements - - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. -#### Create PAM type in Keyfactor Command +- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. +#### Create PAM type in Keyfactor Command ##### Using `kfutil` -Create the required PAM Types in the connected Command platform. - ```shell # Akeyless -kfutil pam types-create -r akeyless-pam -n Akeyless +kfutil pam types-create -r Akeyless PAM Provider -n Akeyless ``` ##### Using the API -For full API docs please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/PAMProvidersPOSTTypes.htm?Highlight=pam%20type) - -Below is the payload to `POST` to the Keyfactor Command API ```json { - "Name": "Akeyless", - "Parameters": [ - { - "Name": "Url", - "DisplayName": "Akeyless URL", - "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "AccessKeyId", - "DisplayName": "Access Key ID", - "Description": "The access key ID used to authenticate to Akeyless using `access_key` authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AccessKey", - "DisplayName": "Access Key", - "Description": "The access key used to authenticate to Akeyless using `access_key` authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AuthType", - "DisplayName": "Auth Type", - "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`.", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "SecretName", - "DisplayName": "Secret Name", - "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "SecretType", - "DisplayName": "Secret Type", - "Description": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "StaticSecretFieldName", - "DisplayName": "Static Secret Field Name", - "Description": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types.", - "DataType": 1, - "InstanceLevel": true - } - ] + "Name": "Akeyless", + "Parameters": [ + { + "Name": "Url", + "DisplayName": "Akeyless URL", + "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "AccessKeyId", + "DisplayName": "Access Key ID", + "Description": "The access key ID used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AccessKey", + "DisplayName": "Access Key", + "Description": "The access key used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AuthType", + "DisplayName": "Auth Type", + "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are \u0060access_key\u0060.", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "SecretName", + "DisplayName": "Secret Name", + "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "SecretType", + "DisplayName": "Secret Type", + "Description": "The type of secret stored in Akeyless. Supported types are \u0060static_kv,static_text,static_json\u0060.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "StaticSecretFieldName", + "DisplayName": "Static Secret Field Name", + "Description": "The field name within a static secret to retrieve the credential from. Required for \u0060static_kv\u0060 and optional for \u0060static_json\u0060 secret types.", + "DataType": 1, + "InstanceLevel": true + } + ] } ``` #### Install PAM provider on Keyfactor Command Host (Local) - - 1. On the server that hosts Keyfactor Command, download and unzip the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. -2. Copy the assemblies to the appropriate directories on the Keyfactor Command server: - -
Keyfactor Command 11+ - - 1. Copy the unzipped assemblies to each of the following directories: - - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\akeyless-pam` - -
- -
Keyfactor Command 10 - - 1. Copy the assemblies to each of the following directories: - - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\akeyless-pam` - * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\akeyless-pam` - - 2. Open a text editor on the Keyfactor Command server as an administrator and open the `web.config` file located in the `WebAgentServices` directory. - - 3. In the `web.config` file, locate the ` ` section and add the following registration: - - ```xml - - ... - - - - - - ``` - - 4. Repeat steps 2 and 3 for each of the directories listed in step 1. The configuration files are located in the following paths by default: - - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\web.config` - * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\web.config` - * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\web.config` - * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\CMSTimerService.exe.config` - -
+2. Copy the assemblies to the appropriate directories on the Keyfactor Command server. 3. Restart the Keyfactor Command services (`iisreset`). - - - #### Install PAM provider on a Universal Orchestrator Host (Remote) +1. Install the Akeyless PAM Provider assemblies using kfutil or manually from the [Releases](../../releases) page. -1. Install the Akeyless PAM Provider assemblies. - - * **Using kfutil**: On the server that that hosts the Universal Orchestrator, run the following command: - - ```shell - # Windows Server - kfutil orchestrator extension -e akeyless-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" - - # Linux - kfutil orchestrator extension -e akeyless-pam@latest --out "/opt/keyfactor/orchestrator/extensions" - ``` - - * **Manually**: Download the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. Extract the contents of the archive to: - - * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\akeyless-pam` - * **Linux**: `/opt/keyfactor/orchestrator/extensions/akeyless-pam` - -2. Included in the release is a `manifest.json` file that contains the following object: - ```json - - { - "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { - "Url": "https://api.akeyless.io", - "AuthType": "access_key", - "AccessId": "", - "AccessKey": "" - } - } - - ``` - - Populate the fields in this object with credentials and configuration data collected in the [requirements](docs/akeyless.md#requirements) section. +2. Included in the release is a `manifest.json` file. Populate with credentials from the [requirements](docs/akeyless.md#requirements) section. 3. Restart the Universal Orchestrator service. - - - - - - - ### Usage - - - - #### From Keyfactor Command Host (Local) - - -##### Define a PAM provider in Command -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. - -2. Select the **Add** button to create a new PAM provider. Click the dropdown for **Provider Type** and select **Akeyless**. - -> [!IMPORTANT] -> If you're running Keyfactor Command 11+, make sure `Remote Provider` is unchecked. - -3. Populate the fields with the necessary information collected in the [requirements](docs/akeyless.md#requirements) section: - | Initialization parameter | Display Name | Description | | --- | --- | --- | | Url | Akeyless URL | The URL to the Akeyless instance. Defaults to: https://api.akeyless.io | @@ -265,91 +132,21 @@ Below is the payload to `POST` to the Keyfactor Command API | AccessKey | Access Key | The access key used to authenticate to Akeyless using `access_key` authentication. | | AuthType | Auth Type | The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`. | - -4. Click **Save**. The PAM provider is now available for use in Keyfactor Command. - -##### Using the PAM provider - -Now, when defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** will be available as a PAM provider option. When defining new Certificate Stores, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. - -Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: - -| Instance parameter | Display Name | Description | -| --- | --- | --- | -| SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | -| SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | -| StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | - - - - - #### From a Universal Orchestrator Host (Remote) - - -
Keyfactor Command 11+ - -##### Define a remote PAM provider in Command - -In Command 11 and greater, before using the Akeyless PAM type, you must define a Remote PAM Provider in the Command portal. - -1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. - -2. Select the **Add** button to create a new PAM provider. - -3. Make sure that `Remote Provider` is checked. - -4. Click the dropdown for **Provider Type** and select **Akeyless**. - -5. Give the provider a unique name. - -6. Click "Save". - -##### Using the PAM provider - -When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. When defining a new Certificate Store, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. - -Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: - | Instance parameter | Display Name | Description | | --- | --- | --- | | SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | | SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | | StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | - -
- -
Keyfactor Command 10 - -When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. - -When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and populate the **Secret Value** field with the following JSON object: - -```json -{"SecretName": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.","SecretType": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.","StaticSecretFieldName": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types."} - -``` - -> We recommend creating this JSON object in a text editor, and copying it into the Secret Value field. - -
- - - - - - > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). - - ## License Apache License 2.0, see [LICENSE](LICENSE) ## Related Integrations -See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). \ No newline at end of file +See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). diff --git a/docs/akeyless.md b/docs/akeyless.md index e7be682..0a3d63b 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -8,8 +8,11 @@ these authentication methods, see the [Akeyless documentation](https://docs.akey - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. +The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. +Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on +these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) - +- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. ## Supported Authentication Methods @@ -160,3 +163,4 @@ akeyless get-secret-value --name /my-org/my-app/db-password --token The full service account setup can be scripted using the Akeyless CLI. The `create-auth-method-api-key` command returns the Access ID and Access Key you'll need for the Keyfactor configuration. ```shell + From 2c87ff622aefa533b1d00eefdc00f5fc6cd77f9b Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:31:51 -0700 Subject: [PATCH 43/46] chore(ci): revert to "old" doctool --- .github/workflows/keyfactor-starter-workflow.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index bd05b07..bd5f384 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,9 +11,17 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v5 + uses: keyfactor/actions/.github/workflows/starter.yml@v4 + with: + command_token_url: ${{ vars.COMMAND_TOKEN_URL }} + command_hostname: ${{ vars.COMMAND_HOSTNAME }} + command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: token: ${{ secrets.V2BUILDTOKEN}} gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} - scan_token: ${{ secrets.SAST_TOKEN }} \ No newline at end of file + scan_token: ${{ secrets.SAST_TOKEN }} + entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} + entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} + command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} + command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} \ No newline at end of file From 887e5f46e5bea8fe05e889ac722a73b9748cccc5 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Fri, 27 Mar 2026 17:33:12 +0000 Subject: [PATCH 44/46] Update generated docs --- README.md | 339 +++++++++++++++++++++++++++++++++++++---------- docs/akeyless.md | 6 +- 2 files changed, 272 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 2106fe5..2205eb5 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,28 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

- Support · - Installation · - License · - Related Integrations + + + Support + + · + + Installation + + · + + License + + · + + Related Integrations +

## Overview @@ -22,109 +34,230 @@ The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. ## Support -The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. +The Akeyless PAM Provider is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. > To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. ## Getting Started -The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. +The Akeyless PAM Provider is used by Command to resolve PAM-eligible credentials for Universal Orchestrator extensions and for accessing Certificate Authorities. When configured, Command will use the Akeyless PAM Provider to retrieve credentials needed to communicate with the target system. There are two ways to install the Akeyless PAM Provider, and you may elect to use one or both methods: + +1. **Locally on the Keyfactor Command server**: PAM credential resolution via the Akeyless PAM Provider will occur on the Keyfactor Command server each time an elegible credential is needed. +2. **Remotely On Universal Orchestrators**: When Jobs are dispatched to Universal Orchestrators, the associated Certificate Store extension assembly will use the Akeyless PAM Provider to resolve eligible PAM credentials. + +Before proceeding with installation, you should consider which pattern is best for your requirements and use case. ### Installation +> [!IMPORTANT] +> For the most up-to-date and complete documentation on how to install a PAM provider extension, please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Preparing%20Third%20Party%20PAM%20Providers%20to%20Work%20with.htm?Highlight=pam%20provider#InstallingCustomPAMProviderExtensions) + + +To install Akeyless PAM Provider, it is recommended you install [kfutil](https://github.com/Keyfactor/kfutil). `kfutil` is a command-line tool that simplifies the process of creating PAM Types in Keyfactor Command. + + + -#### Requirements -- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. + + +#### Requirements + - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. #### Create PAM type in Keyfactor Command + ##### Using `kfutil` +Create the required PAM Types in the connected Command platform. + ```shell # Akeyless -kfutil pam types-create -r Akeyless PAM Provider -n Akeyless +kfutil pam types-create -r akeyless-pam -n Akeyless ``` ##### Using the API +For full API docs please visit our [product documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/PAMProvidersPOSTTypes.htm?Highlight=pam%20type) + +Below is the payload to `POST` to the Keyfactor Command API ```json { - "Name": "Akeyless", - "Parameters": [ - { - "Name": "Url", - "DisplayName": "Akeyless URL", - "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "AccessKeyId", - "DisplayName": "Access Key ID", - "Description": "The access key ID used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AccessKey", - "DisplayName": "Access Key", - "Description": "The access key used to authenticate to Akeyless using \u0060access_key\u0060 authentication.", - "DataType": 2, - "InstanceLevel": false - }, - { - "Name": "AuthType", - "DisplayName": "Auth Type", - "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are \u0060access_key\u0060.", - "DataType": 1, - "InstanceLevel": false - }, - { - "Name": "SecretName", - "DisplayName": "Secret Name", - "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "SecretType", - "DisplayName": "Secret Type", - "Description": "The type of secret stored in Akeyless. Supported types are \u0060static_kv,static_text,static_json\u0060.", - "DataType": 1, - "InstanceLevel": true - }, - { - "Name": "StaticSecretFieldName", - "DisplayName": "Static Secret Field Name", - "Description": "The field name within a static secret to retrieve the credential from. Required for \u0060static_kv\u0060 and optional for \u0060static_json\u0060 secret types.", - "DataType": 1, - "InstanceLevel": true - } - ] + "Name": "Akeyless", + "Parameters": [ + { + "Name": "Url", + "DisplayName": "Akeyless URL", + "Description": "The URL to the Akeyless instance. Defaults to: https://api.akeyless.io", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "AccessKeyId", + "DisplayName": "Access Key ID", + "Description": "The access key ID used to authenticate to Akeyless using `access_key` authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AccessKey", + "DisplayName": "Access Key", + "Description": "The access key used to authenticate to Akeyless using `access_key` authentication.", + "DataType": 2, + "InstanceLevel": false + }, + { + "Name": "AuthType", + "DisplayName": "Auth Type", + "Description": "The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`.", + "DataType": 1, + "InstanceLevel": false + }, + { + "Name": "SecretName", + "DisplayName": "Secret Name", + "Description": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "SecretType", + "DisplayName": "Secret Type", + "Description": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.", + "DataType": 1, + "InstanceLevel": true + }, + { + "Name": "StaticSecretFieldName", + "DisplayName": "Static Secret Field Name", + "Description": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types.", + "DataType": 1, + "InstanceLevel": true + } + ] } ``` #### Install PAM provider on Keyfactor Command Host (Local) + + 1. On the server that hosts Keyfactor Command, download and unzip the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. -2. Copy the assemblies to the appropriate directories on the Keyfactor Command server. +2. Copy the assemblies to the appropriate directories on the Keyfactor Command server: + +
Keyfactor Command 11+ + + 1. Copy the unzipped assemblies to each of the following directories: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\Extensions\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\Extensions\akeyless-pam` + +
+ +
Keyfactor Command 10 + + 1. Copy the assemblies to each of the following directories: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\bin\akeyless-pam` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\akeyless-pam` + + 2. Open a text editor on the Keyfactor Command server as an administrator and open the `web.config` file located in the `WebAgentServices` directory. + + 3. In the `web.config` file, locate the ` ` section and add the following registration: + + ```xml + + ... + + + + + + ``` + + 4. Repeat steps 2 and 3 for each of the directories listed in step 1. The configuration files are located in the following paths by default: + + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\KeyfactorAPI\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\WebConsole\web.config` + * `C:\Program Files\Keyfactor\Keyfactor Platform\Service\CMSTimerService.exe.config` + +
3. Restart the Keyfactor Command services (`iisreset`). + + + #### Install PAM provider on a Universal Orchestrator Host (Remote) -1. Install the Akeyless PAM Provider assemblies using kfutil or manually from the [Releases](../../releases) page. -2. Included in the release is a `manifest.json` file. Populate with credentials from the [requirements](docs/akeyless.md#requirements) section. +1. Install the Akeyless PAM Provider assemblies. + + * **Using kfutil**: On the server that that hosts the Universal Orchestrator, run the following command: + + ```shell + # Windows Server + kfutil orchestrator extension -e akeyless-pam@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions" + + # Linux + kfutil orchestrator extension -e akeyless-pam@latest --out "/opt/keyfactor/orchestrator/extensions" + ``` + + * **Manually**: Download the latest release of the Akeyless PAM Provider from the [Releases](../../releases) page. Extract the contents of the archive to: + + * **Windows Server**: `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\akeyless-pam` + * **Linux**: `/opt/keyfactor/orchestrator/extensions/akeyless-pam` + +2. Included in the release is a `manifest.json` file that contains the following object: + ```json + + { + "Keyfactor:PAMProviders:Akeyless-:InitializationInfo": { + "Url": "https://api.akeyless.io", + "AuthType": "access_key", + "AccessId": "", + "AccessKey": "" + } + } + + ``` + + Populate the fields in this object with credentials and configuration data collected in the [requirements](docs/akeyless.md#requirements) section. 3. Restart the Universal Orchestrator service. + + + + + + + ### Usage + + + + #### From Keyfactor Command Host (Local) + + +##### Define a PAM provider in Command +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. + +2. Select the **Add** button to create a new PAM provider. Click the dropdown for **Provider Type** and select **Akeyless**. + +> [!IMPORTANT] +> If you're running Keyfactor Command 11+, make sure `Remote Provider` is unchecked. + +3. Populate the fields with the necessary information collected in the [requirements](docs/akeyless.md#requirements) section: + | Initialization parameter | Display Name | Description | | --- | --- | --- | | Url | Akeyless URL | The URL to the Akeyless instance. Defaults to: https://api.akeyless.io | @@ -132,21 +265,91 @@ kfutil pam types-create -r Akeyless PAM Provider -n Akeyless | AccessKey | Access Key | The access key used to authenticate to Akeyless using `access_key` authentication. | | AuthType | Auth Type | The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`. | + +4. Click **Save**. The PAM provider is now available for use in Keyfactor Command. + +##### Using the PAM provider + +Now, when defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** will be available as a PAM provider option. When defining new Certificate Stores, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. + +Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: + +| Instance parameter | Display Name | Description | +| --- | --- | --- | +| SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | +| SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | +| StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | + + + + + #### From a Universal Orchestrator Host (Remote) + + +
Keyfactor Command 11+ + +##### Define a remote PAM provider in Command + +In Command 11 and greater, before using the Akeyless PAM type, you must define a Remote PAM Provider in the Command portal. + +1. In the Keyfactor Command Portal, hover over the ⚙️ (settings) icon in the top right corner of the screen and select **Priviledged Access Management**. + +2. Select the **Add** button to create a new PAM provider. + +3. Make sure that `Remote Provider` is checked. + +4. Click the dropdown for **Provider Type** and select **Akeyless**. + +5. Give the provider a unique name. + +6. Click "Save". + +##### Using the PAM provider + +When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. When defining a new Certificate Store, the secret parameter form will display tabs for **Load From Keyfactor Secrets** or **Load From PAM Provider**. + +Select the **Load From PAM Provider** tab, choose the **Akeyless** provider from the list of **Providers**, and populate the fields with the necessary information from the table below: + | Instance parameter | Display Name | Description | | --- | --- | --- | | SecretName | Secret Name | The full name (path) of the secret in Akeyless that contains the credential to retrieve. | | SecretType | Secret Type | The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`. | | StaticSecretFieldName | Static Secret Field Name | The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types. | + +
+ +
Keyfactor Command 10 + +When defining Certificate Stores (**Locations**->**Certificate Stores**), **Akeyless** can be used as a PAM provider. + +When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and populate the **Secret Value** field with the following JSON object: + +```json +{"SecretName": "The full name (path) of the secret in Akeyless that contains the credential to retrieve.","SecretType": "The type of secret stored in Akeyless. Supported types are `static_kv,static_text,static_json`.","StaticSecretFieldName": "The field name within a static secret to retrieve the credential from. Required for `static_kv` and optional for `static_json` secret types."} + +``` + +> We recommend creating this JSON object in a text editor, and copying it into the Secret Value field. + +
+ + + + + + > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). + + ## License Apache License 2.0, see [LICENSE](LICENSE) ## Related Integrations -See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). +See all [Keyfactor PAM Provider extensions](https://github.com/orgs/Keyfactor/repositories?q=pam). \ No newline at end of file diff --git a/docs/akeyless.md b/docs/akeyless.md index 0a3d63b..e7be682 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -8,11 +8,8 @@ these authentication methods, see the [Akeyless documentation](https://docs.akey - Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. -The Akeyless PAM Provider allows for the retrieval of stored account credentials from an Akeyless secret. -Below you will find a list of supported [auth methods](#supported-authentication-methods) and [secret types](#supported-secret-types) for this provider. For more information on -these authentication methods, see the [Akeyless documentation](https://docs.akeyless.io/reference/auth) -- Akeyless credentials w/ permission to access the secret(s) being used. See the [Akeyless documentation](https://docs.akeyless.io/reference/auth) for more information on how to configure the different types of auth. + ## Supported Authentication Methods @@ -163,4 +160,3 @@ akeyless get-secret-value --name /my-org/my-app/db-password --token The full service account setup can be scripted using the Akeyless CLI. The `create-auth-method-api-key` command returns the Access ID and Access Key you'll need for the Keyfactor configuration. ```shell - From 821f2559db2040068ca6af778c3ec66df245ef71 Mon Sep 17 00:00:00 2001 From: "Matthew H. Irby" Date: Wed, 15 Apr 2026 16:16:20 -0400 Subject: [PATCH 45/46] fix: rename AccessKeyId to AccessId on integration-manifest.json Signed-off-by: Matthew H. Irby --- integration-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-manifest.json b/integration-manifest.json index 58ee018..119c13d 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -27,8 +27,8 @@ "InstanceLevel": false }, { - "Name": "AccessKeyId", - "DisplayName": "Access Key ID", + "Name": "AccessId", + "DisplayName": "Access ID", "Description": "The access key ID used to authenticate to Akeyless using `access_key` authentication.", "DataType": 2, "InstanceLevel": false From 8c410181693fda5d53c9d7bbcf7fc9517c7c0de6 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Wed, 15 Apr 2026 20:32:30 +0000 Subject: [PATCH 46/46] Update generated docs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2205eb5..d971a23 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ Below is the payload to `POST` to the Keyfactor Command API "InstanceLevel": false }, { - "Name": "AccessKeyId", - "DisplayName": "Access Key ID", + "Name": "AccessId", + "DisplayName": "Access ID", "Description": "The access key ID used to authenticate to Akeyless using `access_key` authentication.", "DataType": 2, "InstanceLevel": false @@ -261,7 +261,7 @@ Below is the payload to `POST` to the Keyfactor Command API | Initialization parameter | Display Name | Description | | --- | --- | --- | | Url | Akeyless URL | The URL to the Akeyless instance. Defaults to: https://api.akeyless.io | -| AccessKeyId | Access Key ID | The access key ID used to authenticate to Akeyless using `access_key` authentication. | +| AccessId | Access ID | The access key ID used to authenticate to Akeyless using `access_key` authentication. | | AccessKey | Access Key | The access key used to authenticate to Akeyless using `access_key` authentication. | | AuthType | Auth Type | The auth type used to authenticate to the Akeyless platform. Supported types are `access_key`. |