Skip to main content
Version: vNext (upcoming release)

Pomerium Policy Language

Pomerium Policy Language (PPL) is a yaml-based notation for creating easy and flexible authorization policies. This document covers the usage of PPL and provides several example policies.

PPL allows administrators to express authorization policy in a high-level, declarative language that promotes safe, performant, fine-grained controls.

note

See the Policy setting page to learn how to apply a PPL policy to a route.

At a Glance

Each PPL policy has at the top level a set of allow or deny actions, with a list of logical operators, criteria, matchers, and values underneath. For example:

allow:
and:
- domain:
is: example.com
deny:
or:
- email:
is: user2@example.com
- email:
is: user3@example.com

This policy grants access only if the domain portion of a user's email address matches the specified value, example.com.

It will deny access to users with a user2@example.com or user3@example.com email address.

Rules

A PPL document is either an object or an array of objects. The object represents a rule where the action is the key and the value is an object containing the logical operators.

Actions

Only two actions are supported: allow and deny. deny takes precedence over allow. More precisely: a user will have access to a route if at least one allow rule matches and no deny rules match.

Logical Operators

A logical operator combines multiple criteria together for the evaluation of a rule. There are 4 logical operators: and, or, not and nor.

More on Logical Operators

Given the following example with OPERATOR replaced:

allow:
OPERATOR:
- domain:
is: example.com
- claim/groups: admin

If and is used, the user will have access if their email address ends in example.com and they are a member of the admin group. (A ∧ B)

If or is used, the user will have access if their email address ends in example.com or they are a member of the admin group. (A ∨ B)

If not is used, the user will have access if their email address does not end in example.com and they are not a member of the admin group. (¬A ∧ ¬B) = ¬(A ∨ B) (This operation is traditionally called NOR in the Boolean algebra.)

If nor is used, the user will have access if their email address does not end in example.com or they are not a member of the admin group. (¬A ∨ ¬B) = ¬(A ∧ B) (This operation is traditionally called NAND in the Boolean algebra.)

Multiple Operators in a Rule

You can add multiple operators under the same rule, and it would be valid PPL.

For example, this policy would grant access to anyone with a family_name: Smith claim, or users with email addresses ending in domain1 or domain2:

allow:
and:
- claim/family_name: Smith
or:
- domain:
is: domain1.com
- domain:
is: domain2.com

However, you could write an equivalent policy with multiple allow blocks:

- allow:
and:
- claim/family_name: Smith
- allow:
or:
- domain:
is: domain1.com
- domain:
is: domain2.com

Although these policies are equally effective, we recommend using just one operator per rule.

Criteria

Criteria in PPL are represented as an object where the key is the name and optional sub-path of the criterion, and the value changes depending on which criterion is used. A sub-path is indicated with a / in the name:

allow:
and:
- claim/family_name: Smith
deny:
not:
- http_method:
is: GET

Supported PPL Criteria

Below is an exhaustive list of PPL criteria.

Entries marked with * denote criteria that are only available in the Enterprise Console PPL builder. All other entries are available in both Pomerium Core and Pomerium Enterprise.

