Camunda 8: OAuth for Zeebe with Istio

I am working on integrating the Camunda Platform 8 into our SaaS platform, based on Tanzu Kubernetes Grid Integrated Edition (former PKS) and Swisscom Helix (shared services). The most important issue was the missing access security for Zeebe, the workflow execution engine. This article shows how simple and powerful the authentication and authorization configuration for Istio is. We will create roles and permissions with Camunda 8 Identity and use them with the Camunda Desktop Modeler.

Architectural Overview

A short introduction can be viewed here:

 

We are using a dedicated Istio ingress gateway for customer applications. If you replace the ingress objects of the original diagram the Istio architecture for camunda 8 with webmodeler looks like this:

More details on the Istio configuration will follow, but let’s start with Camunda Identity:

Use Camunda Platform 8 security concepts

Create API in Camunda Identity

Camunda 8 Identity distincts between APIs and Applications. Both are simple OAuth clients held in keycloak, but Identity is prepared to add a comfortable layer on top of keycloak.

First you create an API, starting from the lists of APIs ( /identity/apis ):

Set the audience to “workflow-manager-api”.

Create permissions per gRCP call:

Permission gRPC API Call
Topology /gateway_protocol.Gateway/Topology
DeployProcess /gateway_protocol.Gateway/DeployProcess
CreateProcessInstance /gateway_protocol.Gateway/CreateProcessInstance

Create Application in Camunda Identity

An application is a OAuth client. Let’s start with /identity/applications and add a new application:

After the application is created you can get the client ID and secret here:

And now some magic: assign permissions from the previously defined API. You can select the API from the dropdown list.

Test the OAuth client with postman

Postman has a nice OAuth 2.0 integration that automates the token generation:

Attribute Content Description
Token Name keycloak name for saved token
Grant Type Client Credentials we want to use client ID and secret
Access Token URL https://your.server.com/auth/realms/camunda-platform/protocol/openid-connect/token url to your token endpoint
Client ID workflow-manager the ID of your client
Client Secret H6j9vaEC50npsMuqI92w6dTv3sGQnY41 secret for your client, see identity
Scope empty not used
Client Authentication Send as Basic Auth header mode of authentication
Audience (see advanced options tab) workflow-manager mode of authentication

add the audience “workflow-manager”:

 

Now you should be able to successfully login to keycloak and get a valid JWT token.

Parse your JWT token

https://jwt.io/

You should find something like this:

"iss": "https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform",
...
"permissions": {
    "account": [
        "manage-account",
        "manage-account-links",
        "view-profile"
    ],
    "workflow-manager-api": [
        "DeployProcess",
        "CreateProcessInstance",
        "Topology"
    ]
},

Use OAuth 2.0 with Camunda Desktop Modeler

Now we are ready to use the newly created OAuth client with the desktop modeler:

Before you can successfully use the Desktop Modeler if you use self-signed CAs for your certificates, you have to start the client with this powershell script in the directory where the exe is located – or add the environment variables otherwise to your OS:

$env:ZEEBE_NODE_LOG_LEVEL = "DEBUG"
$env:NODE_TLS_REJECT_UNAUTHORIZED = "0"
$env:NODE_DEBUG = "http,http2,pusher-js-aut,zeebe-node,pusher-js,pusher,node"
& '.\Camunda Modeler.exe'

This starts the clients and shows the output in the shell what is pretty handy for debugging problems. The client shows some errors without clear messages.

I completely deactivated TLS security after I tried the official environment variables for the node zeebe client:

$env:ZEEBE_CA_CERTIFICATE_PATH = "C:\Development\camunda-modeler-5.5.0-win-x64\trusted.crt"
$env:ZEEBE_SECURE_CONNECTION = "true"
$env:ZEEBE_CLIENT_SSL_ROOT_CERTS_PATH = "C:\Development\camunda-modeler-5.5.0-win-x64\ca.crt"
$env:ZEEBE_CLIENT_SSL_PRIVATE_KEY_PATH = "C:\Development\camunda-modeler-5.5.0-win-x64\tls.key"
$env:ZEEBE_CLIENT_SSL_CERT_CHAIN_PATH = "C:\Development\camunda-modeler-5.5.0-win-x64\trusted.crt"

This didn’t work for me on Windows 11.

But anyway, with TLS deactivated I could successfully use the OAuth login:

Attribute Content Description
Client ID workflow-manager the client we have set up
Client secret H6j9vaEC50npsMuqI92w6dTv3sGQnY41 secret for your client, see identity
OAuth URL https://your.server.com/auth/realms/camunda-platform/protocol/openid-connect/token url to your token endpoint
Audience workflow-manager the audience for the OAuth authorization

Implement Camunda Platform 8 security concepts with Istio

Istio Ingress Gateway

It is important to use the port naming conventions or the explicit appProtocol property to select grpc or http2. Please see the Istio documentation on the topic of protocol selection.

