Project: SPIRE (Provisioning / Key Management)
File: authorization_policy_engine.md

Warning: Use of custom authorization policies is experimental and can result in security degredation if not configured correctly. Please refer to for more details on extending the default policy. The authorization decisions in SPIRE are determined by a policy engine which bases its decision on a rego policy and databindings with Open Policy Agent (OPA). This is a sample configuration of the policy. ```hcl server { experimental { authopapolicy_engine { local { rego_path = "./conf/server/policy.rego" policydatapath = "./conf/server/policy_data.json" } } } } ``` If the policy engine configuration is not set, it defaults to the [default SPIRE authorization policy](#default-configurations). The policy engine is based on the [Open Policy Agent (OPA)](https://www.openpolicyagent.org/). This is configured via two components, the rego policy, and the policy data path (or databindings as referred to in OPA). The rego policy is a rego policy file defining how to authorize the API calls. The policy data (or databindings) is a JSON blob that defines additional data that can be used in the rego policy. In general there is an overlap in terms of which aspects of the policy can be part of the rego and databindings. However, the general rule is "How it is done" is part of the rego policy, and the "What does this apply to" is part of the databindings file. The rego policy defines how input to the policy engine is evaluated to produce the result used by SPIRE server for authorization decisions. This is defined by the result object: ```rego result = { "allow": true/false, "allowifadmin": true/false, "allowiflocal": true/false, "allowifdownstream": true/false, "allowifagent": true/false, } ``` The fields of the result are the following: `allow`: a boolean that if true, will authorize the call `allowiflocal`: a boolean that if true, will authorize the call only if the caller is a local UNIX socket call `allowifadmin`: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID with the Admin flag set `allowifdownstream`: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID that is downstream `allowifagent`: a boolean that is true, will authorize the call only if the caller is an agent. The results are evaluated by the following semantics where `isX()` is an evaluation of whether the caller has property `X`. ```rego admit_request = allow || (allowiflocal && isLocal()) || (allowifadmin && isAdmin()) || (allowifdownstream && isDownstream()) || (allowifagent && isAgent()) ``` The inputs that are passed into the policy are: `input`: the input from the SPIRE server for the authorization call `data`: the databinding from the policy data file | input field | Description | Example | |-|-|--| | caller | The SPIFFE ID (if available) of the caller | spiffe://example.org/workload1 | | full_method | The full method name of the API call based on the | /spire.api.server.svid.v1.SVID/MintJWTSVID | | req | The API call request body (not available on client or bidirectional streaming RPC calls) | { "filter": {} } | The request (`req`) is the marshalled JSON object from the [SPIRE api sdk](https://github.com/spiffe/spire-api-sdk/). Note that it is not available on client or bidirectional streaming RPC API calls. The policy data file consists of a JSON blob which represents the data that is used in the evaluation of the policy. This is generally free-form and can be used in the rego policy in any

Data in this JSON blob is pre-compiled into the policy evaluation on the policy engine evaluation. Therefore, there it is recommended to put as much data as possible in the databinding so that it can be optimized by the policy engine. These data objects can be accessed via the `data` field in the rego policy. For example, a JSON data object may look like this: ```rego { "apis": [ { "full_method": "/spire.api.server.svid.v1.SVID/MintJWTSVID" }, { "full_method": "/spire.api.server.bundle.v1.Bundle/GetFederatedBundle"}, { "full_method": "/spire.api.server.svid.v1.SVID/BatchNewX509SVID"} ] } ``` With the example data object above, we could construct a policy in rego to check that if the input's full method is equal to one of the objects defined in the `apis` fields' `full_method` sub-field, then `allow` should be set to true. ```rego allow = true { input.fullmethod == data.apis[].full_method } ``` Here are the default rego policy and policy data values. These are what is required to carry out the default SPIRE authorization decisions. The default rego policy is located . The default policy\_data.json is located . The default policy data file contains a field called "apis". This field has a list of APIs that is current being configured with the rego policy. The fields of each object are as follows: | field | Description | Example | ||--|--| | full_method | The full method name of the API call | /spire.api.server.svid.v1.SVID/MintJWTSVID | | allow_any | if true, sets result.allow to true | | | allowlocal | if true, sets result.allowif_local to true | | | allowadmin | if true, sets result.allowif_admin to true | | | allowdownstream | if true, sets result.allowif_downstream to true | | | allowagent | if true, sets result.allowif_agent to true | | This section contains examples of how the authorization policy can be extended. It is important when implementing custom policies that one understands the evaluation semantics and details of OPA rego. An example of subtleties of OPA rego policy is the evaluation of a variable is taken as a logical OR of all the clauses. Therefore, creating an additional rule that sets `allow = false` will not be an effective addition to the policy. It is recommended to familiarize yourself with the before implementing custom policies. In this example, we want to ensure that entries created are namespaced, so we can create namespaces within the trust domain to determine the type of entries that can be created by each client. This would be a scenario of having two departments where one would not be able to create entries for the other. Note that this example is specifically for calls through the TCP endpoint, where the user corresponds to the SPIFFE ID in the x509 certificate presented during invocation of the API. This can be defined by creating some additional objects in the data binding: ```rego { "entrycreatenamespaces": [ { "user": "spiffe://example.org/schedulers/finance", "path_namespace": "^/finance" }, { "user": "spiffe://example.org/schedulers/hr", "path_namespace": "^/hr" } ] } ``` The rego policy can then be updated to compare against the dataset of namespaces of users and path prefixes to compare against the entry create input request. ```rego checkentrycreate_namespace { input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry" b = data.entrycreatenamespaces[_] b.user == input.caller rematch(b.pathnamespace, input.req.entries[].spiffeid.path) } ``` The rego policy can then be updated to check for this, an example of an allow clause would look like the following. Note that it is important to check to see how this fits in with the other parts of the rego

```rego allow = true { checkentrycreate_namespace } ``` Building on top of the previous example, let's say we want to have sub departments, having schedulers for a subset of paths within the trust domain. This can be done by building on top of the previous example, with the addition of an exclusion list. In this example, we have two schedulers: `schedulers/finance` is able to create paths starting with `/finance` `schedulers/finance/EMEA` is able to create paths starting with `/finance/EMEA` `schedulers/finance` should not be able to create paths starting with `/finance/EMEA` To do this, we can use the same policy as the above, adding on an exclusion list. We will use the following policy data: ```rego { "entrycreatenamespaces": [ { "user": "spiffe://example.org/schedulers/finance", "path_namespace": "^/finance", "path_exclusions": [ "^/finance/EMEA" ] }, { "user": "spiffe://example.org/schedulers/finance/EMEA", "path_namespace": "^/finance/EMEA" } ] } ``` We can then add a couple lines to check for the exclusion list: ```rego checkentrycreate_namespace { input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry" b = data.entrycreatenamespaces[_] b.user == input.caller rematch(b.pathnamespace, input.req.entries[].spiffeid.path) exclusions := b.path_exclusions exclusionmatches := { entry | entry := input.req.entries[]; rematch(exclusions[], entry.spiffe_id.path)} count(exclusion_matches) == 0 } checkentrycreate_namespace { input.full_method != "/spire.api.server.entry.v1.Entry/BatchCreateEntry" } ``` This will result in the desired boolean outcome to be stored in `checkentrycreate_namespace`. In this second example, we want to restrict it so that we prevent any entries created with an admin flag. This can be done by modifying the rego policy allow clauses with the following check: ```rego checkentrycreateadminflag { input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry" adminentries := { entry | entry := input.req.entries[]; entry.admin == true} count(admin_entries) == 0 } ``` This sets `checkentrycreateadminflag` to true if the full method is not for entry creation or if it is, that there are no entries that contain the admin flag. The rego policy can then be updated to check for this, an example of an allow clause would look like the following. Note that it is important to check to see how this fits in with the other parts of the rego policy. ```rego allow = true { checkentrycreateadminflag } ``` In this example, we want to restrict deletion of entries. For the first part of this example, we will fully lock down the ability to delete entries. This can be easily done by leveraging the set of default rules. In the default policy data file, there are general allow restrictions for APIs. For example, for the batch deletion of entries, here is the exerpt: ```rego { "full_method": "/spire.api.server.entry.v1.Entry/BatchDeleteEntry", "allow_admin": true, "allow_local": true } ``` If we want to disallow deletion of entries from the local or from admin users, we can easily do this by deleting the `allow*` lines, resulting in: ```rego { "full_method": "/spire.api.server.entry.v1.Entry/BatchDeleteEntry", } ``` In this example, we want to now relax our previous restriction by allowing a single SPIFFE ID to perform deletions via the TCP endpoint. We can first define the data binding to provide the list of users able to delete entries: ```rego { "entrydeleteusers": [ "spiffe://example.org/finance/super-admin-deleter", "spiffe://example.org/hr/super-admin-deleter" ] } ``` We can then define the following rego policy to check the calls to the entry delete endpoint, and add checks that the caller SPIFFE ID is in the list of users defined. ```rego checkentrydelete_users { input.full_method == "/spire.api.server.entry.v1.Entry/BatchDeleteEntry" input.caller == data.entrydeleteusers[_] } ``` The rego policy can then be updated to check for this, an example of an allow clause would look like the following. Note that it is important to check to see how this fits in with the other parts of the rego policy. ```rego allow = true { checkentrydelete_users } ```