Skip to main content
logoTetrate Service BridgeVersion: next

Service to service authorization using external authorization

Tetrate Service Bridge (TSB) provides authorization capabilities to authorize every HTTP request coming to a service from another service ("service-to-service" requests).

TSB supports local authorization by using JWT claims and external authorization which uses a service running externally to determine if a request should be allowed or denied. External authorization can be used on both gateways and workloads (through their sidecars).

You may decide to use an external authorization system if you have a separate in-house system or if you want to integrate with a third party authorization solution such as Open Policy Agent (OPA) or PlainID.

This document describes how to configure service-to-service authorization using OPA as an example. OPA is an open source, general-purpose policy engine that provides a high-level declarative language that lets you specify policy as code.

OPA support

Tetrate does not offer support for OPA. Please look elsewhere if you need support for your use case.

Before you get started, make sure you:
✓ Familiarize yourself with TSB concepts
✓ Install the TSB environment. You can use TSB demo for quick install
✓ Completed TSB usage quickstart. This document assumes you already created a Tenant and are familiar with Workspaces and Config Groups. Also you need to configure tctl to your TSB environment.

Overview

The following diagram shows the request and response flow when using an external authorization system to authorize service-to-service requests.

Service to Service Authorization

The desired result is to be able to send requests from the "Sleep workload" to "httpbin with OPA workload", and have these requests go through proper authorization checks by OPA. If a request from the "Sleep workload" is deemed to be unauthorized, a 403 Forbidden should be returned.

Note that while in this example you deploy OPA as sidecar within a pod, it is also possible to deploy OPA in a separate pod. If you do deploy OPA in a separate pod, you will need to investigate yourself what value you use when you specify the URL of the external system later.

Setting up the services

Setting up the httpbin service

You will first setup the "server side", which is the "httpbin with OPA workload" component in the diagram.

OPA policy

Before starting a service, you need to create the Kubernetes Secret which will contain OPA policy.

Below is an example of OPA policy that you will use to authorize requests. It will allow requests when:

  • JWT token is present
  • JWT token is not expired
  • URL path that you want to access is specified in the JWT token

Create a file named s2s-policy.rego with the following content:

package envoy.authz

import input.attributes.request.http as http_request

default allow = false

token = {"valid": valid, "payload": payload} {
[_, encoded] := split(http_request.headers.authorization, " ")
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
}

allow {
is_token_valid
action_allowed
}

is_token_valid {
token.valid
now := time.now_ns() / 1000000000
token.payload.nbf <= now
now < token.payload.exp
}

action_allowed {
startswith(http_request.path, base64url.decode(token.payload.path))
}

Then store the policy in Kubernetes as a Secret.

kubectl create namespace httpbin
kubectl create secret generic opa-policy -n httpbin --from-file s2s-policy.rego

Create httpbin deployment with OPA and Envoy sidecars

Once you have the policy, deploy the httpbin service that references the policy. Create a file named s2s-httpbin-with-opa.yaml with the following content:

apiVersion: v1
kind: Service
metadata:
name: httpbin-with-opa
namespace: httpbin
labels:
app: httpbin-with-opa
service: httpbin-with-opa
spec:
ports:
- name: http
port: 8000
targetPort: 80
selector:
app: httpbin-with-opa
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: httpbin-with-opa
namespace: httpbin
labels:
app: httpbin-with-opa
spec:
replicas: 1
selector:
matchLabels:
app: httpbin-with-opa
template:
metadata:
labels:
app: httpbin-with-opa
spec:
containers:
- image: docker.io/kennethreitz/httpbin
imagePullPolicy: IfNotPresent
name: httpbin
ports:
- containerPort: 80
- name: opa
image: openpolicyagent/opa:latest-envoy
securityContext:
runAsUser: 1111
volumeMounts:
- readOnly: true
mountPath: /policy
name: opa-policy
args:
- 'run'
- '--server'
- '--addr=localhost:8181'
- '--diagnostic-addr=0.0.0.0:8282'
- '--set=plugins.envoy_ext_authz_grpc.addr=:9191'
- '--set=plugins.envoy_ext_authz_grpc.query=data.envoy.authz.allow'
- '--set=decision_logs.console=true'
- '--ignore=.*'
- '/policy/s2s-policy.rego'
livenessProbe:
httpGet:
path: /health?plugins
scheme: HTTP
port: 8282
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: /health?plugins
scheme: HTTP
port: 8282
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: opa-policy
secret:
secretName: opa-policy

Then apply it with kubectl:

kubectl label namespace httpbin istio-injection=enabled --overwrite=true
kubectl apply -n httpbin -f s2s-httpbin-with-opa.yaml

Setting up the sleep service

Since you will be configuring service-to-service authorization, you need a service to act as a client to your httpbin service.

In this example you will deploy a service that does nothing but sleep, which maps to the "sleep workload" in the previous diagram. You will use kubectl exec later to issue HTTP requests to the httpbin service.

Create a file called s2s-sleep.yaml with the following content:

apiVersion: v1
kind: ServiceAccount
metadata:
name: sleep
---
apiVersion: v1
kind: Service
metadata:
name: sleep
labels:
app: sleep
service: sleep
spec:
ports:
- port: 80
name: http
selector:
app: sleep
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
spec:
replicas: 1
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
terminationGracePeriodSeconds: 0
serviceAccountName: sleep
containers:
- name: sleep
image: curlimages/curl
command: ['/bin/sleep', '3650d']
imagePullPolicy: IfNotPresent

Deploy this sleep service with kubectl:

kubectl create namespace sleep
kubectl label namespace sleep istio-injection=enabled --overwrite=true
kubectl apply -n sleep -f s2s-sleep.yaml

Testing

Test with external authorization disabled

So far you have deployed the services but have not enabled external authorization. Thus requests from the sleep service to the httpbin service are not checked for authorization.

This can be seen by checking if sending HTTP requests from the sleep service results in a 200 OK.

To send a request from sleep service, identify the pod within your sleep service:

export SLEEP_POD=$(kubectl get pod -n sleep -l app=sleep -o jsonpath={.items..metadata.name})

Then send a request from this pod to the httpbin service, which should be reachable at http://httpbin-with-opa.httpbin:8000:

kubectl exec ${SLEEP_POD} -n sleep -c sleep  -- curl -H "X-B3-Sampled: 1" http://httpbin-with-opa.httpbin:8000/headers -s -o /dev/null -w "%{http_code}\n"

With external authorization disabled, the above command should display 200.

Test with external authorization enabled

To see how the external authorization works, you will have to create a workspace and security group.

Create the workspace

Create a file called s2s-workspace.yaml with the following content.

Please note that in the following example we assume that you have deployed your httpbin service in the demo cluster that you have created using the TSB demo install. If you are using another cluster, change the cluster name in the example accordingly.

apiversion: api.tsb.tetrate.io/v2
kind: Workspace
metadata:
organization: tetrate
tenant: tetrate
name: httpbin
spec:
displayName: HttpBin Workspace
namespaceSelector:
names:
- 'demo/httpbin'
---
apiVersion: security.tsb.tetrate.io/v2
kind: Group
Metadata:
organization: tetrate
tenant: tetrate
workspace: httpbin
name: httpbin-security
spec:
namespaceSelector:
names:
- 'demo/httpbin'
configMode: BRIDGED

Then apply it using tctl:

tctl apply -f s2s-workspace.yaml

Create the SecuritySettings

Once you have a workspace, you need to create a SecuritySettings for that workspace to enable external authorization.

Create a file called s2s-security-settings.yaml with the following content.

Please note that the uri points to a local address (grpc://127.0.0.1:9191) because in this example the OPA service is deployed in the same pod as a sidecar. If you have deployed OPA in a separate pod, you will need to change the value for uri accordingly.

apiVersion: security.tsb.tetrate.io/v2
kind: SecuritySetting
metadata:
organization: tetrate
tenant: tetrate
workspace: httpbin
group: httpbin-security
name: httpbin-security-settings
spec:
authorization:
mode: CLUSTER
http:
external:
uri: 'grpc://127.0.0.1:9191'

Then apply it using tctl:

tctl apply -f s2s-security-settings.yaml

Testing the authorization

Send a request to httpbin service again.

With the SecuritySettings applied, plain requests from the sleep service to httpbin service should fail with 403 Forbidden.

kubectl exec ${SLEEP_POD} -n sleep -c sleep  -- curl -H "X-B3-Sampled: 1" http://httpbin-with-opa.httpbin:8000/headers -s -o /dev/null -w "%{http_code}\n"

The above command should display 403.

In order to authorize the requests, you need to add a JWT in the requests. For this example, the raw JWT that we would like to attach to the requests looks like the following:

{
"path": "L2hlYWRlcnM=",
"nbf": 1500000000,
"exp": 1900000000
}

The path claim has value L2hlYWRlcnM=, which is the base64 encoded form of the string /headers.

JWTs need to be passed via the Authorization header, which requires the entire JWT to be base64 encoded as show below. Save this into an environment variable:

export JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXRoIjoiTDJobFlXUmxjbk09IiwibmJmIjoxNTAwMDAwMDAwLCJleHAiOjE5MDAwMDAwMDB9.9yl8LcZdq-5UpNLm0Hn0nnoBHXXAnK4e8RSl9vn6l98"

Finally, send a request with the above JWT token to the httpbin service, making sure that the request is pointing to the path /headers, which matches the claim in the JWT. This time around you should get a 200 OK.

kubectl exec ${SLEEP_POD} -n sleep -c sleep  -- curl -H "X-B3-Sampled: 1" http://httpbin-with-opa.httpbin:8000/headers -H "Authorization: Bearer $JWT_TOKEN" -s -o /dev/null -w "%{http_code}\n"

To check that requests to other paths are not authorized, try sending the following request, which is pointing to the path /get. The following command should result in a 403 Forbidden.

kubectl exec ${SLEEP_POD} -n sleep -c sleep  -- curl -H "X-B3-Sampled: 1" http://httpbin-with-opa.httpbin:8000/get -H "Authorization: Bearer $JWT_TOKEN" -s -o /dev/null -w "%{http_code}\n"