Will Hallam
ServiceNow Employee
ServiceNow Employee

DISCLAIMER: This example comes with no support or warranty, implied or explicit.  Caveat Emptor!

 

Here's an example of using AWS Secrets Manager to store credentials for discovery, service mapping, and orchestration.

 

Bear in mind I'm not a Java coder by trade, so while this implementation worked for my purposes it is inelegant (for example, region is hardcoded) and would need refactoring to be used in the field.

 

I started my journey with the following support KB, which details how to create a custom credential resolver which uses a text file residing on the MID server: https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0714680

 

Inside this KB is a link to the following Docs page, which contains links to the open source Git repos containing examples for CyberArk and HashiCorp Vault integrations:  https://docs.servicenow.com/csh?topicname=external_cred_storage_configuration.html&version=latest

 

I ended up starting with a clone of the HashiCorp Vault integration, then modifying the file src/com/snc/discovery/CredentialResolver.java to contain the following:

 

 

package com.snc.discovery;

import com.snc.automation_common.integration.creds.IExternalCredential;
import com.snc.core_automation_common.logging.Logger;
import com.snc.core_automation_common.logging.LoggerFactory;
import java.io.*;
import java.lang.*;
import java.security.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;

/**
 * AWS Secrets Manager Credential Resolver
 */

public class CredentialResolver implements IExternalCredential {

  // These are the permissible names of arguments passed INTO the resolve()
  // method.

  // the string identifier as configured on the ServiceNow instance...
  public static final String ARG_ID = "id";

  // a dotted-form string IPv4 address (like "10.22.231.12") of the target
  // system...
  public static final String ARG_IP = "ip";

  // the string type (ssh, snmp, etc.) of credential as configured on the
  // instance...
  public static final String ARG_TYPE = "type";

  // the string MID server making the request, as configured on the
  // instance...
  public static final String ARG_MID = "mid";

  // These are the permissible names of values returned FROM the resolve()
  // method.
  // the string user name for the credential, if needed...
  public static final String VAL_USER = "user";

  // the string password for the credential, if needed...
  public static final String VAL_PSWD = "pswd";

  // the string pass phrase for the credential if needed:
  public static final String VAL_PASSPHRASE = "passphrase";

  // the string private key for the credential, if needed
  public static final String VAL_PKEY = "pkey";

  // the string authentication protocol for the credential, if needed
  public static final String VAL_AUTHPROTO  = "authprotocol"; // the string authentication key for the credential, if
                        // needed...
  public static final String VAL_AUTHKEY = "authkey";

  // the string privacy protocol for the credential, if needed...
  public static final String VAL_PRIVPROTO = "privprotocol";

  // the string privacy key for the credential, if needed...
  public static final String VAL_PRIVKEY = "privkey";
  // Logger object to log messages in agent.log
  private static final Logger fLogger = LoggerFactory.getLogger(CredentialResolver.class);
  // PEM pattern
  private static final Pattern PEM = Pattern.compile("-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----\\r?\\n?(.*)\\r?\\n?(-----END \\1 PRIVATE KEY-----)",Pattern.MULTILINE | Pattern.DOTALL);

  public CredentialResolver() { }
  @Override public void config(Map<String, String> configMap)
  {
    // Note: To load config parameters from MID config.xml if not available in
    // configMap. propValue = Config.get().getProperty("<Parameter Name>")
    fLogger.info("Overridden config method");
  }

