OPA Authorization

The OPA feature was introduced with Gloo Enterprise, release 0.18.21. If you are using an earlier version, this tutorial will not work.

The Open Policy Agent (OPA) is an open source, general-purpose policy engine that can be used to define and enforce versatile policies in a uniform way across your organization. Compared to an RBAC authorization system, OPA allows you to create more fine-grained policies. For more information, see the official docs.

Be sure to check the external auth configuration overview for detailed information about how authentication is configured on Virtual Services.

Table of Contents

Setup

This guide assumes that you have deployed Gloo to the gloo-system namespace and that the glooctl command line utility is installed on your machine. glooctl provides several convenient functions to view, manipulate, and debug Gloo resources; in particular, it is worth mentioning the following command, which we will use each time we need to retrieve the URL of the Gloo Gateway that is running inside your cluster:

glooctl proxy url

Validate requests attributes with Open Policy Agent

Deploy a sample application

Let’s deploy a sample application that we will route requests to during this guide:

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo/master/example/petstore/petstore.yaml

Creating a Virtual Service

Now we can create a Virtual Service that routes all requests (note the / prefix) to the petstore service.

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: petstore
  namespace: gloo-system
spec:
  virtualHost:
    domains:
    - '*'
    routes:
    - matcher:
        prefix: /
      routeAction:
        single:
          kube:
            ref:
              name: petstore
              namespace: default
            port: 8080

To verify that the Virtual Service works, let’s send a request to /api/pets:

curl $(glooctl proxy url)/api/pets

You should see the following output:

[{"id":1,"name":"Dog","status":"available"},{"id":2,"name":"Cat","status":"pending"}]

Securing the Virtual Service

The auth configuration format shown on this page was introduced with Gloo Enterprise, release 0.20.1. If you are using an earlier version, please refer to this page to see which configuration formats are supported by each version.

As we just saw, we were able to reach the upstream without having to provide any credentials. This is because by default Gloo allows any request on routes that do not specify authentication configuration. Let’s change this behavior. We will update the Virtual Service so that only requests that comply with a given OPA policy are allowed.

Define an OPA policy

Open Policy Agent policies are written in Rego. The Rego language is inspired from Datalog, which in turn is a subset of Prolog. Rego is more suited to work with modern JSON documents. Let’s create a Policy to control which actions are allowed on our service:

cat <<EOF > policy.rego
package test

default allow = false
allow {
    startswith(input.http_request.path, "/api/pets")
    input.http_request.method == "GET"
}
allow {
    input.http_request.path == "/api/pets/2"
    any({input.http_request.method == "GET",
        input.http_request.method == "DELETE"
    })
}
EOF

This policy:

Create an OPA AuthConfig CRD

Gloo expects OPA policies to be stored in a Kubernetes ConfigMap, so let’s go ahead and create a ConfigMap with the contents of the above policy file:

kubectl -n gloo-system create configmap allow-get-users --from-file=policy.rego

Now we can create an AuthConfig CRD with our OPA authorization configuration:

kubectl apply -f - <<EOF
apiVersion: enterprise.gloo.solo.io/v1
kind: AuthConfig
metadata:
  name: opa
  namespace: gloo-system
spec:
  configs:
  - opa_auth:
      modules:
      - name: allow-get-users
        namespace: gloo-system
      query: "data.test.allow == true"
EOF

The above AuthConfig references the ConfigMap (modules) we created earlier and adds a query that allows access only if the allow variable is true.

Updating the Virtual Service

Once the AuthConfig has been created, we can use it to secure our Virtual Service:

kubectl apply -f - <<EOF
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: petstore
  namespace: gloo-system
spec:
  virtualHost:
    domains:
    - '*'
    routes:
    - matcher:
        prefix: /
      routeAction:
        single:
          kube:
            ref:
              name: petstore
              namespace: default
            port: 8080
    virtualHostPlugins:
      extauth:
        config_ref:
          name: opa
          namespace: gloo-system
EOF

In the above example we have added the configuration to the Virtual Host. Each route belonging to a Virtual Host will inherit its AuthConfig, unless it overwrites or disables it.

Testing the configuration

Paths that don’t start with /api/pets are not authorized (should return 403):

curl -s -w "%{http_code}\n" $(glooctl proxy url)/api/

403

Not allowed to delete pets/1 (should return 403):

curl -s -w "%{http_code}\n" $(glooctl proxy url)/api/pets/1 -X DELETE

403

Allowed to delete pets/2 (should return 204):

curl -s -w "%{http_code}\n" $(glooctl proxy url)/api/pets/2 -X DELETE

204

Cleanup

You can clean up the resources created in this guide by running:

