Security

Prime goals of ODAHU Security system is to provide authentication, authorization and give users a flexible access control management solution.

The first section Security overview shows the general design of authentication and authorization is described. Look at this section to have a deep understanding of how ODAHU security works under the hood or to learn basic concepts.

The second section Policies describes default security policies for different ODAHU services and how to configure them

Implementation details of ODAHU Security system could be found here

Security overview

Component roles

There are some common terms related to access control management systems. In this documentation, the next ones are commonly used.

Identity Provider (idP)
A component that provides information about an entity (user or service). In ODAHU the role of idP can do any OpenID Connect compatible provider.
Policy Enforcement Point (PEP)
A component that enforces security policies against each request to API or other protected resources. In ODAHU the role of PEP plays Envoy proxy.
Policy Decision Point (PDP)
A component that decides whether the request (action in the system) should be permitted or not. In ODAHU role of PDP plays OpenPolicyAgent.

API Request lifecycle

To have a better understanding of how all ODAHU security components work together let’s review the API request lifecycle and describe what is happened for each HTTP request.

_images/odahu_security.png
  1. HTTP Request could be made from the outside of the cluster perimeter.
    1. In this case, the request is handled by OAuth2Proxy via Kubernetes ingress controller
    2. OAuth2Proxy looks up cookies that contain JWT Token issued by idP. If there are no such cookies it redirects the request to idP. After successful login OAuth2Proxy set issued token to cookies (and also to) and send the request to upstream. Before proxying requests to upstream OAuth2Proxy add Authorization Request Header Field from the cookie automatically by setting it from the cookie.
    3. OAuth2Proxy send request to upstream.
  2. HTTP Request from inside the cluster perimeter. Such requests usually made by background processes inside the cluster on behalf of service accounts.
    1. Service should previously authenticate in idP using OpenID Connect protocol. The most suitable way to authenticate services is OAuth2 Client Credentials Grant
    2. Service makes a request to API using issued JWT token as Authorization Request Header Field
  3. Envoy proxy as PEP that is configured as sidecar container by Istio Pilot for those ODAHU components that must be protected ensures that security policy allows making this request to ODAHU API
    1. Envoy verifies JWT token in Authorization Request Header Field using JSON Web Token (JWT) Authentication filter
    2. Envoy makes a query to OpenPolicyAgent sidecar as PDP using External Authorization filter passing parsed JWT token from the previous step. OpenPolicyAgent sidecars are injected for all pods that have odahu-flow-authorization=enabled label
  4. If a request is permitted by OpenPolicyAgent, it is sent to upstream (ODAHU API)

UML sequence diagram of a successful API request described above is shown in the image:

@startuml

participant Client order 10
participant Nginx order 20
participant Auth2Proxy order 30

box "App Pod"
participant "Envoy Proxy\nsidecar" order 40
participant "Authn Filter" order 40 #EEEBDC
participant "Authz Filter" order 50 #EEEBDC

participant "Open Policy Agent\nsidecar" order 60
participant "App Container" order 70
endbox

title Odahu Request Lifecycle

== Cluster ingress processing ==

Client -> Nginx: HTTP Request\n + JWT in cookie\nor in Header
Nginx --> Auth2Proxy: Authenticated?
note over Auth2Proxy: 1. Verify token in\nOpenID Provider
note over Auth2Proxy: 2. Set token from \ncookie to header\nif header is empty
Auth2Proxy --> Nginx: Yes
== In a Cluster processing ==
Nginx -> "Envoy Proxy\nsidecar": HTTP Request\n + JWT in Header

group envoy sidecar forces authentication and authorization
"Envoy Proxy\nsidecar" --> "Authn Filter": Authenticated?
note over "Authn Filter": 1. Verify JWT
note over "Authn Filter": 2. Parse JWT Claims
"Authn Filter" --> "Envoy Proxy\nsidecar": Yes + Parsed Claims
"Envoy Proxy\nsidecar" --> "Authz Filter": Authorized? + Claims
"Authz Filter" --> "Open Policy Agent\nsidecar": Authorized? + Claims + \nHTTP Request Info
"Open Policy Agent\nsidecar" --> "Authz Filter": Yes
"Authz Filter" --> "Envoy Proxy\nsidecar": Yes
end
"Envoy Proxy\nsidecar" -> "App Container": HTTP Request
"App Container" -> "Envoy Proxy\nsidecar": HTTP Response
"Envoy Proxy\nsidecar" -> Nginx: HTTP Response
Nginx -> Client: HTTP Response

@enduml

Policies

ODAHU is distributed with build-in policies that are written on Rego policy language and included into helm charts of appropriate services.