Criterion NameData FormatDescription
acceptAnything. Typically true.Always returns true, thus always allowing access. Equivalent to the allow_public_unauthenticated_access option.
authenticated_userAnything. Typically true.Always returns true for logged-in users. Equivalent to the allow_any_authenticated_user option.
claimAnything. Typically a string.Returns true if a token claim matches the supplied value exactly. The claim to check is determined via the sub-path.
For example, claim/family_name: Smith matches if the user's family_name claim is Smith.
client_certificateCertificate matcherReturns true if a client presented a TLS certificate matching the provided condition.
cors_preflightAnything. Typically true.Returns true if the incoming request uses the OPTIONS method and has both the Access-Control-Request-Method and Origin headers. Used to allow CORS pre-flight requests.
* dateDate MatcherReturns true if the time of the request matches the constraints.
* day_of_weekDay of Week MatcherReturns true if the day of the request matches the constraints.
deviceDevice matcherReturns true if the incoming request includes a valid device ID or type.
domainString MatcherReturns true if the logged-in user's email address domain (the part after @) matches the given value.
emailString MatcherReturns true if the logged-in user's email address matches the given value.
* groupsString List MatcherReturns true if a user's group ID matches the supplied value exactly. groups data is only available after a successful directory sync. See Identity Providers for vendor-specific directory sync steps.
http_methodString MatcherReturns true if the HTTP method matches the given value.
http_pathString MatcherReturns true if the HTTP path matches the given value.
invalid_client_certificateAnything. Typically true.Returns true if the incoming request does not have a trusted client certificate. By default, a deny rule using this criterion is added to all Pomerium policies when downstream mTLS is configured (but this default can be changed using the Enforcement Mode setting.)
pomerium_routesAnything. Typically true.Returns true if the incoming request is for the special .pomerium routes. A default allow rule using this criterion is added to all Pomerium policies.
* recordvariableAllows policies to be extended using data from external data sources. See Record Matcher for more information.
rejectAnything. Typically true.Always returns false. The opposite of accept.
* time_of_dayTime of Day MatcherReturns true if the time of the request (for the current day) matches the constraints.
userString MatcherReturns true if the logged-in user's ID matches the supplied value. (The actual value of the user ID claim depends on how the identity provider sets this value.)

Entries marked with * denote criteria that are only available in the Enterprise Console PPL builder. All other entries are available in both Pomerium Core and Pomerium Enterprise.

Matchers

Certificate Matcher

caution

The certificate matcher is a beta feature. The syntax and capabilities are subject to change in a future Pomerium release.

A certificate matcher can be used to allow or deny certain TLS certificates. This matcher is represented as an object that may have the following key/value entries:

Key NameValue TypeDescription
fingerprintstring or array of stringsThe certificate's SHA-256 fingerprint must match one of the provided values.
san_dnsString MatcherThe certificate must contain a Subject Alternative Name with a DNS name satisfying the provided condition.
san_emailString MatcherThe certificate must contain a Subject Alternative Name with an email address satisfying the provided condition.
san_uriString MatcherThe certificate must contain a Subject Alternative Name with a URI satisfying the provided condition.
spki_hashstring or array of stringsThe base64-encoded SHA-256 hash of the certificate's Subject Public Key Info must match one of the provided values.
Notes on certificate fingerprint

The certificate fingerprint is a SHA-256 hash of the entire certificate. You can compute a certificate's fingerprint using the openssl command:

$ openssl x509 -in path/to/certificate.pem -noout -fingerprint -sha256
sha256 Fingerprint=17:85:92:73:E8:A9:80:63:1D:36:7B:2D:5A:6A:66:35:41:2B:0F:22:83:5F:69:E4:7B:3F:65:62:45:46:A7:04

This is the "long" form of a certificate fingerprint (32 uppercase hexadecimal bytes separated by colons). A "short" form is also acceptable (32 lowercase hexadecimal bytes, without colons). You can also compute this form using the openssl command:

$ openssl x509 -in path/to/certificate.pem -outform DER | openssl dgst -sha256
SHA2-256(stdin)= 17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704
Notes on SPKI hash

The SPKI hash is a base64-encoded SHA-256 hash of the Subject Public Key Info section of the certificate. You can compute a certificate's SPKI hash using a sequence of openssl commands:

$ openssl x509 -in path/to/certificate.pem -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl enc -base64
FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U=

The advantage of using the SPKI hash rather than the certificate fingerprint is that the SPKI hash may be stable across certificate renewals (if the public/private key pair is the same).

For example, to allow only certificates containing a Subject Alternative Name with an email address ending in @yourdomain.com (while also requiring the user to sign in with the configured identity provider):

allow:
and:
- authenticated_user: true
- client_certificate:
san_email:
ends_with: '@yourdomain.com'

Or, to allow only one specific trusted certificate (again, while still requiring the user to sign in with the configured identity provider):

allow:
and:
- authenticated_user: true
- client_certificate:
fingerprint: '17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704'

Or, to enforce an allowlist of trusted certificate key pairs:

allow:
and:
- authenticated_user: true
- client_certificate:
spki_hash:
- 'FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U='
- 'pbdFxDXEtpabt3MZiik71farokMg6ZIn2azvsdXtZYA='
- 'WTu9ETBS1/v/ll20erWcf+TAj7rzrJix/oCUv5GMPtg='
...

Day of Week Matcher

The day of week matcher is a string. The string can either be *, a comma-separated list of days, or a dash-separated list of days.

  • * matches all days.

  • , matches either day (e.g. mon,wed,fri).

  • - matches a range of days. (e.g. mon-fri). Days can be specified as English full day names, or as 3 character abbreviations. For example:

    allow:
    and:
    - day_of_week: tue-fri

Date Matcher

The date matcher is an object with operators as keys. It supports the following operators: after and before. The values are ISO-8601 date strings. after means that the time of the request must be after the supplied date and before means that the time of the request must be before the supplied date. For example:

allow:
and:
- date:
after: 2020-01-02T16:20:00
before: 2150-01-02T16:20:00

Device Matcher

A device matcher is an object with operators as keys. It supports the following operators:

  • is - an exact match of the device ID.
  • approved - true if the device has been approved. This is an enterprise-only feature.
  • type - Specifies the type of device to match on. The available types are enclave_only and any.

For example, a policy to allow any user with a registered device:

- allow:
or:
- device:
type: any

Compare to a policy that only allows a set of specific devices:

- allow:
or:
- device:
is: "5Vn3...C1RS"
- device:
is: "GAtL...doqu"
tip

Users can find their device IDs at the /.pomerium endpoint from any route.

Record Matcher

The record matcher is an object that uses operators as keys. It points to records collected from an external data source defined in the Enterprise Console. Pomerium matches requests to a specific external data source using a record's foreign key. You can use data stored in a record as external context in an authorization policy.

The record matcher supports all of the String Matcher and String List Matcher operators. However, the following operators are specific to the record matcher:

Exists operator

The “exists” operator is a boolean:

  • When set to true, it returns ok if it can find the corresponding external data source record in the Enterprise Console.
  • When set to false, it returns ok if it can't find the corresponding external data source record in the Enterprise Console.
note

The "exists" operator does not require a "field" key.

Builds an authorization policy using the exists operator with an external data source

Numerical comparison operators

The numerical comparison operators (<, <=, =, >, >=) can be used to express conditions for external data sources with numerical fields.

Building a policy using the a numerical comparison operator with an external data source

String Matcher

A string matcher is an object with operators as keys. It supports the following operators: contains, ends_with, is and starts_with.

For example:

allow:
and:
- email:
starts_with: 'admin@'

Or:

allow:
and:
- record:
type: example.com/geoip
field: country
is: 'US'

A string matcher can also be used with an array, a string, a number or a boolean, in which case it is the same as the is operator.

String List Matcher

A string list matcher is an object that supports a single has operator as a key. The has operator checks that a given string is present in a list of strings.

The groups and record criteria both support the has operator.

For example, using the groups criterion:

allow:
and:
- groups:
has: '00gv40ki4gmtCyl5d4x6'

Using the record criterion:

- record:
type: example.com/hr_user
field: departments
has: 'engineering'

A string list matcher can also be used with an array, a string, a number or a boolean, in which case it is the same as the has operator.

Time of Day Matcher

The time of day matcher is an object with operators as keys. It supports the following operators: timezone, after, and before.

timezone is required and specifies the timezone to use when interpreting the supplied times. It is recommended to use city names (like America/Phoenix) instead of standard timezone abbreviations because standard timezones change throughout the year (i.e. EST becomes EDT and back again).

after means the time of the request must be after the supplied time and before means that the time of the request must be before the supplied time. For example:

allow:
and:
- time_of_day:
timezone: UTC
after: 2:20:00
before: 4:30PM

Rego

Rego Usage Requires Extreme Care

Rego policies can be powerful, but improper usage may unintentionally open unauthorized access, deny valid requests, or even leak sensitive data. Whenever possible, use PPL instead. If you're unsure whether your use case requires Rego, work with your Pomerium account representative or contact support to see if your needs can be met using PPL-based policy criteria.

