Policies and Access Control

The zero trust security model is primarily focused on preventing unauthorized access via enforcing dynamic and granular per-request access control at the resource level. Such access control system is enforced in Octelium via Policies. Each Policy has a set of one or more rules that usually share a similar functionality or purpose according to the Cluster administrators' requirements or needs. Each rule has:

  • A condition that must match in order for the rule and subsequently the Policy as a whole to match.
  • An effect that is either ALLOW or DENY deciding the outcome of the rule if it matches.

Vigil is the identity-aware proxy (IAP) that implements the Service and sits between the Users and the protected resource intercepting the requests. Upon each request coming from the Users, Vigil determines whether the request should be allowed or denied by delegating the process of authentication and authorization to another component called Octovigil, the policy decision point (PDP), which in turn identifies the Session and its User and then starts collecting and evaluating all Policies required by that specific request in order to decide whether the request should be allowed or denied. Such required Policies are the ones defined in or attached to the request's Session, User, every Group of the User, the Service and the Namespace. Once all required Polices for the request are collected, Octovigil starts evaluating all the Policies rules as follows:

  • Octovigil starts evaluating the DENY rules of all the Policies, if any rule matches, then the request is denied.
  • If no DENY rule matches, the Octovigil starts evaluating all the ALLOW rules, if any ALLOW matches then the request is allowed.
  • If none of the DENY or ALLOW rules match, then the request is considered denied since no rule explicitly allows it.

Octelium provides you with an extremely flexible, multi-layered access control system in order to make the process of granting access as dynamic and as fine-grained as possible and at the same time keep it stay manageable and maintainable at any scale.

Octelium provides a dynamic context-aware, attribute-based access control system (ABAC) using policy-as-code. ABAC itself is widely considered as the successor to the classical role-based access control (RBAC) as ABAC enables policies to take much more dynamic and granular decisions that are not just based on roles and memberships, but also on the different attributes of the subject, the resource, as well as the request context which might include information about application-specific actions (e.g. HTTP request information, database commands, etc...), contextual information such as time of the day and geo-location, as well as information feeds collected from external sources such as SIEM and threat intelligence tools.

NOTE

It is also worth noting that Octelium has no notion of a "superuser" or an "admin" User. There is simply no User, by default, who has all permissions to access all Services, including the Cluster's API Server, unless having been explicitly allowed by a Policy.

Request Context