`Role-based access control`_ is implemented by default for next services

ODAHU API and Feedback aggregator policies

Overview

API and Feedback aggregator are distributed with a pre-defined set of OpenPolicyAgent policies. These policies implement simple `Role-based access control`_ (RBAC).

Next features are implemented using Rego policy language:

  1. Set of predefined roles with assigned permissions
  2. Default mapper that match JWT Claims to attributes that ODAHU RBAC policy expects
  3. ODAHU RBAC core policy

These features are implemented in the next files:

  • roles.rego – all odahu roles are listed here
  • permissions.rego – permissions for roles
  • input_mapper.rego – mapper to match JWT Claims to attributes ODAHU RBAC rely on. These attributes include:
    • user – info about user or service who makes the request (this property contains roles attribute with a list of roles)
    • action – HTTP verb of the request
    • resource – URL of the request
  • core.rego – core implementation of Role based access control.

All policies customization can be done on the stage of system configuration as described in installation guide

Customize

In this section, different ways to customize pre-defined policies

Extend roles

To define new custom roles, you should add them as a variable in the roles.rego file

roles.rego
1
2
3
4
5
6
7
8
 package odahu.roles

 admin := "admin"
 data_scientist := "data_scientist"
 viewer := "viewer"

 # new role
 connection_manager := "connection_manager"

Then you need to set permissions to that role in file permissions.rego

permissions.rego
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 package odahu.permissions

 import data.odahu.roles

 permissions := {
   roles.data_scientist: [
       [".*", "api/v1/model/deployment.*"],
       [".*", "api/v1/model/packaging.*"],
       [".*", "api/v1/model/training.*"],
       ["GET", "api/v1/connection.*"],
       ["GET", "api/v1/packaging/integration.*"],
       ["GET", "api/v1/toolchain/integration.*"]
     ],
   roles.admin : [
       [".*", ".*"]
   ],
   roles.viewer : [
       ["GET", ".*"]
   ],
   roles.connection_manager : [
       [".*", "api/v1/connection.*"]
   ],
 }

In this file, we:

  • lines 20-22: add permissions to any request to api/v1/connection.* URL for a new role
Customize default mapper

You can configure mapper.rego to extend input that is passed to core.rego file with RBAC implementation

mapper.rego
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package odahu.mapper

import data.odahu.roles

roles_map = {
  "odahu_admin": roles.admin,
  "odahu_data_scientist": roles.data_scientist,
  "odahu_viewer": roles.viewer
}

jwt = input.attributes.metadata_context.filter_metadata["envoy.filters.http.jwt_authn"].fields.jwt_payload

keycloak_user_roles[role]{
  role = jwt.Kind.StructValue.fields.realm_access.Kind.StructValue.fields.roles.Kind.ListValue.values[_].Kind.StringValue
}

user_roles[role]{
  role = roles_map[keycloak_user_roles[_]]
}

parsed_input = {
  "action": input.attributes.request.http.method,
  "resource": input.attributes.request.http.path,
  "user": {
    "roles": user_roles
  }
}

In this file, we:

  • lines 5-9: map roles from jwt claims to policies roles from roles.rego
  • lines 11-19: extract roles from claims and match them to policies roles
  • lines 21-26: create input that is expected by file core.rego that contains resource, action and user’s roles
Create custom policies

If `Role-based access control`_ is not enough for your purposes you can customize policies to use more general `Attribute-based access control`_. For this purpose, rewrite core.rego file or create your own rego policies from scratch

core.rego
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package odahu.core

import data.odahu.mapper.parsed_input
import data.odahu.permissions.permissions

default allow = false

allow {
  any_user_role := parsed_input.user.roles[_]
    any_permission_of_user_role := permissions[any_user_role][_]
    action := any_permission_of_user_role[0]
    resource := any_permission_of_user_role[1]

    re_match(action, parsed_input.action)
    re_match(resource, parsed_input.resource)
}

allow {
    parsed_input.action == "GET"
    parsed_input.resource == "/"
}

allow {
    parsed_input.action == "GET"
    re_match("/swagger*", parsed_input.resource)
}

In this file, we:

  • lines 8-16: allow access if there are required permissions for action and resource for at least one user’s roles
  • lines 18-21: allow access to root for any user
  • lines 23-26: allow access to swagger docs to any user

ODAHU ML Models pods policies

All deployed models contain default policies that permit requests to them for all users that have Model Deployment Access Role Name. This role can be set at the model deployment stage using .Spec.roleName key of the ModelDeployment manifest and also statically configured in policies during ODAHU deployment.