We used the protocol naming convention (“grpc-internal-camunda”):

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  labels:
    app: helix-ingress-gateway-internal-camunda
    app.kubernetes.io/component: gateway
    app.kubernetes.io/instance: helix-ingress-gateway-internal-camunda
    app.kubernetes.io/part-of: istio
    istio: helix-ingress-gateway-internal-camunda
  name: internal-camunda
  namespace: helix-ingress-gateway-internal-camunda
spec:
  selector:
    istio: helix-ingress-gateway-internal-camunda
  servers:
  - hosts:
    - '*.camunda.tanzu.ch'
    port:
      name: http-internal-camunda
      number: 80
      protocol: HTTP
    tls:
      httpsRedirect: false
  - hosts:
    - '*.camunda.tanzu.ch'
    port:
      name: grpc-internal-camunda
      number: 443
      protocol: HTTPS
    tls:
      credentialName: internal-camunda-gateway-credential
      mode: SIMPLE

This component is deployed by the helix operation team as a dedicated ingress gateway per customer.

Istio Virtual Services

These are the virtual service manifests for the whole setup:

  • camunda virtual service
  • zeebe virtual service
  • webmodeler virtual service
  • webmodeler api virtual service
  • webmodeler ws virtual service

The host names are defining the split into multiple virtual service files. Check out the Istio Service Entry as well.

For the RBAC configuration, we focus on the zeebe components and start with the zeebe virtual service – separated from the camunda virtual service because of the two fqdn approach from the camunda helm chart “combined ingress”.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata: 
  name: camunda8-zeebe
spec:
  exportTo:
    - '*'
  gateways:
    - helix-ingress-gateway-internal-camunda/internal-camunda
  hosts:
    - 'zeebe.camunda.tanzu.ch'
  http:
    - match:
        - port: 443
          name: grpc-internal-camunda          
      route:
        - destination:
            host: camunda-zeebe-gateway
            port:
              number: 26500

Important are now the labels of the camunda-zeebe-gateway:

apiVersion: v1
kind: Service
metadata:
  name: "camunda-zeebe-gateway"
  labels:
    app: camunda-platform
    app.kubernetes.io/name: zeebe-gateway
    app.kubernetes.io/instance: camunda
    app.kubernetes.io/part-of: camunda-platform
    app.kubernetes.io/version: "8.1.5"
    app.kubernetes.io/component: zeebe-gateway
  annotations:
spec:
  type: ClusterIP
  selector:
      app: camunda-platform
      app.kubernetes.io/name: zeebe-gateway
      app.kubernetes.io/instance: camunda
      app.kubernetes.io/part-of: camunda-platform
      app.kubernetes.io/component: zeebe-gateway
  ports:
    - port: 9600
      protocol: TCP
      name: http-camunda-internal
    - port: 26500
      protocol: TCP
      name: grpc-camunda-internal

The label app.kubernetes.io/component: zeebe-gateway is subsequently used to attach Istio authorization and authentication policies.

Istio Service Entries

Some of the configurations offer internal service urls. Sometimes this was not always the case and therefore we had to setup Istio a ServiceEntry for this traffic:

apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: external-svc-https
spec:
  hosts:
  - camunda.camunda.tanzu.ch
  - zeebe.camunda.tanzu.ch
  - webmodeler.camunda.tanzu.ch
  - webmodeler-api.camunda.tanzu.ch
  - webmodeler-ws.camunda.tanzu.ch
  location: MESH_INTERNAL
  ports:
  - number: 443
    name: https
    protocol: TLS
  resolution: DNS

Istio RequestAuthentication

In order to protect traffic, Istio can be configured to enable JWT token verification. This can be done very easily based on the keycloack oauth 2.0 provider endpoints:

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: "jwt-camunda-zeebe"
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: zeebe-gateway
  jwtRules:
  - issuer: https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform
    #jwksUri: https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform/protocol/openid-connect/certs
    jwks: |
      {"keys":....}
    audiences:
      - camunda-platform
      - workflow-manager
      - workflow-manager-api

Very important hint: we had to use the jwks: property followed with the whole content of the jwksUri. If you use self-signed CA authorities that are not known to Istio, you cannot use the url directly. Kudos to this blog.

As you can see, the RequestAutentication is bound by its selector directly to the zeebe gateway component. The issuer will be used in further configuration to define users from this IAM.

We have restricted the JWT to the given audiences.

Istio AuthorizationPolicy

Based on the RequestAuthentication definition we can now define rules and apply them to certain contexts. In this case, again, we use the label to identify the zeebe-gateway components.

AuthorizationPolicy Collection

Default DENY

This rule denies any unauthenticated user access:

kind: AuthorizationPolicy
apiVersion: security.istio.io/v1beta1
metadata:
  name: ext-authz-oauth2-keycloak
  namespace: camunda8-dev
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: zeebe-gateway
  action: DENY
  rules:
    - from:
        - source:
            notRequestPrincipals: ["*"]
      to:
        - operation:
            hosts: ["zeebe.camunda.tanzu.ch","zeebe.camunda.tanzu.ch:443"]

Default ALLOW for some paths