Octelium exposes to you the entire current state of all information related to the request in order for you to freely design your policies as code as will be shown later. All such information are contained in a single data structure called RequestContext. RequestContext information can be mainly classified into:

  • Subject/principal-based information (namely the Session, its User and all of the User's Groups)
  • Object/resource-based information (namely the Service and its Namespace)
  • The request specific information (e.g. application layer (i.e. L-7) information such as HTTP request method, headers, etc...) whenever available.

Policy Types

There are 2 types of Policies:

  1. Standalone Policies that are defined as typical Octelium resources in the Core API just like Services or Users. Standalone Policies are meant to be reusable and can be attached to one or more resources, namely Service, Namespace, User, Group or Session resources.

Here is an example of a standalone Policy that is attached to a Service:

1
kind: Policy
2
metadata:
3
name: allow-all
4
spec:
5
rules:
6
- effect: ALLOW
7
condition:
8
matchAny: true
9
---
10
kind: Service
11
metadata:
12
name: example-svc
13
spec:
14
mode: HTTP
15
config:
16
upstream:
17
url: https://example.com
18
authorization:
19
# Now we attach the above Policy to the Service
20
policies: ["allow-all"]
NOTE

It's important to understand that defining Standalone Policies is not enough to make them effective. They must be attached to a resource, such as a Service or a User, in order for the Policy to actually be applied for requests coming to that Service or from that User.

  1. Inline Policies that are defined inside a specific resource. Inline Policies are more useful when the rules written for a specific resource and where creating a separate, reusable, standalone Policy does not make much sense. Here is the equivalent example of the above standalone Policy when defined directly as an inline Policy inside a Service:
1
kind: Service
2
metadata:
3
name: example-svc
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://example.com
9
authorization:
10
inlinePolicies:
11
- spec:
12
rules:
13
- effect: ALLOW
14
condition:
15
matchAny: true

Condition

Every Policy has a set of one or more rules that share a common purpose or functionality of the Policy. Each rule has an effect that either ALLOW or DENY and a corresponding condition that must match in order for the rule to match and trigger its effect decision.

A condition can be one of the following types: match, matchAny, all, any, not, none or opa. As you will see below, main conditions types such as match contain expressions written in Common Expression Language CEL. CEL is chosen not only for its performance but also for its extreme simplicity. Octelium exposes the RequestContext inside the ctx map as will be shown in the examples below.

Here are some examples for the different condition types:

MatchAny

matchAny is simply a boolean that matches anything. Here is an example:

1
kind: Policy
2
metadata:
3
name: allow-all
4
spec:
5
rules:
6
- effect: ALLOW
7
condition:
8
matchAny: true

Match

match is represented by a single conditional CEL expression. Here is an example:

1
kind: Policy
2
metadata:
3
name: deny-contractors
4
spec:
5
rules:
6
# Deny when any User that belongs to the "contractors" Group
7
- effect: DENY
8
condition:
9
match: '"contractors" in ctx.user.spec.groups'
NOTE

The CEL language is rich, performant and self-explanatory in most cases. Refer to this guide here to know more about the capabilities of CEL to write more dynamic and complex expressions.

Not

not is the inversion of match conditions. Here is an example:

1
kind: Policy
2
metadata:
3
name: deny-workloads
4
spec:
5
rules:
6
# Allow when any User type is NOT "WORKLOAD"
7
- effect: ALLOW
8
condition:
9
not: ctx.user.spec.type == "WORKLOAD"

All

all simply acts as a logical AND operator to one or more children conditions. In other words, an all condition matches if all of its children conditions match. Here is an example:

1
kind: Policy
2
metadata:
3
name: match-expression
4
spec:
5
rules:
6
# Allow when ALL such expressions match
7
- effect: ALLOW
8
condition:
9
all:
10
of:
11
- match: ctx.user.spec.type == "HUMAN"
12
- match: ctx.device.status.osType == "WINDOWS"
13
- match: ctx.session.status.type == "CLIENT"
14
- match: '"friends" in ctx.user.spec.groups'
15
- match: ctx.namespace.metadata.name == "production"

Any

any simply acts as a logical OR operator to one or more children conditions. In other words, an any condition matches if any of its children conditions match. Here is an example:

1
kind: Policy
2
metadata:
3
name: match-expression
4
spec:
5
rules:
6
- effect: ALLOW
7
condition:
8
# Match when any of the following expressions match
9
any:
10
of:
11
- match: ctx.user.spec.type == "HUMAN" && "dev" in ctx.user.spec.groups
12
- match: ctx.user.spec.type == "WORKLOAD" && "ops" in ctx.user.spec.groups

None

none simply acts as a logical NOR operator to one or more children conditions. In other words, an none condition matches id none of its children conditions match. Here is an example:

1
kind: Policy
2
metadata:
3
name: neither-dev-nor-ops
4
spec:
5
rules:
6
- effect: ALLOW
7
condition:
8
# Match when none of the following expressions match
9
none:
10
of:
11
- match: '"dev" in ctx.user.spec.groups'
12
- match: '"ops" in ctx.user.spec.groups'

OPA

opa Octelium also enables you to define your condition as an OPA (Open Policy Agent) Rego script. OPA enables you to define complex policies as code that might be too complicated to be described in CEL Conditions. Note that in the case of OPA scripts, the ctx as well as attrs fields are now children of the input field. Also the result must be boolean and set to the match OPA "rule". Here is an example:

1
kind: Policy
2
metadata:
3
name: opa-example
4
spec:
5
rules:
6
- effect: ALLOW
7
condition:
8
opa:
9
inline: |
10
package octelium.condition
11
12
default match = false
13
14
match {
15
input.ctx.user.spec.groups[_] == "group1"
16
}
17
18
match {
19
input.ctx.user.metadata.name != "usr1"
20
input.ctx.user.spec.groups[_] == "group2"
21
startsWith(input.ctx.request.http.path, "/path1")
22
input.ctx.request.http.method == "GET"
23
}

Nesting Conditions

all, any and none conditions can parent any condition type including further all, any or none conditions. This gives you the ability to design more complex rules in a readable, performant and maintainable way. Here is an example:

1
kind: Policy
2
metadata:
3
name: match-expression
4
spec:
5
rules:
6
# Allow when ALL such expressions match
7
- effect: ALLOW
8
condition:
9
all:
10
of:
11
- match: ctx.user.spec.type == "HUMAN"
12
- any:
13
of:
14
- match: '"friends" in ctx.user.spec.groups'
15
- match: '"also-friends" in ctx.user.spec.groups'
16
- none:
17
of:
18
- match: '"bad-friends" in ctx.user.spec.groups'
19
- match: '"former-friends" in ctx.user.spec.groups'
20

Policy Priority

So far, we have assumed that all rules have the same priority across all request's Policies to be evaluated. That assumption made ordering the rules irrelevant since any DENY rule that matches would just deny the request regardless of there is one or more ALLOW rules in any Policy that also matches. To facilitate easier Policy management and provide even richer and more dynamic control, Octelium also provides a priority level integer for each rule priority that is taken into consideration globally across all Policies. In other words, a rule's priority enables an ALLOW rule of a higher priority (i.e. lower number) rule to override any DENY rule that matches but with a lower priority (i.e. higher number).Simply the lower the number, the higher the priority. The priority level can be chosen from within range from -16 (i.e. highest priority) to 16 (i.e. lowest priority) and by default a rule has a priority of zero.

Here is an example:

1
kind: Policy
2
metadata:
3
name: allow-all
4
spec:
5
rules:
6
- effect: ALLOW
7
priority: -1
8
condition:
9
matchAny: true
10
- effect: DENY
11
condition:
12
matchAny: true

And here is another example:

1
kind: Policy
2
metadata:
3
name: allow-management-prod
4
spec:
5
rules:
6
- condition:
7
all:
8
of:
9
- match: ctx.service.metadata.name == "api.production"
10
- match: '"management" in ctx.user.spec.groups'
11
effect: ALLOW
12
priority: 1
13
- condition:
14
all:
15
of:
16
- match: ctx.service.metadata.name == "api.production"
17
effect: DENY
18
priority: 2

Enforcement Rules

Using enforcement rules is optional, enforcement rules can be seen as "pre-conditions" that must be met in order for the Policy to be evaluated. You can think of Enforcement rules as rules for the Policy itself as to whether it should be enforced/evaluated or ignored altogether. The goal of enforcement rules is to make Policy rules more concise, readable and and potentially more performant since in many cases such rules may include shared conditions which can be taken out of the rules and used as pre-conditions for the Policy itself. For example, you might want to enforce a Policy only under certain pre-conditions (e.g. enforcing a Policy only for certain time of the day, for a certain set of Namespaces, Services or Users, etc...). While you can definitely do that inside the rules' conditions, you will probably have to reuse such logic in all of your rules that share the same Policy.

Let's see this example for a Service that allows any User who belongs to the friends Group except for any HTTP path that starts with /admin.

1
kind: Service
2
metadata:
3
name: dashboard
4
spec:
5
authorization:
6
- policies: ["p-dashboard"]
7
8
---
9
kind: Policy
10
metadata:
11
name: p-dashboard
12
spec:
13
rules:
14
- condition:
15
all:
16
of:
17
- match: '"friends" in ctx.user.spec.groups'
18
- match: ctx.request.http.path.startsWith("/admin")
19
effect: DENY
20
- condition:
21
match: '"friends" in ctx.user.spec.groups'
22
effect: ALLOW

You can notice that we had to use the expression "friends" in ctx.user.spec.groups twice since this Policy was supposed to be confined to Users belonging to the friends Group. We could tune this Policy in order to be more readable and performant by separating this precondition as an enforcement rule as follow:

1
kind: Policy
2
metadata:
3
name: p-dashboard
4
spec:
5
enforcementRules:
6
- effect: IGNORE
7
condition:
8
not: '"friends" in ctx.user.spec.groups'
9
rules:
10
- effect: DENY
11
condition:
12
match: ctx.request.http.path.startsWith("/admin")
13
- effect: ALLOW
14
condition:
15
matchAny: true

As you can see from the example above, the only difference in syntax between Enforcement rules and ordinary access rules is that the effect of enforcement rules is either IGNORE which forces the Policy to be ignored or ENFORCE which forces the Policy to be evaluated. If there are no enforcement rule that matches or if the list of enforcement rules is empty then the default behavior is to ENFORCE the Policy. In other words, ENFORCE enforcement rules simply always override IGNORE rules in the same way that DENY rules override ALLOW rules.

Attributes

In some cases, you might want to define Policy-specific structured data that can be (re)used inside the Policy rules. While not required, we strongly recommend that your keys are defined in camel case (e.g. userType) as opposed to snake case (e.g. user_type) to conform with the rest of Octelium API design convention. Here is an example:

1
kind: Policy
2
metadata:
3
name: http-dangerous-methods
4
spec:
5
attrs:
6
deniedGroups: ["grp-1", "grp-2"]
7
deniedMethods: ["POST", "PUT", "DELETE"]
8
userType: "HUMAN"
9
10
rules:
11
- effect: DENY
12
condition:
13
match: ctx.user.spec.groups.hasAny(attrs.deniedGroups)
14
- effect: DENY
15
condition:
16
all:
17
of:
18
- match: ctx.user.spec.groups.hasAny(["grp-3"])
19
- match: ctx.request.http.method in attrs.deniedMethods
20
- effect: DENY
21
condition:
22
all:
23
of:
24
- not: ctx.user.spec.type == attrs.userType
25
- match: ctx.request.http.path.startsWith("/admin")
26
- effect: ALLOW
27
condition:
28
matchAny: true

Disabling a Policy

In some cases you might need to disable a certain Policy( e.g. for diagnostic reasons) instead of having to totally remove it. You can disable a Policy simply as follows:

1
kind: Policy
2
metadata:
3
name: allow-all
4
spec:
5
isDisabled: true
6
rules:
7
- effect: ALLOW
8
condition:
9
matchAny: true

Global Policies

As we have seen so far, all Polices had to be attached to resources such as Users or Services in order to be applied to a relevant request. Moreover, Octelium also lets you to define global Polices that are always triggered across all the Cluster's requests. Such global Polices, as well as inline Polices, are defined in the ClusterConfig as follows:

1
kind: ClusterConfig
2
metadata:
3
name: cluster-config
4
spec:
5
authorization:
6
policies: ["pol-1", "pol-2"]
7
inlinePolicies:
8
- spec:
9
rules:
10
- effect: DENY
11
condition:
12
match: '"junior" in ctx.user.spec.groups && ctx.namespace.metadata.name == "production"'

Extending Access Control

One of the main tenets of zero trust is to grant access based on dynamically evaluated trust on a per-request basis where such trust should be evaluated from as many information sources as possible (e.g. IAM platforms, SIEM tools, threat intelligence tools, incident alerting and on-call management tools, etc...).

With Octelium, you can extend access control to include arbitrary attributes to a various Cluster resources via the attrs field that can contain any arbitrary data. The following Resources support the attrs field: User, Group, Service and Namespace. Here is an example for a User:

1
kind: User
2
metadata:
3
name: john
4
spec:
5
type: HUMAN
7
attrs:
8
pagerDuty:
9
onCall: true
10
azureAD:
11
isAdmin: true
12
anotherTool:
13
key1: val1
14
key2: 100
15
key3: true
16
key4:
17
subKey1: subVal1

Now you can enforce such information in your Policies as follows:

1
kind: Policy
2
metadata:
3
name: allow-on-call
4
spec:
5
rules:
6
- effect: ALLOW
7
condition:
8
match: ctx.user.spec.attrs.pagerDuty.onCall

As stated above in Polices's attributes, we strongly recommend that your keys are defined in camel case (e.g. azureAD) as opposed to snake case (e.g. azure_ad) to conform with the rest of Octelium API design convention.

© 2025 octelium.comOctelium Labs, LLCAll rights reserved
Octelium and Octelium logo are trademarks of Octelium Labs, LLC.
WireGuard is a registered trademark of Jason A. Donenfeld