Manage access to Vault with joint controller authorization
Enterprise only
Control groups require a Vault Enterprise Plus license or HCP Vault Dedicated plus tier cluster.
Control groups add additional authorization factors to be required before processing requests to increase the governance, accountability, and security of your secrets. When a control group is required for a request, the requesting client receives the wrapping token in return. Only when all authorizations are satisfied, the wrapping token can be used to unwrap the requested secrets.
Challenge
In order 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 a database configuration is updated, it requires that one person from the DBA and one person from Security group must approve it.
Solution
Use Control groups in your policies to implement dual controller authorization required.
Prerequisites
To perform the tasks described in this tutorial, you need to have the following:
- A Vault Enterprise environment or an HCP Vault Dedicated plus tier cluster.
- 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 following guides first:
Policy requirements
Since this tutorial demonstrates the creation of policies, log in with a highly
privileged token such as root
. Otherwise, required permissions to perform the
steps in this tutorial are listed below.
# Create and manage ACL policiespath "sys/policies/acl/*"{ capabilities = ["create", "read", "update", "delete", "list", "sudo"]} # To enable secrets enginespath "sys/mounts/*" { capabilities = [ "create", "read", "update", "delete" ]} # Setting up test datapath "EU_GDPR_data/*"{ capabilities = ["create", "read", "update", "delete", "list"]} # Manage userpass auth methodpath "auth/userpass/*"{ capabilities = ["create", "read", "update", "delete", "list"]} # List, create, update, and delete auth methodspath "sys/auth/*"{ capabilities = ["create", "read", "update", "delete"]} # Create and manage entities and groupspath "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
Download the latest version of the Vault Enterprise binary.
Extract the Vault binary where x.x.x is the actual version number.
$ unzip vault_x.x.x+ent.zip
Open a terminal and export an environment variable for the Vault Enterprise license generated by the sign up process.
$ export VAULT_LICENSE=<actual-license-key>
From the directory you extracted the Vault Enterprise binary, start a Vault dev server with
root
as the root token.$ ./vault server -dev -dev-root-token-id root
The Vault dev server defaults to running at
127.0.0.1:8200
. The server is also initialized and unsealed.Insecure operation
Do not run a Vault dev server in production. This approach is only used here to simplify the unsealing process for this demonstration.
Open a new terminal and export an environment variable with the Vault server address.
$ export VAULT_ADDR=http://127.0.0.1:8200
Export an environment variable with the Vault token.
$ export VAULT_TOKEN=root
Verify the Vault server started successfully with the trial license.
$ vault read sys/license/status WARNING! The following warnings were returned from Vault: * time left on license is 703h42m22s Key Value--- -----autoloaded map[expiration_time:2022-06-16T21:03:20Z features:...snip...]autoloading_used truepersisted_autoload map[expiration_time:2022-06-16T21:03:20Z features:...snip...]
The Vault server is ready.
Implement a control group
(Persona: admin)
Create a policy file named
read-gdpr-order.hcl
.$ tee read-gdpr-order.hcl <<EOFpath "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
read
the secrets atEU_GDPR_data/data/orders/*
if someone from theacct_manager
group 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, the number of
approvals
is set to1
to keep it simple and easy to test. Any member of the identity group,acct_manager
can approve the read request. Although this example has only one factor (authorizer
), you can add as many factor blocks as you need.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 requestpath "sys/control-group/authorize" { capabilities = ["create", "update"]} # To check control group request statuspath "sys/control-group/request" { capabilities = ["create", "update"]}EOF
The important thing here is that the authorizer must have
create
andupdate
permission on thesys/control-group/authorize
endpoint 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)
This step only demonstrates CLI commands and Web UI to create entities and groups. Refer to the Identity - Entities and Groups tutorial if you need the full details.
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 user bob
and ellen
so that the scenario can be easily tested.
Enable the
userpass
auth method.$ vault auth enable userpass Success! Enabled userpass auth method at: userpass/
Create a new user,
bob
with password, "training".$ vault write auth/userpass/users/bob password="training" Success! Data written to: auth/userpass/users/bob
Create a new user,
ellen
with 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 Smith
entity 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.txt
Add an entity alias for the
Bob Smith
entity.$ 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-2b3f842b811did 09bdc8af-9bb2-950d-ac63-cfb96fce4d77
Create
Ellen Wright
entity 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.txt
Add an entity alias for the
Ellen Wright
entity.$ 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-a25cbb71070fid 94499a59-5890-9aef-3810-37ffdc6b7511
Create
acct_manager
group and addEllen Wright
entity 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-2c1c6c4bf1aename 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.105114Zcustom_metadata <nil>deletion_time n/adestroyed falseversion 1
(Persona: processor)
Unset the
root
Vault token.$ unset VAULT_TOKEN
Log in as
bob
.$ vault login -method=userpass username="bob" password="training"
Request to read "
EU_GDPR_data/orders/acct1
".$ vault kv get EU_GDPR_data/orders/acct1
Example output:
Key Value--- -----wrapping_token: hvs.AmhW6ULQ7Eoy6o5JMHr679Z0wrapping_accessor: Vbs3d5FGbeox1mjYOgLWvXsPwrapping_token_ttl: 24hwrapping_token_creation_time: 2021-07-21 19:42:35 -0700 PDTwrapping_token_creation_path: EU_GDPR_data/data/orders/acct1
The response includes
wrapping_token
andwrapping_accessor
. Export their values as environment variables for use in later steps.Note
Be sure to replace the example
wrapping_token
andwrapping_accessor
values 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
ellen
who is a member ofacct_manager
group.$ vault login -method=userpass username="ellen" password="training"
Check the current status.
$ vault write sys/control-group/request accessor=$WRAPPING_ACCESSOR Key Value--- -----approved falseauthorizations <nil>request_entity map[name:Bob Smith id:38700386-723d-3d65-43b7-4fb44d7e6c30]request_path EU_GDPR_data/orders/acct1
The
approved
status is currentlyfalse
since it has not been approved.Approve the request.
$ vault write sys/control-group/authorize accessor=$WRAPPING_ACCESSOR Key Value--- -----approved true
Now, the
approved
status istrue
.
(Persona: processor)
Since the control group requires one approval from a member of acct_manager
group, the condition has been met. 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="training"
Unwrap the secrets by passing the
$WRAPPING_TOKEN
environment 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, when both of those policies are 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
Login with the admin persona's token.
Note
For Vault Enterprise, use
root
as the token value. For HCP Vault Dedicated, use the admin token from the HCP Portal.$ vault login <actual-token>
Create a
restrict-paris
policy.$ tee restrict-paris.hcl <<EOFpath "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-paris
policy.$ 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-v2
secrets 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.553795Zcustom_metadata <nil>deletion_time n/adestroyed falseversion 1
Log in as
bob
.$ vault login -method=userpass username="bob" password="training"
The returned token should have
restrict-paris
policy attached.Example output:
Key Value--- -----token hvs.vAUdsbhpSZv0CqsC5qAqdaQ3token_accessor VHuhVHCxzZlWh4qbXTHaPGaktoken_duration 768htoken_renewable truetoken_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.553795Zcustom_metadata <nil>deletion_time n/adestroyed falseversion 1 ===== Data =====Key Value--- -----name Boundaryversion 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_token
andwrapping_accessor
.Example output:
Key Value--- -----wrapping_token: hvs.6myj2lXu7xg39CjrUFiLrwSVwrapping_accessor: X9AISHC5GSMGyDNpoYG3tw8swrapping_token_ttl: 24hwrapping_token_creation_time: 2021-07-21 22:12:16 -0700 PDTwrapping_token_creation_path: paris-kv/data/releases
Optional: If you want to complete the rest of the workflow, repeat the steps you performed previously 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 the read-gdpr-order.hcl
was written as 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.