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: Microsoft Azure Key Vault. 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 Azure Key Vault on the other side.
Note: Secure Remote Access (SRA) is used to refer to both PRA and RS.
Capabilities
- List secrets from a specific Azure Key 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.
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.
Configure Azure Key Vault
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
Here is a quick list of packages that need to be added:
- Json.NET (Newtonsoft.Json);
- Microsoft.Bcl.AsyncInterfaces;
- Microsoft.Identity.Client (for Azure Key Vault);
- Microsoft.Bcl.Cryptography;
- System.Formats.Asn1;
- Microsoft.Extensions.Configuration.
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ECMConfig": {
"SRASiteHostname": "myInstance.beyondtrustcloud.com",
"SRAClientId": "ccc",
"SRAClientSecret": "ddd"
},
"ExamplePluginConfig": {
"Tenant": "myTenant.onmicrosoft.com",
"Vault": "myVault",
"OAuthClientId": "aaa",
"OAuthClientSecret": "bbb",
"EnableSpecialCase": true
}
}
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
//***** Custom Source - Environment Variables - BEGIN
ExamplePluginConfig ExamplePluginConfig = new ExamplePluginConfig
{
Tenant = Environment.GetEnvironmentVariable("Tenant"),
Vault = Environment.GetEnvironmentVariable("Vault"),
OAuthClientId = Environment.GetEnvironmentVariable("OAuthClientId"),
OAuthClientSecret = Environment.GetEnvironmentVariable("OAuthClientSecret")
};
builder.Services.Configure<ExamplePluginConfig>(builder.Configuration);
//***** Custom Source - Environment Variables – END
//***** 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
// Custom Code - START
public async Task<string> getTokenAsync(string CLIENT_ID, string CLIENT_SECRET, string TENANT)
{
try
{
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(CLIENT_ID)
.WithClientSecret(CLIENT_SECRET)
.WithAuthority(new Uri("https://login.microsoftonline.com/" + TENANT))
.Build();
AuthenticationResult result = null;
//e.g. https://graph.microsoft.com/.default
stringr] scopes = new stringf] { "https://vault.azure.net/.default" };
result = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
string accesstoken = result.AccessToken;
return accesstoken;
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to verify Functional Account credentials : " + ex.Message);
}
}
private static HttpResponseMessage GetSecrets(string ACCESS_TOKEN, string vaultName)
{
using (HttpClient httpClient = new HttpClient())
{
try
{
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", ACCESS_TOKEN);
HttpResponseMessage message = httpClient.GetAsync("https://" + vaultName + ".vault.azure.net/secrets?api-version=7.4").Result;
return message;
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Get Secrets list");
}
}
}
private static HttpResponseMessage GetSecret(string ACCESS_TOKEN, string secretId)
{
using (HttpClient httpClient = new HttpClient())
{
try
{
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", ACCESS_TOKEN);
HttpResponseMessage message = httpClient.GetAsync(secretId + "?api-version=7.4").Result;
return message;
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Get Secret value");
}
}
}
// Custom Code – END
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;
}
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 Access Token for Azure Key Vault
var accessToken = await getTokenAsync(examplePluginConfig.OAuthClientId, examplePluginConfig.OAuthClientSecret, examplePluginConfig.Tenant);
// Console.Write("####### Done with Getting Access Token \n");
// Get Managed Accounts - Secrets
var secrets = GetSecrets(accessToken, examplePluginConfig.Vault);
dynamic jsonString_secrets = JsonConvert.DeserializeObject(secrets.Content.ReadAsStringAsync().Result);
List<CredentialSummary> credentials = new List<CredentialSummary>();
foreach (var secret in jsonString_secrets.value)
{
Console.Write("####### Discovered secret = " + secret + "\n");
Console.Write("####### Discovered secret id = " + secret.id + "\n");
CredentialSummary credential = new CredentialSummary { CredentialId = secret.id, DisplayValue = secret.id };
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)}");
}
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
{
string myUser = session.User.Value.Username;
// GET Access Token for Azure Key Vault
var accessToken = await getTokenAsync(examplePluginConfig.OAuthClientId, examplePluginConfig.OAuthClientSecret, examplePluginConfig.Tenant);
// Console.Write("####### Done with Getting Access Token \n");
// Get Secret value
var secretValue = GetSecret(accessToken, credentialId);
dynamic jsonString_secretValue = JsonConvert.DeserializeObject(secretValue.Content.ReadAsStringAsync().Result);
// Then put that all in an ActionResult object to be returned
string username = jsonString_secretValue.tags.username;
string value = jsonString_secretValue.value;
Console.Write("####### Get secret id = " + credentialId + " username = " + username + "\n");
var result = CredentialPackage.BuildUsernamePasswordPackage(credentialId, username, value);
return new ActionResult<CredentialPackage> { ResultValue = result, IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get list of 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)}");
}
Build and Debug
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.
SRA ECM Plugin – Azure Key Vault - Deploy as a Docker container