Examples
Following are some examples that help to introduce concepts. If you are unfamiliar with writing Sentinel policies in Vault, please read through to understand some best practices.
MFA and CIDR Check on Login
The following Sentinel policy requires the incoming user to successfully validate with an Okta MFA push request before authenticating with LDAP. Additionally, it ensures that only users on the 10.20.0.0/16 subnet are able to authenticate using LDAP.
import "sockaddr"import "mfa"import "strings" # We expect logins to come only from our private IP rangecidrcheck = rule { sockaddr.is_contained("10.20.0.0/16", request.connection.remote_addr)} # Require Ping MFA validation to succeedping_valid = rule { mfa.methods.ping.valid} main = rule when strings.has_prefix(request.path, "auth/ldap/login") { ping_valid and cidrcheck}
Note the rule when
construct on the main
rule. This scopes the policy to
the given condition.
Vault takes a default-deny approach to security. Without such scoping, because active Sentinel policies must all pass successfully, the user would be forced to start with a passing status and then define the conditions under which access is denied, breaking the default-deny concept.
By instead indicating the conditions under which the main
rule (and thus, in
this example, the entire policy) should be evaluated, the policy instead
describes the conditions under which a matching request is successful. This
keeps the default-deny feeling of Vault; if the evaluation condition isn't met,
the policy is simply a no-op.
Allow Only Specific Identity Entities or Groups
main = rule { identity.entity.name is "jeff" or identity.entity.id is "fe2a5bfd-c483-9263-b0d4-f9d345efdf9f" or "sysops" in identity.groups.names or "14c0940a-5c07-4b97-81ec-0d423accb8e0" in keys(identity.groups.by_id)}
This example shows accessing Identity properties to make decisions, showing that for Identity values IDs or names can be used for reference.
In general, it is more secure to use IDs. While convenient, entity names and group names can be switched from one entity to another, because their only constraint is that they must be unique. Using IDs guarantees that only that specific entity or group is sufficient; if the group or entity are deleted and recreated with the same name, the match will fail.
Instantly Disallow All Previously-Generated Tokens
Imagine a break-glass scenario where it is discovered that there have been compromises of some unknown number of previously-generated tokens.
In such a situation it would be possible to revoke all previous tokens, but this may take a while for a number of reasons, from requiring revocation of generated secrets to the simple delay required to remove many entries from storage. In addition, it could revoke tokens and generated secrets that later forensic analysis shows were not compromised, unnecessarily widening the impact of the mass revocation.
In Vault's ACL system a simple deny could be put into place, but this is a very coarse-grained control and would require forethought to ensure that a policy that can be modified in such a way is attached to every token. It also would not prevent access to login paths or other unauthenticated paths.
Sentinel offers much more fine-grained control:
import "time" main = rule when not request.unauthenticated { time.load(token.creation_time).unix > time.load("2017-09-17T13:25:29Z").unix}
Created as an EGP on *
, this will block all access to any path Sentinel
operates on with a token created before the given time. Tokens created after
this time, since they were not a part of the compromise, will not be subject to
this restriction.
Delegate EGP Policy Management Under a Path
The following policy gives token holders with this policy (via their tokens or their Identity entities/groups) the ability to write EGP policies that can only take effect at Vault paths below certain prefixes. This effectively delegates policy management to the team for their own key-value spaces.
import "strings" data_match = func() { # Make sure there is request data if length(request.data else 0) is 0 { return false } # Make sure request data includes paths if length(request.data.paths else 0) is 0 { return false } # For each path, verify that it is in the allowed list for strings.split(request.data.paths, ",") as path { # Make it easier for users who might be used to starting paths with # slashes sanitizedPath = strings.trim_prefix(path, "/") if not strings.has_prefix(sanitizedPath, "dev-kv/teama/") and not strings.has_prefix(sanitizedPath, "prod-kv/teama/") { return false } } return true} # Only care about writing; reading can be allowed by normal ACLsprecond = rule { request.operation in ["create", "update"] and strings.has_prefix(request.path, "sys/policies/egp/")} main = rule when precond { strings.has_prefix(request.path, "sys/policies/egp/teama-") and data_match()}