# Policies and Access Control

> Octelium documentation. Canonical page: <https://octelium.com/docs/octelium/latest/management/core/policy>.

## Overview

The [zero trust](https://octelium.com/docs/octelium/latest/overview/zero-trust.md) security model is primarily focused on preventing unauthorized access by enforcing dynamic and granular per-request access control at the resource level. This 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, aligning with the *Cluster* administrators' requirements and needs. Each *rule* has:

- A *condition* that **must** match for the rule and, subsequently the *Policy* as a whole, to match.
- An *effect* that is either `ALLOW` or `DENY`, which decides the outcome of the *rule* if it matches.

[*Vigil*](https://octelium.com/docs/octelium/latest/reference/components.md#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*](https://octelium.com/docs/octelium/latest/reference/components.md#octovigil), the policy decision point (PDP). *Octovigil* in turn identifies the *Session* and its *User*, and then collects and evaluates all *Policies* required by that specific request to decide whether the request should be allowed or denied. Such required *Policies* are those defined in or attached to the request's *Session*, *User*, *User*'s *Groups*, in addition to the *Service* and its *Namespace*. Once all required *Policies* for the request are collected, *Octovigil* starts evaluating all the *Policies* rules as follows:

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

*Diagram: PolicyOverview.* [View the diagram on the canonical HTML page](https://octelium.com/docs/octelium/latest/management/core/policy).

Octelium provides you with an extremely flexible, multi-layered access control system 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 the successor to 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, and the request context. This 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 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*, namely `ctx` is shown later in this guide. *RequestContext* information can be mainly classified into:

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

While CEL conditions are discussed  in detail ([here](https://octelium.com/docs/octelium/latest/management/core/policy.md#condition)), you can see how the *RequestContext* different fields can be used in your access control decisions:

```yaml
kind: Policy
metadata:
  name: example
spec:
  rules:
    - effect: ALLOW
      condition:
        all:
          of:
            - match: ctx.user.spec.type == "HUMAN"
            - match: ctx.service.spec.mode == "WEB"
            - match: ctx.device.status.osType == "WINDOWS"
            - match: ctx.session.status.type == "CLIENT"
            - match: '"friends" in ctx.user.spec.groups'
            - match: ctx.namespace.metadata.name in ["production", "development"]
            - match: ctx.groups.exists(x, x.metadata.name.contains("admin"))
            - match: ctx.request.http.path.startsWith("/api/v1")
```

You can see more detailed examples below ([here](#all), [here](#match), [here](#any), [here](#none) and [here](#opa)) on how the *RequestContext* map fields are used.

## 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*:

```yaml
kind: Policy
metadata:
# !mark
  name: allow-all
spec:
  rules:
    - effect: ALLOW
      condition:
        matchAny: true
---
kind: Service
metadata:
  name: example-svc
spec:
  mode: HTTP
  config:
    upstream:
      url: https://example.com
  authorization:
    # Now we attach the above Policy to the Service
# !mark
    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*.

2. 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*:

```yaml
kind: Service
metadata:
  name: example-svc
spec:
  mode: HTTP
  config:
    upstream:
      url: https://example.com
  authorization:
    inlinePolicies:
      - spec:
          rules:
            - effect: ALLOW
              condition:
                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](https://github.com/google/cel-spec). 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:

```yaml
kind: Policy
metadata:
  name: allow-all
spec:
  rules:
    - effect: ALLOW
      condition:
      # !mark
        matchAny: true
```

### Match

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

```yaml
kind: Policy
metadata:
  name: deny-contractors
spec:
  rules:
    # Deny when any User that belongs to the "contractors" Group
    - effect: DENY
      condition:
      # !mark
        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](https://octelium.com/docs/octelium/latest/management/guide/cel.md) 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:

```yaml
kind: Policy
metadata:
  name: deny-workloads
spec:
  rules:
    # Allow when any User type is NOT "WORKLOAD"
    - effect: ALLOW
      condition:
      # !mark
        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:

```yaml
kind: Policy
metadata:
  name: match-expression
spec:
  rules:
    # Allow when ALL such expressions match
    - effect: ALLOW
      condition:
      # !mark(1:8)
        all:
          of:
            - match: ctx.user.spec.type == "HUMAN"
            - match: ctx.device.status.osType == "WINDOWS"
            - match: ctx.session.status.type == "CLIENT"
            - match: '"friends" in ctx.user.spec.groups'
            - match: ctx.namespace.metadata.name == "production"
            - match: ctx.groups.exists(x, x.metadata.name.contains("admin"))
```

### 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:

```yaml
kind: Policy
metadata:
  name: match-expression
spec:
  rules:
    - effect: ALLOW
      condition:
        # Match when any of the following expressions match
        # !mark(1:4)
        any:
          of:
            - match: ctx.user.spec.type == "HUMAN" && "dev" in ctx.user.spec.groups
            - 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:

```yaml
kind: Policy
metadata:
  name: neither-dev-nor-ops
spec:
  rules:
    - effect: ALLOW
      condition:
        # Match when none of the following expressions match
        # !mark(1:4)
        none:
          of:
            - match: '"dev" in ctx.user.spec.groups'
            - match: '"ops" in ctx.user.spec.groups'
```

### OPA

`opa` Octelium also enables you to define your condition as an [OPA](https://www.openpolicyagent.org/) (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:

```yaml
kind: Policy
metadata:
  name: opa-example
spec:
  rules:
    - effect: ALLOW
      condition:
        opa:
          inline: |
            package octelium.condition

            default match = false

            match if {
              input.ctx.user.spec.groups[_] == "group1"
            }

            match if {
              input.ctx.user.metadata.name != "usr1"
              input.ctx.user.spec.groups[_] == "group2"
              startsWith(input.ctx.request.http.path, "/path1")
              input.ctx.request.http.method == "GET"
            }
```

## 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:

```yaml
kind: Policy
metadata:
  name: match-expression
spec:
  rules:
    # Allow when ALL such expressions match
    - effect: ALLOW
      condition:
        all:
          of:
            - match: ctx.user.spec.type == "HUMAN"
            - any:
                of:
                  - match: '"friends" in ctx.user.spec.groups'
                  - match: '"also-friends" in ctx.user.spec.groups'
                  - none:
                      of:
                        - match: '"bad-friends" in ctx.user.spec.groups'
                        - match: '"former-friends" in ctx.user.spec.groups'

```

## Policy Priority

So far, we have assumed that all rules have the same priority across all of the request's *Policies*. That assumption made ordering the rules irrelevant, since any matching `DENY` rule would deny the request regardless of any matching `ALLOW` rules.

To facilitate easier *Policy* management and provide even richer control, Octelium also provides a priority level integer for each rule that is taken into consideration globally across all *Policies*. The lower the number, the higher the priority. A rule's priority enables an `ALLOW` rule of a higher priority to override a matching `DENY` rule of a lower priority. The priority level can be chosen within the range from -4 (i.e. the highest priority) to 4 (i.e. the lowest priority), and by default a rule has a priority of zero.

Here is an example:

```yaml
kind: Policy
metadata:
  name: allow-all
spec:
  rules:
    - effect: ALLOW
      #!mark
      priority: -1
      condition:
        matchAny: true
    - effect: DENY
      condition:
        matchAny: true
```

And here is another example:

```yaml
kind: Policy
metadata:
  name: allow-management-prod
spec:
  rules:
    - condition:
        all:
          of:
            - match: ctx.service.metadata.name == "api.production"
            - match: '"management" in ctx.user.spec.groups'
      effect: ALLOW
      #!mark
      priority: 1
    - condition:
        all:
          of:
            - match: ctx.service.metadata.name == "api.production"
      effect: DENY
      #!mark
      priority: 2
```

## L7-Aware Policies

Octelium's application layer (L7) awareness allows you to control access at the fine-grained application layer (e.g. application-specific actions such as HTTP request headers and body content, database commands, etc...). Here are some examples:

- [HTTP](https://octelium.com/docs/octelium/latest/management/core/service/http.md#access-control)
- [Kubernetes](https://octelium.com/docs/octelium/latest/management/core/service/kubernetes.md#access-control)
- [gRPC](https://octelium.com/docs/octelium/latest/management/core/service/http.md#grpc-mode)
- [SSH](https://octelium.com/docs/octelium/latest/management/core/service/ssh.md#access-control)
- [PostgreSQL](https://octelium.com/docs/octelium/latest/management/core/service/postgres.md#access-control)
- [MySQL](https://octelium.com/docs/octelium/latest/management/core/service/mysql.md#access-control)
- [DNS](https://octelium.com/docs/octelium/latest/management/core/service/dns.md#access-control)

## 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 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`.

```yaml
kind: Service
metadata:
  name: dashboard
spec:
  authorization:
    policies: ["p-dashboard"]
---
kind: Policy
metadata:
  name: p-dashboard
spec:
  rules:
    - condition:
        all:
          of:
          #!mark
            - match: '"friends" in ctx.user.spec.groups'
            - match: ctx.request.http.path.startsWith("/admin")
      effect: DENY
    - condition:
    #!mark
        match: '"friends" in ctx.user.spec.groups'
      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:

```yaml
kind: Policy
metadata:
  name: p-dashboard
spec:
# !mark(1:4)
  enforcementRules:
    - effect: IGNORE
      condition:
        not: '"friends" in ctx.user.spec.groups'
  rules:
    - effect: DENY
      condition:
        match: ctx.request.http.path.startsWith("/admin")
    - effect: ALLOW
      condition:
        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:

```yaml
kind: Policy
metadata:
  name: http-dangerous-methods
spec:
# !mark(1:4)
  attrs:
    deniedGroups: ["grp-1", "grp-2"]
    deniedMethods: ["POST", "PUT", "DELETE"]
    userType: "HUMAN"

  rules:
    - effect: DENY
      condition:
      # !mark
        match: ctx.user.spec.groups.hasAny(attrs.deniedGroups)
    - effect: DENY
      condition:
        all:
          of:
            - match: ctx.user.spec.groups.hasAny(["grp-3"])
            # !mark
            - match: ctx.request.http.method in attrs.deniedMethods
    - effect: DENY
      condition:
        all:
          of:
          # !mark
            - not: ctx.user.spec.type == attrs.userType
            - match: ctx.request.http.path.startsWith("/admin")
    - effect: ALLOW
      condition:
        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:

```yaml
kind: Policy
metadata:
  name: allow-all
spec:
# !mark
  isDisabled: true
  rules:
    - effect: ALLOW
      condition:
        matchAny: true
```

## Global Policies

As we have seen so far, all *Policies* 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 *Policies* that are always triggered across all the *Cluster*'s requests. Such global *Policies*, as well as inline *Policies*, are defined in the *ClusterConfig* as follows:

```yaml
kind: ClusterConfig
metadata:
  name: cluster-config
spec:
#!mark(1:9)
  authorization:
    policies: ["pol-1", "pol-2"]
    inlinePolicies:
      - spec:
          rules:
            - effect: DENY
              condition:
                match: '"junior" in ctx.user.spec.groups && ctx.namespace.metadata.name == "production"'
```

## Listing Policies

You can list the *Cluster*'s *Policies* (read more about listing resources [here](https://octelium.com/docs/octelium/latest/management/core/overview.md#listing-resources)) as follows:

```bash
octeliumctl get policy
# Or simply
octeliumctl get pol

# Show a certain Policy
octeliumctl get pol <NAME>
```

## 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*:

```yaml
kind: User
metadata:
  name: john
spec:
  type: HUMAN
  email: john@example.com
  # !mark(1:11)
  attrs:
    pagerDuty:
      onCall: true
    azureAD:
      isAdmin: true
    anotherTool:
      key1: val1
      key2: 100
      key3: true
      key4:
        subKey1: subVal1
```

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

```yaml
kind: Policy
metadata:
  name: allow-on-call
spec:
  rules:
    - effect: ALLOW
      condition:
      # !mark
        match: ctx.user.spec.attrs.pagerDuty.onCall
```

As stated above in *Policies*'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.

## API Reference

[Policy JSON Schema (Octelium core/v1, latest documentation)](https://octelium.com/schemas/octelium/latest/core.v1.Policy.schema.json)
