vault

Elegant Cert Governance with Vault Identity and Sentinel Policy

Learn how using policy as code to enforce governance for certificate creation inside HashiCorp Vault reduces cost of ownership and lowers risk.

Sentinel is HashiCorp’s policy as code solution. It provides targeted, shift-left policy enforcement to ensure that organizational security, financial, and operational requirements are met across all workflows. While Sentinel is best known for its use with HashiCorp Terraform, it is embedded in all of HashiCorp’s enterprise offerings.

Sentinel can be used to create many custom guardrails. Examples include:

  • Requiring network access control lists (ACLs) on cloud storage created by Terraform Enterprise and Cloud.
  • Restricting access to specific secrets in Vault Enterprise to the corporate network.
  • Enforcing service naming standards in Consul Enterprise.
  • Restricting Nomad Enterprise jobs created by specific groups to the Docker driver.

In this blog post we are going to use Sentinel and Vault’s identity management system to enforce governance for certificate creation. This post assumes a general knowledge of HashiCorp Vault.

»The Problem

Here’s a hypothetical scenario: Acme Corp. wants to allow its applications in Microsoft Azure to generate short-lived certificates using HashiCorp Vault Enterprise. Each application should get a certificate with a common name of <application name>.acme-app.com. Applications should not be able to get certificates in the acme-app.com domain for other common names.

Acme’s security team wants to avoid giving developers credentials to the secrets management platform, which could result in frequent issues around the secret zero problem. The security team also wants to minimize the amount of administration required on Vault.

»The Solution

One solution to this problem is to build a Vault PKI secrets engine role for each application. Each role would control which certificate common names are allowed for that role. Applications would be granted rights to their respective PKI role through Vault security policy.

However, scalability is a challenge for that approach. A thousand applications would mean a thousand roles, with each role being identical except for the allowed common name. That’s a lot of duplication, onboarding overhead, and administrative complexity, even using templated Vault policies.

A more elegant solution is to use Vault’s identity system and Sentinel to govern access to a single endpoint which can create certificates for any child domain of acme-app.com.

»Workflow

This solution combines the PKI secrets engine, Azure authentication, Vault identity, and Sentinel. The workflow looks like this:

Diagram of this solution combining the PKI secrets engine, Azure authentication, Vault identity, and Sentinel.
  1. The workload uses the Vault SDK to authenticate with Vault using the Azure managed identity of the virtual machine.
  2. Vault verifies access to a PKI secrets engine endpoint using Vault ACL policies.
  3. Sentinel verifies that the identity for the workload is authorized to use the common name that it requested.

Let’s dig in.

»Vault PKI

Vault’s PKI secrets engine will be used to issue certificates to the client workloads. For this example we will configure Vault to be a root certificate authority (CA).

vault secrets enable pki

vault write pki/root/generate/internal \
 common_name=acme.com \
 ttl=8760h

vault write pki/config/urls \
 issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" \
 crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl"
vault secrets enable pki
 
vault write pki/root/generate/internal \
 common_name=acme.com \
 ttl=8760h
 
vault write pki/config/urls \
 issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" \
 crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl"

One of the solution’s goals is to simplify management of Vault endpoints. The API endpoint to issue certificates to our workloads is configured to issue certificates to any subdomain of acme-app.com. This endpoint meets our requirement for management simplicity but not our common-name requirements. That will be addressed later:

vault write pki/roles/app \
 allowed_domains=acme-app.com \
 allow_subdomains=true \
 max_ttl=2h
vault write pki/roles/app \
 allowed_domains=acme-app.com \
 allow_subdomains=true \
 max_ttl=2h

A Vault ACL policy called pki-local policy grants access to our certificate endpoint. It looks like this:

path "pki/issue/app" {
 capabilities = ["create", "update"]
}
path "pki/issue/app" {
 capabilities = ["create", "update"]
}

For more information about using the PKI secrets engine, refer to the HashiCorp Learn topics on intermediate and root CA use cases.

»Azure Authentication

Secret zero, also called secure introduction, is a challenge in secrets management. How can a workload access secrets without having to first pass that workload a credential, itself a secret, to access the secrets management platform? Our problem statement identifies secret zero as a problem to be solved.

HashiCorp Vault provides authentication methods that solve the secret zero problem. One such method is the Azure authentication method. The Azure authentication method uses an Azure managed identity assigned to the workload to authenticate with Vault. An Azure resource such as a virtual machine (VM) or Azure Function has the managed identity built or assigned at creation. Vault SDKs allow developers to use the managed identity to authenticate with Vault without needing access to a password or any other kind of secret.

(Note: Rob Barnes’ blog post on Integrating Azure AD Identity with HashiCorp Vault — Part 3: Azure Managed Identity Auth via Azure Auth Method is an excellent examination of the Azure authentication method.)

We need to create a role within the Azure authentication endpoint in Vault. The role can filter by a number of Azure attributes. For this demonstration we are going to create a role called appcert and limit access to the role by subscription id and resource group of the managed identity.

