Using CyberArk Conjur with Azure Serverless Functions and Managed Identities

Businesses need to provide flexible access to services that scale efficiently while always protecting customer data. Technologies like microservices and RESTful APIs have improved “time to market” by making it easier to deliver services in smaller packages, but many implementations are still delivered on a fixed pool of servers. If the pool has too many servers, it is expensive to operate. If the pool is too small, the application can’t meet peak demands and opportunities are lost.

Worse, traditional implementations often share critical information about the infrastructure by embedding data keys and API secrets on the servers, not only exposing the secrets to unauthorized use, but also making system upgrades difficult.

Secrets and Azure Functions

Microsoft Azure supports “serverless” operations through Azure Functions. Azure provides a way to link an application to a platform event, such as an HTTP request or a change to an Azure Blob. When these events occur, Azure provisions the application on a server and runs it for you.

While this makes it easier to launch functions as needed, many infrastructure secrets that controlled access to other services used to be stored on the server and, without the server, where do you keep them?

CyberArk’s Conjur Open Source Suite provides this service with a lot of benefits: secrets are secured with auditable access, they are available to applications on any platform, and changing a secret or changing access rights to the secret in Conjur changes it for all applications simultaneously.

Conjur provides authenticators for Azure and many other platforms, so you have the tools you need right from the start.

In this article, we’ll explore using Azure Active Directory (AD) User Assigned Managed Identities to assign an identity to a function and how to use that identity to authenticate the function in Conjur.

Conjur’s Role Based Access Control (RBAC) is defined by policy objects stored in Conjur. You create these policies to determine which functions have access to which secrets and to define the variables that hold the secrets. You set the values these variables hold using Conjur commands, so you don’t need to change the policies to change the values. Services that have access to a secret read the value from the variable, so you are always granting access to the variable and never providing the secret directly.

Conjur and the Azure Authenticator

We’ll demonstrate this secrets-focused process by implementing one common use case that has many applications. We will deploy an Azure Function that responds to an HTTP event. The function needs to read data from a CosmosDB data store.

The solution will use the following resources on Azure:

  • An Azure Function that responds to an HTTP event
  • A Cosmos DB database
  • A managed identity
  • A virtual machine to host Conjur

We’ll use the following resource with Conjur:

  • An authentication service
  • A host associated with the managed identity
  • The secrets:the endpoint and primary key to access the Cosmos DB database

To keep this example simple, we created all these resources in the same Azure resource group.

Before we go further, if you are unfamiliar with Conjur, please refer to Conjur’s documentation for an overview.

The Cosmos DB account we created for this example provides one database and one container. To keep the example simple, their names are hardcoded in the Azure Function. Also, we allow the Azure Function to access the database using the Cosmos DB endpoint and master key. However, the Azure Function never stores these values.

The Azure Function is simple: it responds to an HTTP trigger and obtains a JSON Web Token (JWT) containing its user assigned managed identity. The function authenticates to Conjur using the JWT and obtains the Cosmos DB endpoint and master key. To demonstrate success, it reads all of the documents in a fixed collection and writes them as the response to the HTTP request.

We set up Conjur using the Conjur Quickstart (skipping the part for setting up the test resources) and the examples for implementing Azure Authenticator. We used two example policies, but extended the Hosts example to define the Azure Function as a host. This provides a solution demonstrating all the working parts and provides a framework you can extend to explore more advanced configuration elements (for example, layers).

Set up the CosmosDB Database and Collection

Microsoft Azure provides good instructions on how to set up a Cosmos DB, a database, and a collection, so we won’t repeat them here. We will need this data later:

  • Cosmos DB Keys
    • Uniform Resource Identifier (URI) — this is our endpoint
    • Primary key — this is our AuthKey
  • The database name and collection name. Find these in the Cosmos DB explorer.

Provide a Conjur Host Server

We set up this example on an Azure Virtual Machine (VM) running Ubuntu Linux. You’ll want to make sure Docker is installed on your server as it’s not standard on all Linux distributions. We created a user-defined bridge to make the Conjur proxy available to the Azure Function.

Create a Managed Identity

