Softwareentwicklung und Architektur/ 22.05.2023 / Patrick Hefele

Sichere Umgebung mit Prozess - Applikationen, Camunda und Keycloak

Motivation
Das Netz ist gefüllt mit guten Beispielen für jeden erdenklichen Anwendungsfälle und um das Thema Prozess-Verarbeitung mit Hilfe einer Prozess-Engine. Meist sind die Beispiele jedoch recht einfach gehalten, um sie gut und schnell verstehen zu können. In vielen Fällen hält man aber Ausschau nach Lösungen und Vorschlägen für einen produktiven Einsatz. Dafür sind diese Beispiele oft nur von mäßigem Wert. Eine produktive Umgebung sollte unter anderem neben Stabilität auch Sicherheit bieten.  Gerade vor dem Hintergrund eines DEVOPS- Ansatzes, kommen also zur Installation einer Anwendung und benötigter Komponenten, auch Überlegungen hinzu, in welcher Umgebung die Anwendung laufen soll und wie diese gesichert werden kann.
In vielen Microservice-Umgebungen wird bereits ein OAuth2-Verfahren für das Identity und Access Management verwendet und zum Beispiel mit Hilfe von Keycloak1 abgebildet.

Wie könnte man nun ein System mit einer Camunda Prozess-Engine (der Platform 7) und den notwendigen Komponenten aufbauen und entsprechenden Sicherheits-Anforderungen eines produktiven Systems genügen? Dieser Blog-Artikel soll dazu einen Lösungsansatz bieten, die benötigten Konzepte und Komponenten benennen und erläutern.

Im Netz gibt es zahlreiche Beispiele für die Verwendung von Prozess-Engines in verschiedenen Anwendungsfällen. Dabei ist es wichtig, nicht nur die einfacheren Beispiele zu betrachten, sondern auch Lösungen für den produktiven Einsatz zu finden. Eine produktive Umgebung erfordert Stabilität und Sicherheit, insbesondere im DEVOPS-Kontext. Bei der Installation einer Anwendung und der Auswahl der Komponenten muss auch die Umgebung und die Absicherung berücksichtigt werden. In vielen Microservice-Umgebungen wird bereits OAuth2 für das Identity und Access Management eingesetzt, zum Beispiel mit Hilfe von Keycloak. Dieser Blog-Artikel bietet einen Lösungsansatz, wie ein System mit der Camunda Prozess-Engine (Version 7) und den erforderlichen Komponenten aufgebaut werden kann, um den Sicherheitsanforderungen eines produktiven Systems gerecht zu werden. Es werden die relevanten Konzepte und Komponenten erläutert.

Architektur und Implementierung
Als Basis für die Standalone-Process-Engine kann ein fertiges Docker-Image aus dem offiziellen Camunda Repository bei Docker-Hub verwendet werden. Das Einbauen diverser Erweiterung würde hier jedoch schnell an Grenzen stoßen bzw. ist einfacher zu bewerkstelligen, wenn das spätere Container-Image mit Hilfe eines eigenen Dockerfiles gebaut wird.

Ein Architektur-Übersicht  wie in Abb.1 zu sehen ist, enthält den möglichen System-Aufbau und umfasst auch die notwendigen externen Ressourcen. Als Identity-Provider habe ich hier Keycloak gewählt, welches dann per Plugin an Camunda angebunden wird. 

Da ich grundsätzlich einen Container-Ansatz gewählt habe, macht es Sinn in den entsprechenden Container-Netzen einen Web-Server vor zu schalten, um die SSL-Verbindungen aus dem Netz heraus und in das Netz hinein unkomplizierter zu gestalten. Hier kann Apache, oder NGINX (wie in dem Beispiel) verwendet werden. Für die Entwicklung und Staging reicht es aus auch Docker als Ausführungsumgebung zu verwenden. In produktiven Systemen würden die entsprechenden Images jedoch meist in Cloud-Systeme wie AWS installiert. Dies ändert jedoch nichts an der prinzipiellen Architektur - mit einer Ausnahme: ich lasse hier die Skalierung außer Acht. Als Hinweis für alle diejenigen, die mehr Wert auf Themen wie Skalierbarkeit und Cloud legen, sollten sich auch mit der Camunda Platfom 8 näher beschäftigen.

