Virtual Event
Join us for the next HashiConf Digital October 12-15, 2020 Register for Free

Operations

enterprise

Sentinel Policies

Sentinel is a language framework for policy build 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 guide walks you through the authoring of Sentinel policies in Vault. For ACL policy authoring, refer to the Policies guide.

»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 guide, you need to have a Vault Enterprise environment.

»Policy requirements

Since this guide 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 Sentinel Policies

import "<library>"

<variable> = <value>

main = rule {
     <conditions_to_evaluate>
}
  • 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 a main 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.

»Policy requirements

In this guide, you are going to write Sentinel policies that fulfill the following requirements:

  1. 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).

  2. Any create, update and delete operations against Key/Value secret engine (mounted at "secret") must come from an internal IP of 122.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.

»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

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

»Step 3: Deploy your EGP policies

Sentinel policies have three enforcement levels:

LevelDescription
advisoryThe policy is allowed to fail. Can be used as a tool to educate new users.
soft-mandatoryThe policy must pass unless an override is specified.
hard-mandatoryThe 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:
<nil>

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.

»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