From f7b578ac0d9507d69770aa9abe0a34c0e0bf294a Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:09:30 -0700 Subject: [PATCH 01/30] feat(config): support env var overrides for AuthType, AccessId, AccessKey Extends the existing AKEYLESS_API_URL override pattern in InitClient so AKEYLESS_AUTH_TYPE, AKEYLESS_ACCESS_ID, and AKEYLESS_ACCESS_KEY can also override their corresponding configured connection parameters at runtime, letting deployments control Akeyless connection details at the infrastructure level instead of only via manifest.json/Command portal. Closes #10 --- CHANGELOG.md | 6 + README.md | 16 ++ akeyless-pam/AkeylessPam.cs | 38 ++- docs/akeyless.md | 16 ++ docsource/akeyless.md | 16 ++ .../AkeylessPamTests.cs | 265 ++++++++++++++++++ tests/AkeylessPam.Unit.Tests/AssemblyInfo.cs | 15 + 7 files changed, 365 insertions(+), 7 deletions(-) create mode 100644 tests/AkeylessPam.Unit.Tests/AssemblyInfo.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cab5c..6926ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v1.1.0 + +## Features + +- **Environment variable overrides for connection parameters** — `AuthType`, `AccessId`, and `AccessKey` can now be overridden at runtime via the `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables, respectively, matching the existing `AKEYLESS_API_URL` override for the Akeyless API URL. This lets deployments control Akeyless connection details at the infrastructure/deployment level (e.g. process environment, secrets injection) instead of only via `manifest.json` or the Command portal. + # v1.0.0 Initial release of the Akeyless PAM Provider for Keyfactor Command and Universal Orchestrator. diff --git a/README.md b/README.md index f83c1de..f418d9b 100644 --- a/README.md +++ b/README.md @@ -416,6 +416,22 @@ sequenceDiagram UO->>KeyfactorCommand: Job completed. ``` +## Configuration + +Connection and authentication parameters can be set in two ways: + +1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. +2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. + +| Environment Variable | Overrides | Falls Back To | +|---|---|---| +| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | +| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | +| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | +| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | + +Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. + ## Supported Authentication Methods ### Access Key (API Key) Authentication diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index bd0ab7f..6935fbf 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -135,41 +135,65 @@ public string GetPassword(Dictionary instanceParameters, } } + /// + /// Resolves an environment variable override for a connection parameter. + /// + /// The name of the environment variable to check. + /// + /// The environment variable's value if it is set to a non-empty, non-whitespace string; otherwise, null + /// so the caller falls back to the configured value. + /// + /// + /// Unlike a plain ?? null-coalesce against , + /// this treats an env var explicitly set to an empty string the same as an unset env var — it does not + /// override the configured value. This avoids a misconfigured/empty environment variable silently blanking + /// out a valid `manifest.json`/Command portal value (e.g. `Url`, `AccessId`, `AccessKey`, `AuthType`). + /// + private static string ResolveEnvOverride(string envVarName) + { + var value = Environment.GetEnvironmentVariable(envVarName); + return string.IsNullOrEmpty(value) ? null : value; + } + private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) { try { Logger.MethodEntry(); - var basePath = Environment.GetEnvironmentVariable("AKEYLESS_API_URL") ?? + var basePath = ResolveEnvOverride("AKEYLESS_API_URL") ?? configurationInfo.Url ?? "https://api.akeyless.io"; + var authType = ResolveEnvOverride("AKEYLESS_AUTH_TYPE") ?? configurationInfo.AuthType; + var accessId = ResolveEnvOverride("AKEYLESS_ACCESS_ID") ?? configurationInfo.AccessId; + var accessKey = ResolveEnvOverride("AKEYLESS_ACCESS_KEY") ?? configurationInfo.AccessKey; + var client = _clientFactory(basePath); - switch (configurationInfo.AuthType) + switch (authType) { case "access_key": Logger.LogDebug("Authenticating with Akeyless using access_key auth, AccessId: '{AccessId}'", - configurationInfo.AccessId); - var token = client.Authenticate(configurationInfo.AccessId, configurationInfo.AccessKey); + accessId); + var token = client.Authenticate(accessId, accessKey); if (string.IsNullOrEmpty(token)) { Logger.LogError( "Authentication failed: unable to obtain access token from Akeyless for AccessId '{AccessId}'", - configurationInfo.AccessId); + accessId); throw new InvalidTokenException("Unable to obtain access token from Akeyless server"); } AuthToken = token; Logger.LogInformation( "Successfully authenticated with Akeyless using AccessId '{AccessId}'", - configurationInfo.AccessId); + accessId); break; default: Logger.LogWarning( "No authentication performed for unrecognised auth type '{AuthType}'", - configurationInfo.AuthType); + authType); break; } diff --git a/docs/akeyless.md b/docs/akeyless.md index bb26757..663da21 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -111,6 +111,22 @@ sequenceDiagram UO->>KeyfactorCommand: Job completed. ``` +## Configuration + +Connection and authentication parameters can be set in two ways: + +1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. +2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. + +| Environment Variable | Overrides | Falls Back To | +|---|---|---| +| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | +| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | +| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | +| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | + +Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. + ## Supported Authentication Methods ### Access Key (API Key) Authentication diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 0f79681..5efc403 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -8,6 +8,22 @@ 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. +## Configuration + +Connection and authentication parameters can be set in two ways: + +1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. +2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. + +| Environment Variable | Overrides | Falls Back To | +|---|---|---| +| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | +| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | +| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | +| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | + +Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. + ## Supported Authentication Methods ### Access Key (API Key) Authentication diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 9baf37b..26b100f 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -150,6 +150,271 @@ public void GetPassword_UsesConfiguredUrl_WhenNoEnvVar() } } +/// +/// Sets an environment variable for the duration of the test and restores the prior value (or clears it, +/// if it was previously unset) on dispose. Ensures env var overrides used to test one PAM instance +/// don't leak into other tests or other test runs. +/// +internal sealed class EnvVarScope : IDisposable +{ + private readonly string _name; + private readonly string? _previousValue; + + public EnvVarScope(string name, string? value) + { + _name = name; + _previousValue = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(_name, _previousValue); + } +} + +public class EnvironmentVariableOverrideTests +{ + [Fact] + public void GetPassword_AkeylessApiUrlEnvVar_OverridesConfiguredUrl() + { + using var _ = new EnvVarScope("AKEYLESS_API_URL", "https://env-override.akeyless.io"); + + 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://configured.akeyless.io")); + + Assert.Equal("https://env-override.akeyless.io", capturedBasePath); + } + + [Fact] + public void GetPassword_AkeylessApiUrlEnvVarUnset_FallsBackToConfiguredUrl() + { + using var _ = new EnvVarScope("AKEYLESS_API_URL", null); + + 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://configured.akeyless.io")); + + Assert.Equal("https://configured.akeyless.io", capturedBasePath); + } + + [Fact] + public void GetPassword_AkeylessApiUrlEnvVarEmptyString_FallsBackToConfiguredUrl() + { + using var _ = new EnvVarScope("AKEYLESS_API_URL", ""); + + 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://configured.akeyless.io")); + + Assert.Equal("https://configured.akeyless.io", capturedBasePath); + } + + [Fact] + public void GetPassword_AkeylessAccessIdEnvVar_OverridesConfiguredAccessId() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", "env-access-id"); + + string? capturedAccessId = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((accessId, _) => capturedAccessId = accessId) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id")); + + Assert.Equal("env-access-id", capturedAccessId); + } + + [Fact] + public void GetPassword_AkeylessAccessIdEnvVarUnset_FallsBackToConfiguredAccessId() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", null); + + string? capturedAccessId = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((accessId, _) => capturedAccessId = accessId) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id")); + + Assert.Equal("configured-access-id", capturedAccessId); + } + + [Fact] + public void GetPassword_AkeylessAccessIdEnvVarEmptyString_FallsBackToConfiguredAccessId() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", ""); + + string? capturedAccessId = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((accessId, _) => capturedAccessId = accessId) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id")); + + Assert.Equal("configured-access-id", capturedAccessId); + } + + [Fact] + public void GetPassword_AkeylessAccessKeyEnvVar_OverridesConfiguredAccessKey() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", "env-access-key"); + + string? capturedAccessKey = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((_, accessKey) => capturedAccessKey = accessKey) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessKey: "configured-access-key")); + + Assert.Equal("env-access-key", capturedAccessKey); + } + + [Fact] + public void GetPassword_AkeylessAccessKeyEnvVarUnset_FallsBackToConfiguredAccessKey() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", null); + + string? capturedAccessKey = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((_, accessKey) => capturedAccessKey = accessKey) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessKey: "configured-access-key")); + + Assert.Equal("configured-access-key", capturedAccessKey); + } + + [Fact] + public void GetPassword_AkeylessAccessKeyEnvVarEmptyString_FallsBackToConfiguredAccessKey() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", ""); + + string? capturedAccessKey = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((_, accessKey) => capturedAccessKey = accessKey) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessKey: "configured-access-key")); + + Assert.Equal("configured-access-key", capturedAccessKey); + } + + [Fact] + public void GetPassword_AkeylessAuthTypeEnvVar_OverridesConfiguredAuthType() + { + // Override the configured "access_key" auth type with an unrecognised value via env var — the + // provider should skip authentication entirely (default/no-op branch) rather than calling Authenticate + // with the configured access_key credentials. + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", "unrecognised_env_auth_type"); + + var mock = new Mock(); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); + + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void GetPassword_AkeylessAuthTypeEnvVarUnset_FallsBackToConfiguredAuthType() + { + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", 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(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); + + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void GetPassword_AkeylessAuthTypeEnvVarEmptyString_FallsBackToConfiguredAuthType() + { + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", ""); + + 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(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); + + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); + } +} + public class SecretRetrievalTests { private static AkeylessPam PamWithMockReturning(string secretName, string secretValue) diff --git a/tests/AkeylessPam.Unit.Tests/AssemblyInfo.cs b/tests/AkeylessPam.Unit.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..ae8fc57 --- /dev/null +++ b/tests/AkeylessPam.Unit.Tests/AssemblyInfo.cs @@ -0,0 +1,15 @@ +// 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 Xunit; + +// Environment variable override tests (EnvironmentVariableOverrideTests) mutate process-wide environment +// variables (AKEYLESS_API_URL, AKEYLESS_AUTH_TYPE, AKEYLESS_ACCESS_ID, AKEYLESS_ACCESS_KEY). Test classes run +// in parallel by default in xUnit, which could race with other tests that assume these env vars are unset +// (e.g. GetPassword_UsesConfiguredUrl_WhenNoEnvVar). Disabling parallelization keeps env var state +// deterministic across the whole assembly. +[assembly: CollectionBehavior(DisableTestParallelization = true)] From 841842c9ea7fc3fc499a9425372341e186ef5e1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Jul 2026 22:10:13 +0000 Subject: [PATCH 02/30] docs: auto-generate README and documentation [skip ci] --- README.md | 225 ----------------------------------------------- docs/akeyless.md | 109 ++++++++++++++++++++++- 2 files changed, 108 insertions(+), 226 deletions(-) diff --git a/README.md b/README.md index f418d9b..955f6d1 100644 --- a/README.md +++ b/README.md @@ -313,231 +313,6 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). -#### Extension Mechanics - -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. - -### 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) - -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. -``` - -### 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. -``` - -## Configuration - -Connection and authentication parameters can be set in two ways: - -1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. -2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. - -| Environment Variable | Overrides | Falls Back To | -|---|---|---| -| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | -| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | -| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | -| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | - -Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. - -## 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.AkeylessPam" - } - } - }, - "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 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`. | - ---- - -#### `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. - -> **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 -{ - "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` | - - ## License Apache License 2.0, see [LICENSE](LICENSE) diff --git a/docs/akeyless.md b/docs/akeyless.md index 663da21..35c26be 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -8,7 +8,7 @@ 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. -## Extension Mechanics +## Mechanics 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. @@ -111,6 +111,12 @@ sequenceDiagram UO->>KeyfactorCommand: Job completed. ``` +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. + ## Configuration Connection and authentication parameters can be set in two ways: @@ -232,3 +238,104 @@ host=db.example.com | `SecretType` | `static_kv` | | `StaticSecretFieldName` | `password` | +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. + +### 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) + +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. +``` + +### 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. +``` + From a977de59e41445615bee284f69c4262ad093e9d4 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:18:01 -0700 Subject: [PATCH 03/30] docs: document Akeyless API endpoints called by the extension --- docsource/akeyless.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 5efc403..47216c4 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -139,6 +139,17 @@ 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. +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + ### 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. From 6e69bab5a7e4c248ce521200d28a6aa799b01c00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Jul 2026 22:18:39 +0000 Subject: [PATCH 04/30] docs: auto-generate README and documentation [skip ci] --- docs/akeyless.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/akeyless.md b/docs/akeyless.md index 35c26be..abe3c98 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -17,6 +17,17 @@ 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. +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + ### 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. @@ -245,6 +256,17 @@ 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. +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + ### 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. From e7e547d327e577cf508ca260587477d52a18a6ba Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:24:40 -0700 Subject: [PATCH 05/30] docs: surface env var config in Requirements so it bubbles into README --- docsource/akeyless.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 47216c4..6ff2a85 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -7,6 +7,7 @@ these authentication methods, see the [Akeyless documentation](https://docs.akey ## 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. +- (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. ## Configuration From b891cf37d2e6c0eb96ad544f7a02d7f74f534151 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:26:56 -0700 Subject: [PATCH 06/30] docs: regenerate with fixed doctool (dedup Extension Mechanics, include in README) [skip ci] --- README.md | 237 +++++++++++++++++++++++++++++++++++++++++++++++ docs/akeyless.md | 121 +----------------------- 2 files changed, 239 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 955f6d1..f91da1a 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ To install Akeyless PAM Provider, it is recommended you install [kfutil](https:/ #### 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. + - (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. #### Create PAM type in Keyfactor Command @@ -313,6 +314,242 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). +#### Extension Mechanics + +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. + +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + +### 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) + +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. +``` + +### 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. +``` + +## Configuration + +Connection and authentication parameters can be set in two ways: + +1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. +2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. + +| Environment Variable | Overrides | Falls Back To | +|---|---|---| +| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | +| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | +| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | +| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | + +Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. + +## 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.AkeylessPam" + } + } + }, + "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 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`. | + +--- + +#### `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. + +> **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 +{ + "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` | + + ## License Apache License 2.0, see [LICENSE](LICENSE) diff --git a/docs/akeyless.md b/docs/akeyless.md index abe3c98..3e00e07 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -7,8 +7,9 @@ these authentication methods, see the [Akeyless documentation](https://docs.akey ## 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. +- (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. -## Mechanics +## Extension Mechanics 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. @@ -122,12 +123,6 @@ sequenceDiagram UO->>KeyfactorCommand: Job completed. ``` -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. - ## Configuration Connection and authentication parameters can be set in two ways: @@ -249,115 +244,3 @@ host=db.example.com | `SecretType` | `static_kv` | | `StaticSecretFieldName` | `password` | -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. - -### Akeyless API Endpoints Used - -The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): - -| Endpoint | Method | Called from | Purpose | -|---|---|---|---| -| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | -| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | - -No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. - -### 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) - -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. -``` - -### 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. -``` - From deddf75f3f4b3ca3424526d64b4b8756c851d4f2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jul 2026 17:29:55 +0000 Subject: [PATCH 07/30] docs: auto-generate README and documentation [skip ci] --- README.md | 236 ----------------------------------------------- docs/akeyless.md | 121 +++++++++++++++++++++++- 2 files changed, 120 insertions(+), 237 deletions(-) diff --git a/README.md b/README.md index f91da1a..7406170 100644 --- a/README.md +++ b/README.md @@ -314,242 +314,6 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). -#### Extension Mechanics - -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. - -### Akeyless API Endpoints Used - -The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): - -| Endpoint | Method | Called from | Purpose | -|---|---|---|---| -| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | -| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | - -No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. - -### 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) - -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. -``` - -### 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. -``` - -## Configuration - -Connection and authentication parameters can be set in two ways: - -1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. -2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. - -| Environment Variable | Overrides | Falls Back To | -|---|---|---| -| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | -| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | -| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | -| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | - -Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. - -## 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.AkeylessPam" - } - } - }, - "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 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`. | - ---- - -#### `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. - -> **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 -{ - "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` | - - ## License Apache License 2.0, see [LICENSE](LICENSE) diff --git a/docs/akeyless.md b/docs/akeyless.md index 3e00e07..ce69934 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -9,7 +9,7 @@ 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. - (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. -## Extension Mechanics +## Mechanics 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. @@ -123,6 +123,13 @@ sequenceDiagram UO->>KeyfactorCommand: Job completed. ``` +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. +- (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. + ## Configuration Connection and authentication parameters can be set in two ways: @@ -244,3 +251,115 @@ host=db.example.com | `SecretType` | `static_kv` | | `StaticSecretFieldName` | `password` | +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. + +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + +### 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) + +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. +``` + +### 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. +``` + From 44ded50005e85b66fb113ad4b70f28acb3cef68b Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:41:03 -0700 Subject: [PATCH 08/30] fix(tests): prevent env var override from masking bad-credentials test GetPassword_BadCredentials_ThrowsInvalidClientConfigurationException asserted that bad server-config credentials fail auth, but AKEYLESS_ACCESS_ID/ AKEYLESS_ACCESS_KEY (required for every other integration test to run) now override those credentials via this PR's own env var override feature, making auth succeed instead of failing. Clear both env vars for the duration of this test only, and disable assembly-level parallelization so that doesn't race with AkeylessApiClientTests reading the same vars. Also documents the previously-missing Debug_K8sOrchestratorSecret_PrintsRawValue test in the integration tests README. --- .../AkeylessPamIntegrationTests.cs | 28 +++++++++++++++++++ .../AssemblyInfo.cs | 15 ++++++++++ tests/AkeylessPam.Integration.Tests/README.md | 3 +- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/AkeylessPam.Integration.Tests/AssemblyInfo.cs diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs index bad91ab..fd95965 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs +++ b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs @@ -199,6 +199,12 @@ public void GetPassword_BadCredentials_ThrowsInvalidClientConfigurationException ["SecretName"] = Env("AKEYLESS_SECRET_STATIC_TEXT") }; + // AKEYLESS_ACCESS_ID/AKEYLESS_ACCESS_KEY (required for every other test in this suite to run) + // would otherwise override the bad credentials above via AkeylessPam's env var override support, + // making auth succeed instead of failing. Clear them for the duration of this test only. + using var idScope = new EnvVarScope("AKEYLESS_ACCESS_ID", null); + using var keyScope = new EnvVarScope("AKEYLESS_ACCESS_KEY", null); + var pam = new AkeylessPam(); var ex = Assert.Throws(() => pam.GetPassword(instance, server)); Assert.IsType(ex.InnerException); @@ -254,3 +260,25 @@ public void GetPassword_StaticJson_WhitespaceFieldName_ReturnsRawJsonBlob_K8sOrc "Expected raw JSON blob even when StaticSecretFieldName is whitespace-only"); } } + +/// +/// Sets an environment variable for the duration of a test and restores the prior value (or clears it, +/// if it was previously unset) on dispose. +/// +internal sealed class EnvVarScope : IDisposable +{ + private readonly string _name; + private readonly string? _previousValue; + + public EnvVarScope(string name, string? value) + { + _name = name; + _previousValue = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(_name, _previousValue); + } +} diff --git a/tests/AkeylessPam.Integration.Tests/AssemblyInfo.cs b/tests/AkeylessPam.Integration.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..9e3253d --- /dev/null +++ b/tests/AkeylessPam.Integration.Tests/AssemblyInfo.cs @@ -0,0 +1,15 @@ +// 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 Xunit; + +// GetPassword_BadCredentials_ThrowsInvalidClientConfigurationException temporarily clears the process-wide +// AKEYLESS_ACCESS_ID/AKEYLESS_ACCESS_KEY environment variables so the bad credentials it passes via server +// config parameters aren't overridden by AkeylessPam's env var override support. Test classes run in +// parallel by default in xUnit, which could race with AkeylessApiClientTests reading those same env vars. +// Disabling parallelization keeps env var state deterministic across the whole assembly. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/AkeylessPam.Integration.Tests/README.md b/tests/AkeylessPam.Integration.Tests/README.md index c4ace5d..48f6418 100644 --- a/tests/AkeylessPam.Integration.Tests/README.md +++ b/tests/AkeylessPam.Integration.Tests/README.md @@ -42,7 +42,7 @@ End-to-end tests for `AkeylessPam.GetPassword()` against a live Akeyless instanc | `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_BadCredentials_ThrowsInvalidClientConfigurationException` | `AKEYLESS_SECRET_STATIC_TEXT` | Intentionally uses invalid credentials and asserts `InvalidClientConfigurationException` is thrown. Clears `AKEYLESS_ACCESS_ID`/`AKEYLESS_ACCESS_KEY` for the duration of the test so the env var override feature doesn't replace the bad credentials with real ones | | `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 | @@ -62,3 +62,4 @@ Lower-level tests for `AkeylessApiClient` — the adapter that wraps the Akeyles | `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 | +| `Debug_K8sOrchestratorSecret_PrintsRawValue` | Retrieves the hardcoded `/pam/test/k8s-orchestrator` secret directly via the API client and asserts the response contains that key (the value itself is intentionally not written to output, to avoid secret exposure in CI logs) | From bc1bc0f1379b0cff2e334ee9fa0687b3e8314152 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:42:26 -0700 Subject: [PATCH 09/30] fix(docs): revert CI doc regeneration regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI's auto-generate-docs workflow ran a stale/buggy doctool version against this branch (docsource/akeyless.md unchanged), which re-stripped Extension Mechanics from README.md and duplicated content into docs/akeyless.md — the exact bug already fixed on this branch via the "regenerate with fixed doctool" commit. Restoring that fixed content. --- README.md | 236 +++++++++++++++++++++++++++++++++++++++++++++++ docs/akeyless.md | 121 +----------------------- 2 files changed, 237 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 7406170..f91da1a 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,242 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). +#### Extension Mechanics + +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. + +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + +### 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) + +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. +``` + +### 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. +``` + +## Configuration + +Connection and authentication parameters can be set in two ways: + +1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. +2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. + +| Environment Variable | Overrides | Falls Back To | +|---|---|---| +| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | +| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | +| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | +| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | + +Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. + +## 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.AkeylessPam" + } + } + }, + "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 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`. | + +--- + +#### `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. + +> **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 +{ + "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` | + + ## License Apache License 2.0, see [LICENSE](LICENSE) diff --git a/docs/akeyless.md b/docs/akeyless.md index ce69934..3e00e07 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -9,7 +9,7 @@ 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. - (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. -## Mechanics +## Extension Mechanics 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. @@ -123,13 +123,6 @@ sequenceDiagram UO->>KeyfactorCommand: Job completed. ``` -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. -- (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. - ## Configuration Connection and authentication parameters can be set in two ways: @@ -251,115 +244,3 @@ host=db.example.com | `SecretType` | `static_kv` | | `StaticSecretFieldName` | `password` | -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. - -### Akeyless API Endpoints Used - -The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): - -| Endpoint | Method | Called from | Purpose | -|---|---|---|---| -| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | -| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | - -No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. - -### 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) - -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. -``` - -### 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. -``` - From f411c058631f7c1d1fcedfc9a3abe814f94a39b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jul 2026 17:42:49 +0000 Subject: [PATCH 10/30] docs: auto-generate README and documentation [skip ci] --- README.md | 236 ----------------------------------------------- docs/akeyless.md | 121 +++++++++++++++++++++++- 2 files changed, 120 insertions(+), 237 deletions(-) diff --git a/README.md b/README.md index f91da1a..7406170 100644 --- a/README.md +++ b/README.md @@ -314,242 +314,6 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). -#### Extension Mechanics - -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. - -### Akeyless API Endpoints Used - -The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): - -| Endpoint | Method | Called from | Purpose | -|---|---|---|---| -| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | -| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | - -No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. - -### 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) - -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. -``` - -### 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. -``` - -## Configuration - -Connection and authentication parameters can be set in two ways: - -1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. -2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. - -| Environment Variable | Overrides | Falls Back To | -|---|---|---| -| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | -| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | -| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | -| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | - -Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. - -## 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.AkeylessPam" - } - } - }, - "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 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`. | - ---- - -#### `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. - -> **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 -{ - "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` | - - ## License Apache License 2.0, see [LICENSE](LICENSE) diff --git a/docs/akeyless.md b/docs/akeyless.md index 3e00e07..ce69934 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -9,7 +9,7 @@ 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. - (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. -## Extension Mechanics +## Mechanics 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. @@ -123,6 +123,13 @@ sequenceDiagram UO->>KeyfactorCommand: Job completed. ``` +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. +- (Optional) `AKEYLESS_API_URL`, `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables can be set on the provider's host process to override the corresponding `manifest.json`/Command portal parameters at runtime. See [Configuration](docs/akeyless.md#configuration) for details and precedence. + ## Configuration Connection and authentication parameters can be set in two ways: @@ -244,3 +251,115 @@ host=db.example.com | `SecretType` | `static_kv` | | `StaticSecretFieldName` | `password` | +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. + +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + +### 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) + +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. +``` + +### 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. +``` + From c373bf523d2beb0555cff6079502eb56f9ab8e22 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:56:11 -0700 Subject: [PATCH 11/30] fix: whitespace-only env var overrides and add override audit log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResolveEnvOverride only checked IsNullOrEmpty, so a whitespace-only env var (e.g. AKEYLESS_ACCESS_ID=" ") was treated as an active override rather than falling back, contradicting its own doc comment and the codebase's existing whitespace-only-is-empty convention elsewhere. Switch to IsNullOrWhiteSpace, and add whitespace-only unit tests for all four env vars alongside the existing empty-string cases. Also log which specific env var is overriding (never the value) when an override is active, so an incident investigation can tell whether the effective connection parameter used at runtime actually matches Command's recorded configuration — previously nothing distinguished a configured value from an env-var-overridden one in production logs. --- README.md | 2 +- akeyless-pam/AkeylessPam.cs | 19 +++-- docs/akeyless.md | 2 +- docsource/akeyless.md | 2 +- .../AkeylessPamTests.cs | 79 +++++++++++++++++++ 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f91da1a..2eccea4 100644 --- a/README.md +++ b/README.md @@ -442,7 +442,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. ## Supported Authentication Methods diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 6935fbf..e2b8b53 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -145,14 +145,23 @@ public string GetPassword(Dictionary instanceParameters, /// /// /// Unlike a plain ?? null-coalesce against , - /// this treats an env var explicitly set to an empty string the same as an unset env var — it does not - /// override the configured value. This avoids a misconfigured/empty environment variable silently blanking - /// out a valid `manifest.json`/Command portal value (e.g. `Url`, `AccessId`, `AccessKey`, `AuthType`). + /// this treats an env var explicitly set to an empty or whitespace-only string the same as an unset env + /// var — it does not override the configured value. This avoids a misconfigured/blank environment variable + /// silently blanking out a valid `manifest.json`/Command portal value (e.g. `Url`, `AccessId`, `AccessKey`, + /// `AuthType`). + /// + /// Audit trail: when an override is active, this logs which environment variable is overriding — + /// never the value — so an incident investigation can tell whether the effective connection + /// parameter used at runtime matches Command's recorded configuration. + /// /// - private static string ResolveEnvOverride(string envVarName) + private string ResolveEnvOverride(string envVarName) { var value = Environment.GetEnvironmentVariable(envVarName); - return string.IsNullOrEmpty(value) ? null : value; + if (string.IsNullOrWhiteSpace(value)) return null; + + Logger.LogInformation("Environment variable override active for {EnvVar}", envVarName); + return value; } private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) diff --git a/docs/akeyless.md b/docs/akeyless.md index 3e00e07..1ca5b3a 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -137,7 +137,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. ## Supported Authentication Methods diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 6ff2a85..7f01172 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -23,7 +23,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty string, is treated as "not overriding" and falls through to the configured value. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. ## Supported Authentication Methods diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 26b100f..c2f40e6 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -241,6 +241,28 @@ public void GetPassword_AkeylessApiUrlEnvVarEmptyString_FallsBackToConfiguredUrl Assert.Equal("https://configured.akeyless.io", capturedBasePath); } + [Fact] + public void GetPassword_AkeylessApiUrlEnvVarWhitespaceOnly_FallsBackToConfiguredUrl() + { + using var _ = new EnvVarScope("AKEYLESS_API_URL", " "); + + 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://configured.akeyless.io")); + + Assert.Equal("https://configured.akeyless.io", capturedBasePath); + } + [Fact] public void GetPassword_AkeylessAccessIdEnvVar_OverridesConfiguredAccessId() { @@ -301,6 +323,26 @@ public void GetPassword_AkeylessAccessIdEnvVarEmptyString_FallsBackToConfiguredA Assert.Equal("configured-access-id", capturedAccessId); } + [Fact] + public void GetPassword_AkeylessAccessIdEnvVarWhitespaceOnly_FallsBackToConfiguredAccessId() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", " "); + + string? capturedAccessId = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((accessId, _) => capturedAccessId = accessId) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id")); + + Assert.Equal("configured-access-id", capturedAccessId); + } + [Fact] public void GetPassword_AkeylessAccessKeyEnvVar_OverridesConfiguredAccessKey() { @@ -361,6 +403,26 @@ public void GetPassword_AkeylessAccessKeyEnvVarEmptyString_FallsBackToConfigured Assert.Equal("configured-access-key", capturedAccessKey); } + [Fact] + public void GetPassword_AkeylessAccessKeyEnvVarWhitespaceOnly_FallsBackToConfiguredAccessKey() + { + using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", " "); + + string? capturedAccessKey = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((_, accessKey) => capturedAccessKey = accessKey) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessKey: "configured-access-key")); + + Assert.Equal("configured-access-key", capturedAccessKey); + } + [Fact] public void GetPassword_AkeylessAuthTypeEnvVar_OverridesConfiguredAuthType() { @@ -413,6 +475,23 @@ public void GetPassword_AkeylessAuthTypeEnvVarEmptyString_FallsBackToConfiguredA mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); } + + [Fact] + public void GetPassword_AkeylessAuthTypeEnvVarWhitespaceOnly_FallsBackToConfiguredAuthType() + { + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", " "); + + 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(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); + + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); + } } public class SecretRetrievalTests From 06c8db13484f28ade7d78af207f67a71755ba5cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jul 2026 17:57:53 +0000 Subject: [PATCH 12/30] docs: auto-generate README and documentation [skip ci] --- README.md | 236 ------------------------------------------------------ 1 file changed, 236 deletions(-) diff --git a/README.md b/README.md index 2eccea4..7406170 100644 --- a/README.md +++ b/README.md @@ -314,242 +314,6 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). -#### Extension Mechanics - -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. - -### Akeyless API Endpoints Used - -The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): - -| Endpoint | Method | Called from | Purpose | -|---|---|---|---| -| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | -| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | - -No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. - -### 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) - -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. -``` - -### 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. -``` - -## Configuration - -Connection and authentication parameters can be set in two ways: - -1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. -2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. - -| Environment Variable | Overrides | Falls Back To | -|---|---|---| -| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | -| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | -| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | -| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | - -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. - -## 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.AkeylessPam" - } - } - }, - "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 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`. | - ---- - -#### `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. - -> **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 -{ - "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` | - - ## License Apache License 2.0, see [LICENSE](LICENSE) From 5c5089cc904a06c8c59cb6e69ad25d083b437a1c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:03:08 -0700 Subject: [PATCH 13/30] fix(docs): revert another CI doc regeneration regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same recurring issue: the CI auto-generate-docs workflow (Keyfactor Bootstrap Workflow, pinned keyfactor/actions@v5) ran a stale/buggy doctool version against this branch again (docsource/akeyless.md unchanged), stripping the Extension Mechanics section from README.md. Restoring the correct content. This has now happened on every push to this branch — see PR discussion for a permanent fix recommendation. --- README.md | 236 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/README.md b/README.md index 7406170..2eccea4 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,242 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). +#### Extension Mechanics + +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. + +### Akeyless API Endpoints Used + +The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): + +| Endpoint | Method | Called from | Purpose | +|---|---|---|---| +| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | +| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | + +No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. + +### 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) + +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. +``` + +### 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. +``` + +## Configuration + +Connection and authentication parameters can be set in two ways: + +1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. +2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. + +| Environment Variable | Overrides | Falls Back To | +|---|---|---| +| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | +| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | +| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | +| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | + +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. + +## 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.AkeylessPam" + } + } + }, + "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 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`. | + +--- + +#### `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. + +> **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 +{ + "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` | + + ## License Apache License 2.0, see [LICENSE](LICENSE) From bf1f5b823653b1464e4da6220e9253dd1042e03c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jul 2026 18:03:42 +0000 Subject: [PATCH 14/30] docs: auto-generate README and documentation [skip ci] --- README.md | 236 ------------------------------------------------------ 1 file changed, 236 deletions(-) diff --git a/README.md b/README.md index 2eccea4..7406170 100644 --- a/README.md +++ b/README.md @@ -314,242 +314,6 @@ When entering Secret fields, select the **Load From Keyfactor Secrets** tab, and > [!NOTE] > Additional information on Akeyless can be found in the [supplemental documentation](docs/akeyless.md). -#### Extension Mechanics - -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. - -### Akeyless API Endpoints Used - -The provider calls exactly two Akeyless REST API endpoints, both against the configured base URL (default `https://api.akeyless.io`, see the `Url` initialization parameter / `AKEYLESS_API_URL` environment variable above): - -| Endpoint | Method | Called from | Purpose | -|---|---|---|---| -| [`/auth`](https://docs.akeyless.io/reference/auth) | `POST` | `AkeylessApiClient.Authenticate` (invoked once per `GetPassword` call, before secret retrieval) | Exchanges the configured `AccessId`/`AccessKey` for a short-lived auth token. | -| [`/get-secret-value`](https://docs.akeyless.io/reference/getsecretvalue) | `POST` | `AkeylessApiClient.GetSecretValuesAsync` (invoked once per `GetPassword` call, after authentication) | Retrieves the value of the secret named by the `SecretName` instance parameter, using the token from `/auth`. | - -No other Akeyless API endpoints are called by this provider — it only ever authenticates and reads a single static secret value per credential lookup. It never creates, updates, deletes, or lists items in Akeyless. - -### 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) - -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. -``` - -### 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. -``` - -## Configuration - -Connection and authentication parameters can be set in two ways: - -1. **`manifest.json`/Command portal parameters** — set via the `manifest.json` `InitializationInfo` block (Universal Orchestrator installs) or the corresponding fields in the Command portal PAM provider configuration (Command host installs). This is the standard way to configure the provider. -2. **Environment variables** — if set on the host process running the PAM provider (the Keyfactor Command server for local installs, or the Universal Orchestrator host for remote installs), these override whatever value is configured via `manifest.json` or the Command portal. This is useful when connection details need to be controlled at the infrastructure/deployment level rather than baked into provider configuration — for example, pointing different environments (dev/stage/prod) at different Akeyless instances or credentials without changing `manifest.json` or Command PAM provider settings. - -| Environment Variable | Overrides | Falls Back To | -|---|---|---| -| `AKEYLESS_API_URL` | `Url` | configured `Url` initialization parameter, then default (`https://api.akeyless.io`) | -| `AKEYLESS_AUTH_TYPE` | `AuthType` | configured `AuthType` initialization parameter | -| `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | -| `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | - -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. - -## 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.AkeylessPam" - } - } - }, - "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 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`. | - ---- - -#### `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. - -> **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 -{ - "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` | - - ## License Apache License 2.0, see [LICENSE](LICENSE) From 4fa6b0f83ee62590acc363868cf354fdac90c920 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:58:35 -0700 Subject: [PATCH 15/30] fix: validate AKEYLESS_AUTH_TYPE override, trim env-var overrides, log effective URL - InitClient now re-validates the resolved AuthType against the supported-auth-type allowlist before use. Previously an AKEYLESS_AUTH_TYPE override bypassed the validation BuildAkeylessConfiguration already performed on the configured value, so a typo'd or unsupported override silently skipped authentication (no throw, AuthToken left empty) and let execution continue into an unauthenticated secret fetch. Unrecognized overrides now fail fast with InvalidClientConfigurationException. - ResolveEnvOverride now trims the resolved value, since env vars sourced from file-mounted secrets/configmaps commonly carry an incidental trailing newline, which previously failed the exact-match auth-type switch and could send whitespace-polluted credentials to Akeyless. - The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL used to construct the API client instead of the stale pre-override configured value, so log-based incident review can actually confirm the true destination host. --- CHANGELOG.md | 3 ++ akeyless-pam/AkeylessPam.cs | 30 +++++++---- docs/akeyless.md | 2 +- docsource/akeyless.md | 2 +- .../AkeylessPamTests.cs | 52 +++++++++++++++++-- 5 files changed, 71 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6926ea0..7a2f19a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Features - **Environment variable overrides for connection parameters** — `AuthType`, `AccessId`, and `AccessKey` can now be overridden at runtime via the `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables, respectively, matching the existing `AKEYLESS_API_URL` override for the Akeyless API URL. This lets deployments control Akeyless connection details at the infrastructure/deployment level (e.g. process environment, secrets injection) instead of only via `manifest.json` or the Command portal. +- Environment variable override values are trimmed of leading/trailing whitespace before use, so an incidental trailing newline (a common artifact of file-mounted/`envFrom` secret provisioning) no longer breaks the override. +- An `AKEYLESS_AUTH_TYPE` override is now validated against the supported auth types before use; an unrecognized value fails fast with `InvalidClientConfigurationException` instead of silently skipping authentication and proceeding with an unauthenticated request. +- The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL rather than the pre-override configured value. # v1.0.0 diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index e2b8b53..7df0f2b 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -140,15 +140,17 @@ public string GetPassword(Dictionary instanceParameters, /// /// The name of the environment variable to check. /// - /// The environment variable's value if it is set to a non-empty, non-whitespace string; otherwise, null - /// so the caller falls back to the configured value. + /// The environment variable's value, trimmed of leading/trailing whitespace, if it is set to a non-empty, + /// non-whitespace string; otherwise, null so the caller falls back to the configured value. /// /// /// Unlike a plain ?? null-coalesce against , /// this treats an env var explicitly set to an empty or whitespace-only string the same as an unset env /// var — it does not override the configured value. This avoids a misconfigured/blank environment variable /// silently blanking out a valid `manifest.json`/Command portal value (e.g. `Url`, `AccessId`, `AccessKey`, - /// `AuthType`). + /// `AuthType`). The returned value is trimmed because env vars sourced from mounted secret files/configmaps + /// commonly carry an incidental trailing newline, which would otherwise fail exact-match comparisons + /// (auth type) or authentication (AccessId/AccessKey) with no indication that whitespace was the cause. /// /// Audit trail: when an override is active, this logs which environment variable is overriding — /// never the value — so an incident investigation can tell whether the effective connection @@ -161,7 +163,7 @@ private string ResolveEnvOverride(string envVarName) if (string.IsNullOrWhiteSpace(value)) return null; Logger.LogInformation("Environment variable override active for {EnvVar}", envVarName); - return value; + return value.Trim(); } private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) @@ -171,11 +173,24 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) Logger.MethodEntry(); var basePath = ResolveEnvOverride("AKEYLESS_API_URL") ?? configurationInfo.Url ?? "https://api.akeyless.io"; + Logger.LogDebug("Connecting to Akeyless at '{Url}'", basePath); var authType = ResolveEnvOverride("AKEYLESS_AUTH_TYPE") ?? configurationInfo.AuthType; var accessId = ResolveEnvOverride("AKEYLESS_ACCESS_ID") ?? configurationInfo.AccessId; var accessKey = ResolveEnvOverride("AKEYLESS_ACCESS_KEY") ?? configurationInfo.AccessKey; + // AuthType from configurationInfo already passed ValidateServerConfigurationParams, but an + // AKEYLESS_AUTH_TYPE override bypasses that check entirely, so re-validate the resolved value + // here — otherwise an unrecognised override silently falls through to "no authentication + // performed" below instead of failing fast. + if (!AkeylessConfiguration.SupportedAuthMethods.Contains(authType)) + { + Logger.LogError( + "Unsupported auth type '{AuthType}' resolved for Akeyless client initialization", authType); + throw new InvalidClientConfigurationException( + $"Invalid auth type '{authType}' specified. Supported auth types are: [{string.Join(", ", AkeylessConfiguration.SupportedAuthMethods)}]."); + } + var client = _clientFactory(basePath); switch (authType) @@ -198,12 +213,6 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) "Successfully authenticated with Akeyless using AccessId '{AccessId}'", accessId); break; - - default: - Logger.LogWarning( - "No authentication performed for unrecognised auth type '{AuthType}'", - authType); - break; } return client; @@ -364,7 +373,6 @@ private async Task GetAkeylessSecretAsync(AkeylessConfiguration configur try { Logger.MethodEntry(); - Logger.LogDebug("Connecting to Akeyless at '{Url}'", configurationInfo.Url); var client = InitClient(configurationInfo); switch (configurationInfo.SecretType) diff --git a/docs/akeyless.md b/docs/akeyless.md index c9074f2..44e15dc 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -144,7 +144,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 7f01172..467f12d 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -23,7 +23,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index c2f40e6..f46c38a 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -343,6 +343,28 @@ public void GetPassword_AkeylessAccessIdEnvVarWhitespaceOnly_FallsBackToConfigur Assert.Equal("configured-access-id", capturedAccessId); } + [Fact] + public void GetPassword_AkeylessAccessIdEnvVarHasTrailingNewline_IsTrimmedBeforeUse() + { + // A trailing newline is a common artifact of file-mounted/envFrom secret provisioning; it must not + // be sent to Akeyless as part of the credential. + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", "env-access-id\n"); + + string? capturedAccessId = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((accessId, _) => capturedAccessId = accessId) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id")); + + Assert.Equal("env-access-id", capturedAccessId); + } + [Fact] public void GetPassword_AkeylessAccessKeyEnvVar_OverridesConfiguredAccessKey() { @@ -424,11 +446,10 @@ public void GetPassword_AkeylessAccessKeyEnvVarWhitespaceOnly_FallsBackToConfigu } [Fact] - public void GetPassword_AkeylessAuthTypeEnvVar_OverridesConfiguredAuthType() + public void GetPassword_AkeylessAuthTypeEnvVarUnrecognised_ThrowsInvalidClientConfigurationException() { - // Override the configured "access_key" auth type with an unrecognised value via env var — the - // provider should skip authentication entirely (default/no-op branch) rather than calling Authenticate - // with the configured access_key credentials. + // Override the configured "access_key" auth type with an unrecognised value via env var — this must + // fail fast rather than silently skipping authentication and proceeding with an unauthenticated request. using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", "unrecognised_env_auth_type"); var mock = new Mock(); @@ -437,8 +458,10 @@ public void GetPassword_AkeylessAuthTypeEnvVar_OverridesConfiguredAuthType() var pam = new AkeylessPam(_ => mock.Object); - pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key"))); + Assert.IsType(ex.InnerException); mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); } @@ -492,6 +515,25 @@ public void GetPassword_AkeylessAuthTypeEnvVarWhitespaceOnly_FallsBackToConfigur mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); } + + [Fact] + public void GetPassword_AkeylessAuthTypeEnvVarHasTrailingNewline_IsTrimmedBeforeUse() + { + // A trailing newline (e.g. from file-mounted/envFrom secret provisioning) must not cause the + // resolved auth type to fail the "access_key" match and fall into the unsupported-auth-type path. + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", "access_key\n"); + + 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(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); + + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); + } } public class SecretRetrievalTests From 354cfad061f08884278e0f3be750d24569cb37be Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:14:13 -0700 Subject: [PATCH 16/30] fix: reject embedded line breaks in env overrides, log AccessId on auth failure - ResolveEnvOverride now rejects a value that still contains an embedded line break after trimming (InvalidClientConfigurationException). Trim() only strips leading/trailing whitespace, so a value sourced from a multi-line mounted secret file could otherwise flow unchanged into the structured log messages that echo AccessId/AuthType, forging extra log lines. - InitClient's ApiException catch block now logs the AccessId that the failed authentication attempt used. It previously omitted AccessId (unlike the success and empty-token paths), so a failed auth attempt left no record of which identity was used -- a real gap once AKEYLESS_ACCESS_ID can make the runtime identity diverge from Command's recorded configuration. --- CHANGELOG.md | 2 ++ akeyless-pam/AkeylessPam.cs | 28 ++++++++++++++-- docs/akeyless.md | 2 +- docsource/akeyless.md | 2 +- .../AkeylessPamTests.cs | 33 +++++++++++++++++++ 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2f19a..c3cf8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Environment variable override values are trimmed of leading/trailing whitespace before use, so an incidental trailing newline (a common artifact of file-mounted/`envFrom` secret provisioning) no longer breaks the override. - An `AKEYLESS_AUTH_TYPE` override is now validated against the supported auth types before use; an unrecognized value fails fast with `InvalidClientConfigurationException` instead of silently skipping authentication and proceeding with an unauthenticated request. - The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL rather than the pre-override configured value. +- An override value containing an embedded line break (surviving the leading/trailing trim) is now rejected with `InvalidClientConfigurationException` instead of being trimmed only at the ends and passed through — none of the overridable parameters are legitimately multi-line, and letting one through risked forging extra lines in the log messages that echo the resolved value. +- A failed Akeyless authentication attempt (`ApiException`) now logs the `AccessId` that was used, matching the existing success/empty-token log lines — previously this was the one authentication-failure path with no record of which identity (configured or env-overridden) the failed attempt used. # v1.0.0 diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 7df0f2b..6cfb3ab 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -151,23 +151,43 @@ public string GetPassword(Dictionary instanceParameters, /// `AuthType`). The returned value is trimmed because env vars sourced from mounted secret files/configmaps /// commonly carry an incidental trailing newline, which would otherwise fail exact-match comparisons /// (auth type) or authentication (AccessId/AccessKey) with no indication that whitespace was the cause. + /// An override that still contains an embedded line break after trimming is rejected outright — none of + /// the overridable parameters are legitimately multi-line, and letting one through would let it forge + /// extra lines in the structured log messages that echo the resolved value (e.g. AccessId, AuthType). /// /// Audit trail: when an override is active, this logs which environment variable is overriding — /// never the value — so an incident investigation can tell whether the effective connection /// parameter used at runtime matches Command's recorded configuration. /// /// + /// + /// Thrown if the environment variable's trimmed value still contains an embedded line break. + /// private string ResolveEnvOverride(string envVarName) { var value = Environment.GetEnvironmentVariable(envVarName); if (string.IsNullOrWhiteSpace(value)) return null; + var trimmed = value.Trim(); + if (trimmed.Contains('\n') || trimmed.Contains('\r')) + { + Logger.LogError( + "Environment variable {EnvVar} contains an embedded line break; refusing to use it as an override", + envVarName); + throw new InvalidClientConfigurationException( + $"Environment variable '{envVarName}' contains an embedded line break and cannot be used as a connection parameter override."); + } + Logger.LogInformation("Environment variable override active for {EnvVar}", envVarName); - return value.Trim(); + return trimmed; } private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) { + // Hoisted above the try block (rather than declared with the other resolved values inside it) so the + // ApiException catch below can still report which AccessId the failed authentication attempt used — + // that identity may differ from Command's recorded configuration when an env var override is active. + string accessId = null; try { Logger.MethodEntry(); @@ -176,7 +196,7 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) Logger.LogDebug("Connecting to Akeyless at '{Url}'", basePath); var authType = ResolveEnvOverride("AKEYLESS_AUTH_TYPE") ?? configurationInfo.AuthType; - var accessId = ResolveEnvOverride("AKEYLESS_ACCESS_ID") ?? configurationInfo.AccessId; + accessId = ResolveEnvOverride("AKEYLESS_ACCESS_ID") ?? configurationInfo.AccessId; var accessKey = ResolveEnvOverride("AKEYLESS_ACCESS_KEY") ?? configurationInfo.AccessKey; // AuthType from configurationInfo already passed ValidateServerConfigurationParams, but an @@ -221,7 +241,9 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) { // 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); + Logger.LogError(ex, + "Akeyless API exception during authentication (HTTP {StatusCode}) for AccessId '{AccessId}'", + ex.ErrorCode, accessId); throw new InvalidClientConfigurationException( $"Unable to authenticate to Akeyless API (HTTP {ex.ErrorCode}). Check AccessId and AccessKey configuration."); } diff --git a/docs/akeyless.md b/docs/akeyless.md index 44e15dc..f207039 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -144,7 +144,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded line break after trimming is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 467f12d..0a11c6a 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -23,7 +23,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded line break after trimming is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index f46c38a..316067e 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -5,6 +5,7 @@ // 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 Moq; using Xunit; @@ -128,6 +129,21 @@ public void GetPassword_AuthenticateReturnsEmptyToken_ThrowsInvalidTokenExceptio Assert.IsType(ex.InnerException); } + [Fact] + public void GetPassword_AuthenticateThrowsApiException_WrapsAsInvalidClientConfigurationException() + { + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Throws(new ApiException(401, "Unauthorized")); + + 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() { @@ -365,6 +381,23 @@ public void GetPassword_AkeylessAccessIdEnvVarHasTrailingNewline_IsTrimmedBefore Assert.Equal("env-access-id", capturedAccessId); } + [Fact] + public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedLineBreak_ThrowsInvalidClientConfigurationException() + { + // An embedded (non-trailing) line break survives Trim() and would otherwise flow unchanged into the + // structured log messages that echo AccessId, forging extra log lines. It must be rejected outright. + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", "env-access-id\nforged log line"); + + var mock = new Mock(); + var pam = new AkeylessPam(_ => mock.Object); + + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id"))); + + Assert.IsType(ex.InnerException); + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public void GetPassword_AkeylessAccessKeyEnvVar_OverridesConfiguredAccessKey() { From 737af79fbd98d98860ab1dc94eaa4b4e3c338294 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:30:52 -0700 Subject: [PATCH 17/30] fix: reject control characters and Unicode line separators, not just \n/\r ResolveEnvOverride's embedded-line-break guard only checked for '\n'/'\r', missing other control characters (ESC/ANSI escape sequences, NUL, form feed, vertical tab) and the Unicode line/paragraph separators (U+2028/U+2029) that survive Trim() and .NET's char.IsWhiteSpace() the same way an embedded '\n' would, and could equally forge or tamper with rendered log output on sinks that treat them as line breaks or escape codes. Switched the check from a '\n'/'\r' blocklist to rejecting on Unicode category (char.IsControl, plus LineSeparator/ParagraphSeparator), which covers all of the above in one check. --- CHANGELOG.md | 2 +- akeyless-pam/AkeylessPam.cs | 15 +++++++++---- docs/akeyless.md | 2 +- docsource/akeyless.md | 2 +- .../AkeylessPamTests.cs | 21 +++++++++++++++++++ 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3cf8be..ba40f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Environment variable override values are trimmed of leading/trailing whitespace before use, so an incidental trailing newline (a common artifact of file-mounted/`envFrom` secret provisioning) no longer breaks the override. - An `AKEYLESS_AUTH_TYPE` override is now validated against the supported auth types before use; an unrecognized value fails fast with `InvalidClientConfigurationException` instead of silently skipping authentication and proceeding with an unauthenticated request. - The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL rather than the pre-override configured value. -- An override value containing an embedded line break (surviving the leading/trailing trim) is now rejected with `InvalidClientConfigurationException` instead of being trimmed only at the ends and passed through — none of the overridable parameters are legitimately multi-line, and letting one through risked forging extra lines in the log messages that echo the resolved value. +- An override value containing an embedded control character or Unicode line/paragraph separator (surviving the leading/trailing trim — e.g. an embedded newline, ANSI escape sequence, or U+2028/U+2029) is now rejected with `InvalidClientConfigurationException` instead of being trimmed only at the ends and passed through — none of the overridable parameters are legitimately multi-line, and letting one through risked forging extra lines in the log messages that echo the resolved value. - A failed Akeyless authentication attempt (`ApiException`) now logs the `AccessId` that was used, matching the existing success/empty-token log lines — previously this was the one authentication-failure path with no record of which identity (configured or env-overridden) the failed attempt used. # v1.0.0 diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 6cfb3ab..c0a8dbe 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -161,7 +162,8 @@ public string GetPassword(Dictionary instanceParameters, /// /// /// - /// Thrown if the environment variable's trimmed value still contains an embedded line break. + /// Thrown if the environment variable's trimmed value still contains an embedded control character or + /// line/paragraph separator. /// private string ResolveEnvOverride(string envVarName) { @@ -169,13 +171,18 @@ private string ResolveEnvOverride(string envVarName) if (string.IsNullOrWhiteSpace(value)) return null; var trimmed = value.Trim(); - if (trimmed.Contains('\n') || trimmed.Contains('\r')) + // Reject by Unicode category rather than a \n/\r blocklist — this also catches ANSI escape + // sequences (ESC is a control character), NUL/form-feed/vertical-tab, and the Unicode + // line/paragraph separators (U+2028/U+2029), any of which could otherwise forge an extra + // rendered line in the log messages that echo the resolved value. + if (trimmed.Any(c => char.IsControl(c) || + char.GetUnicodeCategory(c) is UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator)) { Logger.LogError( - "Environment variable {EnvVar} contains an embedded line break; refusing to use it as an override", + "Environment variable {EnvVar} contains an embedded control character or line separator; refusing to use it as an override", envVarName); throw new InvalidClientConfigurationException( - $"Environment variable '{envVarName}' contains an embedded line break and cannot be used as a connection parameter override."); + $"Environment variable '{envVarName}' contains an embedded control character or line separator and cannot be used as a connection parameter override."); } Logger.LogInformation("Environment variable override active for {EnvVar}", envVarName); diff --git a/docs/akeyless.md b/docs/akeyless.md index f207039..0f7b897 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -144,7 +144,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded line break after trimming is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded control character or line/paragraph separator after trimming (e.g. an embedded newline, ANSI escape sequence, or Unicode line separator) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 0a11c6a..21bb213 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -23,7 +23,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded line break after trimming is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded control character or line/paragraph separator after trimming (e.g. an embedded newline, ANSI escape sequence, or Unicode line separator) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 316067e..780fb6a 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -398,6 +398,27 @@ public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedLineBreak_ThrowsInvalid mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); } + [Theory] + [InlineData("env-access-id\u2028forged log line")] // Unicode LINE SEPARATOR + [InlineData("env-access-id\u2029forged log line")] // Unicode PARAGRAPH SEPARATOR + [InlineData("env-access-idtampered")] // ANSI escape sequence (ESC is a control character) + public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedControlCharacter_ThrowsInvalidClientConfigurationException( + string maliciousValue) + { + // The \n/\r-only check misses non-ASCII line-terminating separators and other control characters + // (e.g. ANSI escape sequences) that could equally forge or tamper with rendered log output. + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", maliciousValue); + + var mock = new Mock(); + var pam = new AkeylessPam(_ => mock.Object); + + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id"))); + + Assert.IsType(ex.InnerException); + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public void GetPassword_AkeylessAccessKeyEnvVar_OverridesConfiguredAccessKey() { From 1eb02ca5ea7f5d9909f0e999ae3486774d130848 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:54:53 -0700 Subject: [PATCH 18/30] fix: reject Unicode format/bidi chars in overrides; stop logging raw ApiException - ResolveEnvOverride's character filter covered Control and Line/ParagraphSeparator categories but missed Format-category characters (Cf) such as U+202E RIGHT-TO-LEFT OVERRIDE and zero-width characters, which could make a logged AccessId/AuthType render differently than its actual content. Broadened the rejection to all "other" Unicode categories (Control, Format, Surrogate, PrivateUse, OtherNotAssigned) plus LineSeparator/ParagraphSeparator. - InitClient's ApiException catch no longer passes the exception object to Logger.LogError. The adjacent comment already said ex.Message is excluded because Akeyless's error response may echo back credentials, but passing `ex` as the exception parameter still exposes it, since most ILogger providers render an attached exception's Message/ToString() independent of the message template -- the comment's intent wasn't actually enforced by the code. --- CHANGELOG.md | 3 +- akeyless-pam/AkeylessPam.cs | 36 ++++++++++++------- docs/akeyless.md | 2 +- docsource/akeyless.md | 2 +- .../AkeylessPamTests.cs | 1 + 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba40f01..241c098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ - Environment variable override values are trimmed of leading/trailing whitespace before use, so an incidental trailing newline (a common artifact of file-mounted/`envFrom` secret provisioning) no longer breaks the override. - An `AKEYLESS_AUTH_TYPE` override is now validated against the supported auth types before use; an unrecognized value fails fast with `InvalidClientConfigurationException` instead of silently skipping authentication and proceeding with an unauthenticated request. - The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL rather than the pre-override configured value. -- An override value containing an embedded control character or Unicode line/paragraph separator (surviving the leading/trailing trim — e.g. an embedded newline, ANSI escape sequence, or U+2028/U+2029) is now rejected with `InvalidClientConfigurationException` instead of being trimmed only at the ends and passed through — none of the overridable parameters are legitimately multi-line, and letting one through risked forging extra lines in the log messages that echo the resolved value. +- An override value containing a non-printable or directionality-control character (surviving the leading/trailing trim — e.g. an embedded newline, ANSI escape sequence, U+2028/U+2029, or a Unicode bidirectional-override/zero-width character such as U+202E) is now rejected with `InvalidClientConfigurationException` instead of being trimmed only at the ends and passed through — none of the overridable parameters are legitimately multi-line or right-to-left, and letting one through risked forging extra lines or spoofed rendering in the log messages that echo the resolved value. - A failed Akeyless authentication attempt (`ApiException`) now logs the `AccessId` that was used, matching the existing success/empty-token log lines — previously this was the one authentication-failure path with no record of which identity (configured or env-overridden) the failed attempt used. +- The `ApiException` catch in `InitClient` no longer passes the exception object itself to the logger — only the HTTP status code and `AccessId` are logged. Passing the exception object defeated the log call's own stated intent to exclude `ex.Message` (which may echo back the raw Akeyless response body, including credentials), since most logging providers render an attached exception's `Message`/`ToString()` regardless of the message template. # v1.0.0 diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index c0a8dbe..853bb87 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -162,8 +162,8 @@ public string GetPassword(Dictionary instanceParameters, /// /// /// - /// Thrown if the environment variable's trimmed value still contains an embedded control character or - /// line/paragraph separator. + /// Thrown if the environment variable's trimmed value still contains a non-printable or + /// directionality-control character. /// private string ResolveEnvOverride(string envVarName) { @@ -171,18 +171,25 @@ private string ResolveEnvOverride(string envVarName) if (string.IsNullOrWhiteSpace(value)) return null; var trimmed = value.Trim(); - // Reject by Unicode category rather than a \n/\r blocklist — this also catches ANSI escape - // sequences (ESC is a control character), NUL/form-feed/vertical-tab, and the Unicode - // line/paragraph separators (U+2028/U+2029), any of which could otherwise forge an extra - // rendered line in the log messages that echo the resolved value. - if (trimmed.Any(c => char.IsControl(c) || - char.GetUnicodeCategory(c) is UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator)) + // Reject by Unicode category rather than a \n/\r blocklist. This covers ANSI escape sequences + // and NUL/form-feed/vertical-tab (Control), the Unicode line/paragraph separators U+2028/U+2029 + // (LineSeparator/ParagraphSeparator), and bidirectional-override/zero-width characters such as + // U+202E RIGHT-TO-LEFT OVERRIDE (Format) — any of which could otherwise forge an extra rendered + // log line or make the value displayed in a log/terminal differ from its actual content. + if (trimmed.Any(c => char.GetUnicodeCategory(c) is + UnicodeCategory.Control or + UnicodeCategory.Format or + UnicodeCategory.Surrogate or + UnicodeCategory.PrivateUse or + UnicodeCategory.OtherNotAssigned or + UnicodeCategory.LineSeparator or + UnicodeCategory.ParagraphSeparator)) { Logger.LogError( - "Environment variable {EnvVar} contains an embedded control character or line separator; refusing to use it as an override", + "Environment variable {EnvVar} contains a non-printable or directionality-control character; refusing to use it as an override", envVarName); throw new InvalidClientConfigurationException( - $"Environment variable '{envVarName}' contains an embedded control character or line separator and cannot be used as a connection parameter override."); + $"Environment variable '{envVarName}' contains a non-printable or directionality-control character and cannot be used as a connection parameter override."); } Logger.LogInformation("Environment variable override active for {EnvVar}", envVarName); @@ -246,9 +253,12 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) } catch (ApiException ex) { - // NOTE: ex.Message is intentionally excluded — ApiException error content may echo back - // portions of the auth request body, including credentials. - Logger.LogError(ex, + // NOTE: the exception object itself (not just ex.Message) is intentionally excluded from + // the log call — ApiException error content may echo back portions of the auth request + // body, including credentials, and most ILogger providers render an attached exception's + // Message/ToString() regardless of the message template, so passing `ex` here would defeat + // that exclusion. + Logger.LogError( "Akeyless API exception during authentication (HTTP {StatusCode}) for AccessId '{AccessId}'", ex.ErrorCode, accessId); throw new InvalidClientConfigurationException( diff --git a/docs/akeyless.md b/docs/akeyless.md index 0f7b897..2a2d6ad 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -144,7 +144,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded control character or line/paragraph separator after trimming (e.g. an embedded newline, ANSI escape sequence, or Unicode line separator) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains a non-printable or directionality-control character after trimming (e.g. an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 21bb213..dccb8f4 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -23,7 +23,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains an embedded control character or line/paragraph separator after trimming (e.g. an embedded newline, ANSI escape sequence, or Unicode line separator) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains a non-printable or directionality-control character after trimming (e.g. an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 780fb6a..d4c820f 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -402,6 +402,7 @@ public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedLineBreak_ThrowsInvalid [InlineData("env-access-id\u2028forged log line")] // Unicode LINE SEPARATOR [InlineData("env-access-id\u2029forged log line")] // Unicode PARAGRAPH SEPARATOR [InlineData("env-access-idtampered")] // ANSI escape sequence (ESC is a control character) + [InlineData("env-access-id\u202Ehidden-suffix")] // Unicode RIGHT-TO-LEFT OVERRIDE (Format category) public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedControlCharacter_ThrowsInvalidClientConfigurationException( string maliciousValue) { From 9ad415610d633fd3d5c8089cc9adcbdc2a5e836e Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:14:02 -0700 Subject: [PATCH 19/30] fix: switch to a printable-ASCII allowlist, applied to both override and configured values Rounds 3-5 kept finding one more Unicode category (ANSI escapes, U+2028/U+2029, bidi-override/zero-width Format characters, now variation selectors) that survived the growing character blocklist -- an inherently unbounded whack-a-mole problem. Replaced ResolveEnvOverride's blocklist with a single printable-ASCII allowlist (EnsurePrintableAscii), which rejects everything outside 0x20-0x7E in one check instead of naming specific bad characters. This also closes a second, more consequential gap: the blocklist only ever guarded the env-var override path. A malicious/malformed AccessId or Url supplied through Command's normal server configuration (no override involved) reached the exact same log statements completely unvalidated. The allowlist is now applied to the final resolved value in InitClient regardless of which source it came from, so both paths get the same guarantee. --- CHANGELOG.md | 2 +- akeyless-pam/AkeylessPam.cs | 68 ++++++++++--------- docs/akeyless.md | 2 +- docsource/akeyless.md | 2 +- .../AkeylessPamTests.cs | 26 ++++++- 5 files changed, 63 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 241c098..6ff032b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Environment variable override values are trimmed of leading/trailing whitespace before use, so an incidental trailing newline (a common artifact of file-mounted/`envFrom` secret provisioning) no longer breaks the override. - An `AKEYLESS_AUTH_TYPE` override is now validated against the supported auth types before use; an unrecognized value fails fast with `InvalidClientConfigurationException` instead of silently skipping authentication and proceeding with an unauthenticated request. - The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL rather than the pre-override configured value. -- An override value containing a non-printable or directionality-control character (surviving the leading/trailing trim — e.g. an embedded newline, ANSI escape sequence, U+2028/U+2029, or a Unicode bidirectional-override/zero-width character such as U+202E) is now rejected with `InvalidClientConfigurationException` instead of being trimmed only at the ends and passed through — none of the overridable parameters are legitimately multi-line or right-to-left, and letting one through risked forging extra lines or spoofed rendering in the log messages that echo the resolved value. +- `Url`, `AuthType`, `AccessId`, and `AccessKey` are now required to be printable ASCII, whether the effective value came from an env var override or from Command's/`manifest.json`'s configuration — a value containing anything else (an embedded newline, ANSI escape sequence, Unicode line/paragraph separator, bidirectional-override or zero-width character, etc.) is rejected with `InvalidClientConfigurationException` instead of being passed through. None of these parameters are legitimately anything but printable ASCII, and letting a non-ASCII value through risked forging extra lines or spoofed rendering in the log messages that echo the resolved value. - A failed Akeyless authentication attempt (`ApiException`) now logs the `AccessId` that was used, matching the existing success/empty-token log lines — previously this was the one authentication-failure path with no record of which identity (configured or env-overridden) the failed attempt used. - The `ApiException` catch in `InitClient` no longer passes the exception object itself to the logger — only the HTTP status code and `AccessId` are logged. Passing the exception object defeated the log call's own stated intent to exclude `ex.Message` (which may echo back the raw Akeyless response body, including credentials), since most logging providers render an attached exception's `Message`/`ToString()` regardless of the message template. diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 853bb87..bd51a11 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -152,48 +151,49 @@ public string GetPassword(Dictionary instanceParameters, /// `AuthType`). The returned value is trimmed because env vars sourced from mounted secret files/configmaps /// commonly carry an incidental trailing newline, which would otherwise fail exact-match comparisons /// (auth type) or authentication (AccessId/AccessKey) with no indication that whitespace was the cause. - /// An override that still contains an embedded line break after trimming is rejected outright — none of - /// the overridable parameters are legitimately multi-line, and letting one through would let it forge - /// extra lines in the structured log messages that echo the resolved value (e.g. AccessId, AuthType). /// /// Audit trail: when an override is active, this logs which environment variable is overriding — /// never the value — so an incident investigation can tell whether the effective connection - /// parameter used at runtime matches Command's recorded configuration. + /// parameter used at runtime matches Command's recorded configuration. The resolved value is not + /// content-validated here — validates it (and the equivalent + /// configured value, when no override is active) once the caller has settled on the final value to + /// use, so the same content guarantee applies regardless of which source it came from. /// /// - /// - /// Thrown if the environment variable's trimmed value still contains a non-printable or - /// directionality-control character. - /// private string ResolveEnvOverride(string envVarName) { var value = Environment.GetEnvironmentVariable(envVarName); if (string.IsNullOrWhiteSpace(value)) return null; - var trimmed = value.Trim(); - // Reject by Unicode category rather than a \n/\r blocklist. This covers ANSI escape sequences - // and NUL/form-feed/vertical-tab (Control), the Unicode line/paragraph separators U+2028/U+2029 - // (LineSeparator/ParagraphSeparator), and bidirectional-override/zero-width characters such as - // U+202E RIGHT-TO-LEFT OVERRIDE (Format) — any of which could otherwise forge an extra rendered - // log line or make the value displayed in a log/terminal differ from its actual content. - if (trimmed.Any(c => char.GetUnicodeCategory(c) is - UnicodeCategory.Control or - UnicodeCategory.Format or - UnicodeCategory.Surrogate or - UnicodeCategory.PrivateUse or - UnicodeCategory.OtherNotAssigned or - UnicodeCategory.LineSeparator or - UnicodeCategory.ParagraphSeparator)) - { - Logger.LogError( - "Environment variable {EnvVar} contains a non-printable or directionality-control character; refusing to use it as an override", - envVarName); - throw new InvalidClientConfigurationException( - $"Environment variable '{envVarName}' contains a non-printable or directionality-control character and cannot be used as a connection parameter override."); - } - Logger.LogInformation("Environment variable override active for {EnvVar}", envVarName); - return trimmed; + return value.Trim(); + } + + /// + /// Validates that a resolved connection parameter contains only printable ASCII characters. + /// + /// The resolved value to validate (from either an env var override or Command's configuration). + /// The name of the parameter, used in the error message. + /// + /// None of Url, AuthType, AccessId, or AccessKey are legitimately anything + /// other than printable ASCII. Rather than maintain a growing blocklist of specific problem characters + /// (control characters, ANSI escape sequences, Unicode line/paragraph separators, bidirectional-override + /// and zero-width/variation-selector characters have all been found, one at a time, to let a value forge + /// extra log lines or render differently than its actual content in the structured log messages that + /// echo these values), this allowlists the narrow range of characters that are actually legitimate and + /// rejects everything else in one check. Applied uniformly to the final resolved value regardless of + /// whether it came from an env var override or Command's/manifest.json's configured value, since both + /// reach the same log statements and the same Akeyless API calls. + /// + /// + /// Thrown if the value contains any character outside the printable ASCII range (0x20-0x7E). + /// + private static void EnsurePrintableAscii(string value, string parameterName) + { + if (value != null && value.All(c => c is >= (char)0x20 and <= (char)0x7E)) return; + + throw new InvalidClientConfigurationException( + $"{parameterName} contains a non-printable or non-ASCII character and cannot be used."); } private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) @@ -207,11 +207,15 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) Logger.MethodEntry(); var basePath = ResolveEnvOverride("AKEYLESS_API_URL") ?? configurationInfo.Url ?? "https://api.akeyless.io"; + EnsurePrintableAscii(basePath, "Url"); Logger.LogDebug("Connecting to Akeyless at '{Url}'", basePath); var authType = ResolveEnvOverride("AKEYLESS_AUTH_TYPE") ?? configurationInfo.AuthType; + EnsurePrintableAscii(authType, "AuthType"); accessId = ResolveEnvOverride("AKEYLESS_ACCESS_ID") ?? configurationInfo.AccessId; + EnsurePrintableAscii(accessId, "AccessId"); var accessKey = ResolveEnvOverride("AKEYLESS_ACCESS_KEY") ?? configurationInfo.AccessKey; + EnsurePrintableAscii(accessKey, "AccessKey"); // AuthType from configurationInfo already passed ValidateServerConfigurationParams, but an // AKEYLESS_AUTH_TYPE override bypasses that check entirely, so re-validate the resolved value diff --git a/docs/akeyless.md b/docs/akeyless.md index 2a2d6ad..89a0dab 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -144,7 +144,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains a non-printable or directionality-control character after trimming (e.g. an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). The effective value of `Url`, `AuthType`, `AccessId`, and `AccessKey` — whether it came from an override or from Command's/`manifest.json`'s configuration — must be printable ASCII; any other character (an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/docsource/akeyless.md b/docsource/akeyless.md index dccb8f4..8ddae11 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -23,7 +23,7 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning); a value that still contains a non-printable or directionality-control character after trimming (e.g. an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). The effective value of `Url`, `AuthType`, `AccessId`, and `AccessKey` — whether it came from an override or from Command's/`manifest.json`'s configuration — must be printable ASCII; any other character (an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. ## Supported Authentication Methods diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index d4c820f..3c188ea 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -403,11 +403,14 @@ public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedLineBreak_ThrowsInvalid [InlineData("env-access-id\u2029forged log line")] // Unicode PARAGRAPH SEPARATOR [InlineData("env-access-idtampered")] // ANSI escape sequence (ESC is a control character) [InlineData("env-access-id\u202Ehidden-suffix")] // Unicode RIGHT-TO-LEFT OVERRIDE (Format category) + [InlineData("env-access-id\uFE0Ftampered")] // Unicode Variation Selector (Nonspacing-Mark category) public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedControlCharacter_ThrowsInvalidClientConfigurationException( string maliciousValue) { - // The \n/\r-only check misses non-ASCII line-terminating separators and other control characters - // (e.g. ANSI escape sequences) that could equally forge or tamper with rendered log output. + // Non-ASCII line-terminating separators, ANSI escape sequences, bidi-override characters, and + // zero-width variation selectors can all equally forge or tamper with rendered log output. Rather + // than blocklisting each character class as it's discovered, the printable-ASCII allowlist rejects + // all of them (and anything else non-ASCII) in one check. using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", maliciousValue); var mock = new Mock(); @@ -420,6 +423,25 @@ public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedControlCharacter_Throws mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); } + [Fact] + public void GetPassword_ConfiguredAccessIdHasEmbeddedControlCharacter_ThrowsInvalidClientConfigurationException() + { + // The printable-ASCII allowlist is applied to the final resolved value regardless of source, so a + // malicious/malformed AccessId supplied via Command's server configuration (no env var override + // involved) is rejected too -- not just the override path. Explicitly unset the env var so an + // ambient AKEYLESS_ACCESS_ID on the host running the tests can't silently override it. + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", null); + + var mock = new Mock(); + var pam = new AkeylessPam(_ => mock.Object); + + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-id\r\nforged log line"))); + + Assert.IsType(ex.InnerException); + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public void GetPassword_AkeylessAccessKeyEnvVar_OverridesConfiguredAccessKey() { From 43ee79c8468f0784f8a318070f0117c1f1e7191e Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:25:23 -0700 Subject: [PATCH 20/30] fix: validate configured Url/AuthType/AccessId/AccessKey before BuildAkeylessConfiguration logs them EnsurePrintableAscii was only ever called from InitClient, which runs on a later call path (GetAkeylessSecretAsync -> InitClient) than BuildAkeylessConfiguration's own "Using Akeyless URL ..." and "Access key auth configured with AccessId ..." debug logs. A malicious/malformed value supplied via Command's server configuration (no env var override needed) was echoed by those two log statements before InitClient's validation ever ran -- the allowlist protected the override path and the InitClient-side re-resolution of the configured value, but not the earlier BuildAkeylessConfiguration log statements that fire first. Added the same EnsurePrintableAscii calls to BuildAkeylessConfiguration, right after each value is read and before it's logged. --- CHANGELOG.md | 2 +- akeyless-pam/AkeylessPam.cs | 16 +++++++++--- .../AkeylessPamTests.cs | 26 ++++++++++++++++--- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff032b..0218a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Environment variable override values are trimmed of leading/trailing whitespace before use, so an incidental trailing newline (a common artifact of file-mounted/`envFrom` secret provisioning) no longer breaks the override. - An `AKEYLESS_AUTH_TYPE` override is now validated against the supported auth types before use; an unrecognized value fails fast with `InvalidClientConfigurationException` instead of silently skipping authentication and proceeding with an unauthenticated request. - The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL rather than the pre-override configured value. -- `Url`, `AuthType`, `AccessId`, and `AccessKey` are now required to be printable ASCII, whether the effective value came from an env var override or from Command's/`manifest.json`'s configuration — a value containing anything else (an embedded newline, ANSI escape sequence, Unicode line/paragraph separator, bidirectional-override or zero-width character, etc.) is rejected with `InvalidClientConfigurationException` instead of being passed through. None of these parameters are legitimately anything but printable ASCII, and letting a non-ASCII value through risked forging extra lines or spoofed rendering in the log messages that echo the resolved value. +- `Url`, `AuthType`, `AccessId`, and `AccessKey` are now required to be printable ASCII, whether the effective value came from an env var override or from Command's/`manifest.json`'s configuration — a value containing anything else (an embedded newline, ANSI escape sequence, Unicode line/paragraph separator, bidirectional-override or zero-width character, etc.) is rejected with `InvalidClientConfigurationException` instead of being passed through. None of these parameters are legitimately anything but printable ASCII, and letting a non-ASCII value through risked forging extra lines or spoofed rendering in the log messages that echo the resolved value. This check runs both where the configured value is first read (before `BuildAkeylessConfiguration`'s own debug logging echoes it) and again on the final resolved value in `InitClient` (which may instead be an env var override), so a malicious/malformed value can't reach either log statement unvalidated. - A failed Akeyless authentication attempt (`ApiException`) now logs the `AccessId` that was used, matching the existing success/empty-token log lines — previously this was the one authentication-failure path with no record of which identity (configured or env-overridden) the failed attempt used. - The `ApiException` catch in `InitClient` no longer passes the exception object itself to the logger — only the HTTP status code and `AccessId` are logged. Passing the exception object defeated the log call's own stated intent to exclude `ex.Message` (which may echo back the raw Akeyless response body, including credentials), since most logging providers render an attached exception's `Message`/`ToString()` regardless of the message template. diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index bd51a11..a46593a 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -181,9 +181,11 @@ private string ResolveEnvOverride(string envVarName) /// and zero-width/variation-selector characters have all been found, one at a time, to let a value forge /// extra log lines or render differently than its actual content in the structured log messages that /// echo these values), this allowlists the narrow range of characters that are actually legitimate and - /// rejects everything else in one check. Applied uniformly to the final resolved value regardless of - /// whether it came from an env var override or Command's/manifest.json's configured value, since both - /// reach the same log statements and the same Akeyless API calls. + /// rejects everything else in one check. Called from two places so the guarantee holds no matter which + /// log statement runs first: validates the Command-/ + /// manifest.json-configured value before its own log statements echo it, and + /// separately validates the final resolved value (which may instead be an env var override) before its + /// own log statements echo that. /// /// /// Thrown if the value contains any character outside the printable ASCII range (0x20-0x7E). @@ -631,6 +633,12 @@ private AkeylessConfiguration BuildAkeylessConfiguration( AkeylessConstants.DefaultAkeylessApiUrl), AuthType = authType }; + // Validated here, before the first log statement that echoes these values, rather than relying + // on InitClient's later EnsurePrintableAscii calls — those run on a separate, later call path + // (GetAkeylessSecretAsync -> InitClient) and would leave a malicious/malformed Command-configured + // value (no env var override needed) logged raw by the two LogDebug calls below. + EnsurePrintableAscii(config.Url, "Url"); + EnsurePrintableAscii(config.AuthType, "AuthType"); Logger.LogDebug("Using Akeyless URL '{Url}', auth type '{AuthType}'", config.Url, config.AuthType); switch (authType) @@ -641,6 +649,8 @@ private AkeylessConfiguration BuildAkeylessConfiguration( case "access_key": config.AccessId = connectionConfiguration[AkeylessConfiguration.ACCESS_ID]; config.AccessKey = connectionConfiguration[AkeylessConfiguration.ACCESS_KEY]; + EnsurePrintableAscii(config.AccessId, "AccessId"); + EnsurePrintableAscii(config.AccessKey, "AccessKey"); // NOTE: AccessId logged (not secret), AccessKey intentionally omitted. Logger.LogDebug("Access key auth configured with AccessId '{AccessId}'", config.AccessId); break; diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 3c188ea..da4f1c3 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -426,8 +426,9 @@ public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedControlCharacter_Throws [Fact] public void GetPassword_ConfiguredAccessIdHasEmbeddedControlCharacter_ThrowsInvalidClientConfigurationException() { - // The printable-ASCII allowlist is applied to the final resolved value regardless of source, so a - // malicious/malformed AccessId supplied via Command's server configuration (no env var override + // The printable-ASCII allowlist is applied to the configured value in BuildAkeylessConfiguration + // (before its own log statements echo it), not just to the final resolved value in InitClient, so + // a malicious/malformed AccessId supplied via Command's server configuration (no env var override // involved) is rejected too -- not just the override path. Explicitly unset the env var so an // ambient AKEYLESS_ACCESS_ID on the host running the tests can't silently override it. using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", null); @@ -435,10 +436,27 @@ public void GetPassword_ConfiguredAccessIdHasEmbeddedControlCharacter_ThrowsInva var mock = new Mock(); var pam = new AkeylessPam(_ => mock.Object); - var ex = Assert.Throws(() => + // BuildAkeylessConfiguration runs synchronously (before the .Result call boundary), so this throws + // directly rather than wrapped in an AggregateException. + Assert.Throws(() => pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-id\r\nforged log line"))); - Assert.IsType(ex.InnerException); + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void GetPassword_ConfiguredUrlHasEmbeddedControlCharacter_ThrowsInvalidClientConfigurationException() + { + // Same as above but for Url: BuildAkeylessConfiguration's own "Using Akeyless URL ..." debug log + // echoes config.Url before InitClient is ever reached, so this must be validated at that point too. + using var _ = new EnvVarScope("AKEYLESS_API_URL", null); + + var mock = new Mock(); + var pam = new AkeylessPam(_ => mock.Object); + + Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer(url: "https://api.akeyless.io\r\nforged log line"))); + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); } From 8eabfec59879befbcc5407badec8b3a8ad285660 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:40:03 -0700 Subject: [PATCH 21/30] fix: log before EnsurePrintableAscii rejects a non-ASCII value EnsurePrintableAscii was the one validation-throw site in the file with no corresponding Logger.LogError call -- every other validation failure (ValidateRequiredParameter, ValidateAuthTypeAccessKey, the unsupported-auth-type checks, model validation, the ApiException catch) logs before throwing. Made the method an instance method so it can log the parameter name (never the value) before throwing, giving an audit trail for rejected log-injection/ spoofing attempts instead of silent fail-closed with no record. --- CHANGELOG.md | 1 + akeyless-pam/AkeylessPam.cs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0218a67..1864f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `Url`, `AuthType`, `AccessId`, and `AccessKey` are now required to be printable ASCII, whether the effective value came from an env var override or from Command's/`manifest.json`'s configuration — a value containing anything else (an embedded newline, ANSI escape sequence, Unicode line/paragraph separator, bidirectional-override or zero-width character, etc.) is rejected with `InvalidClientConfigurationException` instead of being passed through. None of these parameters are legitimately anything but printable ASCII, and letting a non-ASCII value through risked forging extra lines or spoofed rendering in the log messages that echo the resolved value. This check runs both where the configured value is first read (before `BuildAkeylessConfiguration`'s own debug logging echoes it) and again on the final resolved value in `InitClient` (which may instead be an env var override), so a malicious/malformed value can't reach either log statement unvalidated. - A failed Akeyless authentication attempt (`ApiException`) now logs the `AccessId` that was used, matching the existing success/empty-token log lines — previously this was the one authentication-failure path with no record of which identity (configured or env-overridden) the failed attempt used. - The `ApiException` catch in `InitClient` no longer passes the exception object itself to the logger — only the HTTP status code and `AccessId` are logged. Passing the exception object defeated the log call's own stated intent to exclude `ex.Message` (which may echo back the raw Akeyless response body, including credentials), since most logging providers render an attached exception's `Message`/`ToString()` regardless of the message template. +- Rejecting a non-printable/non-ASCII `Url`/`AuthType`/`AccessId`/`AccessKey` now logs an error (naming the parameter, never the value) before throwing, matching every other validation failure in the provider — previously this was the one validation-throw site with no corresponding log entry, leaving no audit trail that a log-injection/spoofing attempt was rejected. # v1.0.0 diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index a46593a..d2b96fc 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -190,10 +190,16 @@ private string ResolveEnvOverride(string envVarName) /// /// Thrown if the value contains any character outside the printable ASCII range (0x20-0x7E). /// - private static void EnsurePrintableAscii(string value, string parameterName) + private void EnsurePrintableAscii(string value, string parameterName) { if (value != null && value.All(c => c is >= (char)0x20 and <= (char)0x7E)) return; + // Logged before throwing, like every other validation failure in this class (ValidateRequiredParameter, + // ValidateAuthTypeAccessKey, the unsupported-auth-type checks, model validation) — without this, a + // rejected log-injection/spoofing attempt would leave no audit trail at all, defeating the point of + // having caught it. The value itself is never logged, only the parameter name. + Logger.LogError( + "{ParameterName} contains a non-printable or non-ASCII character; refusing to use it", parameterName); throw new InvalidClientConfigurationException( $"{parameterName} contains a non-printable or non-ASCII character and cannot be used."); } From 20e63b4e8707ec58fa60726ea5d35f89f53a7dcc Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:22:48 -0700 Subject: [PATCH 22/30] refactor: extract ResolveAndValidate helper in InitClient Collapses the repeated resolve-override-then-EnsurePrintableAscii pattern for Url/AuthType/AccessId/AccessKey into a single helper, so the sequencing only needs to be verified in one place instead of four. --- akeyless-pam/AkeylessPam.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index d2b96fc..e95fe47 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -204,6 +204,17 @@ private void EnsurePrintableAscii(string value, string parameterName) $"{parameterName} contains a non-printable or non-ASCII character and cannot be used."); } + /// + /// Resolves a connection parameter from its env var override (if active) or its configured value, then + /// validates the result via . + /// + private string ResolveAndValidate(string envVarName, string configuredValue, string parameterName) + { + var value = ResolveEnvOverride(envVarName) ?? configuredValue; + EnsurePrintableAscii(value, parameterName); + return value; + } + private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) { // Hoisted above the try block (rather than declared with the other resolved values inside it) so the @@ -213,17 +224,13 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) try { Logger.MethodEntry(); - var basePath = ResolveEnvOverride("AKEYLESS_API_URL") ?? - configurationInfo.Url ?? "https://api.akeyless.io"; - EnsurePrintableAscii(basePath, "Url"); + var basePath = ResolveAndValidate("AKEYLESS_API_URL", configurationInfo.Url ?? "https://api.akeyless.io", + "Url"); Logger.LogDebug("Connecting to Akeyless at '{Url}'", basePath); - var authType = ResolveEnvOverride("AKEYLESS_AUTH_TYPE") ?? configurationInfo.AuthType; - EnsurePrintableAscii(authType, "AuthType"); - accessId = ResolveEnvOverride("AKEYLESS_ACCESS_ID") ?? configurationInfo.AccessId; - EnsurePrintableAscii(accessId, "AccessId"); - var accessKey = ResolveEnvOverride("AKEYLESS_ACCESS_KEY") ?? configurationInfo.AccessKey; - EnsurePrintableAscii(accessKey, "AccessKey"); + var authType = ResolveAndValidate("AKEYLESS_AUTH_TYPE", configurationInfo.AuthType, "AuthType"); + accessId = ResolveAndValidate("AKEYLESS_ACCESS_ID", configurationInfo.AccessId, "AccessId"); + var accessKey = ResolveAndValidate("AKEYLESS_ACCESS_KEY", configurationInfo.AccessKey, "AccessKey"); // AuthType from configurationInfo already passed ValidateServerConfigurationParams, but an // AKEYLESS_AUTH_TYPE override bypasses that check entirely, so re-validate the resolved value From 11dd8439c051121d9e1e4dd2345f5c3b310eae18 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:28:33 -0700 Subject: [PATCH 23/30] refactor: drop vacuous single-case switch in InitClient The SupportedAuthMethods guard clause immediately above already proves authType is "access_key" by this point, making the switch a no-op wrapper. --- akeyless-pam/AkeylessPam.cs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index e95fe47..29cfc4b 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -246,28 +246,25 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) var client = _clientFactory(basePath); - switch (authType) + // authType is provably "access_key" here — the SupportedAuthMethods guard clause above + // already rejected anything else, and that's currently the only supported value. + Logger.LogDebug("Authenticating with Akeyless using access_key auth, AccessId: '{AccessId}'", + accessId); + var token = client.Authenticate(accessId, accessKey); + + if (string.IsNullOrEmpty(token)) { - case "access_key": - Logger.LogDebug("Authenticating with Akeyless using access_key auth, AccessId: '{AccessId}'", - accessId); - var token = client.Authenticate(accessId, accessKey); - - if (string.IsNullOrEmpty(token)) - { - Logger.LogError( - "Authentication failed: unable to obtain access token from Akeyless for AccessId '{AccessId}'", - accessId); - throw new InvalidTokenException("Unable to obtain access token from Akeyless server"); - } - - AuthToken = token; - Logger.LogInformation( - "Successfully authenticated with Akeyless using AccessId '{AccessId}'", - accessId); - break; + Logger.LogError( + "Authentication failed: unable to obtain access token from Akeyless for AccessId '{AccessId}'", + accessId); + throw new InvalidTokenException("Unable to obtain access token from Akeyless server"); } + AuthToken = token; + Logger.LogInformation( + "Successfully authenticated with Akeyless using AccessId '{AccessId}'", + accessId); + return client; } catch (ApiException ex) From e755d96679e1e8b251076f4e4c2e5657ffad140e Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:40:35 -0700 Subject: [PATCH 24/30] fix: trim configured Url/AuthType/AccessId/AccessKey before validating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BuildAkeylessConfiguration validated configured connection values with EnsurePrintableAscii but never trimmed them first, unlike env-var overrides (which ResolveEnvOverride already trims). A previously-working configured value with an incidental trailing newline/tab — a common artifact of hand-edited manifest.json or portal paste — silently worked before this PR's hardening (Uri/HttpClient normalize it) but now hard-fails on every GetPassword call. Trim configured values the same way overrides are, so the hardening doesn't regress previously-valid configuration. Also dedupes the EnvVarScope test helper (previously copy-pasted across both test projects) into a single linked file, and reuses AkeylessConstants.DefaultAkeylessApiUrl instead of a duplicated literal default in InitClient. --- akeyless-pam/AkeylessPam.cs | 15 ++- .../AkeylessPam.Integration.Tests.csproj | 4 + .../AkeylessPamIntegrationTests.cs | 22 ----- .../AkeylessPamTests.cs | 97 ++++++++++++++++--- tests/AkeylessPam.Unit.Tests/EnvVarScope.cs | 22 +++++ 5 files changed, 119 insertions(+), 41 deletions(-) create mode 100644 tests/AkeylessPam.Unit.Tests/EnvVarScope.cs diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 29cfc4b..b465118 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -224,8 +224,8 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) try { Logger.MethodEntry(); - var basePath = ResolveAndValidate("AKEYLESS_API_URL", configurationInfo.Url ?? "https://api.akeyless.io", - "Url"); + var basePath = ResolveAndValidate("AKEYLESS_API_URL", + configurationInfo.Url ?? AkeylessConstants.DefaultAkeylessApiUrl, "Url"); Logger.LogDebug("Connecting to Akeyless at '{Url}'", basePath); var authType = ResolveAndValidate("AKEYLESS_AUTH_TYPE", configurationInfo.AuthType, "AuthType"); @@ -637,10 +637,15 @@ private AkeylessConfiguration BuildAkeylessConfiguration( authType = "access_key"; } + authType = authType.Trim(); + var config = new AkeylessConfiguration { + // Trimmed like ResolveEnvOverride's env-var values are — Command-portal/manifest.json + // values commonly carry an incidental trailing newline too, and this PR's own hardening + // must not turn that previously-benign artifact into a hard configuration failure. Url = connectionConfiguration.GetValueOrDefault(AkeylessConfiguration.AKEYLESS_API_URL, - AkeylessConstants.DefaultAkeylessApiUrl), + AkeylessConstants.DefaultAkeylessApiUrl).Trim(), AuthType = authType }; // Validated here, before the first log statement that echoes these values, rather than relying @@ -657,8 +662,8 @@ private AkeylessConfiguration BuildAkeylessConfiguration( Logger.LogDebug("Implicit auth type configured; credentials expected via environment variables"); break; case "access_key": - config.AccessId = connectionConfiguration[AkeylessConfiguration.ACCESS_ID]; - config.AccessKey = connectionConfiguration[AkeylessConfiguration.ACCESS_KEY]; + config.AccessId = connectionConfiguration[AkeylessConfiguration.ACCESS_ID].Trim(); + config.AccessKey = connectionConfiguration[AkeylessConfiguration.ACCESS_KEY].Trim(); EnsurePrintableAscii(config.AccessId, "AccessId"); EnsurePrintableAscii(config.AccessKey, "AccessKey"); // NOTE: AccessId logged (not secret), AccessKey intentionally omitted. diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj b/tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj index bc95379..cbe1d82 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj +++ b/tests/AkeylessPam.Integration.Tests/AkeylessPam.Integration.Tests.csproj @@ -25,4 +25,8 @@ + + + + diff --git a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs index fd95965..6cee4b1 100644 --- a/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs +++ b/tests/AkeylessPam.Integration.Tests/AkeylessPamIntegrationTests.cs @@ -260,25 +260,3 @@ public void GetPassword_StaticJson_WhitespaceFieldName_ReturnsRawJsonBlob_K8sOrc "Expected raw JSON blob even when StaticSecretFieldName is whitespace-only"); } } - -/// -/// Sets an environment variable for the duration of a test and restores the prior value (or clears it, -/// if it was previously unset) on dispose. -/// -internal sealed class EnvVarScope : IDisposable -{ - private readonly string _name; - private readonly string? _previousValue; - - public EnvVarScope(string name, string? value) - { - _name = name; - _previousValue = Environment.GetEnvironmentVariable(name); - Environment.SetEnvironmentVariable(name, value); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable(_name, _previousValue); - } -} diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index da4f1c3..597b2e9 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -166,26 +166,95 @@ public void GetPassword_UsesConfiguredUrl_WhenNoEnvVar() } } -/// -/// Sets an environment variable for the duration of the test and restores the prior value (or clears it, -/// if it was previously unset) on dispose. Ensures env var overrides used to test one PAM instance -/// don't leak into other tests or other test runs. -/// -internal sealed class EnvVarScope : IDisposable +public class ConfiguredValueSanitizationTests { - private readonly string _name; - private readonly string? _previousValue; + // Mirrors the env-var-override trimming tests: Command-portal/manifest.json-configured values + // commonly carry an incidental trailing newline too (e.g. from hand-editing JSON or a paste with a + // stray line terminator), and BuildAkeylessConfiguration must trim it the same way + // ResolveEnvOverride trims env-var-sourced values, rather than hard-failing on a previously-benign + // artifact. + + [Fact] + public void GetPassword_ConfiguredUrlHasTrailingNewline_IsTrimmedBeforeUse() + { + // Cleared, not just left alone, so this test is hermetic against a real AKEYLESS_API_URL + // already present in the ambient environment (e.g. a developer's shell). + using var _ = new EnvVarScope("AKEYLESS_API_URL", null); + + 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://api.akeyless.io\n")); + + Assert.Equal("https://api.akeyless.io", capturedBasePath); + } + + [Fact] + public void GetPassword_ConfiguredAuthTypeHasTrailingNewline_IsTrimmedBeforeUse() + { + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", 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(_ => mock.Object); - public EnvVarScope(string name, string? value) + pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key\n")); + + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void GetPassword_ConfiguredAccessIdHasTrailingNewline_IsTrimmedBeforeUse() { - _name = name; - _previousValue = Environment.GetEnvironmentVariable(name); - Environment.SetEnvironmentVariable(name, value); + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", null); + + string? capturedAccessId = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((accessId, _) => capturedAccessId = accessId) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id\n")); + + Assert.Equal("configured-access-id", capturedAccessId); } - public void Dispose() + [Fact] + public void GetPassword_ConfiguredAccessKeyHasTrailingNewline_IsTrimmedBeforeUse() { - Environment.SetEnvironmentVariable(_name, _previousValue); + using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", null); + + string? capturedAccessKey = null; + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Callback((_, accessKey) => capturedAccessKey = accessKey) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); + + var pam = new AkeylessPam(_ => mock.Object); + + pam.GetPassword(Params.Instance(), Params.ValidServer(accessKey: "configured-access-key\n")); + + Assert.Equal("configured-access-key", capturedAccessKey); } } diff --git a/tests/AkeylessPam.Unit.Tests/EnvVarScope.cs b/tests/AkeylessPam.Unit.Tests/EnvVarScope.cs new file mode 100644 index 0000000..1ade9e4 --- /dev/null +++ b/tests/AkeylessPam.Unit.Tests/EnvVarScope.cs @@ -0,0 +1,22 @@ +/// +/// Sets an environment variable for the duration of a test and restores the prior value (or clears it, +/// if it was previously unset) on dispose. Ensures env var overrides used to test one PAM instance +/// don't leak into other tests or other test runs. +/// +internal sealed class EnvVarScope : IDisposable +{ + private readonly string _name; + private readonly string? _previousValue; + + public EnvVarScope(string name, string? value) + { + _name = name; + _previousValue = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(_name, _previousValue); + } +} From 9925975fc65d28f363e8d4e1d922cace06540ac0 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:52:50 -0700 Subject: [PATCH 25/30] fix: null-safe AuthType/Url trim; warn on ambient credential env-var override BuildAkeylessConfiguration's new .Trim() calls threw an unhandled NullReferenceException when the connectionConfiguration dictionary had an explicit null value (key present, value null) for AuthType or Url, bypassing the previously-handled InvalidClientConfigurationException path and its logging. Both now default/fall back the same way an absent key already did, before trimming. Also bumps ResolveEnvOverride's "Environment variable override active" log from Information to Warning: a host that already has AKEYLESS_AUTH_TYPE/AKEYLESS_ACCESS_ID/AKEYLESS_ACCESS_KEY set for an unrelated reason (e.g. a co-located Akeyless CLI using the same variable names) will now silently authenticate with a different identity than Command's recorded configuration after upgrading, with zero config change on Command's side. This is the PR's intended behavior extended from the pre-existing AKEYLESS_API_URL override, not a bug, but the upgrade-collision risk deserves louder visibility than Information level, plus a documented callout in docsource/akeyless.md. --- akeyless-pam/AkeylessPam.cs | 24 ++++++++-- docsource/akeyless.md | 4 +- .../AkeylessPamTests.cs | 45 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index b465118..610b0f5 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -159,13 +159,21 @@ public string GetPassword(Dictionary instanceParameters, /// configured value, when no override is active) once the caller has settled on the final value to /// use, so the same content guarantee applies regardless of which source it came from. /// + /// + /// Logged at Warning, not Information: a host that already has one of these AKEYLESS_-prefixed + /// variables set for an unrelated reason (e.g. a co-located Akeyless CLI install using the same + /// conventional variable names) will silently authenticate with a different identity than Command's + /// recorded configuration after upgrading to a version that reads it — with zero configuration + /// change on Command's side. That's a connection-identity change, not routine operation, so it + /// should surface to an operator without them having to go looking for it at Information level. + /// /// private string ResolveEnvOverride(string envVarName) { var value = Environment.GetEnvironmentVariable(envVarName); if (string.IsNullOrWhiteSpace(value)) return null; - Logger.LogInformation("Environment variable override active for {EnvVar}", envVarName); + Logger.LogWarning("Environment variable override active for {EnvVar}", envVarName); return value.Trim(); } @@ -629,7 +637,11 @@ private AkeylessConfiguration BuildAkeylessConfiguration( "Akeyless configuration is invalid, please review server logs."); } - if (!connectionConfiguration.TryGetValue(AkeylessConfiguration.AUTH_TYPE, out var authType)) + // string.IsNullOrEmpty (not just TryGetValue's presence check) so a dictionary entry explicitly + // bound to null - as well as an absent key - defaults to 'access_key' instead of crashing the + // .Trim() call below with a NullReferenceException. + if (!connectionConfiguration.TryGetValue(AkeylessConfiguration.AUTH_TYPE, out var authType) || + string.IsNullOrEmpty(authType)) { Logger.LogWarning( "'{AuthType}' parameter not provided; defaulting to 'access_key'", @@ -639,13 +651,17 @@ private AkeylessConfiguration BuildAkeylessConfiguration( authType = authType.Trim(); + // Same null-safety as above: GetValueOrDefault only substitutes the fallback for an absent key, + // not one explicitly bound to null, so the null case is handled before .Trim() rather than by it. + var configuredUrl = connectionConfiguration.GetValueOrDefault(AkeylessConfiguration.AKEYLESS_API_URL); + var config = new AkeylessConfiguration { // Trimmed like ResolveEnvOverride's env-var values are — Command-portal/manifest.json // values commonly carry an incidental trailing newline too, and this PR's own hardening // must not turn that previously-benign artifact into a hard configuration failure. - Url = connectionConfiguration.GetValueOrDefault(AkeylessConfiguration.AKEYLESS_API_URL, - AkeylessConstants.DefaultAkeylessApiUrl).Trim(), + Url = (string.IsNullOrEmpty(configuredUrl) ? AkeylessConstants.DefaultAkeylessApiUrl : configuredUrl) + .Trim(), AuthType = authType }; // Validated here, before the first log statement that echoes these values, rather than relying diff --git a/docsource/akeyless.md b/docsource/akeyless.md index 8ddae11..aeb8154 100644 --- a/docsource/akeyless.md +++ b/docsource/akeyless.md @@ -23,7 +23,9 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). The effective value of `Url`, `AuthType`, `AccessId`, and `AccessKey` — whether it came from an override or from Command's/`manifest.json`'s configuration — must be printable ASCII; any other character (an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). The effective value of `Url`, `AuthType`, `AccessId`, and `AccessKey` — whether it came from an override or from Command's/`manifest.json`'s configuration — must be printable ASCII; any other character (an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs a **Warning** stating which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. + +**Upgrading an existing installation:** if the provider's host process already has `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, or `AKEYLESS_ACCESS_KEY` set for an unrelated reason (for example, a co-located Akeyless CLI or another Akeyless SDK conventionally uses these same variable names), upgrading to a version of this provider that reads them will silently start authenticating with that ambient identity instead of the one recorded in `manifest.json`/Command — with no configuration change on Command's side. Check the host environment for these variable names before upgrading, and watch for the Warning-level "Environment variable override active" log line afterward. ## Supported Authentication Methods diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 597b2e9..e9f9aa9 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -256,6 +256,51 @@ public void GetPassword_ConfiguredAccessKeyHasTrailingNewline_IsTrimmedBeforeUse Assert.Equal("configured-access-key", capturedAccessKey); } + + [Fact] + public void GetPassword_ConfiguredAuthTypeKeyPresentButNull_DefaultsToAccessKeyInsteadOfThrowingNullReferenceException() + { + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", 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(_ => mock.Object); + var server = Params.ValidServer(); + server["AuthType"] = null!; // key present, value null — legal for Dictionary + + pam.GetPassword(Params.Instance(), server); + + mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void GetPassword_ConfiguredUrlKeyPresentButNull_FallsBackToDefaultInsteadOfThrowingNullReferenceException() + { + using var _ = new EnvVarScope("AKEYLESS_API_URL", null); + + 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; + }); + var server = Params.ValidServer(); + server["Url"] = null!; // key present, value null — legal for Dictionary + + pam.GetPassword(Params.Instance(), server); + + Assert.Equal("https://api.akeyless.io", capturedBasePath); + } } public class EnvironmentVariableOverrideTests From 4c3e2b86efe28ae60fcb31fbc2c05699580519c7 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:04:23 -0700 Subject: [PATCH 26/30] fix: whitespace-only configured Url falls back to default, not empty basePath BuildAkeylessConfiguration checked string.IsNullOrEmpty before substituting the default Url, so a whitespace-only configured value (e.g. a single stray space) skipped the fallback, then got trimmed into an empty string that passed EnsurePrintableAscii vacuously (LINQ .All() over an empty sequence). The empty base path then reached the Akeyless SDK client with no further validation. Switched to IsNullOrWhiteSpace, matching the existing env-var-override path which already handled this correctly. Also collapses four sets of near-duplicate [Fact] tests (Url/AccessId/ AccessKey/AuthType env-var unset/empty/whitespace-only, differing only in the literal) into four [Theory]/[InlineData] tests, consistent with the file's existing Theory usage. --- akeyless-pam/AkeylessPam.cs | 2 +- .../AkeylessPamTests.cs | 218 ++++-------------- 2 files changed, 41 insertions(+), 179 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index 610b0f5..bb041ec 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -660,7 +660,7 @@ private AkeylessConfiguration BuildAkeylessConfiguration( // Trimmed like ResolveEnvOverride's env-var values are — Command-portal/manifest.json // values commonly carry an incidental trailing newline too, and this PR's own hardening // must not turn that previously-benign artifact into a hard configuration failure. - Url = (string.IsNullOrEmpty(configuredUrl) ? AkeylessConstants.DefaultAkeylessApiUrl : configuredUrl) + Url = (string.IsNullOrWhiteSpace(configuredUrl) ? AkeylessConstants.DefaultAkeylessApiUrl : configuredUrl) .Trim(), AuthType = authType }; diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index e9f9aa9..c335e64 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -301,40 +301,16 @@ public void GetPassword_ConfiguredUrlKeyPresentButNull_FallsBackToDefaultInstead Assert.Equal("https://api.akeyless.io", capturedBasePath); } -} - -public class EnvironmentVariableOverrideTests -{ - [Fact] - public void GetPassword_AkeylessApiUrlEnvVar_OverridesConfiguredUrl() - { - using var _ = new EnvVarScope("AKEYLESS_API_URL", "https://env-override.akeyless.io"); - - 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://configured.akeyless.io")); - - Assert.Equal("https://env-override.akeyless.io", capturedBasePath); - } [Fact] - public void GetPassword_AkeylessApiUrlEnvVarUnset_FallsBackToConfiguredUrl() + public void GetPassword_ConfiguredUrlIsWhitespaceOnly_FallsBackToDefaultInsteadOfEmptyBasePath() { using var _ = new EnvVarScope("AKEYLESS_API_URL", null); string? capturedBasePath = null; var mock = new Mock(); - mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())).Returns("fake-token"); + 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" }); @@ -344,15 +320,18 @@ public void GetPassword_AkeylessApiUrlEnvVarUnset_FallsBackToConfiguredUrl() return mock.Object; }); - pam.GetPassword(Params.Instance(), Params.ValidServer(url: "https://configured.akeyless.io")); + pam.GetPassword(Params.Instance(), Params.ValidServer(url: " ")); - Assert.Equal("https://configured.akeyless.io", capturedBasePath); + Assert.Equal("https://api.akeyless.io", capturedBasePath); } +} +public class EnvironmentVariableOverrideTests +{ [Fact] - public void GetPassword_AkeylessApiUrlEnvVarEmptyString_FallsBackToConfiguredUrl() + public void GetPassword_AkeylessApiUrlEnvVar_OverridesConfiguredUrl() { - using var _ = new EnvVarScope("AKEYLESS_API_URL", ""); + using var _ = new EnvVarScope("AKEYLESS_API_URL", "https://env-override.akeyless.io"); string? capturedBasePath = null; var mock = new Mock(); @@ -368,13 +347,16 @@ public void GetPassword_AkeylessApiUrlEnvVarEmptyString_FallsBackToConfiguredUrl pam.GetPassword(Params.Instance(), Params.ValidServer(url: "https://configured.akeyless.io")); - Assert.Equal("https://configured.akeyless.io", capturedBasePath); + Assert.Equal("https://env-override.akeyless.io", capturedBasePath); } - [Fact] - public void GetPassword_AkeylessApiUrlEnvVarWhitespaceOnly_FallsBackToConfiguredUrl() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void GetPassword_AkeylessApiUrlEnvVarUnsetOrBlank_FallsBackToConfiguredUrl(string? envValue) { - using var _ = new EnvVarScope("AKEYLESS_API_URL", " "); + using var _ = new EnvVarScope("AKEYLESS_API_URL", envValue); string? capturedBasePath = null; var mock = new Mock(); @@ -413,50 +395,13 @@ public void GetPassword_AkeylessAccessIdEnvVar_OverridesConfiguredAccessId() Assert.Equal("env-access-id", capturedAccessId); } - [Fact] - public void GetPassword_AkeylessAccessIdEnvVarUnset_FallsBackToConfiguredAccessId() - { - using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", null); - - string? capturedAccessId = null; - var mock = new Mock(); - mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) - .Callback((accessId, _) => capturedAccessId = accessId) - .Returns("fake-token"); - mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) - .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); - - var pam = new AkeylessPam(_ => mock.Object); - - pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id")); - - Assert.Equal("configured-access-id", capturedAccessId); - } - - [Fact] - public void GetPassword_AkeylessAccessIdEnvVarEmptyString_FallsBackToConfiguredAccessId() - { - using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", ""); - - string? capturedAccessId = null; - var mock = new Mock(); - mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) - .Callback((accessId, _) => capturedAccessId = accessId) - .Returns("fake-token"); - mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) - .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); - - var pam = new AkeylessPam(_ => mock.Object); - - pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id")); - - Assert.Equal("configured-access-id", capturedAccessId); - } - - [Fact] - public void GetPassword_AkeylessAccessIdEnvVarWhitespaceOnly_FallsBackToConfiguredAccessId() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void GetPassword_AkeylessAccessIdEnvVarUnsetOrBlank_FallsBackToConfiguredAccessId(string? envValue) { - using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", " "); + using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", envValue); string? capturedAccessId = null; var mock = new Mock(); @@ -594,50 +539,13 @@ public void GetPassword_AkeylessAccessKeyEnvVar_OverridesConfiguredAccessKey() Assert.Equal("env-access-key", capturedAccessKey); } - [Fact] - public void GetPassword_AkeylessAccessKeyEnvVarUnset_FallsBackToConfiguredAccessKey() - { - using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", null); - - string? capturedAccessKey = null; - var mock = new Mock(); - mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) - .Callback((_, accessKey) => capturedAccessKey = accessKey) - .Returns("fake-token"); - mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) - .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); - - var pam = new AkeylessPam(_ => mock.Object); - - pam.GetPassword(Params.Instance(), Params.ValidServer(accessKey: "configured-access-key")); - - Assert.Equal("configured-access-key", capturedAccessKey); - } - - [Fact] - public void GetPassword_AkeylessAccessKeyEnvVarEmptyString_FallsBackToConfiguredAccessKey() - { - using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", ""); - - string? capturedAccessKey = null; - var mock = new Mock(); - mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) - .Callback((_, accessKey) => capturedAccessKey = accessKey) - .Returns("fake-token"); - mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) - .ReturnsAsync(new Dictionary { ["pam/test/secret"] = "value" }); - - var pam = new AkeylessPam(_ => mock.Object); - - pam.GetPassword(Params.Instance(), Params.ValidServer(accessKey: "configured-access-key")); - - Assert.Equal("configured-access-key", capturedAccessKey); - } - - [Fact] - public void GetPassword_AkeylessAccessKeyEnvVarWhitespaceOnly_FallsBackToConfiguredAccessKey() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void GetPassword_AkeylessAccessKeyEnvVarUnsetOrBlank_FallsBackToConfiguredAccessKey(string? envValue) { - using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", " "); + using var _ = new EnvVarScope("AKEYLESS_ACCESS_KEY", envValue); string? capturedAccessKey = null; var mock = new Mock(); @@ -674,63 +582,17 @@ public void GetPassword_AkeylessAuthTypeEnvVarUnrecognised_ThrowsInvalidClientCo mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); } - [Fact] - public void GetPassword_AkeylessAuthTypeEnvVarUnset_FallsBackToConfiguredAuthType() - { - using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", 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(_ => mock.Object); - - pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); - - mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public void GetPassword_AkeylessAuthTypeEnvVarEmptyString_FallsBackToConfiguredAuthType() - { - using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", ""); - - 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(_ => mock.Object); - - pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); - - mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public void GetPassword_AkeylessAuthTypeEnvVarWhitespaceOnly_FallsBackToConfiguredAuthType() - { - using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", " "); - - 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(_ => mock.Object); - - pam.GetPassword(Params.Instance(), Params.ValidServer(authType: "access_key")); - - mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public void GetPassword_AkeylessAuthTypeEnvVarHasTrailingNewline_IsTrimmedBeforeUse() - { - // A trailing newline (e.g. from file-mounted/envFrom secret provisioning) must not cause the - // resolved auth type to fail the "access_key" match and fall into the unsupported-auth-type path. - using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", "access_key\n"); + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + // A trailing newline (e.g. from file-mounted/envFrom secret provisioning) must not cause the + // resolved auth type to fail the "access_key" match and fall into the unsupported-auth-type path. + [InlineData("access_key\n")] + public void GetPassword_AkeylessAuthTypeEnvVarUnsetOrBlankOrHasTrailingNewline_FallsBackToOrTrims( + string? envValue) + { + using var _ = new EnvVarScope("AKEYLESS_AUTH_TYPE", envValue); var mock = new Mock(); mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())).Returns("fake-token"); From 568aa1f53cdccfd685aaddad1d1f7e43eac4ec0d Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:10:09 -0700 Subject: [PATCH 27/30] refactor: drop dead default-URL fallback in InitClient BuildAkeylessConfiguration already guarantees configurationInfo.Url is never null/whitespace, so InitClient's second "?? DefaultAkeylessApiUrl" fallback was unreachable. --- akeyless-pam/AkeylessPam.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index bb041ec..dc4d97d 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -232,8 +232,9 @@ private IAkeylessApiClient InitClient(AkeylessConfiguration configurationInfo) try { Logger.MethodEntry(); - var basePath = ResolveAndValidate("AKEYLESS_API_URL", - configurationInfo.Url ?? AkeylessConstants.DefaultAkeylessApiUrl, "Url"); + // configurationInfo.Url is always populated by BuildAkeylessConfiguration (which owns the + // default-URL fallback), so no second fallback is needed here. + var basePath = ResolveAndValidate("AKEYLESS_API_URL", configurationInfo.Url, "Url"); Logger.LogDebug("Connecting to Akeyless at '{Url}'", basePath); var authType = ResolveAndValidate("AKEYLESS_AUTH_TYPE", configurationInfo.AuthType, "AuthType"); From 97e8f742d54edabc6daf60121fa3e8b183f70c50 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:17:42 -0700 Subject: [PATCH 28/30] refactor: fold embedded-line-break AccessId test into the control-char Theory Was a standalone Fact with a byte-for-byte identical body to the adjacent control-character Theory, differing only in the injected string. --- .../AkeylessPamTests.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index c335e64..58da6be 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -440,30 +440,14 @@ public void GetPassword_AkeylessAccessIdEnvVarHasTrailingNewline_IsTrimmedBefore Assert.Equal("env-access-id", capturedAccessId); } - [Fact] - public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedLineBreak_ThrowsInvalidClientConfigurationException() - { - // An embedded (non-trailing) line break survives Trim() and would otherwise flow unchanged into the - // structured log messages that echo AccessId, forging extra log lines. It must be rejected outright. - using var _ = new EnvVarScope("AKEYLESS_ACCESS_ID", "env-access-id\nforged log line"); - - var mock = new Mock(); - var pam = new AkeylessPam(_ => mock.Object); - - var ex = Assert.Throws(() => - pam.GetPassword(Params.Instance(), Params.ValidServer(accessId: "configured-access-id"))); - - Assert.IsType(ex.InnerException); - mock.Verify(c => c.Authenticate(It.IsAny(), It.IsAny()), Times.Never); - } - [Theory] + [InlineData("env-access-id\nforged log line")] // embedded LF, survives Trim() [InlineData("env-access-id\u2028forged log line")] // Unicode LINE SEPARATOR [InlineData("env-access-id\u2029forged log line")] // Unicode PARAGRAPH SEPARATOR [InlineData("env-access-idtampered")] // ANSI escape sequence (ESC is a control character) [InlineData("env-access-id\u202Ehidden-suffix")] // Unicode RIGHT-TO-LEFT OVERRIDE (Format category) [InlineData("env-access-id\uFE0Ftampered")] // Unicode Variation Selector (Nonspacing-Mark category) - public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedControlCharacter_ThrowsInvalidClientConfigurationException( + public void GetPassword_AkeylessAccessIdEnvVarHasEmbeddedLineBreakOrControlCharacter_ThrowsInvalidClientConfigurationException( string maliciousValue) { // Non-ASCII line-terminating separators, ANSI escape sequences, bidi-override characters, and From a0f09804540427f0ff9b0f5cc20f39dd67eea9d7 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:31:47 -0700 Subject: [PATCH 29/30] docs: condense CHANGELOG entries for env var override feature Collapses the detailed per-fix bullet points into higher-level summary entries for the release notes. --- CHANGELOG.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1864f03..c1802ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,9 @@ ## Features -- **Environment variable overrides for connection parameters** — `AuthType`, `AccessId`, and `AccessKey` can now be overridden at runtime via the `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables, respectively, matching the existing `AKEYLESS_API_URL` override for the Akeyless API URL. This lets deployments control Akeyless connection details at the infrastructure/deployment level (e.g. process environment, secrets injection) instead of only via `manifest.json` or the Command portal. -- Environment variable override values are trimmed of leading/trailing whitespace before use, so an incidental trailing newline (a common artifact of file-mounted/`envFrom` secret provisioning) no longer breaks the override. -- An `AKEYLESS_AUTH_TYPE` override is now validated against the supported auth types before use; an unrecognized value fails fast with `InvalidClientConfigurationException` instead of silently skipping authentication and proceeding with an unauthenticated request. -- The "Connecting to Akeyless at" debug log now reports the effective (post-override) URL rather than the pre-override configured value. -- `Url`, `AuthType`, `AccessId`, and `AccessKey` are now required to be printable ASCII, whether the effective value came from an env var override or from Command's/`manifest.json`'s configuration — a value containing anything else (an embedded newline, ANSI escape sequence, Unicode line/paragraph separator, bidirectional-override or zero-width character, etc.) is rejected with `InvalidClientConfigurationException` instead of being passed through. None of these parameters are legitimately anything but printable ASCII, and letting a non-ASCII value through risked forging extra lines or spoofed rendering in the log messages that echo the resolved value. This check runs both where the configured value is first read (before `BuildAkeylessConfiguration`'s own debug logging echoes it) and again on the final resolved value in `InitClient` (which may instead be an env var override), so a malicious/malformed value can't reach either log statement unvalidated. -- A failed Akeyless authentication attempt (`ApiException`) now logs the `AccessId` that was used, matching the existing success/empty-token log lines — previously this was the one authentication-failure path with no record of which identity (configured or env-overridden) the failed attempt used. -- The `ApiException` catch in `InitClient` no longer passes the exception object itself to the logger — only the HTTP status code and `AccessId` are logged. Passing the exception object defeated the log call's own stated intent to exclude `ex.Message` (which may echo back the raw Akeyless response body, including credentials), since most logging providers render an attached exception's `Message`/`ToString()` regardless of the message template. -- Rejecting a non-printable/non-ASCII `Url`/`AuthType`/`AccessId`/`AccessKey` now logs an error (naming the parameter, never the value) before throwing, matching every other validation failure in the provider — previously this was the one validation-throw site with no corresponding log entry, leaving no audit trail that a log-injection/spoofing attempt was rejected. +- **Environment variable overrides for connection parameters** — `AuthType`, `AccessId`, and `AccessKey` can now be overridden at runtime via the `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, and `AKEYLESS_ACCESS_KEY` environment variables, matching the existing `AKEYLESS_API_URL` override. This lets deployments control Akeyless connection details at the infrastructure level instead of only via `manifest.json` or the Command portal. +- Environment variable overrides are trimmed of leading/trailing whitespace before use. +- Hardened validation and logging around connection parameters and authentication failures. # v1.0.0 From f0f60039ac466d7c178fc8d62255f7d7525618c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Aug 2026 19:16:36 +0000 Subject: [PATCH 30/30] docs: auto-generate README and documentation [skip ci] --- docs/akeyless.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/akeyless.md b/docs/akeyless.md index 89a0dab..1a66bb5 100644 --- a/docs/akeyless.md +++ b/docs/akeyless.md @@ -144,7 +144,9 @@ Connection and authentication parameters can be set in two ways: | `AKEYLESS_ACCESS_ID` | `AccessId` | configured `AccessId` initialization parameter | | `AKEYLESS_ACCESS_KEY` | `AccessKey` | configured `AccessKey` initialization parameter | -Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). The effective value of `Url`, `AuthType`, `AccessId`, and `AccessKey` — whether it came from an override or from Command's/`manifest.json`'s configuration — must be printable ASCII; any other character (an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. +Precedence for each: environment variable (if set to a non-empty, non-whitespace-only value) > configured initialization parameter > default (`Url` only). An environment variable that is unset, or explicitly set to an empty or whitespace-only string, is treated as "not overriding" and falls through to the configured value. An override value is trimmed of leading/trailing whitespace before use (a trailing newline is a common artifact of file-mounted/`envFrom` secret provisioning). The effective value of `Url`, `AuthType`, `AccessId`, and `AccessKey` — whether it came from an override or from Command's/`manifest.json`'s configuration — must be printable ASCII; any other character (an embedded newline, ANSI escape sequence, Unicode line separator, or a bidirectional-override/zero-width character such as U+202E) is rejected with an error rather than used as-is. When an override is active, the provider logs a **Warning** stating which environment variable is overriding (never the value), so an incident investigation can confirm whether the effective connection parameter matches Command's recorded configuration. An `AKEYLESS_AUTH_TYPE` override that does not match a supported auth type fails the request immediately rather than silently skipping authentication. + +**Upgrading an existing installation:** if the provider's host process already has `AKEYLESS_AUTH_TYPE`, `AKEYLESS_ACCESS_ID`, or `AKEYLESS_ACCESS_KEY` set for an unrelated reason (for example, a co-located Akeyless CLI or another Akeyless SDK conventionally uses these same variable names), upgrading to a version of this provider that reads them will silently start authenticating with that ambient identity instead of the one recorded in `manifest.json`/Command — with no configuration change on Command's side. Check the host environment for these variable names before upgrading, and watch for the Warning-level "Environment variable override active" log line afterward. ## Supported Authentication Methods