Pomerium supports policies expressed in Rego for organizations that prefer to use OPA.

See the Outputs, Inputs, and Functions reference sections below to learn how Rego policies apply to policy evaluation.

Pomerium Enterprise

Custom Rego policies is a Pomerium Enterprise feature.

In the Enterprise Console, you can write custom Rego policies in the Rego Editor:

Apply Rego in Console editor

note

A policy can only support PPL or Rego. Once one is set, the other tab is disabled.

Outputs

Authorization policy written in Rego is expected to return results in allow and/or deny rules:

# a policy that always allows access
allow := true
# a policy that always denies access
deny := true

Pomerium grants access according to the same rules as PPL:

Only two actions are supported: allow and deny. deny takes precedence over allow. More precisely: a user will have access to a route if at least one allow rule matches and no deny rules match.

allow and deny rules support four forms:

  1. A simple boolean:
allow := true
  1. An array with a single boolean value:
deny := [true]
  1. An array with two values: a boolean and a reason:
allow := [false, "user-unauthorized"]
  1. An array with three values: a boolean, a reason, and additional data:
allow := [false, "user-unauthorized", { "key": "value" }]

The reason value is useful for debugging, since it appears in authorization logs. There are two special reasons that trigger functionality in Pomerium:

  • user-unauthenticated indicates that the user needs to sign in, and results in a redirect to the Authenticate service
  • device-unauthenticated indicates that the user needs to register a new device

Inputs

Rego scripts are evaluated with inputs available on the input object:

allow if input.http.method == "POST"

Rego defines the following inputs:

Input nameTypeDescription
httpObjectRepresents the HTTP request
http.methodStringThe method used in the HTTP request
http.hostnameStringThe hostname in the HTTP request
http.pathStringThe path in the HTTP request
http.urlStringThe full URL in the HTTP request
http.headersObjectThe headers in the HTTP request
http.client_certificateObjectThe client certificate details
http.client_certificate.presentedBooleantrue if the client presented a certificate
http.client_certificate.leafStringThe leaf certificated provided by the client (unvalidated)
http.client_certificate.intermediatesStringThe remainder of the client certificate chain
http.ipStringThe user's IP address
http.sessionObjectRepresents the user's session
http.session.idStringThe session ID
http.is_valid_client_certificateBooleantrue if the presented client certificate is valid

Functions

The function below is available in Rego scripts:

  • get_databroker_record(record_type, record_id): Returns data from the Databroker service.

For example:

session := get_databroker_record("type.googleapis.com/session.Session", input.session.id)

Example Rego policy

This example policy compares the given_name claim from a user's session against a list of popular first names, and only allows the 100 most popular first names.

package pomerium.policy
session = s {
s = gset_databroker_record("type.googleapis.com/user.ServiceAccount", input.session.id)
s != null
} else = s {
s = get_databroker_record("type.googleapis.com/session.Session", input.session.id)
s != null
} else = {} {
true
}
user = u {
u = get_databroker_record("type.googleapis.com/user.User", session.user_id)
} else = {} {
true
}
allow = [true, {"custom-rego-authorized"}] {
# grab all the claims from the user and session objects
session_claims := object.get(session, "claims", {})
user_claims := object.get(user, "claims", {})
all_claims := object.union(session_claims, user_claims)
# get the given_name claim. claim values are always an array of strings
given_names := object.get(all_claims, "given_name", [])
# query a JSON dump of the most popular baby names from 2020
response := http.send({
"method": "GET",
"url": "https://raw.githubusercontent.com/aruljohn/popular-baby-names/master/2020/boy_names_2020.json",
"force_json_decode": true,
})
# only include the top 100 names
all_names := response.body.names
popular_names := array.slice(all_names, 0, 99)
# check that there's a given name in the popular names
some i
some j
popular_names[i] == given_names[j]
} else = [false, {"custom-rego-unauthorized"}] {
session.id != ""
} else = [false, {"user-unauthenticated"}] {
true
}

This example pulls session data from the Databroker service using type.googleapis.com/session.Session for users and type.googleapis.com/user.ServiceAccount for service accounts.