We will use a managed identity two ways:

  • First, to provide an identity to a host in Conjur.
  • Second, to associate that same identity to an Azure Function.

This means that a single set of secrets in Conjur will be available to all functions that present this identity.

Configuring Conjur Azure Authenticator

The instructions we used for setting up Conjur Open Source Suite using Quickstart are available at CyberArk’s Conjur website . Note that we only worked through the “Setup a Conjur OSS Environment” section and skipped the rest. The other steps are useful, but they add resources we don’t need.

In all of the following examples, myConjurAccount refers to an account created during Quickstart.

Instructions for configuring Conjur Azure Authenticator are also at the Conjur website. We made two changes to the example policy:

Explicitly declare the group ID.

Add the update privilege so you can enable the authenticator later on.

Use this code:

- !policy
  id: conjur/authn-azure/AzureWS1
  body:
  - !webservice

  - !variable
    id: provider-uri

  - !group
    id: apps
    annotations:
      description: Group of hosts that can authenticate using the authn-azure/AzureWS1 authenticator

  - !permit
    role: !group apps
    privilege: [ read, authenticate, update ]
    resource: !webservice

The purpose of this policy is to associate a Conjur group with an Azure AD Authenticator. Because the authenticate privilege is provided, when Conjur sees a request from a host that is a member of the “apps” group, Conjur validates tokens that it receives using this provider-uri.

As you follow the instructions to set up the Azure Authenticator, don’t forget to set the provider-uri Conjur variable. For our example, the command to do this is:

conjur variable values add conjur/authn-azure/AzureWS1/provider-uri https://sts.windows.net/<AD Tenant ID>/

We modified the example Azure Host policy as shown below. Note that the name of the user-assigned managed identity is provided, not the client_id. The name is also case-sensitive, so if you copy-and-paste from Azure, be careful to provide  the name’s original case. The name is just a string passed between Azure and Conjur. This means you can assign any string you like, with 24 or fewer ASCII characters, to the managed identity user.

- !policy
  id: azure-apps
  body:
    - !group

    - &hosts
      - !host
        id: ConjurCosmosDemoAccessFunction
        annotations:
          authn-azure/subscription-id: f89...18
          authn-azure/resource-group: <MyAzureResourceGroup>
          authn-azure/user-assigned-identity: ConjurCosmosDemoAccess

    - !grant
      role: !group
      members: *hosts

- !grant
  role: !group conjur/authn-azure/AzureWS1/apps
  member: !group azure-apps

After loading this policy, make sure you enable the Azure Authenticator with the following command, run from a conjur_client shell:

curl -v -k -H "$(conjur authn authenticate -H)" -X PATCH --data "enabled=true" https://proxy/authn-azure/AzureWS1/myConjurAccount

As you continue to follow the instructions, define the variables holding database secrets with the following policy:

- !policy
  id: secrets
  body:
    - !group consumers

    - !variable endpoint
    - !variable authKey

    - !permit
      role: !group consumers
      privilege: [ read, execute ]
      resource: !variable endpoint

    - !permit
      role: !group consumers
      privilege: [ read, execute ]
      resource: !variable authKey

- !grant
  role: !group secrets/consumers
  member: !group azure-apps

We use these commands (run on the conjur_client container) to set the values:

conjur variable values add secrets/endpoint
    https://conjurcosmosdemo.documents.azure.com:443/

conjur variable values add secrets/authKey <COSMOS DB Primary Key goes here>

One important feature of this configuration is that the important secret, the database master key, is not hard-coded anywhere. Only the people needing it have it, and you can easily change it if it becomes compromised.

Finally, after you complete the instructions, connect the “proxy” container to the Docker user-defined bridge you created when you set up the server. Then, expose port 8443 on the server firewall to provide external access to Conjur.

Incorporating an Azure Function

We used Visual Studio 2019 to create an example Azure Function. This particular function responds to an HTTP trigger, but you could use any other trigger. While the complete example is available, the code below highlights only the portions demonstrating how to interact with Conjur.

One crucial setup step is using the Azure Function identity feature to assign the managed identity (created earlier) to  this function. Also, you must test this within the Azure Portal as the code to return the function identity only runs within Azure, not in your local environment.

