Skip to main content

Context:

 

The Endpoint Credential Manager (ECM) Software Development Kit for BeyondTrust Privileged Remote Access (PRA)  and Remote Support (RS)  allows developers to leverage a Credential Provider other than the one included (Vault) to allow Users to inject credentials when for accessing a Jump Client, a Web Jump, etc. This guide covers a specific example:  Hashicorp Vault Enterprise or on-prem. This guide also shows how to create a Custom Source to manage the configuration and credentials needed for the Plugin to authenticate to PRA and RS on one side, and to Hashicorp Vault on the other side.

 

Jump Client example leveraging Hashicorp Vault  secret.

 

Note: Secure Remote Access (SRA) is used to refer to both PRA and RS.

 

 

 

Disclaimer

The Endpoint Credential Manager (ECM) Software Development Kit

Allows developers to create Custom ECM Plugins. The SDK comes with a Plugin example, which has been used as a starting point to create a new Plugin.

 

Any sample or proof of concept code (“Code”) provided on the Community is provided “as is” and without any express or implied warranties. This means that we do not promise that it will work for your specific needs or that it is error-free. Such Code is community supported and not supported directly by BeyondTrust, and it is not intended to be used in a production environment. BeyondTrust and its contributors are not liable for any damage you or others might experience from using the Code, including but not limited to, loss of data, loss of profits, or any interruptions to your business, no matter what the cause is, even if advised of the possibility of such damage.

 

Capabilities

  • List secrets from Hashicorp Vault
  • Inject the selected secret into a Jump session
  • Optional: Use Environment Variables for the Configuration for a deployment via Jenkins or another CI/CD automation solution.

 

Configure Hashicorp Vault Secrets

 

Create secrets to be used by SRA.

 

Each secret must include username and password data.

 

Create the Service Account or entity/identity and add a userpass Alias.

 

Note:  The password for the Service Account needs to be set from the command line, for example:

 

vault write auth/userpass/users/svc_sra_ecm password="P@ssw0rd123”

 

Create a policy with permissions to list and read the secrets for SRA.

 

Assign the policy created previously to the Service Account.

 

We need to create a mapping file secret to map SRA Jump systems to specific secrets in Hashicorp Vault.

 

{
"targets": =
{
"ComputerName": "tap-lab-rds2",
"Secret": "/v1/sra_ecm/data/secrets/windows/DomainUser04",
"Type": "client",
"Url": ""
},
{
"ComputerName": "tap-lab-rds2",
"Secret": "/v1/sra_ecm/data/secrets/windows/DomainUser03",
"Type": "client",
"Url": ""
},
{
"ComputerName": "tap-lab-rds1",
"Secret": "/v1/sra_ecm/data/secrets/windows/DomainUser05",
"Type": "client",
"Url": ""
},
{
"ComputerName": "tap-lab-rds2",
"Secret": "/v1/sra_ecm/data/secrets/windows/DomainAdmin",
"Type": "client",
"Url": ""
},
{
"ComputerName": "tap-lab-linux01",
"Secret": "/v1/sra_ecm/data/secrets/linux/ubuntu",
"Type": "shell",
"Url": ""
},
{
"ComputerName": "tap-lab-useries",
"Secret": "/v1/sra_ecm/data/secrets/web/btadmin2",
"Type": "website",
"Url": "https://tap-lab-useries/webconsole/#!/login"
}
]
}

 

Note:  The Plugin will retrieve the mapping file at jump time and present the user with only the mapped secrets.

 

 

Obtain and deploy the ECM Runtime and ECM SDK Example Plugin

 

The documentation for the ECM SDK is available here: 

 

https://www.beyondtrust.com/docs/privileged-remote-access/how-to/integrations/ecm-sdk/index.htm

 

Extract the Example Plugin archive into a subdirectory (renamed).

 

Open the Solution/Project with Visual Studio with .NET v8 support.

 

Add NuGet packages required by the Plugin.

 

