Warning: Use of custom authorization policies is experimental and can result in security degredation if not configured correctly. Please refer to this section 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.
server {
experimental {
auth_opa_policy_engine {
local {
rego_path = "./conf/server/policy.rego"
policy_data_path = "./conf/server/policy_data.json"
}
}
}
}
If the policy engine configuration is not set, it defaults to the default SPIRE authorization policy.
The policy engine is based on the Open Policy Agent (OPA). 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:
result = {
"allow": true/false,
"allow_if_admin": true/false,
"allow_if_local": true/false,
"allow_if_downstream": true/false,
"allow_if_agent": true/false,
}
The fields of the result are the following:
allow
: a boolean that if true, will authorize the callallow_if_local
: a boolean that if true, will authorize the call only if the caller is a local UNIX socket callallow_if_admin
: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID with the Admin flag setallow_if_downstream
: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID that is downstreamallow_if_agent
: 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
.
admit_request =
allow || (allow_if_local && isLocal()) || (allow_if_admin && isAdmin()) ||
(allow_if_downstream && isDownstream()) || (allow_if_agent && isAgent())
The inputs that are passed into the policy are:
input
: the input from the SPIRE server for the authorization calldata
: 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 | /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. 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 way. 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:
{
"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.
allow = true {
input.full_method == 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 here.
The default policy_data.json is located here.
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 | |
allow_local | if true, sets result.allow_if_local to true | |
allow_admin | if true, sets result.allow_if_admin to true | |
allow_downstream | if true, sets result.allow_if_downstream to true | |
allow_agent | if true, sets result.allow_if_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 OPA rego language 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:
{
"entry_create_namespaces": [
{
"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.
check_entry_create_namespace {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
# caller has the registrar role
b = data.entry_create_namespaces[_]
b.user == input.caller
# spiffe id to be registered is in correct namespace
re_match(b.path_namespace, input.req.entries[_].spiffe_id.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 policy.
# Any allow check
allow = true {
check_entry_create_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:
{
"entry_create_namespaces": [
{
"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:
check_entry_create_namespace {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
# caller has the registrar role
b = data.entry_create_namespaces[_]
b.user == input.caller
# spiffe id to be registered is in correct namespace
re_match(b.path_namespace, input.req.entries[_].spiffe_id.path)
# check if the spiffe id to be registered doesn't hit an exclusion
exclusions := b.path_exclusions
exclusion_matches := { entry | entry := input.req.entries[_]; re_match(exclusions[_], entry.spiffe_id.path)}
count(exclusion_matches) == 0
}
check_entry_create_namespace {
input.full_method != "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
}
This will result in the desired boolean outcome to be stored in
check_entry_create_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:
check_entry_create_admin_flag {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
admin_entries := { entry | entry := input.req.entries[_]; entry.admin == true}
count(admin_entries) == 0
}
This sets check_entry_create_admin_flag
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.
# Any allow check
allow = true {
check_entry_create_admin_flag
}
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:
{
"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:
{
"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:
{
"entry_delete_users": [
"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.
check_entry_delete_users {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchDeleteEntry"
# caller has the registrar role
input.caller == data.entry_delete_users[_]
}
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.
# Any allow check
allow = true {
check_entry_delete_users
}