This was necessary, have to investigate why. It should be bound only to zeebe-gateway but it had side effects on camunda identity as well.

kind: AuthorizationPolicy
apiVersion: security.istio.io/v1beta1
metadata:
  name: ext-authz-oauth2-keycloak2
  namespace: camunda8-dev
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: zeebe-gateway
  action: ALLOW
  rules:
    - from:
        - source:
            notRequestPrincipals: ["*"]
      to:
        - operation:
            hosts: ["zeebe.camunda.tanzu.ch","zeebe.camunda.tanzu.ch:443"]
            notPaths: ["/auth/*","/identity/*"]

RBAC ALLOW – the job we want to do!

Very handy solution on this protocol level. Remember the permissions we have created in camunda identity:

Create individual permissions per zeebe API call:

Permission gRPC API Call
Topology /gateway_protocol.Gateway/Topology
DeployProcess /gateway_protocol.Gateway/DeployProcess
CreateProcessInstance /gateway_protocol.Gateway/CreateProcessInstance

We see it in the JWT like so:

"iss": "https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform",
...
"permissions": {
    "account": [
        "manage-account",
        "manage-account-links",
        "view-profile"
    ],
    "workflow-manager-api": [
        "DeployProcess",
        "CreateProcessInstance",
        "Topology"
    ]
},

Attention to the syntax of the rules blocks:

I am using from: followed by to: followed by when:. Take care not to miss the dash “-” that indicates the beginning of a new rule.

First, we check in the from: clause that we have users authenticated by our keycloak installation by using the iss: claim from the JWT:

- from:
    - source:                
        requestPrincipals: ["https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform/*"]

Authenticated users are identified by iss/sub, in this case sub is replaced by “*” meaning all users from this issuer. You could add an explicit user-id (usually a UUID) as well.

Next, we use the to: to create useful business functions:

to:
  - operation: 
      hosts: ["zeebe.camunda.tanzu.ch","zeebe.camunda.tanzu.ch:443"]
      paths: ["/gateway_protocol.Gateway/Topology"]
      methods: ["POST"]

Have you seen? No dash at the beginning of to:! It is part of a block describing a rule. If you set a dash, you create a new rule…

OK, this rule matches to a host pattern (both entries are needed!), paths and methods. This one filters the /gateway_protocol.Gateway/Topology call on POST (grpc uses POST).

Now we can create when: conditions like

when:
- key: request.auth.claims[permissions][workflow-manager-api]
  values: [ "Topology" ]

meaning if a user has the right permissions this rule will apply. As you can see, this expression is also capable to evaluate nested maps also for combinations of values.

And here the full manifest:

kind: AuthorizationPolicy
apiVersion: security.istio.io/v1beta1
metadata:
  name: ext-authz-oauth2-keycloak3
  namespace: camunda8-dev
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: zeebe-gateway
  action: ALLOW
  rules:
    #/gateway_protocol.Gateway/Topology
    - from:
        - source:                
            requestPrincipals: ["https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform/*"]
      to:
        - operation: 
            hosts: ["zeebe.camunda.tanzu.ch","zeebe.camunda.tanzu.ch:443"]
            paths: ["/gateway_protocol.Gateway/Topology"]
            methods: ["POST"]
      when:
      - key: request.auth.claims[permissions][workflow-manager-api]
        values: [ "Topology" ] 
    #/gateway_protocol.Gateway/DeployProcess
    - from:
        - source:
            requestPrincipals: ["https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform/*"]
      to:
        - operation: 
            hosts: ["zeebe.camunda.tanzu.ch","zeebe.camunda.tanzu.ch:443"]
            paths: ["/gateway_protocol.Gateway/DeployProcess"]
            methods: ["POST"]
      when:
      - key: request.auth.claims[permissions][workflow-manager-api]
        values: [ "DeployProcess" ] 
    #/gateway_protocol.Gateway/CreateProcessInstance
    - from:
        - source:
            requestPrincipals: ["https://camunda.camunda.tanzu.ch/auth/realms/camunda-platform/*"]
      to:
        - operation: 
            hosts: ["zeebe.camunda.tanzu.ch","zeebe.camunda.tanzu.ch:443"]
            paths: ["/gateway_protocol.Gateway/CreateProcessInstance"]
            methods: ["POST"]
      when:
      - key: request.auth.claims[permissions][workflow-manager-api]
        values: [ "CreateProcessInstance" ]

Wrap up

This is a good sample for Istio in action: Just define your RequestAuthentication and integrate your token endpoint. With the AuthorizationPolicy you can define your business rules. In this case Istio enables to add fine granular access permissions without any code change or other impact on the component (zeebe). This creates dependencies on the strings used for the API client and the permissions configured within Istio but it is very handy especially for the given case.

Keep in mind self-signed CAs!

I plan to release the camunda platform 8 kustomize project for Istio. Stay tuned!

Be the first to comment

Leave a Reply

Your email address will not be published.


*



The reCAPTCHA verification period has expired. Please reload the page.