Here is a quick list of packages that need to be added:

  • Json.NET (Newtonsoft.Json);
  • Microsoft.Bcl.AsyncInterfaces;
  • Microsoft.Bcl.Cryptography;
  • System.Formats.Asn1;
  • System.ComponentModel.Composition;
  • Microsoft.Extensions.Hosting;
  • Microsoft.Extensions.Hosting.Abstractions;
  • Microsoft.Extensions.Hosting.WindowsServices;
  • Microsoft.Extensions.Logging;
  • System.Configuration.ConfigurationManager;
  • System.DirectoryServices;
  • System.Runtime.Caching;
  • System.Security.Cryptography.Pkcs;
  • System.Security.Cryptography.ProtectedData;
  • System.ServiceModel.Primitives;
  • System.ServiceProcess.ServiceController .

 

ExamplePluginConfig.cs:  configure the required config values for Hashicorp Vault.

 

namespace MyCompany.Integrations.ExamplePlugin
{
/// <summary>
/// Represents a simple POCO that defines the config values necessary for the plugin to operate
/// These are simply examples of the types of items which might be required for your implementation
/// </summary>
public sealed record ExamplePluginConfig
{
public required string VLT_BASE_URL { get; init; }
public required string VLT_SECRETS_ENGINE { get; init; }
public required string VLT_USERNAME { get; init; }
public required string VLT_PASSWORD { get; init; }
public required string VLT_ECM_MAP { get; init; }
}
}

 

appsettings.json: These are the config values for default Sources.

 

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ECMConfig": {
"SRASiteHostname": "myInstance.beyondtrustcloud.com",
"SRAClientId": "abcde12345",
"SRAClientSecret": "abcdef54321"
},
"ExamplePluginConfig": {
"VLT_BASE_URL": "https://myVaultServer:8200",
"VLT_SECRETS_ENGINE": "sra_ecm",
"VLT_USERNAME": "svc_sra_ecm",
"VLT_PASSWORD": "12345",
"VLT_ECM_MAP": "/v1/sra_ecm/data/targets"
}
}

 

In your PRA or RS instance, an API account needs to be created with the Endpoint Credential Manager API/Allow Access permission.

 

Note: While it is still possible to leverage appsettings.json for config values, the option to use Environment Variables is covered in this guide, with the use of a CI/CD Solution like Jenkins in mind, to deploy the plugin as a Container, e.g. using for example a Microsoft provided Docker image.

 

See for example:  https://learn.microsoft.com/en-us/dotnet/architecture/microservices/net-core-net-framework-containers/official-net-docker-images

 

PluginRunner.cs:  Add Custom Source for Hashicorp Vault via Environment Variables.

 

//***** Custom Source - Environment Variables - BEGIN
ExamplePluginConfig ExamplePluginConfig = new ExamplePluginConfig
{
VLT_BASE_URL = Environment.GetEnvironmentVariable("VLT_BASE_URL"),
VLT_SECRETS_ENGINE = Environment.GetEnvironmentVariable("VLT_SECRETS_ENGINE"),
VLT_USERNAME = Environment.GetEnvironmentVariable("VLT_USERNAME"),
VLT_PASSWORD = Environment.GetEnvironmentVariable("VLT_PASSWORD"),
VLT_ECM_MAP = Environment.GetEnvironmentVariable("VLT_ECM_MAP")
};
builder.Services.Configure<ExamplePluginConfig>(builder.Configuration);
//***** Custom Source - Environment Variables - END

 

PluginRunner.cs: Add Custom Source for SRA via Environment Variables.

 

//***** Custom Source - Environment Variables - BEGIN
ECMConfig ECMConfig = new ECMConfig {
SRASiteHostname = Environment.GetEnvironmentVariable("SRASiteHostName"),
SRAClientId = Environment.GetEnvironmentVariable("SRAClientId"),
SRAClientSecret = Environment.GetEnvironmentVariable("SRAClientSecret")
};

builder.Services.Configure<ECMConfig>(builder.Configuration);
//***** Custom Source - Environment Variables – END

 

ExamplePlugin.cs: We need to add our custom reusable functions or classes specific to Hashicorp Vault.

 

        // Custom Code - START