  /**
   * Resolve a credential.
   */
  public Map<String, String> resolve(Map<String, String> args)
  {
    String secretName = (String)args.get(ARG_ID) + "/" + (String)args.get(ARG_TYPE);
    Region region = Region.of("<insert your region here, or parameterize it>");

    System.err.println("Looking for secret \"" + secretName + "\"");
    fLogger.info("Looking for secret \"" + secretName + "\"");

    if (secretName.equalsIgnoreCase("misbehave/ssh")) throw new RuntimeException("I've been a baaaaaaaaad CredentialResolver!");

    // Create a Secrets Manager client
    SecretsManagerClient client = SecretsManagerClient.builder().region(region).build();

    GetSecretValueRequest getSecretValueRequest = GetSecretValueRequest.builder().secretId(secretName).build();

    GetSecretValueResponse getSecretValueResponse;

    try {
      getSecretValueResponse = client.getSecretValue(getSecretValueRequest);
    } catch (Exception e) {
      // For a list of exceptions thrown, see
      // https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
      throw e;
    }

    String secret = getSecretValueResponse.secretString();
    System.err.println("Retrieved secret \"" + secretName + "\"");
    fLogger.info("Retrieved secret \"" + secretName + "\"");

    // JSON parsing
    JSONParser jsonParser = new JSONParser();
    JSONObject secretObj = new JSONObject();
    try {
      secretObj = (JSONObject)jsonParser.parse(secret);
    } catch (ParseException e) {
      e.printStackTrace();
    }

    // the resolved credential is returned in a HashMap...
    Map<String, String> result = new HashMap<String, String>();
    result.put(VAL_USER, (String)secretObj.get(VAL_USER));
    if (secretObj.get(VAL_PSWD) != null) {
      fLogger.info("Populating password for " + (String)result.get(VAL_USER));
    }
    result.put(VAL_PSWD, (String)secretObj.get(VAL_PSWD));
    // result.put(VAL_PKEY, (String) secretObj.get(VAL_PKEY));
    // if we have a PKEY, spiff it up so the MID SSH code can read it
    if (secretObj.get(VAL_PKEY) != null) {
      Matcher mat = PEM.matcher((String)secretObj.get(VAL_PKEY));
      if (!mat.find()) {
        fLogger.error(
            "Private key decode failure: Did not recognize PEM Header / Footer or unsupported algorithm");
      } else {
        fLogger.info("Populating private key: -----BEGIN "+ (String)mat.group(1) + " PRIVATE KEY-----"+((String)mat.group(2)).substring(0, 10) + (String)mat.group(3));
        result.put(VAL_PKEY,"-----BEGIN " + (String)mat.group(1) + " PRIVATE KEY-----\n"+ (String)mat.group(2) + "\n" +(String)mat.group(3));
      }
    }
    // result.put(VAL_PASSPHRASE, (String) secretObj.get(VAL_PASSPHRASE));
    // result.put(VAL_AUTHPROTO, (String) secretObj.get(VAL_AUTHPROTO));
    // result.put(VAL_AUTHKEY, (String) secretObj.get(VAL_AUTHKEY));
    // result.put(VAL_PRIVPROTO, (String) secretObj.get(VAL_PRIVPROTO));
    // result.put(VAL_PRIVKEY, (String) secretObj.get(VAL_PRIVKEY));

    return result;
  }

  /**
   * Return the API version supported by this class.
   */
  public String getVersion() { return "1.0"; }

  /* uncomment for testing

      public static void main(String[] args) {
          CredentialResolver obj = new CredentialResolver();

          Map inputs = new HashMap();
          Map result = new HashMap();

          inputs.put(ARG_ID,"<test cred id>");
          inputs.put(ARG_TYPE,"<test cred type>");

          result=obj.resolve(inputs);

          System.err.println("I spy the following credentials: ");

          result.forEach((key, value) -> System.out.println(key + " " + value));

      }
  */
}

 

 

I also updated the pom.xml file so that the dependencies matched up with the modified set of imports and changed the name of the generated JAR file.  Once that was done, I was able to generate a JAR file using Maven, via the "mvn package" command.

 

Making the custom JAR available on MID servers is as simple as creating a JAR File record in the ServiceNow instance and attaching the custom JAR to it.  The platform takes care of syncing it to the MIDs.  Enabling external credential support on the platform is covered in the support KB referenced above.

 

The other piece required to retrieve credentials is requisite AWS IAM permissions.  I added a stanza similar to this to my existing MID server/AWS discovery policy:

 

{
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "arn:aws:secretsmanager:*:*:secret:discovery*"
            ],
            "Effect": "Allow"
}

 

 

With that final piece in place, I was able to populate secrets in Secrets Manager as follows:

 

- for an SSH private key credential, naming syntax of "<key name>/ssh_private_key", e.g. "my-disco-creds/ssh_private_key"

- for a Windows credential, naming syntax of "<key name>/windows", e.g. "my-windows-disco-creds/windows"

 

On the ServiceNow side, I would match the "<key name>" portion to the "Credential ID" attribute; the custom resolver concatenates the Credential ID with the Credential Type using a slash between them in order to generate the secret ID to retrieve.

 

All in all, not a bad exercise once I figured out some of the fiddly bits (the SSH private key format was a bit fussy between AWS Secrets Manager and the MID SSH engine).  Starting with one of the existing resolvers is a great way to jumpstart the effort.  Hopefully this helps others looking to leverage this capability with something other than the two supported providers.