kubectl delete vs -n gloo-system petstore
kubectl delete ac -n gloo-system opa
kubectl delete -f https://raw.githubusercontent.com/solo-io/gloo/master/example/petstore/petstore.yaml
rm policy.rego

Validate JWTs with Open Policy Agent

The Open Policy Agent policy language has in-built support for JSON Web Tokens (JWTs), allowing you to define policies based on the claims contained in a JWT. If you are using an authentication mechanism that conveys identity information via JWTs (e.g. OpenID Connect), this feature makes it easy to implement authorization for authenticated users.

In this guide we will see how to use OPA to enforce policies on the JWTs produced by Gloo’s OpenID Connect (OIDC) authentication module.

Deploy sample application

The sample petclinic application deploys a MySql server. If you are using minikube v1.5 to run this guide, this service is likely to crash due a minikube issue. To get around this, you can start minikube with the following flag:

minikube start --docker-opt="default-ulimit=nofile=102400:102400" 

Let’s deploy a sample web application that we will use to demonstrate these features:

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo/v0.8.4/example/petclinic/petclinic.yaml

Create a Virtual Service

Now we can create a Virtual Service that routes all requests (note the / prefix) to the petclinic service.

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: petclinic
  namespace: gloo-system
spec:
  virtualHost:
    domains:
    - '*'
    routes:
    - matcher:
        prefix: /
      routeAction:
        single:
          kube:
            ref:
              name: petclinic
              namespace: default
            port: 80

To verify that the Virtual Service has been accepted by Gloo, let’s port-forward the Gateway Proxy service so that it is reachable from you machine at localhost:8080:

kubectl -n gloo-system port-forward svc/gateway-proxy-v2 8080:80

If you open your browser and navigate to localhost:8080 you should see the following page:

Pet Clinic app homepage

Secure the Virtual Service

As we just saw, we were able to reach the service without having to provide any credentials. This is because by default Gloo allows any request on routes that do not specify authentication configuration. Let’s change this behavior. We will update the Virtual Service so that each request to the sample application is:

Install Dex

To implement the authentication flow, we need an OpenID Connect provider to be running in your cluster. To this end, we will deploy the Dex identity service, as it easy to install and configure.

Let’s start by defining a dex-values.yaml Helm values file with some bootstrap configuration for Dex:

cat > dex-values.yaml <<EOF
config:
  # The base path of dex and the external name of the OpenID Connect service.
  # This is the canonical URL that all clients MUST use to refer to dex. If a
  # path is provided, dex's HTTP service will listen at a non-root URL.
  issuer: http://dex.gloo-system.svc.cluster.local:32000

  # Instead of reading from an external storage, use this list of clients.
  staticClients:
  - id: gloo
    redirectURIs:
    - 'http://localhost:8080/callback'
    name: 'GlooApp'
    secret: secretvalue
  
  # A static list of passwords to login the end user. By identifying here, dex
  # won't look in its underlying storage for passwords.
  staticPasswords:
  - email: "admin@example.com"
    # bcrypt hash of the string "password"
    hash: "\$2a\$10\$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
    username: "admin"
    userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
  - email: "user@example.com"
    # bcrypt hash of the string "password"
    hash: "\$2a\$10\$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
    username: "user"
    userID: "123456789-db88-4b73-90a9-3cd1661f5466"
EOF

This configures Dex with two static users. Notice the client secret with value secretvalue.

Using this configuration, we can deploy Dex to our cluster using Helm:

helm install --name dex --namespace gloo-system stable/dex -f dex-values.yaml

Make the client secret accessible to Gloo

To be able to act as our OIDC client, Gloo needs to have access to the client secret we just defined, so that it can use it to identify itself with the Dex authorization server. Gloo expects the client secret to be stored in a specific format inside of a Kubernetes Secret.

Let’s create the secret and name it oauth:


glooctl create secret oauth --client-secret secretvalue oauth

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  annotations:
    resource_kind: '*v1.Secret'
  name: oauth
  namespace: gloo-system
data:
  # The value is a base64 encoding of the following YAML:
  # config:
  #   client_secret: secretvalue
  # Gloo expected OAuth client secrets in this format.
  extension: Y29uZmlnOgogIGNsaWVudF9zZWNyZXQ6IHNlY3JldHZhbHVlCg==

Create a Policy

We now need to define a Policy to control access to our sample application based on the properties contained in the JWT ID tokens issued to authenticated requests by our OIDC provider. Let’s store the policy in a file named check-jwt.rego (see the previous guide for more info about the policy language):

cat <<EOF > check-jwt.rego
package test

default allow = false

allow {
    [header, payload, signature] = io.jwt.decode(input.state.jwt)
    payload["email"] = "admin@example.com"
}
allow {
    [header, payload, signature] = io.jwt.decode(input.state.jwt)
    payload["email"] = "user@example.com"
    not startswith(input.http_request.path, "/owners")
}
EOF