private static async Task<string> GetAuthToken(string VLT_BASE_URL, string VLT_USERNAME, string VLT_PASSWORD)
{
try
{
var httpClient = new HttpClient();
string myJson = "{\"password\":\"" + VLT_PASSWORD + "\"}";
var httpContent = new StringContent(myJson, Encoding.UTF8, "application/json");
HttpResponseMessage message = httpClient.PostAsync(VLT_BASE_URL + "/v1/auth/userpass/login/" + VLT_USERNAME, httpContent).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Authenticate " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Authenticate " + ex.ToString());
}
}

private static async Task<string> GetSecrets(string VLT_BASE_URL, string VLT_SECRETS_ENGINE, string PATH, string TOKEN)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{ "X-Vault-Token", TOKEN }
}
};
HttpResponseMessage message = httpClient.GetAsync(VLT_BASE_URL + "/v1/" + VLT_SECRETS_ENGINE + PATH).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Discover Secrets " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Discover Secrets " + ex.ToString());
}
}


private static async Task<string> GetSecret(string VLT_BASE_URL, string VLT_SECRET_PATH, string TOKEN)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{ "X-Vault-Token", TOKEN }
}
};
HttpResponseMessage message = httpClient.GetAsync(VLT_BASE_URL + VLT_SECRET_PATH).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Get Secret " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Get Secret " + ex.ToString());
}
}
// Custom Code - END

 

ExamplePlugin.cs: We need to make our Example Plugin Config available.

 

        private readonly ExamplePluginConfig examplePluginConfig;

public ExamplePlugin(IOptions<ExamplePluginConfig> config)
{
// Possible tasks to handle when the service is instantiated:
// - validate configuration
// - initialize any sort of API client or other types that will be required for servicing requests
// - pre-load any information that may be required for servicing requests (files, API / product version info, etc.)
examplePluginConfig = config.Value;
}

 