Abbildung 1

Setup
Ein Service, eine Benutzeroberfläche, eine Standalone-Camunda-Instanz mit exponierter REST-Schnittstelle, eine Keycloak-Instanz. Es müssen nun alle Anfragen des Systems über sichere Verbindungen aufgebaut werden, also TLS und zusätzlich müssen die Anfragen der Komponenten unter einander authentifiziert und autorisiert werden:

  1. Aufrufe der Benutzerinnen und Benutzer an die Benutzeroberfläche
  2. Kommunikation zwischen der Benutzeroberfläche und der Prozessapplikation
  3. Kommunikation zwischen der Prozessapplikation und der Prozess-Engine
  4. Anfragen der Camunda-Instanz mit den Services

Abbildung 1

Basis Projekt 
Für die Realisierung verwende ich hier ein Maven-Projekt. Wie eingangs bereits erwähnt, ist es notwendig einige Erweiterungen zur Camunda-Instanz hinzuzufügen. Die pom.xml enthält daher neben den Abhängigkeiten zu Spring-Boot, auch die notwendigen Komponenten für die Camunda-Engine, sowie die "keycloak-identity-provider-extension". Zusätzlich soll eine Prozess-Instanz auch mit REST-Schnittstellen direkt kommunizieren können. Dazu werden das Camunda-Connector-Packages benötigt, also auch SPIN.
Eine produktive Applikation würde den Zustand der Prozess-Engine darüber nicht in einer In-Memory-Datenbank, wie der H2 persistieren, sondern eine persistente Datenbank verwenden - hier PostgreSQL.
 
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>secure-camunda-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>15</maven.compiler.source>
        <maven.compiler.target>15</maven.compiler.target>
        <maven-exec-plugin.version>1.6.0</maven-exec-plugin.version>
        <spring-boot.version>2.6.6</spring-boot.version>
        <camunda.version>7.17.0</camunda.version>
        <camunda-connect.version>1.5.6</camunda-connect.version>
        <camunda-spin.version>1.14.0</camunda-spin.version>
        <camunda-identity-keycloak.version>2.2.1</camunda-identity-keycloak.version>
        <camunda-mail-extension.version>1.2.0</camunda-mail-extension.version>
        <groovy.version>2.4.16</groovy.version>
        <jaxb.version>2.3.6</jaxb.version>
        <postgres.version>42.2.19</postgres.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.camunda.connect</groupId>
                <artifactId>camunda-connect-bom</artifactId>
                <version>${camunda-connect.version}</version>
            </dependency>
            <dependency>
                <groupId>org.camunda.spin</groupId>
                <artifactId>camunda-spin-bom</artifactId>
                <version>${camunda-spin.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.camunda.bpm.springboot</groupId>
            <artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
            <version>${camunda.version}</version>
        </dependency>
        <dependency>
            <groupId>org.camunda.bpm.springboot</groupId>
            <artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
            <version>${camunda.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- Extensions -->
        <dependency>
            <groupId>org.camunda.bpm.extension</groupId>
            <artifactId>camunda-bpm-identity-keycloak</artifactId>
            <version>${camunda-identity-keycloak.version}</version>
        </dependency>
        <!-- Connectoren -->
        <dependency>
            <groupId>org.camunda.bpm</groupId>
            <artifactId>camunda-engine-plugin-connect</artifactId>
            <version>${camunda.version}</version>
        </dependency>
        <dependency>
            <groupId>org.camunda.connect</groupId>
            <artifactId>camunda-connect-connectors-all</artifactId>
            <version>${camunda-connect.version}</version>
        </dependency>
        <dependency>
            <groupId>org.camunda.bpm</groupId>
            <artifactId>camunda-engine-plugin-spin</artifactId>
            <version>${camunda.version}</version>
        </dependency>
        <dependency>
            <groupId>org.camunda.spin</groupId>
            <artifactId>camunda-spin-core</artifactId>
            <version>${camunda-spin.version}</version>
        </dependency>
        <dependency>
            <groupId>org.camunda.spin</groupId>
            <artifactId>camunda-spin-dataformat-all</artifactId>
            <version>${camunda-spin.version}</version>
        </dependency>


        <!-- -->
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>${groovy.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>${jaxb.version}</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>${postgres.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <layout>ZIP</layout>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>${maven-exec-plugin.version}</version>
                <configuration>
                    <mainClass>de.ausy.secure.camunda.ProcessApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>  

Ein paar Worte zu der Abbildung Abb.2. Hier gibt es eine Prozessapplikation und eine Prozess-Engine. Obige pom.xml beinhaltet jedoch beide Komponenten. Dies ist sicherlich korrekt, jedoch enthält eine Prozessapplikation noch weitere Komponenten wie Delegates u.ä. . In der obigen Abbildung jedoch können diese beiden Komponenten getrennt dargestellt werden, um entsprechenden Komponenten zu identifizieren.
Nachdem mit obiger pom.xml das Basis-Projekt erzeugt wird, folgt nun die Container-Konfiguration. Hier in Form eines Dockerfile:
 
#
# Build stage
#
FROM maven:3.6.3-jdk-11-slim AS build
COPY src /home/app/src
COPY pom.xml /home/app
COPY env/start.sh /home/app
RUN mvn -f /home/app/pom.xml clean package

#
# Package stage
#
FROM openjdk:11-jre
COPY --from=build /home/app/target/secure-camunda-example-0.0.1-SNAPSHOT.jar /camunda/camunda.jar
COPY --from=build /home/app/start.sh /camunda/start.sh
RUN chmod 777 /camunda/start.sh
EXPOSE 8443
#EXPOSE 8080
#ENTRYPOINT ["java","-jar","/camunda/camunda.jar","--spring.config.location=/camunda/configuration/application.yaml"]
ENTRYPOINT ["/camunda/start.sh"]
Dieses baut das Projekt und im Anschluss ein Docker-Image. Im Grunde könnte der kommentierte Entrypoint verwendet werden, es hatte sich jedoch als hilfreich erwiesen eine Skriptdatei "start.sh" zu verwenden. Diese kann einige Arbeiten zuvor übernehmen z.B. das Bestücken des JDK-Keystore mit den notwendigen Zertifikaten.

Konfigurationen
Zunächst möchte ich auf die unten gezeigte Spring-Boot Konfiguration eingehen. Aus der pom.xml ist bereits ersichtlich gewesen, dass unter anderem auch OAtuh2-Komponenten aus dem Spring-Framework verwendet werden. Die OAuth2 / Keycloak Konfiguration in meinem Beispiel sieht dann wie folgt aus:
# Spring Boot Security OAuth2 SSO
spring.security.oauth2:
  client:
    registration:
      keycloak:
        provider: keycloak
        client-id: ${KEYCLOAK_CLIENT_ID}
        client-secret: ${KEYCLOAK_CLIENT_SECRET}
        authorization-grant-type: authorization_code
        redirect-uri-template: "{baseUrl}/{action}/oauth2/code/{registrationId}"
        scope: openid, profile, email
    provider:
      keycloak:
        issuer-uri: ${KEYCLOAK_HOST}/auth/realms/${KEYCLOAK_REALM}
        authorization-uri: ${KEYCLOAK_HOST}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth
        user-info-uri: ${KEYCLOAK_HOST}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/userinfo
        token-uri: ${KEYCLOAK_HOST}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token
        jwk-set-uri: ${KEYCLOAK_HOST}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs
        # set user-name-attribute one of:
        # - sub                -> default; using keycloak ID as camunda user ID
        # - email              -> useEmailAsCamundaUserId=true
        # - preferred_username -> useUsernameAsCamundaUserId=true
        user-name-attribute: preferred_username

Ausgehend von der obigen Konfiguration, müssen nun noch zusätzliche Konfigurationen eingetragen werden, die das Identity-Provider-Plugin für Camunda betreffen:
plugin.identity.keycloak:
  keycloakIssuerUrl: ${KEYCLOAK_HOST}/auth/realms/${KEYCLOAK_REALM}
  keycloakAdminUrl: ${KEYCLOAK_HOST}/auth/admin/realms/${KEYCLOAK_REALM}
  clientId: ${KEYCLOAK_CLIENT_ID}
  clientSecret: ${KEYCLOAK_CLIENT_SECRET}
  useEmailAsCamundaUserId: false
  useUsernameAsCamundaUserId: true
  useGroupPathAsCamundaGroupId: true
  administratorGroupName: camunda-admin
  disableSSLCertificateValidation: true

Erweiterungen
Ein genereller Vorteil der Camunda Platform ist die Erweiterbarkeit. Diese kann, wie in meinem Beispiel, aus verschiedenen Konnektoren (Connectors) und Erweiterungen (Extensions) bestehen. Im Wesentlichen werden die hier aufgeführten Erweiterungen durch die Nutzung der Keycloak identity provider extension und der Architektur (REST-basiert) vorgegeben.

SPIN - Damit auch simple Operationen auf REST-Calls aus einem Prozess heraus z.B. per Skript durchgeführt werden können, wird die SPIN library benötigt. 
Keycloak identity provider extension - Eine Erweiterung, mit deren Hilfe ein Keycloak identity provider an Camunda angebunden werden kann.
Neben der oben erwähnten SPIN-library kann auch eine Mail extension eingesetzt werden, die es ermöglicht z.B. Benutzer-Rückmeldung aus dem Prozess heraus zu realisieren. Die kann einfach eine Rückmeldung sein, wie dass ein Task der/m betreffenden Benutzer/in zugewiesen worden ist, oder auch einen Deep-Link zu einer entsprechenden Benutzeroberfläche enthalten, in der die Taskliste eines Benutzers dargestellt wird. 

Stolpersteine
Nicht triviale Zugriffsrechte
Zu den Stolpersteinen in einem produktiven Szenario gehören zweifellos nicht triviale Zugriffsrechte von Benutzerinnen und Benutzern. Ein Beispiel eines solchen Falls könnte wie folg lauten:

In einem Identity-Management System werden Zugriffsrechte für viele verschiedene System verwaltet. So soll auch Camunda als eines dieser Systeme hinzugefügt werden. Ein erster Wurf sieht vor, dass zwischen Benutzerinnen und Benutzern unterschieden werden muss, die Zugriff auf Prozesse - ganz allgemein - bekommen sollen und solche, die Administrationsrechte bekommen sollen. Oftmals bilden Identity-Management-Systeme jedoch auch Strukturen des Unternehmens ab. Also z.B. verschiedene Abteilungen. Jetzt sollen jedoch Personen aus verschiedenen Abteilungen Rechte für die Prozess-Engine eingeräumt werden. Kein Problem. Alle Benutzerinnen und Benutzer, die der Abteilung A angehören bekommen die GruppeA und auch eine Gruppe CamundaUser, oder eben CamundaAdmin.  So weit, so einfach. Nur was, wenn Benutzerinnen der Abteilung A nur Zugriff auf Prozesse der Abteilung A bekommen sollen und nicht auch aller andren Abteilungen? Ein naiver Ansatz wäre entsprechende Rechtegruppen einzuführen z.B. nicht mehr CamundaUser, sondern ABT-A-CAM-USR oder ABT-A-CAM-ADM. Je nachdem wieviel Abteilungen das sind, entstehen jedoch schnell sehr viele neue Gruppen. Camunda bietet die Möglichkeit nach Tenants zu unterschieden und damit würde Abteilung A ein Tenant und Abteilung B ein anderer Tenant.

Was würde nun aber geschehen, wenn innerhalb einer Abteilung auch die Prozesse nach Domänen aufgeteilt sind und diese z.B. Webseite- und HR- Prozesse beinhalten. Es entsteht erneut ein Problem, wenn nur Benutzer und Administratoren unterschiedliche Gruppen bekommen. So könnten Mitarbeiterinnen und Mitarbeiter der Webseiten-Entwicklung auch Prozesse der HR-Abteilung sehen und eventuell auch starten können. Um dies wiederum zu verhindern, müssten erneut fein granularer Rechte vergeben werden. Also erneut die Überlegung, nun anstatt CamundaUser / CamundaAdmin, die Gruppen HR-CAM-USR/ADM und WEB-CAM-USR/ADM.
All diese Probleme sind zu bewältigen. Es muss nur bei der Konzeption genau darauf geachtet werden, wie die Benutzer-Rechte aufgeteilt und verwaltet werden, um sich schwierige Änderungen, die eventuell Auswirkungen auf die Benutzerrechte vieler Benutzerinnen und Benutzer haben, zu vermeiden.

Testen des Systems
Nun da das System steht, die Benutzerrechte abgebildet sind, bleibt die Frage, wie alle getestet werden kann. Je nach der Art und Weise, wie Prozesse in das System eingespielt werden - also als Artefakt oder per Schnittstelle, kann es sein, oder auch nicht, z.B. mit JUnit-Tests zu arbeiten. Weit häufiger wird jedoch, wenn man meinem Architektur-Vorschlag folgt, ein zentrales System etabliert, das von verschiedenen Stakeholdern verwendet werden kann. Wichtig ist allgemein beim Einsatz einer Prozess-Engine die niederschwellige Nutzung. Wenn ein aufwendiger Release-Prozess zum Einspielen neuer Prozesse aufgesetzt wird, der mehrere Artefakte mit der Beteiligung von Fachabteilungen und der Entwicklung voraussetzt, wird dieses Ziel wohl eher nicht erreicht. Wenn jedoch im Idealfall eine Fachabteilung ihre Prozesse selbst auf die Prozess-Engine  ausbringen kann, bleibt erneut die Frage, wie man einen Ende-zu-Ende-Test durchführen kann. Dazu ein Ansatz, der mir als Entwickler sehr gute Dienste geleistet hat. Durch das Design als REST-API getriebene Prozess-Engine, die sowohl über ein Benutzer-Interface (UI) gesteuert werden kann, oder eben auch durch Werkzeuge wie Postman, gibt es die Möglichkeit Test-Suiten zu entwickeln, die Ende-zu-Ende-Tests ermöglichen. 

Abbildung 3

Zusammenfassung
REST-basierte Architektur ermöglicht einfache Nutzung der Funktionalitäten der Camunda-Prozess-Engine gerade auch in modernen Microservice- bzw. Service-Umgebungen. Diese Architektur ermöglicht auch das Testen des Systems.

Mit Containern lassen sich leicht verschiedene Umgebungen aufsetzen, die später auch in produktiven Umgebungen eingesetzt werden können. Die Kapselung der Komponenten in den Containern erhöhen auch die Sicherheit, da speziell die Schnittstellen in die Container-Netze und aus den Container-Netzen heraus betrachtet und gesichert werden können. Es müssen insbesondere keine Ports nach außen geöffnet werden, die nicht unmittelbar für die Verwendung der Komponenten zwingend erforderlich sind. Z.B. Datenbank Ports.

Einfache Konfiguration von Zugriffsrechten mit Hilfe einer OAuth2-Umgebung. Oftmals wird damit eine Integration in bereits bestehende Strukturen in Bezug auf die Rollen und Rechte von Benutzern und Services erleichtert.

Die Verbindungen zwischen den Komponenten und der Zugriff auf die Benutzerschnittstellen, sollten per SSL abgesichert sein. Die Verbindungen zwischen den Services und der Prozess-Engine sollten dabei nicht vergessen werden.

 

Quellen

  1. https://www.keycloak.org
  2. https://docs.camunda.io/docs/components/best-practices/operations/securing-camunda-c7/
  3. https://camunda.com/blog/2019/08/keycloak-identity-provider-extension/
  4. https://docs.camunda.org/manual/7.18/reference/spin/
  5. https://github.com/camunda-community-hub/camunda-platform-7-mail

Einblicke

Shaping the future with our clients