Vault
Manage access to Vault with joint controller authorization
Enterprise only
Control groups require a Vault Enterprise license or HCP Vault Dedicated standard tier cluster.
Control groups allow you to require additional authorization factors before processing requests. These additional authorizations help you increase the governance, accountability, and security of your secrets. When you specify a control group for a request, the requesting client receives the wrapping token in return. When all authorizations are satisfied, the client can use the wrapping token to unwrap the requested secrets.
Challenge
To operate in EU, a company must abide by the General Data Protection Regulation (GDPR) as of May 2018. The regulation enforces two or more controllers jointly determine the purposes and means of processing (Chapter 4: Controller and Processor).
Consider the following scenarios:
Anytime an authorized user requests to read data at "
EU_GDPR_data/data/orders/*", at least two people from the security group must approve to ensure that the user has a valid business reason for requesting the data.Anytime you update a database configuration, it requires that one person from the DBA group and one person from the security group must approve it.
Solution
Use control groups in your policies to implement dual controller authorization requirements.
Prerequisites
To perform the tasks described in this tutorial, you need to have the following:
An HCP Vault Dedicated cluster or a Vault Enterprise environment and license.
jq installed to process the JSON output for readability.
This tutorial assumes that you have some hands-on experience with ACL policies as well as identities. If you are not familiar, go through the Vault get started tutorial series.
Policy requirements
Since this tutorial demonstrates the creation of policies, log in with a highly
privileged token such as root. Otherwise, review the required permissions to perform the
steps in this tutorial.
# Create and manage ACL policies
path "sys/policies/acl/*"
{
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# To enable secrets engines
path "sys/mounts/*" {
capabilities = [ "create", "read", "update", "delete" ]
}
# Setting up test data
path "EU_GDPR_data/*"
{
capabilities = ["create", "read", "update", "delete", "list"]
}
# Manage userpass auth method
path "auth/userpass/*"
{
capabilities = ["create", "read", "update", "delete", "list"]
}
# List, create, update, and delete auth methods
path "sys/auth/*"
{
capabilities = ["create", "read", "update", "delete"]
}
# Create and manage entities and groups
path "identity/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
Scenario introduction
The scenario in this tutorial is that a user, Bob Smith has
read-only permission on the "EU_GDPR_data/data/orders/*" path; however,
someone in the acct_manager group must approve it before he can actually
read the data.
As a member of the acct_manager group, Ellen Wright can authorize
Bob's request.

Personas:
- admin with privileged permissions to create policies and identities
- processor with limited permission to access secrets
- controller with permission to approve secret access
Scenario workflow
You are going to perform the following:
- Lab setup
- Implement a control group
- Deploy the policies
- Setup entities and a group
- Test the control group
If you want to implement control groups in Sentinel, read the ACL Policies vs. Sentinel Policies section.
Lab setup
Note
You must use a HCP Vault Dedicated standard tier cluster to complete this tutorial. Visit the Create a Vault Cluster on HCP tutorial for the steps to create a cluster.
Launch the HCP Portal and login.
Click Vault in the left navigation pane.
In the Vault clusters pane, click vault-cluster.
Under Cluster URLs, click Public Cluster URL.

In a terminal, set the
VAULT_ADDRenvironment variable to the copied address.$ export VAULT_ADDR=<Public_Cluster_URL>Return to the Overview page and click Generate token.

Copy the Admin Token.

Return to the terminal and set the
VAULT_TOKENenvironment variable.$ export VAULT_TOKEN=<token>Set the
VAULT_NAMESPACEenvironment variable toadmin.$ export VAULT_NAMESPACE=adminThe
adminnamespace is the top-level namespace automatically created by HCP Vault. All CLI operations default to use the namespace defined in this environment variable.Type
vault statusto verify your connectivity to the Vault cluster.$ vault status Key Value --- ----- Recovery Seal Type shamir Initialized true Sealed false Total Recovery Shares 1 Threshold 1 Version 1.9.2+ent Storage Type raft ...snipped...The Vault Dedicated server is ready.
Implement a control group
(Persona: admin)
Create a policy file named
read-gdpr-order.hcl.$ tee read-gdpr-order.hcl <<EOF path "EU_GDPR_data/data/orders/*" { capabilities = [ "read" ] control_group = { factor "authorizer" { identity { group_names = [ "acct_manager" ] approvals = 1 } } } } EOFExamine the policy
The condition is that Bob can
readthe secrets atEU_GDPR_data/data/orders/*if someone from theacct_managergroup approves.read-gdpr-order.hcl
path "EU_GDPR_data/data/orders/*" { capabilities = [ "read" ] control_group = { factor "authorizer" { identity { group_names = [ "acct_manager" ] approvals = 1 } } } }For the purpose of this tutorial, set the number of
approvalsto1. Any member of the identity group,acct_managercan approve the read request. Although this example has only one factor (authorizer), you can add more factor blocks to meet your needs.Create another policy file named,
acct_manager.hcl. This is the policy needed for the member of controller (acct_manager) to approve Bob's request.$ tee acct_manager.hcl <<EOF # To approve the request path "sys/control-group/authorize" { capabilities = ["create", "update"] } # To check control group request status path "sys/control-group/request" { capabilities = ["create", "update"] } EOFThe authorizer must have
createandupdatecapabilities on thesys/control-group/authorizeendpoint so that they can approve the request.
Deploy the policies
(Persona: admin)
Deploy the read-gdpr-order and acct_manager policies that you wrote.
Create a new policy named
read-gdpr-order.$ vault policy write read-gdpr-order read-gdpr-order.hcl Success! Uploaded policy: read-gdpr-orderCreate a new policy named
acct_manager.$ vault policy write acct_manager acct_manager.hcl Success! Uploaded policy: acct_manager
Setup entities and a group
(Persona: admin)
Now that you have policies created, create a user bob, and an acct_manager group with
ellen as a group member.
Note
For the purpose of this tutorial, use the userpass auth method to
create the users bob and ellen so you can test the scenario.
Enable the
userpassauth method.$ vault auth enable userpass Success! Enabled userpass auth method at: userpass/Create a new user,
bobwith password, "training".$ vault write auth/userpass/users/bob password="training" Success! Data written to: auth/userpass/users/bobCreate a new user,
ellenwith password, "training".$ vault write auth/userpass/users/ellen password="training" Success! Data written to: auth/userpass/users/ellenRetrieve the userpass mount accessor and save it in a file named
accessor.txt.$ vault auth list -format=json | jq -r '.["userpass/"].accessor' > accessor.txtCreate
Bob Smithentity and save the identity ID in theentity_id_bob.txt.$ vault write -format=json identity/entity name="Bob Smith" \ policies="read-gdpr-order" \ metadata=team="Processor" \ | jq -r ".data.id" > entity_id_bob.txtAdd an entity alias for the
Bob Smithentity.$ vault write identity/entity-alias name="bob" \ canonical_id=$(cat entity_id_bob.txt) \ mount_accessor=$(cat accessor.txt)Example output:
Key Value --- ----- canonical_id e57d0eff-ca1d-d4d8-b7b7-2b3f842b811d id 09bdc8af-9bb2-950d-ac63-cfb96fce4d77Create
Ellen Wrightentity and save the identity ID in theentity_id_ellen.txt.$ vault write -format=json identity/entity name="Ellen Wright" \ policies="default" \ metadata=team="Acct Controller" \ | jq -r ".data.id" > entity_id_ellen.txtAdd an entity alias for the
Ellen Wrightentity.$ vault write identity/entity-alias name="ellen" \ canonical_id=$(cat entity_id_ellen.txt) \ mount_accessor=$(cat accessor.txt)Example output:
Key Value --- ----- canonical_id 78762f37-a651-229d-6db3-a25cbb71070f id 94499a59-5890-9aef-3810-37ffdc6b7511Create
acct_managergroup and addEllen Wrightentity as a member.$ vault write identity/group name="acct_manager" \ policies="acct_manager" \ member_entity_ids=$(cat entity_id_ellen.txt)Example output:
Key Value --- ----- id 898f0f44-e2c1-efcf-7a55-2c1c6c4bf1ae name acct_manager
Test the control group
(Persona: admin)
Validate the control groups work by request a secret as Bob and approving the request as Ellen.
Enable the key/value secrets engine at
EU_GDPR_data.$ vault secrets enable -path=EU_GDPR_data -version=2 kv Success! Enabled the kv secrets engine at: EU_GDPR_data/Write some mock data.
$ vault kv put EU_GDPR_data/orders/acct1 \ order_number="12345678" product_id="987654321"Example output:
========= Secret Path ========= EU_GDPR_data/data/orders/acct1 ======= Metadata ======= Key Value --- ----- created_time 2022-05-18T19:11:20.105114Z custom_metadata <nil> deletion_time n/a destroyed false version 1
(Persona: processor)
Unset the
rootVault token.$ unset VAULT_TOKENLog in as
bob.$ vault login -method=userpass username="bob" Password (will be hidden):Enter the password
trainingwhen prompted, and pressreturn.Request to read "
EU_GDPR_data/orders/acct1".$ vault kv get EU_GDPR_data/orders/acct1Example output:
Key Value --- ----- wrapping_token: hvs.AmhW6ULQ7Eoy6o5JMHr679Z0 wrapping_accessor: Vbs3d5FGbeox1mjYOgLWvXsP wrapping_token_ttl: 24h wrapping_token_creation_time: 2021-07-21 19:42:35 -0700 PDT wrapping_token_creation_path: EU_GDPR_data/data/orders/acct1The response includes
wrapping_tokenandwrapping_accessor. Export their values as environment variables for use in later steps.Note
Be sure to replace the example
wrapping_tokenandwrapping_accessorvalues with your actual values.Export the wrapping token value as
WRAPPING_TOKEN.$ export WRAPPING_TOKEN=<wrapping_token>Export the wrapping token accessor as
WRAPPING_ACCESSOR.$ export WRAPPING_ACCESSOR=<wrapping_accessor>Example:
$ export WRAPPING_TOKEN=hvs.AmhW6ULQ7Eoy6o5JMHr679Z0 $ export WRAPPING_ACCESSOR=Vbs3d5FGbeox1mjYOgLWvXsP
(Persona: controller)
A user who is a member of the acct_manager group can check
and authorize Bob's request using the request and authorize
commands.
Log in as
ellenwho is a member ofacct_managergroup.$ vault login -method=userpass username="ellen" Password (will be hidden):Enter the password
trainingwhen prompted, and pressreturn.Check the current status.
$ vault write sys/control-group/request accessor=$WRAPPING_ACCESSOR Key Value --- ----- approved false authorizations <nil> request_entity map[name:Bob Smith id:38700386-723d-3d65-43b7-4fb44d7e6c30] request_path EU_GDPR_data/orders/acct1The
approvedstatus isfalsesince Ellen has not approved the request.Approve the request.
$ vault write sys/control-group/authorize accessor=$WRAPPING_ACCESSOR Key Value --- ----- approved trueNow, the
approvedstatus istrue.
(Persona: processor)
Since the control group requires one approval from a member of acct_manager
group, you have met the required condition. Log back in as bob and unwrap the
secret.
Log back in as bob using the userpass auth method.
$ vault login -method=userpass username="bob" Password (will be hidden):Enter the password
trainingwhen prompted, and pressreturn.Unwrap the secrets by passing the
$WRAPPING_TOKENenvironment variable.$ vault unwrap $WRAPPING_TOKEN Key Value --- ----- data map[order_number:12345678 product_id:987654321] metadata map[created_time:2021-03-24T16:25:20.405776245Z deletion_time: destroyed:false version:1]
Define a control group for operations
Assume that Bob's token has read-paris and change-paris policies attached.
The read-paris allows Bob to perform read and list operations against the
paris-kv/* path.
read-paris.hcl
path "paris-kv/*" {
capabilities = [ "read", "list" ]
}
The change-paris policy requires 1 approval from eng-managers group if Bob
tries to perform create, update or delete operation against the paris-kv/*
path.
change-paris.hcl
path "paris-kv/*" {
capabilities = [ "create", "update", "delete" ]
control_group = {
factor "managers" {
identity {
group_names = [ "eng-managers" ]
approvals = 1
}
}
}
}
The intention was that Bob can read or list secrets at paris-kv/* without
authorization. However, with both of those policies applied, even the read
and list operations will trigger the control groups. The read and list
capabilities from the read-paris policy get aggregated into the change-paris
capabilities list.
Controlled capabilities
To solve this, use controlled_capabilities in the policy to narrow the scope
of control group to the operation level.
restrict-paris.hcl
path "paris-kv/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
control_group = {
factor "managers" {
controlled_capabilities = [ "create", "update", "delete" ]
identity {
group_names = [ "acct_manager" ]
approvals = 1
}
}
}
}
Test controlled capabilities
Log in with the admin persona's token.
Note
For Vault Enterprise, use
rootas the token value. For HCP Vault Dedicated, use the admin token from the HCP Portal.$ vault login Token (will be hidden):Enter the actual token value when prompted, and press
return.Create a
restrict-parispolicy.$ tee restrict-paris.hcl <<EOF path "paris-kv/*" { capabilities = [ "create", "read", "update", "delete", "list" ] control_group = { factor "managers" { controlled_capabilities = [ "create", "update", "delete" ] identity { group_names = [ "acct_manager" ] approvals = 1 } } } } EOFDeploy the
restrict-parispolicy.$ vault policy write restrict-paris restrict-paris.hcl Success! Uploaded policy: restrict-parisAdd the policy to Bob's entity.
$ vault write identity/entity/id/$(cat entity_id_bob.txt) \ policies="read-gdpr-order,restrict-paris"Example output:
Success! Data written to: identity/entity/id/84f39aab-8ae2-a647-6a9d-hiJKlmnopEnable
kv-v2secrets engine atparis-kv.$ vault secrets enable -path="paris-kv" kv-v2 Success! Enabled the kv-v2 secrets engine at: paris-kv/Create some mock data at
paris-kv.$ vault kv put paris-kv/product name="Boundary" version="0.4.0" ==== Secret Path ==== paris-kv/data/product ======= Metadata ======= Key Value --- ----- created_time 2022-05-18T19:18:16.553795Z custom_metadata <nil> deletion_time n/a destroyed false version 1Log in as
bob.$ vault login -method=userpass username="bob" Password (will be hidden):Enter the password
trainingwhen prompted, and pressreturn.The returned token should have
restrict-parispolicy attached.Example output:
Key Value --- ----- token hvs.vAUdsbhpSZv0CqsC5qAqdaQ3 token_accessor VHuhVHCxzZlWh4qbXTHaPGak token_duration 768h token_renewable true token_policies ["default"] identity_policies ["read-gdpr-order" "restrict-paris"] policies ["default" "read-gdpr-order" "restrict-paris"] token_meta_username bobRead the secrets at
paris-kv/product.$ vault kv get paris-kv/product ==== Secret Path ==== paris-kv/data/product ======= Metadata ======= Key Value --- ----- created_time 2022-05-18T19:18:16.553795Z custom_metadata <nil> deletion_time n/a destroyed false version 1 ===== Data ===== Key Value --- ----- name Boundary version 0.4.0You should be able to read the secrets without triggering the control group.
Try to delete the secrets at
paris-kv/product.$ vault kv delete paris-kv/productThis time, Vault returns
wrapping_tokenandwrapping_accessor.Example output:
Key Value --- ----- wrapping_token: hvs.6myj2lXu7xg39CjrUFiLrwSV wrapping_accessor: X9AISHC5GSMGyDNpoYG3tw8s wrapping_token_ttl: 24h wrapping_token_creation_time: 2021-07-21 22:12:16 -0700 PDT wrapping_token_creation_path: paris-kv/data/releases
Optional: If you want to complete the rest of the workflow, repeat the steps you performed earlier in the Test the control group section.
- Login as
ellen. - Authorize Bob's request using the wrapping accessor value.
- Login as
bob. - Unwrap the secrets using the wrapping token.
ACL policy vs. Sentinel policy
Although this tutorial shows writing
read-gdpr-order.hcl as an ACL policy, you
can implement control groups in either ACL or Sentinel policies.
Using Sentinel, the same policy may look something like.
read-gdpr-order.sentinel
import "controlgroup"
control_group = func() {
numAuthzs = 0
for controlgroup.authorizations as authz {
if "acct_manager" in authz.groups.by_name {
numAuthzs = numAuthzs + 1
}
}
if numAuthzs >= 1 {
return true
}
return false
}
main = rule {
control_group()
}
Deploy this policy as an Endpoint Governing Policy attached to
"EU_GDPR_data/data/orders/*" path.
Tip
Refer to the Sentinel Properties documentation for the list of available properties associated with control groups. If you are new to Sentinel, go through the Sentinel Policies tutorial.







