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 to- admin.- $ export VAULT_NAMESPACE=admin- The - 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 } } } } EOF- Examine the policy- The condition is that Bob can - readthe secrets at- EU_GDPR_data/data/orders/*if someone from the- acct_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 - approvalsto- 1. 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"] } EOF- The authorizer must have - createand- updatecapabilities on the- sys/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-order
- Create 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/bob
- Create a new user, - ellenwith password, "training".- $ vault write auth/userpass/users/ellen password="training" Success! Data written to: auth/userpass/users/ellen
- Retrieve the userpass mount accessor and save it in a file named - accessor.txt.- $ vault auth list -format=json | jq -r '.["userpass/"].accessor' > accessor.txt
- Create - Bob Smithentity and save the identity ID in the- entity_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.txt
- Add 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-cfb96fce4d77
- Create - Ellen Wrightentity and save the identity ID in the- entity_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.txt
- Add 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-37ffdc6b7511
- Create - acct_managergroup and add- Ellen 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_TOKEN
- Log in as - bob.- $ vault login -method=userpass username="bob" Password (will be hidden):- Enter the password - trainingwhen prompted, and press- return.
- Request to read " - EU_GDPR_data/orders/acct1".- $ vault kv get EU_GDPR_data/orders/acct1- Example 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/acct1- The response includes - wrapping_tokenand- wrapping_accessor. Export their values as environment variables for use in later steps.- Note - Be sure to replace the example - wrapping_tokenand- wrapping_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 of- acct_managergroup.- $ vault login -method=userpass username="ellen" Password (will be hidden):- Enter the password - trainingwhen prompted, and press- return.
- 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/acct1- The - approvedstatus is- falsesince Ellen has not approved the request.
- Approve the request. - $ vault write sys/control-group/authorize accessor=$WRAPPING_ACCESSOR Key Value --- ----- approved true- Now, the - approvedstatus is- true.
(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 press- return.
- 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 } } } } EOF
- Deploy the - restrict-parispolicy.- $ vault policy write restrict-paris restrict-paris.hcl Success! Uploaded policy: restrict-paris
- Add 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-hiJKlmnop
- Enable - kv-v2secrets engine at- paris-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 1
- Log in as - bob.- $ vault login -method=userpass username="bob" Password (will be hidden):- Enter the password - trainingwhen prompted, and press- return.- 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 bob
- Read 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.0- You 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/product- This time, Vault returns - wrapping_tokenand- wrapping_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.