This policy allows the request if:

Notice how we are using the io.jwt.decode function to decode the JWT and how we access claims in the payload.

Gloo expects OPA policies to be stored in a Kubernetes ConfigMap, so let’s go ahead and create a ConfigMap with the contents of the above policy file:

kubectl --namespace=gloo-system create configmap allow-jwt --from-file=check-jwt.rego

Create a multi-step AuthConfig

The auth configuration format shown on this page was introduced with Gloo Enterprise, release 0.20.1. If you are using an earlier version, please refer to this page to see which configuration formats are supported by each version.

Now that all the necessary resources are in place we can create the AuthConfig resource that we will use to secure our Virtual Service.

apiVersion: enterprise.gloo.solo.io/v1
kind: AuthConfig
metadata:
  name: jwt-opa
  namespace: gloo-system
spec:
  configs:
  - oauth:
      app_url: http://localhost:8080/
      callback_path: /callback
      client_id: gloo
      client_secret_ref:
        name: oauth
        namespace: gloo-system
      issuer_url: http://dex.gloo-system.svc.cluster.local:32000/
      scopes:
      - email
  - opa_auth:
      modules:
      - name: allow-jwt
        namespace: gloo-system
      query: "data.test.allow == true"

The above AuthConfig defines two configurations that Gloo will execute in order:

  1. First, Gloo will use its extauth OIDC module to authenticate the incoming request. If authentication was successful, Gloo will add the JWT ID token to the Authorization request header and execute the next configuration; otherwise it will deny the request. Notice how the configuration references the client secret we created earlier and compare the configuration values with the ones we used to bootstrap Dex.
  2. If authentication was successful, Gloo will check the request against the allow-jwt OPA policy to determine whether it should be allowed. Notice how the configuration references the modules ConfigMap we created earlier and defines a query that allows access only if the allow variable in the policy evaluates to true.

Update the Virtual Service

Once the AuthConfig has been created, we can use it to secure our Virtual Service:

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: petclinic
  namespace: gloo-system
spec:
  virtualHost:
    domains:
    - '*'
    routes:
    - matcher:
        prefix: /
      routeAction:
        single:
          kube:
            ref:
              name: petclinic
              namespace: default
            port: 80
    virtualHostPlugins:
      extauth:
        config_ref:
          name: jwt-opa
          namespace: gloo-system

Testing our configuration

The OIDC flow redirects the client (in this case, your browser) to a login page hosted by Dex. Since Dex is running in your cluster and is not publicly reachable, we need some additional configuration to make our example work. Please note that this is just a workaround to reduce the amount of configuration necessary for this example to work.

  1. Port-forward the Dex service so that it is reachable from you machine at localhost:32000:

    kubectl -n gloo-system port-forward svc/dex 32000:32000 & 
    portForwardPid1=$! # Store the port-forward pid so we can kill the process later
  2. Add an entry to the /etc/hosts file on your machine, mapping the dex.gloo-system.svc.cluster.local hostname to your localhost (the loopback IP address 127.0.0.1).

    echo "127.0.0.1 dex.gloo-system.svc.cluster.local" | sudo tee -a /etc/hosts
  3. Port-forward the Gloo Gateway Proxy service so that it is reachable from you machine at localhost:8080:

    kubectl -n gloo-system port-forward svc/gateway-proxy-v2 8080:80 &
    portForwardPid2=$!
    

Now we are ready to test our complete setup! Open you browser and navigate to localhost:8080. You should see the following login page:

Dex login page

As the demo app doesn’t have a sign-out button, use a private browser window (also known as incognito mode) to access the demo app. This will make it easy to change the user we logged in with. If you would like to change the logged in user, just close and re-open the private browser window.

You can login with admin@example.com or user@example.com with the password password. Notice that the admin user has access to all pages, while the regular user can’t access the "Find Owners" page.

Cleanup

You can clean up the resources created in this guide by running:

sudo sed '/127.0.0.1 dex.gloo-system.svc.cluster.local/d' /etc/hosts # remove line from hosts file
kill $portForwardPid1
kill $portForwardPid2
helm delete --purge dex
kubectl delete -n gloo-system secret oauth dex-grpc-ca  dex-grpc-client-tls  dex-grpc-server-tls  dex-web-server-ca  dex-web-server-tls
kubectl delete virtualservice -n gloo-system petclinic
kubectl delete authconfig -n gloo-system jwt-opa
kubectl delete -n gloo-system configmap allow-jwt
kubectl delete -f https://raw.githubusercontent.com/solo-io/gloo/v0.8.4/example/petclinic/petclinic.yaml
rm check-jwt.rego dex-values.yaml