Introduction
HashiCorp offers a powerful tool with some of its products, known as Sentinel policies. These can be leveraged with the Vault product (see an excellent tutorial to get started with Sentinel policies and Vault here).
One useful thing you can do with Sentinel policies is manage the manipulation of Vault namespaces. This is often tricky to do with standard Vault policies. This is because Vault namespaces alter the path
of requests. For example, doing a token lookup in the root
namespace would result in a request with a path auth/token/lookup
, while the same request in the ns1
namespace would result in a path ns1/auth/token/lookup
.
Vault ACL policies have a few tools to help you deal with unknown in paths like +
and *
. However, currently, there is no Vault ACL policy path that will match any number of nested namespaces. That's where Sentinel policies come in.
You can write a Sentinel policy which leverages regex, something that Vault ACL policies don't currently support. This allows you to write regex that matches any number of nested namespaces and either allows or denies the action.
To explain this further, we will walk through a full demo. We will set the scene with a scenario, setup and configure Vault, and finally test the Sentinel policy to prove it functions as expected.
Explanation
Note: this example will use Docker along with a Vault Enterprise license. Your license should be exported into an environment variable named
VAULT_LICENSE
.
Our use case is the following: We will have users who have the Vault policy admin
, granting them permissions to do effectively anything in Vault. We want to then limit their permissions and disallow any manipulation of namespaces except for one super-admin named bob
.
Setup Vault
docker run \
--cap-add=IPC_LOCK \
-e VAULT_DEV_LISTEN_ADDRESS='0.0.0.0:8200' \
-e VAULT_ADDR='http://0.0.0.0:8200' \
-e VAULT_DEV_ROOT_TOKEN_ID=root \
-e VAULT_LICENSE=$(echo $VAULT_LICENSE) \
-p 8200:8200 \
--name vault \
--detach \
hashicorp/vault-enterprise
export VAULT_ADDR=http://localhost:8200
sleep 2
vault login root
Setup Vault Users
# Create an admin policy to provide user admin permissions
vault policy write admin - << EOF
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
EOF
vault auth enable userpass
# Create 2 users, both of which will be assigned admin policies
vault write auth/userpass/users/bob password=root token_policies=admin
vault write auth/userpass/users/jill password=root token_policies=admin
USERPASS_ACCESSOR=$(vault auth list -format=json | jq -r '.["userpass/"].accessor')
# Create an entity for bob with the same admin policy
vault write -format=json identity/entity name="bob" policies="admin"
ENTITY_ID=$(vault read -format=json identity/entity/name/bob | jq -r '.data.id')
# Use an entity alias to bind bind together the entity and the userpass user
vault write identity/entity-alias name="bob" \
canonical_id=$ENTITY_ID \
mount_accessor=$USERPASS_ACCESSOR
# Demonstrate that we now have an entity with the name bob
vault read -format=json identity/entity/name/bob | jq -r '.data.name'
Setup Sentinel Policy
cat <<'EOF' > blockns.sentinel
# Pre-condition to check if the path contains 'sys/namespaces/<ns>' prior to evaluating rule
precond = rule {
request.path matches "^(.*)sys\\/namespaces\\/(.*)$" and request.operation in ["create", "update", "delete"]
}
# Rule to be processed to prevent namespace manipulation if user is not 'bob'
prevent_namespaces = rule {
identity.entity.name is "bob"
}
# Main rule to be processed only when pre-condition is true
main = rule when precond {
prevent_namespaces
}
EOF
# Policy must be base64 encoded prior to posting to Vault
POLICY=$(base64 blockns.sentinel)
# Create and configure EGP policy in Vault
# Note: Since we need to block namespace manipulation in root and
# child namespaces, we must use '*' for 'paths' parameter
# as there is no ACL style Vault path that can match all namespaces
# in root or in child namespaces
vault write sys/policies/egp/blockns \
policy="${POLICY}" \
paths="*" \
enforcement_level="hard-mandatory"
vault read sys/policies/egp/blockns
Sentinel Unit Tests
# Standard Sentinel test folder structure is: 'test/<policy>/*.[hcl|json]'
mkdir -p test/blockns
# Write test that will pass for namespace manipulation if entity name is 'bob'
cat <<'EOF' > test/blockns/allow-ns.json
{
"global": {
"request": {
"path": "sys/namespaces/test",
"operation": "create"
},
"identity": {
"entity": {
"name": "bob"
}
}
},
"test": {
"precond": true,
"prevent_namespaces": true,
"main": true
}
}
EOF
# Write test that will fail for namespace manipulation if entity name is not bob
cat <<'EOF' > test/blockns/block-ns.json
{
"global": {
"request": {
"path": "sys/namespaces/test",
"operation": "create"
},
"identity": {
"entity": {
"name": "jill"
}
}
},
"test": {
"precond": true,
"prevent_namespaces": false,
"main": false
}
}
EOF
# Write a test that demonstrates how this Sentinel policy does
# nothing if path isn't manipulating namespaces
cat <<'EOF' > test/blockns/do-nothing.json
{
"global": {
"request": {
"path": "auth/token/lookup-self",
"operation": "create"
},
"identity": {
"entity": {
"name": "jill"
}
}
},
"test": {
"precond": false
}
}
EOF
# Run tests
sentinel test
# Output of test run should look like the following
#PASS - blockns.sentinel
# PASS - test/blockns/allow-ns.json
# PASS - test/blockns/block-ns.json
# PASS - test/blockns/do-nothing.json
Field Testing Sentinel Policy
# Perform some 'field' tests proving the policy works as expected
vault login -method=userpass username=bob password=root
vault namespace create ns1
# Should be allowed
vault namespace create -namespace=ns1 ns1.1
# Should be allowed
vault login -method=userpass username=jill password=root
vault namespace create ns2
# Should be blocked
vault namespace create -namespace=ns1 ns1.2
# Should be blocked