In the following code, the DemoItem class specified as the reference type to the Repository class template is a trivial class containing three properties: id, key, and value.

public class DemoItem
{
    public String Id { get; set; }
    public String Key { get; set; }
    public String Value { get; set; }

    public DemoItem()
    {
        Id = "none";
        Key = "no key";
        Value = "empty";
    }
}

The Run function for the Azure Function is below. The three key lines of code stand out to the left. We’ve separated the calls to get the database service endpoint and the authKey as they only need to be made once. We have also chosen to provide a wrapper class for database access to isolate the database operations that are just here to prove the values from Conjur work.

public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post",
                 Route = null)] HttpRequest req,
    HttpRequest httpRequest,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");
    string responseMessage = "";

    string name = req.Query["name"];

    responseMessage += string.IsNullOrEmpty(name)
        ? "For a personalized greeting, please pass a name in the”
          + “ query string or in the request body (the parameter name"
          + " is \"name\"). Here's what we found in the database:"
        : $"Hello, {name}. Here are the items in the database:";

    try
    {
        StreamReader reader = new StreamReader(req.Body);
        string requestBody = await reader.ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        name = name ?? data?.name;

        reader.Close();
        reader.Dispose();

        String endpoint = await ConjurAuthenticator.GetEndpoint();
        String authKey = await ConjurAuthenticator.GetAuthKey();

        responseMessage += "\r\nEndpoint: '" + endpoint + "'";
        responseMessage += "\r\nAuthKey: '" + authKey + "'";
        Task<IEnumerable<DemoItem>> itemList =
            CosmosDocumentRepository.Repository<DemoItem>.GetItemsAsync(
                endpoint, authKey);

         //Get all of the DB items
         IEnumerator<DemoItem> iter = itemList.Result.GetEnumerator();

         DemoItem item;
         for (int i = 1; iter.MoveNext(); i++)
         {
             item = iter.Current;
             responseMessage += "\r\nRow: " + i + "\t key: "
                                + item.Key + "\tvalue: " + item.Value;
         }
         responseMessage += "\r\n";
    }
    catch(Exception e)
    {
         log.LogError("Exception: " + e.Message);
         Console.WriteLine("Exception: " + e.Message);
    }

    return new OkObjectResult(responseMessage);
}

The class authenticating the Azure Function to Conjur and returning the secrets is below. The AzureServiceTokenProvider’ command returns the JWT that the application sends to Azure. It contains all the data listed in the policy. The AppId’ is found in Azure AD under Enterprise Applications. The https://management.azure.com/ URL directs the request to the Azure Instance Metadata Service (IMDS).

This URL is used in the POST webservice call to Conjur to obtain the access token we need to read the database endpoint and authKey.

https://<my VM Public IP>:8443/authn-azure/AzureWS1/myConjurAccount/host%2Fazure-apps%2FConjurCosmosDemoAccessFunction/authenticate

The general format is defined in the Conjur documentation, but you’ll see:

  • The AzureWS1 web service was defined in the first example policy.
  • myConjurAccount is passed in the URL.
  • The host azure-apps/ConjurCosmosDemoAccessFunction is defined in the second example policy. The value needs to be URL-encoded.

The JWT identifying our Azure Function is passed as a URL-encoded form in the body of the request. The string returned by this call is our access token to Conjur. If this request succeeds, we send a GET request to Conjur to obtain the secret value:

https://<my VM Public IP>:8443/secrets/myConjurAccount/variable/secrets%2F" + key;

The names sent here are the same names provided when setting the variables:

  • secrets/endpoint
  • secrets/authKey

These names also need to be URL encoded.

Here is the class code:

class ConjurAuthenticator
{
    public static async Task<String> GetEndpoint()
    {
        String retVal = await GetSecret("endpoint");
        return retVal;
    }