Comments
William50
Tera Contributor

Hey @Will Hallam can you please give me a hand with my post CredentialResolver.class - What happens? 

 

DagarB
Tera Contributor

Hey @Will Hallam  how does your secret value string looks like ?

DagarB
Tera Contributor

i mean in case of windows for eg, user name is part of secret value ?

Will Hallam
ServiceNow Employee
ServiceNow Employee

The user name is part of the secret, defined via the "user" key.  An example secret payload for a Windows credential:

 

{"user":"mydomain\\sn.disco","pswd":"s00p3rS3cr3t"}
DagarB
Tera Contributor

Thanks @Will Hallam . Is it possible to share the pom.xml ?

Will Hallam
ServiceNow Employee
ServiceNow Employee

Sure -- it is a modified version of the HashCorp Vault one from https://github.com/ServiceNow/mid-hashicorp-external-credential-resolver

 

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>custom-credential-resolver</groupId>
	<artifactId>custom-credential-resolver</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	
	<properties>
		<!-- This must point to the MID Server installation location (agent directory path).
			 $ mvn -D midserver.agent.dir="/path/to/mid_server/agent" (maven command to build)
		-->
		
		<midserver.agent.dir>/opt/snmid/agent</midserver.agent.dir>
	</properties>
	
	<build>
		<sourceDirectory>src</sourceDirectory>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.7.0</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

    <dependencyManagement>
      <dependencies>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>bom</artifactId>
            <version>2.20.99</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>

	<dependencies>
	
		<!-- Direct dependencies to be uploaded to MID Server -->
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>secretsmanager</artifactId>
        </dependency>
		
		<!-- MID server dependencies, not required to be uploaded -->
		<!-- MID jar dependency for config APIs -->
		<dependency> 
			<groupId>com.snc</groupId> 
			<artifactId>mid</artifactId> 
			<version>19.0.0.0-SNAPSHOT</version>
			<scope>system</scope>
			<systemPath>${midserver.agent.dir}/lib/mid.jar</systemPath>
		</dependency> 
		
		<dependency> 
			<groupId>com.snc</groupId> 
			<artifactId>commons-glide</artifactId> 
			<version>19.0.0.0-SNAPSHOT</version>
			<scope>system</scope>
			<systemPath>${midserver.agent.dir}/lib/commons-glide.jar</systemPath>
		</dependency>
		
		<dependency> 
			<groupId>com.snc</groupId> 
			<artifactId>commons-core-automation</artifactId> 
			<version>19.0.0.0-SNAPSHOT</version>
			<scope>system</scope>
			<systemPath>${midserver.agent.dir}/lib/commons-core-automation.jar</systemPath>
		</dependency>
		
		<dependency> 
			<groupId>com.snc</groupId> 
			<artifactId>snc-automation-api</artifactId> 
			<version>19.0.0.0-SNAPSHOT</version>
			<scope>system</scope>
			<systemPath>${midserver.agent.dir}/lib/snc-automation-api.jar</systemPath>
		</dependency>
		
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>2.8.2</version>
			<scope>system</scope>
			<systemPath>${midserver.agent.dir}/lib/guava.jar</systemPath>
		</dependency>
		
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.8.2</version>
			<scope>system</scope>
			<systemPath>${midserver.agent.dir}/lib/gson.jar</systemPath>
		</dependency>
	</dependencies>
</project>

 

 

DagarB
Tera Contributor

Hi @Will Hallam  , thanks, with your example and hashicorp example,  i have been able to generate the jar file for my project. I am trying to do a poc for Azure key vault.

For the local test though, i tried to run the jar file but its giving me "no main attribute in the jar file" error. I also tried "java -cp jarfile package.class" command but it keeps giving me "could not find or load main class". Can you pls guide how to test it locally first ?

Will Hallam
ServiceNow Employee
ServiceNow Employee

I didn't test it locally.  I tested by deploying to a test MID and invoking a test credential from my instance.  I see a main method defined in the source, but I don't have any firsthand experience with invoking it unfortunately.

DagarB
Tera Contributor

Thanks @Will Hallam , i am also now testing on a dev mid. This post has been a real help.

Version history
Last update:
‎07-26-2023 12:44 PM
Updated by:
Contributors