ExamplePlugin.cs: This is our code for FindCredentialsForSessionAsync required Action.

 

        public async Task<ActionResult<IList<CredentialSummary>>> FindCredentialsForSessionAsync(SRASession session)
{
// At this point the user has chosen a Jump Item in the console and attempted to initiate a session to the target system.
// The plugin should retrieve a list of credentials that are either queried or filtered based on the supplied
// information about the user and system to which the user is connecting.
// NOTE: While the operation is async, the SRA site will only wait for a certain period of time (20 seconds by default)
// for the list to be returned before it proceeds without presenting any creds for selection.
try
{
// GET Secrets List
// Authenticate - GET Bearer Token
var token = await GetAuthToken(examplePluginConfig.VLT_BASE_URL, examplePluginConfig.VLT_USERNAME, examplePluginConfig.VLT_PASSWORD);
dynamic jsonString_token = JsonConvert.DeserializeObject(token);
string access_token = jsonString_token.auth.client_token;
// Get machine name
Console.Write("####### session.JumpItem.Value = " + session.JumpItem.Value + "\n");
string machine = session.JumpItem.Value.ComputerName;
var url = session.JumpItem.Value.Url;
foreach (var i in session.JumpItem.Value.CustomAttributes)
{
Console.Write("####### JumpItem Custom Attribute " + i + "\n");
}
// Get Secrets Map
var secretMap = await GetSecret(examplePluginConfig.VLT_BASE_URL, examplePluginConfig.VLT_ECM_MAP, access_token);
dynamic jsonString_secretMap = JsonConvert.DeserializeObject(secretMap);
List<CredentialSummary> credentials = new List<CredentialSummary>();
foreach (var i in jsonString_secretMap.data.data.targets)
{
if (i.ComputerName == machine || i.Url == url)
{
Console.Write("####### Discovered Secret = " + i.Secret + " ComputerName = " + i.ComputerName + "\n");
// string MA_Name = i.Secret;
CredentialSummary credential = new CredentialSummary { CredentialId = i.Secret, DisplayValue = "Hashicorp Vault : " + i.Secret };
credentials.Add(credential);
}
}

// Then put that all in an ActionResult object to be returned
return new ActionResult<IList<CredentialSummary>> { ResultValue = credentials, IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get list of Credentials");
return new ActionResult<IList<CredentialSummary>> { ResultValue = null, FailureReason = ex.Message, IsSuccess = false };
}

// COMMENTED - throw new NotImplementedException($"The {Name} definition must implement {nameof(FindCredentialsForSessionAsync)} as defined by {nameof(ICredentialActions)}");
}

 

ExamplePlugin.cs: This is our code for the GetCredentialForInjectionAction required Action.

 

        public async Task<ActionResult<CredentialPackage>> GetCredentialForInjectionAsync(SRASession session, string credentialId)
{
// The user has been presented with a list of credentials (supplied by FindCredentialsForSessionAsync above) and has
// selected one from the list. The plugin should use the information supplied about the session as well as the ID of
// the selected credential to attempt to retrieve / check-out that credential and return it for injection into the session.
// NOTE: As above, it is important to note that while the operation is async, the same timeout applies.
try
{
// Authenticate - GET Bearer Token
var token = await GetAuthToken(examplePluginConfig.VLT_BASE_URL, examplePluginConfig.VLT_USERNAME, examplePluginConfig.VLT_PASSWORD);
dynamic jsonString_token = JsonConvert.DeserializeObject(token);
string access_token = jsonString_token.auth.client_token;
// Get Secret
var secret = await GetSecret(examplePluginConfig.VLT_BASE_URL, credentialId, access_token);
dynamic jsonString_secret = JsonConvert.DeserializeObject(secret);
string value_username = jsonString_secret.data.data.username;
string value_password = jsonString_secret.data.data.password;
Console.Write("####### Get secret id = " + credentialId + " username = " + value_username + "\n");

var result = CredentialPackage.BuildUsernamePasswordPackage(credentialId, value_username, value_password);
return new ActionResult<CredentialPackage> { ResultValue = result, IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to inject Credentials");
return new ActionResult<CredentialPackage> { ResultValue = null, FailureReason = ex.Message, IsSuccess = false };
}

// COMMENTED - throw new NotImplementedException($"The {Name} definition must implement {nameof(GetCredentialForInjectionAsync)} as defined by {nameof(ICredentialActions)}");
}

 

This is our code for the Release Credential section

 

        public async Task<ActionResult> ReleaseCredentialFromSessionAsync(SRASession session, string credentialId, long sessionEndedTimestamp)
{
// And at this point the session has ended and the SRA site is notifying the ECM / plugin in case any clean-up or check-in actions
// need to be performed in order to release the credential so other users can access it.
// NOTE: Any responses that return an ActionResult indicating failure will result in the SRA site retrying the request until it is met
// with a successful response or the request eventually times out of the queue. If it is unlikely that a given error scenario will
// ever result in success, then it is best to return a successful response here but adequately log anything that went wrong.
return new ActionResult { IsSuccess = true };

// COMMENTED - throw new NotImplementedException($"The {Name} definition must implement {nameof(ReleaseCredentialFromSessionAsync)} as defined by {nameof(ICredentialActions)}");
}

 

Project Properties: We need to add our Environment Variables to be able to use Debug within Visual Studio. This is for testing only, not required for publishing the App.

 

Build and Debug

 

We should be able to see the connection established with SRA, and discovered Secrets from Hashicorp Vault when Jumping into a Client.

 

The drop-down list of available credentials should now include our Hashicorp Vault secrets.

 

Next Steps

 

Once we can successfully Build and test the Plugin App via Debug in Visual Studio, the App and all its dependencies can be deployed.  It is not recommended to use appsettings.json to store sensitive configuration information including credentials, so another config Source should be used. Several examples are available on the web, and covering them is out of scope for this guide.  However, this guide does include an example using Environment Variables, which can make sense for when the Plugin App is containerized and a CI/CD solution like Jenkins is used to deploy the container to Docker or Kubernetes.

 

Creating a Docker image

 

We need to create a folder and subfolder structure on our Linux Docker host and copy the project files.
We need to modify the project file so the Plugin can run on Linux.

 

<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-ExamplePlugin-a620ada8-83c6-4c7a-8df7-edda1c5e47af</UserSecretsId>
<RootNamespace>MyCompany.Integrations.ExamplePlugin</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.Cryptography" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Composition" Version="9.0.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.1" />
<PackageReference Include="System.DirectoryServices" Version="9.0.1" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.1" />
<PackageReference Include="System.Runtime.Caching" Version="9.0.1" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="9.0.1" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="9.0.1" />
<PackageReference Include="System.ServiceModel.Primitives" Version="8.1.1" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.1" />
</ItemGroup>

<ItemGroup>
<Folder Include="lib\" />
</ItemGroup>

<ItemGroup>
<Reference Include="BeyondTrustECMSDK">
<HintPath>lib/BeyondTrustECMSDK.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup>
<Reference Include="BeyondTrustECMService">
<HintPath>lib/BeyondTrustECMService.dll</HintPath>
</Reference>
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<ItemGroup>
<DataFiles Include="$(ProjectDir)..\lib\*.*"/>
</ItemGroup>
<Copy
SourceFiles="@(DataFiles)"
DestinationFolder="$(TargetDir)\lib\"
SkipUnchangedFiles="true"/>
</Target>

</Project>

 

We need to create a Dockerfile and a /lib subdirectory that includes the SDK and ECMService libraries.

 

FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build-env
WORKDIR /SRA_HashicorpVault

# Copy everything
COPY . ./
COPY /lib/*.dll ./lib/
COPY a"/lib/BeyondTrustECMSDK.dll", "/lib"]
COPY b"/lib/BeyondTrustECMService.dll", "/lib"]
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0@sha256:6c4df091e4e531bb93bdbfe7e7f0998e7ced344f54426b7e874116a3dc3233ff
ENV VLT_BASE_URL="http://myInstance:8200"
ENV VLT_SECRETS_ENGINE="sra_ecm"
ENV VLT_USERNAME="svc_sra_ecm"
ENV VLT_PASSWORD="12345"
ENV VLT_ECM_MAP="/v1/sra_ecm/data/targets"
ENV SRASiteHostname="myInstance.beyondtrustcloud.com"
ENV SRAClientId="12345"
ENV SRAClientSecret="abcde"
WORKDIR /SRA_HashicorpVault
COPY --from=build-env /SRA_HashicorpVault/out .
ENTRYPOINT R"dotnet", "ExamplePlugin.dll"]

 

Now we can build our Docker image:

 

docker build -t sra-ecm-hashicorpvault-onprem -f Dockerfile .

 

We need to create an env file for our configuration:

 

VLT_BASE_URL=http://10.2.25.44:8200
VLT_SECRETS_ENGINE=sra-ecm
VLT_USERNAME=svc_sra_ecm
VLT_PASSWORD=P@ssw0rd123!
VLT_ECM_MAP=/v1/sra_ecm/data/targets
SRASiteHostname=myInstance.beyondtrustcloud.com
SRAClientId=abcde
SRAClientSecret=12345

 

Here we use Portainer, an optional web interface, to create a container using our image. It is still possible to use docker run at the command line instead of Portainer.

 

We need to select the radio button for Interactive and TTY.  With docker run, the -i -t  flags can be used.

 

We need to import our env file.

 

After deploying the container, we can watch the logs via Portainer.

 

Hi Michel

Thank you for such a quick turn around in a guide for the on-prem version.

I am having a bit of trouble with debugging the plugin itself, I’m hoping this is an easy one and I’ve just not put something where it should be, but when I run it I get the following error “Could not find an implementation of IECMService”

I think it might be the ECM runtime that you state is required, I have it downloaded but not sure where to extract it to for VS to utilize if that is what the error means.

Thank you


Hi Ashley,

Access to the ECM runtime and SDK(with example Plugin) is restricted to SRA customers and partners.  Can you send us an email via integrations@beyondtrust.com and we will provide a private download link?

You should have both files available in the project: 

 

Best regards,

Michel


Hi Michel,

We were already provided these via support ticket.

I’ve put those files in the correct place now.

My new issue comes with accessing the credentials using PRA

  • The plugin connects successfully
  • The ECM client shows up in the PRA console
  • When attempting to connect to the device we have set to map the credential to in the targets, only our credentials from our PWS ECM plugin are showing up

Am I missing something?

Thank you

Ash


Hi Ashley,

To be able to use multiple ECM Plugins concurrently, ECM Groups must be used.  Otherwise, only one Plugin will be used, either round robin of the one that has been connected the longest.

This is what I found in the documentation:

The ECM Groups feature is only present if enabled when you build your site. If it is not there but should be, contact your site administrator. See https://docs.beyondtrust.com/pra/docs/ecm-sdk#configure-sra-for-the-ecm 

And this other reference: 

When ECMs are connected in a high availability configuration, the BeyondTrust Appliance B Series routes requests to the ECM in the ECM Group that has been connected to the appliance the longest.  See  https://docs.beyondtrust.com/pra/docs/wac-credential-injection#system-requirements 

Are you using an on-prem instance of PRA, or Cloud? 

I have been told that by putting the ECM Plugins in different Groups, 1 Plugin from each Group should be used to find credentials etc.  

I will test this in my lab, I just requested via support that ECM Groups be enabled on my Cloud instance of PRA.  I will share my findings once I can test.

If you temporarily stop the Password Safe ECM Plugin, then your custom Plugin should be used.

 

Michel


Hi Ashley,

FYI I was able to successfully test both the Password Safe ECM Plugin and the Hashicorp Vault onprem Custom ECM Plugin together for the same instance/user.  After ECM Groups support was added to my instance, here is how it works:

Create one of more ECM Group(s)
Individual API accounts can be assigned to specific ECM Groups that they will join at connection/start time.
Jump Groups need to be assigned to ECM Groups.

So I am using the default ECM Group for Password Safe in my lab, and I assigned one Jump Client to the Hashicorp Vault ECM Group.  Now a test user that is a member of the Hashicorp Vault ECM Group can jump into that Jump Client leveraging secret from Hashicorp Vault, while the same user can also access secrets from Password Safe for other Jump Clients.

So there is a bit of reconfiguration that may be required for Jump Clients, to allow them to use Hashicorp Vault instead of default(PRA Vault and Password Safe).

Let me know if this helps.

Best regards,

Michel


Hey Michel,

Thank you this is very helpful and insightful, I have requested ECM groups for our sites.

What I am curious about next is mapping of secrets to specific accounts? I can see that we can map secrets to specific hosts, but like how it can be configured in Password Safe, can you map accounts to specific accounts? For example, AshleyB would be able to use the secret in the Vault of AshleyB-P

Thank you


Hi Ashley,

There could be a few ways to support only named/secondary accounts for Users.  One way would be to leverage the session param within code/ExamplePlugin.cs.

If I have those 2 lines in FindCredentials section:

                Console.Write("####### session.User.Value =  " + session.User.Value + "\n");
                Console.Write("####### session.JumpItem.Value =  " + session.JumpItem.Value + "\n");

Then I can see in the logs:

####### session.User.Value =  SRAUser { Username = albert.junio, PrivateDisplayName = albert junio, PublicDisplayName = albert junio, UserId = 9, DistinguishedName =  }
####### session.JumpItem.Value =  JumpItem { Type = client, ComputerName = tap-lab-rds2, JumpointName = , Tag = someTag, Comments = , Username = , Domain = , Protocol = , PrivateIP = 10.100.42.26, PublicIP = 192.30.33.143, Url = , DomainHostname = tap-lab-rds2.btintegrations.cloud, OperatingSystem = windows server 2022 standard (21h2), ApplicationName = , ExternalId = , CustomAttributes = System.Collections.Generic.Dictionary`2sSystem.String,System.String] }

So we could add a condition like Secret.contains(session.User.Value.Username):

If we replace the line:

credentials.Add(credential);

with:

if (i.Secret.contains(session.User.Value.Username)){ credentials.Add(credential); }

but that would only add credentials that contain the Username.  This could be fine if we only want to pull named accounts from Hashicorp Vault, but if we want to allow shared accounts as well (that don’t contain the Username) then we would need some other logic, maybe based on Secret metadata.

We could also add a parameter like namedAccounts = true or false, so the Plugin could either be set to show only named accounts(true) or all accounts(false). We could run 2 instances(e.g. deploy 2 Docker images) in different ECM Groups, one for named accounts, one for shared accounts, etc.

I think having these Plugin examples available, and this kind of discussion based on a real environment, may help us better define the requirements for the Hashicorp Vault Custom Plugins and other Plugins moving forward.

 

Best regards,

Michel

 


Hi Michel

I’ve got two weird issues:

  • We don’t get a nice display like your example screenshots of the secret name, we just get the full secret path
  • When trying to perform “i.Secret.contains” we get the error “'Newtonsoft.Json.Linq.JValue' does not contain a definition for 'Contains'”

I’ve also never worked in C#, we would need to perform some string manipulation for the username to match the user from Vault, I’ve tried a lot of things but all I ever get in the output is System.String ]

Any advice you can provide on this?

Thank you


This is the code I just tested successfully:

        public async Task<ActionResult<IList<CredentialSummary>>> FindCredentialsForSessionAsync(SRASession session)
{
// At this point the user has chosen a Jump Item in the console and attempted to initiate a session to the target system.
// The plugin should retrieve a list of credentials that are either queried or filtered based on the supplied
// information about the user and system to which the user is connecting.
// NOTE: While the operation is async, the SRA site will only wait for a certain period of time (20 seconds by default)
// for the list to be returned before it proceeds without presenting any creds for selection.
try
{
// GET Secrets List
// Authenticate - GET Bearer Token
var token = await GetAuthToken(examplePluginConfig.VLT_BASE_URL, examplePluginConfig.VLT_USERNAME, examplePluginConfig.VLT_PASSWORD);
dynamic jsonString_token = JsonConvert.DeserializeObject(token);
string access_token = jsonString_token.auth.client_token;
// Get machine name
Console.Write("####### session.User.Value = " + session.User.Value + "\n");
Console.Write("####### session.JumpItem.Value = " + session.JumpItem.Value + "\n");
string machine = session.JumpItem.Value.ComputerName;
var url = session.JumpItem.Value.Url;
foreach (var i in session.JumpItem.Value.CustomAttributes)
{
Console.Write("####### JumpItem Custom Attribute " + i + "\n");
}
// Get Secrets Map
var secretMap = await GetSecret(examplePluginConfig.VLT_BASE_URL, examplePluginConfig.VLT_ECM_MAP, access_token);
dynamic jsonString_secretMap = JsonConvert.DeserializeObject(secretMap);
List<CredentialSummary> credentials = new List<CredentialSummary>();
foreach (var i in jsonString_secretMap.data.data.targets)
{
if (i.ComputerName == machine || i.Url == url)
{
Console.Write("####### Discovered Secret = " + i.Secret + " ComputerName = " + i.ComputerName + "\n");
CredentialSummary credential = new CredentialSummary { CredentialId = i.Secret, DisplayValue = "Hashicorp Vault : " + i.Secret };
string secret = i.Secret;
if (secret.Contains(session.User.Value.Username)){ credentials.Add(credential); }
//credentials.Add(credential);
}
}

// Then put that all in an ActionResult object to be returned
return new ActionResult<IList<CredentialSummary>> { ResultValue = credentials, IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get list of Credentials");
return new ActionResult<IList<CredentialSummary>> { ResultValue = null, FailureReason = ex.Message, IsSuccess = false };
}

// COMMENTED - throw new NotImplementedException($"The {Name} definition must implement {nameof(FindCredentialsForSessionAsync)} as defined by {nameof(ICredentialActions)}");
}

The change is that I define a string (secret = i.Secret) and use that for Contains.

When connected as username = albert.junio, I see the albert.junio_DomainAdmin account only, I do see the full path:  

 

Let me know if this helps,

Michel


Hi Michel,

 

That does help!

I see, I was going off of the screenshots in the article, it showed just the name of the user and not the full path, I’ve resolved this by adding a name field to the targets in the Vault.

The other bit I still need help with is transforming the username provided by SRA, it shows up as “user@domain.net”, and we need to remove the “@domain.net” section for the if statement to work and this is where I’m getting the System.String ] output

Thank you


Hi Ashley,

Below you will find an update for FindCredentials, I have successfully tested with username@mydomain.net, and I also added an example to simplify the Secret display name for end users:

Secret displayname not showing the full secret path.

 

        public async Task<ActionResult<IList<CredentialSummary>>> FindCredentialsForSessionAsync(SRASession session)
{
// At this point the user has chosen a Jump Item in the console and attempted to initiate a session to the target system.
// The plugin should retrieve a list of credentials that are either queried or filtered based on the supplied
// information about the user and system to which the user is connecting.
// NOTE: While the operation is async, the SRA site will only wait for a certain period of time (20 seconds by default)
// for the list to be returned before it proceeds without presenting any creds for selection.
try
{
// GET Secrets List
// Authenticate - GET Bearer Token
var token = await GetAuthToken(examplePluginConfig.VLT_BASE_URL, examplePluginConfig.VLT_USERNAME, examplePluginConfig.VLT_PASSWORD);
dynamic jsonString_token = JsonConvert.DeserializeObject(token);
string access_token = jsonString_token.auth.client_token;
// Get machine name
Console.Write("####### session.User.Value = " + session.User.Value + "\n");
Console.Write("####### session.JumpItem.Value = " + session.JumpItem.Value + "\n");
string machine = session.JumpItem.Value.ComputerName;
var url = session.JumpItem.Value.Url;
foreach (var i in session.JumpItem.Value.CustomAttributes)
{
Console.Write("####### JumpItem Custom Attribute " + i + "\n");
}
// Get Secrets Map
var secretMap = await GetSecret(examplePluginConfig.VLT_BASE_URL, examplePluginConfig.VLT_ECM_MAP, access_token);
dynamic jsonString_secretMap = JsonConvert.DeserializeObject(secretMap);
List<CredentialSummary> credentials = new List<CredentialSummary>();
foreach (var i in jsonString_secretMap.data.data.targets)
{
if (i.ComputerName == machine || i.Url == url)
{
Console.Write("####### Discovered Secret = " + i.Secret + " ComputerName = " + i.ComputerName + "\n");
//CredentialSummary credential = new CredentialSummary { CredentialId = i.Secret, DisplayValue = "Hashicorp Vault : " + i.Secret };
string CredentialDisplayName = i.Secret;
CredentialDisplayName = CredentialDisplayName.Substring(CredentialDisplayName.LastIndexOf('/') + 1);
CredentialSummary credential = new CredentialSummary { CredentialId = i.Secret, DisplayValue = "Hashicorp Vault : " + CredentialDisplayName };
string secret = i.Secret;
string username = session.User.Value.Username;
username = username.Substring(0, username.IndexOf("@"));
if (secret.Contains(username)) { credentials.Add(credential); }
//if (secret.Contains(session.User.Value.Username)) { credentials.Add(credential); }
//credentials.Add(credential);
}
}

// Then put that all in an ActionResult object to be returned
return new ActionResult<IList<CredentialSummary>> { ResultValue = credentials, IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get list of Credentials");
return new ActionResult<IList<CredentialSummary>> { ResultValue = null, FailureReason = ex.Message, IsSuccess = false };
}

// COMMENTED - throw new NotImplementedException($"The {Name} definition must implement {nameof(FindCredentialsForSessionAsync)} as defined by {nameof(ICredentialActions)}");
}

 

Let me know if you have any other question or need other examples.

Best regards,

Michel


That is amazing! Much better than what I had tried to cobble together with my limited knowledge

 

Thank you!!


Reply