vault write auth/azure/role/appcert \
 token_policies="default,pki-local" \
 bound_subscription_ids=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
 bound_resource_groups=Csmith-proj-sentinel-resources
vault write auth/azure/role/appcert \
 token_policies="default,pki-local" \
 bound_subscription_ids=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
 bound_resource_groups=Csmith-proj-sentinel-resources

This authentication endpoint allows any Azure workload running within the given subscription and the Csmith-proj-sentinel-resources resource group to authenticate. Workloads authenticating to this endpoint are given a token with the pki-local policy. With this policy, the workloads will be able to generate certificates for child domains of the acme-app.com domain.

Next, we build infrastructure to run our workload. For our demo we will create a virtual machine. We will also create a user-defined managed identity and assign it to the VM. The identity can be used on multiple VMs or other Azure resources. The important thing to remember is that the identity is tied to the workload, not to the specific Azure resource.

A snippet of the relevant Terraform code looks like this:

resource "azurerm_user_assigned_identity" "ident_demo_app" {
  resource_group_name = azurerm_resource_group.proj-rg.name
  location            = azurerm_resource_group.proj-rg.location

  name = "demo_app_csmith_sentinel"
}


// Bastion Host Compute
resource "azurerm_linux_virtual_machine" "bastion" {
  name                = "${var.prefix}-bastion-machine"
  resource_group_name = azurerm_resource_group.proj-rg.name
  location            = azurerm_resource_group.proj-rg.location
  size                = "Standard_F2"
  admin_username      = "adminuser"
  network_interface_ids = [
    azurerm_network_interface.bastion-nic.id,
  ]

  admin_ssh_key {
    username   = "adminuser"
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  identity {
    type = "UserAssigned"
    identity_ids = [azurerm_user_assigned_identity.ident_demo_app.id]
  }

  source_image_reference {
    publisher = var.linux-info.publisher
    offer     = var.linux-info.offer
    sku       = var.linux-info.sku
    version   = var.linux-info.version
  }
}

output "identity_id" {
  value = azurerm_user_assigned_identity.ident_demo_app.principal_id
}
resource "azurerm_user_assigned_identity" "ident_demo_app" {
  resource_group_name = azurerm_resource_group.proj-rg.name
  location            = azurerm_resource_group.proj-rg.location
 
  name = "demo_app_csmith_sentinel"
}
 
 
// Bastion Host Compute
resource "azurerm_linux_virtual_machine" "bastion" {
  name                = "${var.prefix}-bastion-machine"
  resource_group_name = azurerm_resource_group.proj-rg.name
  location            = azurerm_resource_group.proj-rg.location
  size                = "Standard_F2"
  admin_username      = "adminuser"
  network_interface_ids = [
    azurerm_network_interface.bastion-nic.id,
  ]
 
  admin_ssh_key {
    username   = "adminuser"
    public_key = var.ssh_public_key
  }
 
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
 
  identity {
    type = "UserAssigned"
    identity_ids = [azurerm_user_assigned_identity.ident_demo_app.id]
  }
 
  source_image_reference {
    publisher = var.linux-info.publisher
    offer     = var.linux-info.offer
    sku       = var.linux-info.sku
    version   = var.linux-info.version
  }
}
 
output "identity_id" {
  value = azurerm_user_assigned_identity.ident_demo_app.principal_id
}

»Vault Identity

So far we have:

  • A Vault endpoint to distribute certificates to child domains of acme-app.com.
  • An Azure authentication endpoint that will authenticate Azure managed identities within a specific resource group and subscription.
  • A policy allowing workloads authenticated by the Azure endpoint to generate certificates using the PKI secrets endpoint.
  • A VM running under a user-defined managed identity that meets the criteria of the Azure endpoint.

If we stopped here, workloads on the VM would be able to generate certificates without secret zero challenges, but we have not completely solved our problem. The workload has the ability to create a certificate for a child domain of acme-app.com with any common name. We need to restrict the common name allowed by the application.

To restrict by common name we will build an entity in Vault for the workload. The entity will combine the Azure managed identity with user-defined metadata. Conceptually, it will look like this:

Vault identity entity

In this entity:

  • The entity is the name of the workload or application.
  • The alias name is the Azure object ID for the managed identity.
  • The metadata allowed_app is the common name that we want the Azure managed identity to be able to use for certificates, in this case alpha.

The entity can be created in the same Terraform code used to create the workload VM:

resource "azurerm_user_assigned_identity" "ident_demo_app" {
  resource_group_name = azurerm_resource_group.proj-rg.name
  location            = azurerm_resource_group.proj-rg.location

  name = "demo_app_csmith_sentinel"
  tags = local.common_tags
}

/*
SNIP...other Terraform removed
*/

provider "vault" {
  # This will default to using $VAULT_ADDR
  # But can be set explicitly
  address = var.vault_addr
  token = var.token
}

data "vault_auth_backend" "azure_auth" {
  path = "azure"
}

resource "vault_identity_entity" "demo_app" {
  name      = "demo_app_csmith_sentinel"
  policies  = ["default"]
  metadata  = {
    allowed_app = "alpha"
  }
}

resource "vault_identity_entity_alias" "demo_alias" {
  name            = azurerm_user_assigned_identity.ident_demo_app.principal_id
  mount_accessor  = data.vault_auth_backend.azure_auth.accessor
  canonical_id    = vault_identity_entity.demo_app.id
}
resource "azurerm_user_assigned_identity" "ident_demo_app" {
  resource_group_name = azurerm_resource_group.proj-rg.name
  location            = azurerm_resource_group.proj-rg.location
 
  name = "demo_app_csmith_sentinel"
  tags = local.common_tags
}
 
/*
SNIP...other Terraform removed
*/
 
provider "vault" {
  # This will default to using $VAULT_ADDR
  # But can be set explicitly
  address = var.vault_addr
  token = var.token
}
 
data "vault_auth_backend" "azure_auth" {
  path = "azure"
}
 
resource "vault_identity_entity" "demo_app" {
  name      = "demo_app_csmith_sentinel"
  policies  = ["default"]
  metadata  = {
    allowed_app = "alpha"
  }
}
 
resource "vault_identity_entity_alias" "demo_alias" {
  name            = azurerm_user_assigned_identity.ident_demo_app.principal_id
  mount_accessor  = data.vault_auth_backend.azure_auth.accessor
  canonical_id    = vault_identity_entity.demo_app.id
}

Alternatively, it can be created from the Vault CLI:

#!/usr/bin/env bash
set -euo pipefail 

readonly ALIAS_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

ACCESSOR_ID=$(vault auth list -format=json | jq -r '.["azure/"].accessor')

ENTITY_ID=$(vault write -format=json identity/entity name="demo_app_csmith_sentinel" policies="default" \
     metadata=allowed_app="alpha" \
     | jq -r ".data.id") 

vault write identity/entity-alias name="$ALIAS_ID" \
     canonical_id=$ENTITY_ID \
     mount_accessor=$ACCESSOR_ID > /dev/null

echo "Complete"
#!/usr/bin/env bash
set -euo pipefail 
 
readonly ALIAS_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
 
ACCESSOR_ID=$(vault auth list -format=json | jq -r '.["azure/"].accessor')
 
ENTITY_ID=$(vault write -format=json identity/entity name="demo_app_csmith_sentinel" policies="default" \
     metadata=allowed_app="alpha" \
     | jq -r ".data.id") 
 
vault write identity/entity-alias name="$ALIAS_ID" \
     canonical_id=$ENTITY_ID \
     mount_accessor=$ACCESSOR_ID > /dev/null
 
echo "Complete"

»Sentinel Policy

The final step is to attach a Sentinel policy to govern the common names that a workload can use. Sentinel policies in Vault come in two different flavors. The first type of policy is the role governing policy (RGP). RGPs are attached directly to Vault entities or authentication aliases. These policies are evaluated when authentication occurs.

The other type of Sentinel policy in Vault is an endpoint governing policy (EGP). EGPs are attached to a Vault path. These policies activate when an authenticated and authorized user or workload attempts to access secrets at the targeted path. EGP policies can use metadata from Vault entities as part of their access calculations. This provides a more finely tuned access policy than what is provided by standard Vault ACL policies. Our policy is an EGP:

import "strings"

id_value = identity.entity.metadata.allowed_app + ".acme-app.com"
host_req = request.data["common_name"]

cert_ident = rule {
    host_req == id_value
}

main = rule  {
    cert_ident
}
import "strings"
 
id_value = identity.entity.metadata.allowed_app + ".acme-app.com"
host_req = request.data["common_name"]
 
cert_ident = rule {
    host_req == id_value
}
 
main = rule  {
    cert_ident
}

This policy:

  • Builds an acceptable common name from the entity metadata.
  • Compares the acceptable common name to the name in the request for a certificate.
  • Allows work to go ahead if they match.

The policy is attached to the pki/issue/app endpoint.

»Testing

Let’s put it all together from the developer’s viewpoint. The developer builds an application on .NET Core using the VaultSharp SDK. The workload will run on Azure compute using the managed identity and Vault entity that we configured. The developer does not need to pass any credentials to Vault. The SDK does that using the managed identity of the VM.

Let’s use this sample application to test our solution. The application requests a certificate from the pki/issue/app endpoint with the common name of the VAULT_COMMON_NAME environmental variable prepended to .acme-app.com.

Vault builds a cert when the developer asks for a common name of alpha:

A certificate being generated for common name alpha.

But when the developer asks for beta:

A certificate for common name beta will not be generated.

We can see that the EGP policy pki-protect has been tripped and no certificate has been generated.

»Conclusion

Sentinel is a key part of enforcing corporate governance in all HashiCorp Enterprise products. Identity is at the heart of Vault. Pairing Sentinel with Vault’s entity model enables more granular authorization and allows a simplified secrets architecture — one that doesn’t require the creation and management of hundreds or thousands of roles.

»Resources

Sign up for the latest HashiCorp news