Conjur controls access to secrets using role-based access control (RBAC). We cover this in detail in Policy Concepts, but, to summarize, Conjur uses policies to define “permissions”, “resources”, and “roles”, and to establish relationships between them.
Associating group resources with “roles” defines relationships that determine who accesses resources and what operations they can perform. For example, a role can allow one group to read a variable while allowing a different group to update that variable. Conjur policies are built using several types of resources, but this article will focus on the webservice, host, layer, and group resources.
While this article uses Azure serverless functions in the example, the same basic policy concepts apply to other platforms and applications.
We use the following resources to develop policies for serverless functions:
- Webservices to associate a name with data needed to use the service
- Hosts to associate a name with data needed to identify the host
- Layers to model the “many-to-many” relationships between hosts and variables
- Groups to define collections of users
Policies define relationships using two statement types: grant statements that determine which roles have access to resources and permit statements that assign rights to roles.
To do their job, functions need access to secrets that are stored in variables. These examples will use hosts to model functions, so we will grant host access permissions to variables. In the first example, we will grant these permissions directly to the hosts, but a better practice is to create a layer resource to decouple the hosts and variables, demonstrated in the second example.
Before a function can obtain a secret, it sends its identifying data to Conjur. Conjur looks for a host resource that has the same identifying data. If Conjur finds a resource, it uses the groups associated with the host to find a webservice to authenticate the function’s identifying data. Once Conjur verifies the host’s identity, Conjur returns a token to the function. The function then requests to read a variable with this token attached. In the groups associated with the host, Conjur finds the layer resource that provides access to the variables the function is requesting to read.
Azure uses system-assigned identities and user-managed identities to provide the identifying data mentioned above. While system-assigned identities are associated with a single function, user-managed identities can be assigned to many functions. A Conjur policy provides the same rights to all functions with the same identity, so you can grant the same rights to many functions by assigning them the same user-managed identity. Also, you can define a Conjur host resource as soon as you create the user-managed identity, so you can proactively create Conjur policies to provide access rights while deployment is in progress.
Conjur stores policies in a tree structure, starting with a “root” level, then branching into policies. Organizing policies this way allows “leaf” policies to inherit properties from other policies closer to the root.
Administrators write policies in text files using the YAML policy language syntax. Load policy files into Conjur using the “policy load” command. You can execute the command from the Conjur command-line interface (CLI) or by functions in the application programming interface (API).
Now that we have covered the concepts, let’s create a set of policies that will allow an Azure function to read a variable. For this example, we will grant access with just a webservice, host, and variable policy.
The version of the “policy load” command we use in this example has this form:
conjur policy load root policy-file-name.yml
To implement this example, save each policy described below in a separate text file, then execute the “policy load” command from the Conjur client to load each policy. For example, if the policies are in “myWebservicePolicy.yml”, “myHostPolicy.yml”, and “myVariablePolicy.yml” in the current working directory, you execute these commands from the Conjur client:
conjur policy load root myWebservicePolicy.yml conjur policy load root myHostPolciy.yml conjur policy load root myVariablePolciy.yml
The following webservice policy creates a resource that makes the Azure authenticator available to the host policy.
- !policy id: conjur/authn-azure/AzureWS1 body: - !webservice - !variable id: provider-uri - !group id: apps annotations: description: Define hosts that authenticate with this authenticator - !permit role: !group apps privilege: [ read, authenticate, update ] resource: !webservice
The “!policy” tag identifies the beginning of a new policy resource.
The “id” tag below the “!policy” tag does two things: it tells Conjur to store the policy in the “conjur/authn-azure” policy branch and it assigns the name “AzureWS1” to the policy.
Since the branch name does not start with a slash, you can store this policy within a branch such as a “dev” branch. If the branch started with a slash, such as “/conjur”, then the policy would be rooted and it would always be stored at the root level.
Various tags define the policy as follows:
- The “!webservice” tag defines that this is a webservice resource.
- The “!variable” tag is a variable, named “provider-uri”, that is created to contain the URL Conjur uses to access the authentication resource.
- The “!group” tag defines the “apps” group and associates it with this policy..
- The “!permit” tag grants members of the “apps” group “read”, “authenticate”, and “update” permissions on this webservice resource.
The policy does not store data in the “provider-uri” variable. It just creates a variable to hold this data. This allows us to fill in the data later and change the data without changing the policy.
The “annotations” tag in the “!group” definition holds documentation about the purpose of the group. While you can omit this, it is good practice to provide this information so others understand the group’s purpose.
The following policy, specific to Azure, defines a “hosts” group containing two host resources. Each host represents an Azure serverless function. We’ve assigned a system-assigned identity to one function and a user-managed identity to the other to show how both operate
- !policy id: azure-apps body: - !group - &hosts - !host id: ConjurDemoAccessFunctionSystemAssigned annotations: authn-azure/subscription-id: f89ce…..e91dc18 authn-azure/resource-group: Conjur_Resources authn-azure/system-assigned-identity: 7098...0761e13861 - !host id: ConjurDemoAccessFunctionUserAssigned annotations: authn-azure/subscription-id: f89ced...e91dc18 authn-azure/resource-group: Conjur_Resources authn-azure/user-assigned-identity: ConjurDemoAccess - !grant role: !group members: *hosts - !grant role: !group conjur/authn-azure/AzureWS1/apps member: !group azure-apps
The “!group” tag in the policy creates a group with the same name as the policy: azure-apps. The “hosts” collection starts with the “&hosts” tag and each host declaration starts with the “!host” tag. The tags under the “annotation” tag in each “!host” tag contain data that Azure sends in the JSON web token (JWT). These annotations are specific to the Azure authenticator.
The first “!grant” tag is part of the policy “body”, so the resources it creates will also be part of the policy. It declares that the members of the “&hosts” collection are also members of the “azure-apps” group (the “*hosts” tag means “all hosts”). You do not have to provide the group name in the “role” tag because the name of the policy is the default name for any resource created within a policy.
The second “!grant” tag is outside the policy. Note that this tag is not indented. It states that members of the “azure-apps” group are also members of the “conjur/authn-azure/AzureWS1/apps” group, which we created in the webservice policy. This is important because it tells Conjur which webservice to use to authenticate these hosts. Also, the “azure-apps” name is required in the “member” tag because the “!grant” is outside the policy.
The last policy of this example, below, allows each “host” to be authenticated using the “webservice” to read data from two variables.
- !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
This policy creates a group named “consumers”, a variable named “endpoint”, and a variable named “authKey”. The two “!permit” statements in the policy grant “read” and “execute” privileges on the two variables to members of the “consumers” group.
The last “!grant” statement, outside the policy, adds members of the “azure-apps” group to the “consumers” group created by this policy. This adds the “hosts” collection to the “consumers” group so the hosts can access these data items.
As we saw in the webservice policy, the policy doesn’t store data in these variables. It just creates a secure place for the data, which you can add or update using other Conjur commands (see “Examples – Use the CLI to load and set the variable value”). Conjur keeps the last twenty versions of a secret (that is, the value assigned to a variable) and provides a way to retrieve a specific version of a secret, letting you pre-populate secrets.
Improving the Solution
In the previous set of policies, the host and variable policies are directly connected. This works for small implementations, but as you create more hosts — and when the host resources represent machines instead of services — it becomes difficult to manage all the “host to variable” relationships. The layer policy decouples the variables and hosts so they can change independently.
The first step to implementing this improvement is to create the layer resource, as shown below:
- !policy id: azureApps body: - !layer id: azureAppsLayer - !grant role: !layer azureAppsLayer members: - !host /azure-apps/ConjurDemoAccessFunctionSystemAssigned - !host /azure-apps/ConjurDemoAccessFunctionUserAssigned
The “!layer” tag creates the layer resource and the “!grant” tag associates the two hosts with this layer. The host names start with “/azure-apps” to indicate that the hosts are defined in a separate policy stored at the “root” level. If we did not include the leading slash, Conjur would throw an error because it would look for a host defined in the “azureApps” policy.
Modify the Variable Policy
The final step is to break the relationship between the variables and the hosts. We need to replace the current “secrets” policy with an updated version that removes the “!grant” tag and changes the “!permit” tag to give “read” and “execute” privileges to the layer resource we just created.
- !policy id: secrets body: - !variable endpoint - !variable authKey - !permit resource: !variable endpoint role: !layer /azureApps/azureAppsLayer privilege: [ read, execute ] - !permit resource: !variable authKey role: !layer /azureApps/azureAppsLayer privilege: [ read, execute ] - !delete record: !group secrets/consumers
The “!delete” tag removes the “secrets/consumers” group, breaking the relationship between the “azure-apps” group and the variables. We have to load this policy with the following “policy load’ command to delete the group resource:
conjur policy load --delete root myNewVariablePolicy.yml
With this structure in place, the group managing the host policy can add a newly-created host, and withhold variable access until the host resource is added to the layer policy. The group managing the variables policy can create a new variable when needed, knowing which services and hosts have access to it without searching all the host policies.
As we pointed out earlier, policies are stored in a policy tree within Conjur. Different versions of the layer policy can exist within a hierarchy across different branches. This means the host policy and variables policy can exist at the root level, but the layer policy in each branch assigns appropriate access for that environment. This is discussed in detail in “Understanding Conjur Policy”
Conjur’s security-policy-as-code system provides the tools you need to express complex security relationships in code that can be safely checked into code repositories. The hierarchical policy structure enables you to build a set of common resources like authenticator webservices, hosts, and variables for use in all environments. The branch structure enables you to implement multiple specific layer resources that tailor access, in turn, to common resources, meeting requirements of specific environments.
Let’s review the key points illustrated in this article for defining policies for serverless functions:
- You need to define a webservice resource for each authenticator.
- Policies defining authenticator webservice resources are usually root-level policies unless the data associated with them changes for different environments.
- Host resources model functions.
- Host resources are associated with authenticators.
- Variables are defined in their own policies.
- Layer policies allow hosts to access variables.
Using user-managed identities, this policy structure creates and maintains entire permissions hierarchies separately from any application or service implementation. This is possible because the identity can be encoded in the policy before the infrastructure is created. You can grant or recall rights by changing the policies, and you can change policies without affecting any functions or machines.
My serverless example differs from CyberArk’s best practices, which recommend using host policies to model physical infrastructure and layer policies to model applications and functions. That practice assumes the host is a physical machine authenticating to Conjur. However, when a serverless function is authenticating to Conjur like a host would, it makes sense to model the serverless function as a host rather than a layer.
Conjur is platform independent, so applications running on any platform, such as Amazon Web Services (AWS) and Azure, can use the same secrets. Although the authenticators for each platform are different, everything else in the Conjur hierarchy remains the same.
These examples are just the starting point for investigating Conjur policies. In this article, you will find many links to Conjur documentation to help you investigate these topics further. You can also investigate the policy tutorials to learn more.
Check out our new fully hosted interactive tutorials for securing CI/CD pipelines, securing Ansible automation, and securing Kubernetes secrets. If you liked this tutorial then signup for our monthly DevOps security newsletter for developer community news, tutorials, and Conjur news.
Jeff Griffith initially thought he was interested in building computers but discovered that he greatly preferred using computers to solve problems. As a result, three years into a career in Digital System Design, he began developing desktop applications with Graphical User Interfaces before Windows was developed. From there, he worked on ultrasound machines, automatic test equipment and related data analysis applications, robot control systems, workflow applications, air traffic control systems, and a wide variety of web sites and web-based applications. Some systems have been very small, single user monolithic applications, and some have been enterprise-scale systems. He has worked with almost every design methodology and has been engaged in every step of the development of software systems from inception through to delivery and training (he has even written the manuals and test plans). When he is not developing solutions, Mr. Griffith enjoys amateur radio (he holds an Extra class license), film photography and he even plays a few online games.