Enterprise Only: Sentinel requires Vault Enterprise with the Governance & Policy Module.
Sentinel is a language framework for policy built to be embedded in Vault Enterprise to enable fine-grained, logic-based policy decisions which cannot be fully handled by the ACL policies.
Role Governing Policies (RGPs) and Endpoint Governing Policies (EGPs) can be defined using Sentinel:
- RGPs are tied to particular tokens, identity entities, or identity groups
- EGPs are tied to particular paths (e.g.
aws/creds/
)
This tutorial walks you through the authoring of Sentinel policies in Vault. For ACL policy authoring, refer to the Policies tutorial.
»Challenge
ACL policies are path-based and present the following challenges:
- Cannot grant permissions based on logic other than paths
- Paths are merged in ACL policies which could potentially cause a conflict as the number of policies grows
What if the policy requirement was to grant read permission on secret/orders
path only if the request came from an IP address within a certain CIDR?
»Solution
Use Sentinel policies (RGPs and/or EGPs) to fulfill more complex policy requirements.
Sentinel can access properties of the incoming requests and make a decision based on a certain set of conditions. Available properties include:
- request - Information about the request itself (path, operation type, parameters, etc.)
- token - Information about the token being used (creation time, attached policies, etc.)
- identity - Identity entities and all related data
- mfa - Information about successful MFA validations
»Prerequisites
To perform the tasks described in this tutorial, you need to have a Vault Enterprise environment.
NOTE: To explore Vault Enterprise features, you can sign up for a free 30-day trial from here.
»Policy requirements
Since this tutorial demonstrates the creation of policies, log in with highly
privileged token such as root
.
The specific required policy capabilities are as follows.
# To list policies
path "sys/policies/*"
{
capabilities = ["list"]
}
# Create and manage EGPs
path "sys/policies/egp/*"
{
capabilities = ["create", "read", "update", "delete", "list"]
}
»Step 1: Write Sentinel Policies
»Anatomy of a Sentinel Policy
import ""
=
main = rule {
}
import
- Enables your policy to access reusable libraries. There are a set of built-in imports available to help define your policy rules.main
(required) - Every Sentinel policy must have amain
rule which is evaluated to determine the result of a policy.rule
- A first-class construct in Sentinel. It describes a set of conditions resulting in either true or false. (NOTE: Refer to the Boolean Expressions for the full list of available operators in writing rules.)<variable>
- Variables are dynamically typed in Sentinel. You can define its value explicitly or implicitly by the host system or function.
NOTE: The Sentinel language supports many features such as functions, loops, slices, etc. You can learn about all of this in the Sentinel Language documentation.
»Policy requirements
In this tutorial, you are going to write Sentinel policies that fulfill the following requirements:
Any incoming request against the "
secret/accounting/*
" to be performed during the business hours (7:00 am to 6:00 pm during the work days).Any
create
,update
anddelete
operations against Key/Value secret engine (mounted at "secret
") must come from an internal IP of122.22.3.4/32
CIDR.
»Sentinel Policies
Requirement #1: business-hrs.sentinel
import "time"
# Expect requests to only happen during work days (Monday through Friday)
# 0 for Sunday and 6 for Saturday
workdays = rule {
time.now.weekday > 0 and time.now.weekday < 6
}
# Expect requests to only happen during work hours (7:00 am - 6:00 pm)
workhours = rule {
time.now.hour > 7 and time.now.hour < 18
}
main = rule {
workdays and workhours
}
Requirement #2: cidr-check.sentinel
import "sockaddr"
import "strings"
# Only care about create, update, and delete operations against secret path
precond = rule {
request.operation in ["create", "update", "delete"] and
strings.has_prefix(request.path, "secret/")
}
# Requests to come only from our private IP range
cidrcheck = rule {
sockaddr.is_contained(request.connection.remote_addr, "122.22.3.4/32")
}
# Check the precondition before execute the cidrcheck
main = rule when precond {
cidrcheck
}
The main
has conditional rule (when precond
) to ensure that the rule gets evaluated only if the request is relevant.
Refer to the Sentinel Properties documentation for available properties which Vault injects to Sentinel to allow fine-grained controls.
»Step 2: Test the Sentinel Policies
You can test the Sentinel policies prior to deployment in order to validate syntax and to document expected behavior.
First, you need to download the Sentinel simulator.
$ wget https://releases.hashicorp.com/sentinel/0.15.5/sentinel_0.15.5_darwin_amd64.zip
Unzip the downloaded file.
$ unzip sentinel_0.15.5_darwin_amd64.zip -d /usr/local/bin
Create a sub-folder named test
where cidr-check.sentinel
and
business-hrs.sentinel
policies are located.
Under the test
folder, create a sub-folder for cidr-check
.
$ mkdir -p test/cidr-check
Also, create a sub-folder for business-hrs
under the test
directory.
$ mkdir -p test/business-hrs
NOTE: The test should be created under /test/<policy_name>
folder.
Write a passing test case in a file named success.json
under
test/business-hrs
directory.
{
"mock": {
"time": {
"now": {
"weekday": 1,
"hour": 12
}
}
},
"test": {
"main": true
}
}
Under mock
, you specify the mock test data. In this example, the
weekday
is set to 1
which is Monday
and hour
is set to 12
which is noon
. Therefore the main
should return true
.
Write a failing test in a file named fail.json
under
test/business-hrs
.
{
"mock": {
"time": {
"now": {
"weekday": 0,
"hour": 12
}
}
},
"test": {
"main": false
}
}
The mock data is set to Sunday
at noon
; therefore Therefore, the main
should return false
.
Similarly, write a passing test case for cidr-check
policy,
test/cidr-check/success.json
.
{
"global": {
"request": {
"connection": {
"remote_addr": "122.22.3.4"
},
"operation": "create",
"path": "secret/orders"
}
}
}
In this example, the global
specifies the create
operation is invoked on
secret/orders
endpoint which initiated from an IP address 122.22.3.4
.
Therefore the main
should return true
.
Write a failing test for cidr-check
policy, test/cidr-check/fail.json
.
{
"global": {
"request": {
"connection": {
"remote_addr": "122.22.3.10"
},
"operation": "create",
"path": "secret/orders"
}
},
"test": {
"precond": true,
"main": false
}
}
This test will fail because of the IP address mismatch. However, the
precond
should pass since the requested operation is create
and the
targeted endpoint is secret/orders
.
The optional test
definition adds more context to why the test
should fail. The expected behavior is that the test fails because main
returns false
but precond
should return true
.
Now, you have written both success and failure tests.
├── business-hrs.sentinel
├── cidr-check.sentinel
└── test
├── business-hrs
│ ├── fail.json
│ └── success.json
└── cidr-check
├── fail.json
└── success.json
Execute the test.
$ sentinel test
Successful output:
PASS - business-hrs.sentinel
PASS - test/business-hrs/fail.json
PASS - test/business-hrs/success.json
PASS - cidr-check.sentinel
PASS - test/cidr-check/fail.json
PASS - test/cidr-check/success.json
NOTE: If you want to see the tracing and log output for those tests,
run the command with -verbose
flag.
»Step 3: Deploy your EGP policies
Sentinel policies have three enforcement levels:
Level | Description |
---|---|
advisory | The policy is allowed to fail. Can be used as a tool to educate new users. |
soft-mandatory | The policy must pass unless an override is specified. |
hard-mandatory | The policy must pass no matter what! |
Since both policies are tied to specific paths, the policy type that you are going to create is Endpoint Governing Policies (EGPs).
Store the Base64 encoded cidr-check.sentinel
policy in an environment
variable named POLICY
.
$ POLICY=$(base64 cidr-check.sentinel)
Create a policy cidr-check
with enforcement level of hard-mandatory to
reject all requests coming from IP addressed that are not internal.
$ vault write sys/policies/egp/cidr-check \
policy="${POLICY}" \
paths="secret/*" \
enforcement_level="hard-mandatory"
You can read the policy by executing the following command:
$ vault read sys/policies/egp/cidr-check
Successful output:
Key Value
--- -----
enforcement_level hard-mandatory
name cidr-check
paths [secret/*]
policy import "sockaddr"
import "strings"
# Only care about create, update, and delete operations against secret path
precond = rule {
request.operation in ["create", "update", "delete"] and
strings.has_prefix(request.path, "secret/")
}
# Requests to come only from our private IP range
cidrcheck = rule {
sockaddr.is_contained(request.connection.remote_addr, "122.22.3.4/32")
}
# Check the precondition before execute the cidrcheck
main = rule when precond {
cidrcheck
}
Repeat the steps to create a policy named business-hrs
. First, encode the
business-hrs
policy.
$ POLICY2=$(base64 business-hrs.sentinel)
Create a policy with soft-mandatory enforcement-level.
$ vault write sys/policies/egp/business-hrs \
policy="${POLICY2}" \
paths="secret/accounting/*" \
enforcement_level="soft-mandatory"
To read the policy you just created, execute the following command.
$ vault read sys/policies/egp/business-hrs
Successful output:
Key Value
--- -----
enforcement_level soft-mandatory
name business-hrs
paths [secret/accounting/*]
policy import "time"
# Expect requests to only happen during work days (Monday through Friday)
# 0 for Sunday and 6 for Saturday
workdays = rule {
time.now.weekday > 0 and time.now.weekday < 6
}
# Expect requests to only happen during work hours (7:00 am - 6:00 pm)
workhours = rule {
time.now.hour > 7 and time.now.hour < 18
}
main = rule {
workdays and workhours
}
»Verification
Once the policies are deployed, create
, update
and delete
operations
coming from an IP address other than 122.22.3.4
will be denied.
$ vault kv put secret/accounting/test acct_no="293472309423"
Expected failure output:
Error writing data to secret/accounting/test: Error making API request.
URL: PUT https://127.0.0.1:8200/v1/secret/accounting/test
Code: 403. Errors:
* 2 errors occurred:
* egp standard policy "root/cidr-check" evaluation resulted in denial.
The specific error was:
A trace of the execution for policy "root/cidr-check" is available:
Result: false
Description: Check the precondition before execute the cidrcheck
Rule "main" (byte offset 442) = false
false (offset 314): sockaddr.is_contained(request.connection.remote_addr, "122.22.3.4/32")
Rule "cidrcheck" (byte offset 291) = false
Rule "precond" (byte offset 113) = true
true (offset 134): request.operation in ["create", "update", "delete"]
true (offset 194): strings.has_prefix(request.path, "secret/")
* permission denied
Similarly, you will get an error if any request is made outside of the business
hours defined by the business-hrs
policy.
Warning: As with ACL policies, root
tokens are NOT subject to
Sentinel policy checks. Therefore, use a non-root token for verification test.
»Step 4: Delete Sentinel Policies
To delete the business-hrs
EGP, execute the following command.
$ vault delete sys/policies/egp/business-hrs
To delete the cidr-check
EGP, execute the following command.
$ vault delete sys/policies/egp/cidr-check
»Help and Reference
- Your First Sentinel Policy
- Sentinel documentation
- Vault Sentinel documentation
- Security and Fundamentals at Scale with Vault
- Identity - Entities and Groups tutorial
- Sentinel Properties