    public static async Task<String> GetAuthKey(
    {
       String retVal = await GetSecret("authKey");
       return retVal;
    }

    public static async Task<String> GetSecret(String key)
    {
        String retVal = "";

        try
        {
            // First, obtain the Managed Identity for this function
            var azureServiceTokenProvider = new
                AzureServiceTokenProvider("RunAs=App;AppId=<Tenancy ID>");
            Task<string> accessResponse =
                azureServiceTokenProvider.GetAccessTokenAsync(
                      "https://management.azure.com/");
            String jwt = await accessResponse;

            // Now that we have the JWT, authenticate to Conjur
            var values = new Dictionary<string, string>
                {
                    { "jwt", jwt }
                };

            var content = new FormUrlEncodedContent(values);

            String url = "https://<my VM Public IP>:8443/authn-azure/AzureWS1/myConjurAccount/host%2Fazure-apps%2FConjurCosmosDemoAccessFunction/authenticate";

            // This handler ignores self-signed certificates
            var handler = new HttpClientHandler();
            handler.ClientCertificateOptions =
                ClientCertificateOption.Manual;
            handler.ServerCertificateCustomValidationCallback =
                (httpRequestMessage, cert, cetChain, policyErrors) =>
                {
                    return true;
                };

            HttpClient client = new HttpClient(handler);
            HttpResponseMessage response;

            using (var requestMessage =
                new HttpRequestMessage(HttpMethod.Post, url))
            {
                requestMessage.Content = content;
                requestMessage.Headers.Add("Accept-Encoding", "base64");
                response = await client.SendAsync(requestMessage);

                if (response.IsSuccessStatusCode)
                {
                    var responseString =
                        await response.Content.ReadAsStringAsync();

                    // Now that we have the authentication token,
                    // obtain the secret
                    url = "https://<my VM Public IP>:8443/secrets/myConjurAccount/variable/secrets%2F" + key;
                    using (var getRequestMessage =
                        new HttpRequestMessage(HttpMethod.Get, url))
                    {
                        String token = responseString;
                        getRequestMessage.Headers.Authorization =
                            new AuthenticationHeaderValue("Token",
                                "token=\"" + token + "\"");
                        response = await
                             client.SendAsync(getRequestMessage);
                        if (response.IsSuccessStatusCode)
                        {
                            retVal =
                               await response.Content.ReadAsStringAsync();
                        }
                    }
                }
                else
                {
                    retVal += "Auth Response: "
                           + response.Content.ReadAsStringAsync() + "\n";
                }
            }
        }
        catch(Exception e)
        {
            retVal += "Exception: " + e.Message;
        }

        return retVal;
    }
}

When you have implemented the Cosmos DB database, Conjur, and the Azure Function, and you test the function in the Azure Portal, you will see a response like this:

For a personalized greeting, please pass a name in the query string or in the request body (the parameter name is "name"). Here's what we found in the database:
Endpoint: 'https://conjurcosmosdemo.documents.azure.com:443/'
AuthKey: 'dUcMaAc9....uq2r6HWSug=='
Row: 1      key: FirstKey    value: First Value
Row: 2      key: no key      value: empty
Row: 3      key: Key1  value: Value1

This confirms you have successfully implemented Conjur to manage secrets security on Azure Active Directory.

Next Steps

Our example used a host resource to authenticate the Azure Function. We did this to keep the example simple, but in a real implementation, we would normally associate a layer resource with the Azure Function.

You should use hosts to represent long-lived processes and services, such as a virtual machine, and layers to represent the short-lived services running on the hosts. This provides you a way to say a service is only accessible from a specific host, such as allowing an administrator who can log into a Bastion host to run a cleanup service only from that Bastion host.

This method also supports more complex permission layering like we see in identity providers like Microsoft Active Directory.

We also highlighted the use of managed identities by showing how to allow an Azure Function to access a secret. However, since we did this with an identity provided by Azure, we can apply this same model to anything having an identity. The key is associating the identity to a Conjur Group as we did in the Hosts policy.

Conjur’s Secretless Broker services provide full isolation between a service’s consumer and the provider, allowing services to be hosted anywhere and possibly moved between hosts without the consumer being aware of the changes. This ensures an application is not tied to a specific platform and yet is always secure.

Ready to boost your service provider security? Visit Conjur.org to investigate these services in detail and discover all the features Conjur provides to help keep your data secure.