Beliebte Suchanfragen
//

Anypoint Flex Gateway individuell anpassen: Custom Policies leicht gemacht

20.1.2025 | 11 Minuten Lesezeit

API-Gateways sind ein zentraler Bestandteil moderner Architekturen. Sie spielen eine wichtige Rolle bei der Erhöhung von Sicherheit und Skalierbarkeit und tragen dazu bei, die Nutzung von Backend-Diensten zu vereinheitlichen. Die Einsatzmöglichkeiten und die Liste der verfügbaren Anbieter sind vielfältig. Umso wichtiger ist es, ein API-Gateway auszuwählen, das genau auf die spezifischen Anforderungen zugeschnitten ist.

Ein besonders wichtiger Aspekt bei der Auswahl eines geeigneten API-Gateways ist dessen Erweiterbarkeit durch sogenannte Custom Policies. Anforderungen können sich im Laufe der Zeit verändern, und die vordefinierten Policies reichen möglicherweise nicht mehr aus, um diese neuen Bedürfnisse zu erfüllen. In diesem Blogbeitrag zeigen wir, wie Sie eine Custom Policy für das Anypoint Flex Gateway erstellen und veröffentlichen können, um auf sich wandelnde Anforderungen flexibel reagieren zu können.

Die neue Anforderung

Stellen wir uns folgendes Szenario vor: Aus Debugging-Zwecken sollen alle Header beim Zugriff auf eine API protokolliert werden. Dabei ist es jedoch essenziell, dass sensible Informationen wie Passwörter oder andere vertrauliche Daten nicht im Log auftauchen. Um dies zu erreichen schaltet man die "Message Logging Policy" ein und könnte versuchen, mithilfe der Funktion filterObject gezielt Header mit sensiblen Daten herauszufiltern. Eine weitere Möglichkeit wäre der Einsatz des Update-Operators, um bestimmte Header direkt zu maskieren. In der Praxis erwiesen sich beide Ansätze jedoch als unzureichend, da sie dazu führten, dass das Flex Gateway nicht korrekt startete. Deshalb wurde klar, dass eine andere Herangehensweise notwendig ist, um die Anforderungen zu erfüllen. Die Lösung besteht darin, eine Custom Policy zu entwickeln, die es erlaubt, eine Liste von Headernamen zu definieren, die beim Loggen maskiert werden. Dadurch wird sichergestellt, dass alle relevanten Header weiterhin sichtbar sind, während sensible Informationen zuverlässig geschützt werden.

Vorbereitung für die Entwicklung einer Custom Policy

Bevor wir mit der Entwicklung einer Custom Policy für das Anypoint Flex Gateway starten können, müssen einige wichtige Tools installiert und eingerichtet werden. Da eine detaillierte Installationsanleitung den Rahmen dieses Artikels sprengen würde, listen wir hier die benötigten Tools auf und verweisen auf weiterführende Links. Die folgende Übersicht richtet sich an Entwickler, die unter Linux arbeiten – viele der Tools sind jedoch bereits in typischen Entwicklungsumgebungen vorhanden. Dennoch ist dieser Schritt häufig der zeitintensivste.

Benötigte Tools:

Eine ausführlichere Installationsanleitung finden Sie auf der Seite des Anypoint Flex Gateway. Wichtiger Hinweis: Ab Rust 1.84 wird das Rust WASI Target wasm32-wasi nicht mehr unterstützt. Es ist daher ratsam, ab Januar 2025 nicht die neueste Rust-Version zu verwenden, um Kompatibilitätsprobleme zu vermeiden.

Start der Entwicklung: Projekt-Setup

Nach Abschluss der Vorbereitungen kann es mit der eigentlichen Entwicklung losgehen. Wer sich während der Installation der Tools und Plugins genauer umgesehen hat, wird bemerkt haben, dass das Flex Gateway auf Envoy basiert und Custom Policies in der Programmiersprache Rust implementiert werden. Diese werden anschließend in WebAssembly kompiliert. Auch wenn Rust anfangs eine Herausforderung sein kann, erleichtern die Anypoint CLI und das PDK (Policy Development Kit) den Einstieg erheblich. Sie automatisieren viele Schritte, sodass wir uns auf die Implementierung der eigentlichen Policy konzentrieren können. Mit dem folgenden Befehl erstellt uns die Anypoint CLI eine Projektvorlage für unsere Custom Policy mit dem Namen „Masked-Header-Log“:

1anypoint-cli-v4 pdk policy-project create --name masked-header-log

Das Ergebnis ist ein Projekt mit der folgenden Verzeichnisstruktur. Nun können wir versuchen das Projekt mit dem Befehl make build zu bauen.

1├── Cargo.lock
2├── Cargo.toml
3├── definition
4│   ├── gcl.yaml
5│   └── target
6│       ├── exchange.json
7│       ├── gcl.yaml
8│       └── metadata.yaml
9├── Makefile
10├── playground
11│   ├── config
12│   │   ├── api.yaml
13│   │   ├── custom-policies
14│   │   │   └── note.txt
15│   │   └── logging.yaml
16│   └── docker-compose.yaml
17├── README.md
18├── src
19│   ├── generated
20│   │   ├── config.rs
21│   │   └── mod.rs
22│   └── lib.rs
23├── target
24│   ├── exchange.json
25│   └── metadata.yaml
26└── tests
27    ├── common
28    │   ├── logging.yaml
29    │   └── note.txt
30    ├── common.rs
31    └── requests.rs

Leider wird der Build nicht vollständig erfolgreich abgeschlossen, da die Integrationstests, die im Projekt enthalten sind, automatisch ausgeführt werden und ein Flex Gateway im Local Mode innerhalb eines Docker-Containers benötigen. Da die Integrationstests für unsere Policy nicht relevant sind, können wir sie entweder einfach löschen oder die Datei registration.yaml vom Flex Gateway im Local Mode in den Ordner /tests kopieren. Letzteres ist der eigentlich richtige Weg, besonders wenn komplexere Custom Policies entwickelt werden müssen.

Definition und Konfiguration der Custom Policy

Nachdem das Projekt-Setup abgeschlossen ist, geht es nun um die eigentliche Definition der Custom Policy. Diese Definition ist entscheidend, weil sie die Brücke zwischen der Benutzeroberfläche und der technischen Implementierung schlägt. Sie bestimmt, wie die Policy in der Anypoint Platform konfiguriert werden kann und welche Parameter dafür nötig sind. Während des Build-Prozesses wird auf Basis dieser Definition ein Config-Objekt erstellt, das es ermöglicht, auf die konfigurierten Parameter in der Implementierung zuzugreifen. Darüber hinaus wird auch der Titel festgelegt, der später in der Anypoint Platform angezeigt wird. Die Definition erfolgt in der Datei definition/gcl.yaml.

1apiVersion: gateway.mulesoft.com/v1alpha1
2kind: Extension
3metadata:
4  labels:
5    title: Masked Header Log
6    category: Custom
7    metadata/interfaceScope: api,resource
8spec:
9  extends:
10    - name: extension-definition
11      namespace: default
12  properties:
13    header:
14        type: array
15        items:
16            type: string
17        description: Header names that are to be masked in the request as well as in the response.
18  required:
19    - header

In unserer „Masked Header Log“-Policy definieren wir ein Array von Strings, in dem die Namen der Header angegeben werden, die beim Loggen maskiert werden sollen. Durch die Angabe „Required“ stellen wir sicher, dass die Policy nur dann gespeichert wird, wenn mindestens ein Header angegeben wurde. Das verhindert unvollständige Konfigurationen und stellt sicher, dass die Policy korrekt funktioniert.

Implementierung der Maskierung

Die Implementierung unserer Custom Policy erfolgt in der Datei src/lib.rs. In dieser Datei befindet sich ein Beispielcode, den wir anpassen können, um die gewünschte Logik für unsere Policy zu erstellen. Im folgenden Abschnitt zeigen wir die angepasste Einstiegsfunktion, die den Startpunkt für die Verarbeitung darstellt:

1#[entrypoint]
2async fn configure(launcher: Launcher, Configuration(bytes): Configuration) -> Result<()> {
3    let config: Config = serde_json::from_slice(&bytes).map_err(|err| {
4        anyhow!(
5            "Failed to parse configuration '{}'. Cause: {}",
6            String::from_utf8_lossy(&bytes),
7            err
8        )
9    })?;
10    let filter = 
11        on_request(|rs| request_filter(rs, &config)).
12        on_response(|rs| response_filter(rs, &config));
13    
14    launcher.launch(filter).await?;
15    Ok(())
16}

Im ersten Teil der Funktion wird das Config-Objekt aus den übergebenen Bytes erstellt. Dieses Objekt enthält die in der Policy-Definition hinterlegte Liste der zu maskierenden Headernamen. Diese Liste muss sowohl bei der Anfrage (Request) als auch bei der Antwort (Response) überprüft werden, um sicherzustellen, dass keine sensiblen Daten im Log erscheinen. Das Policy Development Kit (PDK) stellt uns zwei Callbacks zur Verfügung: on_request und on_response, in denen wir die Logik für die Verarbeitung der Header implementieren. Da die Logik für Request und Response sehr ähnlich ist, konzentrieren wir uns hier nur auf das Beispiel für die Anfrageverarbeitung.

Anfrageverarbeitung (Request Filtering)

Die folgende Funktion zeigt, wie wir die Header einer Anfrage verarbeiten und nur die relevanten Informationen protokollieren. Zuerst wird aus dem RequestState der HeaderState extrahiert. Anschließend erhalten wir ein Array von Tuples, das die Headernamen und deren Werte enthält. Diese Header werden dann an die Funktion header_log übergeben, die die eigentliche Maskierung vornimmt.

1async fn request_filter(request_state: RequestState, config: &Config) {
2    let headers_state = request_state.into_headers_state().await;
3    let headers_handler = headers_state.handler();
4
5    header_log(&headers_handler.headers(), config);
6}

Die Funktion header_log prüft zunächst, ob Header vorhanden sind. Falls keine Header vorhanden sind, wird eine Logmeldung auf Info-Level ausgegeben. Andernfalls werden die Header in der Funktion create_masked_text maskiert und anschließend als JSON-String protokolliert:

1fn header_log(headers: &[(String, String)], config: &Config) {
2    if headers.is_empty() {
3        logger::info!("No headers to log.");
4        return;
5    }
6
7    let masked_text = create_masked_text(headers, config);
8
9    logger::info!("{masked_text}");     
10}

Die Maskierung selbst erfolgt in create_masked_text. Hier wird jeder Header überprüft, ob er in der Liste der zu maskierenden Header enthalten ist. Wenn dies der Fall ist, wird der Wert des Headers durch ***** ersetzt. Andernfalls bleibt der Wert unverändert. Am Ende wird die gesamte Map an Headern als JSON-String zurückgegeben.

1fn create_masked_text(headers: &[(String, String)], config: &Config) -> String {
2    let mut headers_map = HashMap::new();
3    
4    for (header_name,  header_value) in headers {
5        if config.header.contains(&header_name) {
6            headers_map.insert(header_name, "*****");
7        } else {
8            headers_map.insert(header_name, header_value);
9        }
10    }
11
12    return serde_json::to_string(&headers_map).unwrap();
13}

Testen der Custom Policy: Unit-Tests und lokale Tests

Um die Funktionsweise unserer Custom Policy zu überprüfen, können wir sowohl einfache Unit-Tests als auch Tests in einer lokalen Umgebung durchführen. Während Unit-Tests schnell und effizient sind, ermöglichen uns lokale Tests mit dem Flex Gateway im Local Mode eine genauere Validierung der Funktionalität ohne eine Veröffentlichung der Policy.

Unit-Test der Maskierungslogik

Die einfachste Möglichkeit, eine Funktion zu testen, ist der Unit-Test. Diese Art von Test wird direkt in der Datei durchgeführt, die auch die zu testende Funktion enthält. Ein Vorteil von Unit-Tests ist, dass sie schnell ausgeführt werden und der getestete Code sofort sichtbar ist. Leider werden Rust Documentation Tests nicht unterstützt, da der generierte Code in einer cdylib (C-kompatiblen dynamischen Bibliothek) vorliegt. Dennoch bleibt der Unit-Test eine sehr nützliche Möglichkeit, eine grundlegende Funktionalität zu überprüfen. Hier ein Beispiel für einen Unit-Test, der die Funktion create_masked_text überprüft:

1#[cfg(test)]
2mod tests {
3    use super::*;
4
5    #[test]
6    fn test_create_masked_text_with_some_headers_masked() {
7        let config = Config {
8            header: vec![String::from("X-Secret")],
9        };
10
11        let headers = vec![
12            (String::from("X-Secret"), String::from("secret_value")),
13            (String::from("X-Public"), String::from("public_value")),
14        ];
15
16        let result = create_masked_text(&headers, &config);
17
18        // X-Secret should be masked, X-Public should be visible:
19        assert_eq!(result, "{\"X-Secret\":\"*****\",\"X-Public\":\"public_value\"}");
20    }
21}

In diesem Test erstellen wir eine Konfiguration (Config) mit dem Header, der maskiert werden soll, und einem Array von Headern, die beispielsweise bei einem eingehenden Request gesendet werden. Anschließend wird die Funktion create_masked_text aufgerufen, die uns einen String zurückgibt. Dieser wird mit den erwarteten Ergebnissen verglichen. Um den Test auszuführen, verwenden wir den folgenden Befehl:

1make test

Lokales Testen der Policy mit Flex Gateway

Unit-Tests sind hilfreich für die grundlegende Überprüfung, jedoch testen sie nur einen kleinen Teil unserer Policy. Um sicherzustellen, dass alle Header korrekt geloggt und maskiert werden, können wir die Policy auch lokal mit einem Flex Gateway im Local Mode testen. Dazu ist es erforderlich, die Datei registration.yaml im Ordner playground/config abzulegen, genau wie bei den Integrationstests.

Die Ordnerstruktur des Playground sieht wie folgt aus:

1├── playground
2│   ├── config
3│   │   ├── api.yaml
4│   │   ├── custom-policies
5│   │   │   └── note.txt
6│   │   └── logging.yaml
7│   └── docker-compose.yaml

In der Datei api.yaml können wir einen Routing-Eintrag für das Flex Gateway im Local Mode konfigurieren.
Die wichtigsten Properties sind:

  • address: Die Adresse, unter der das Flex Gateway erreichbar ist (z. B. http://0.0.0.0:8081)
  • services: Der Upstream-Service (http://backend), der den eingehenden Datenverkehr empfängt
  • policies: Die Referenz zur Policy, die wir erstellt haben
  • header: Die Header die maskiert werden sollen; In diesem Beispiel x-secret
1apiVersion: gateway.mulesoft.com/v1alpha1
2kind: ApiInstance
3metadata:
4  name: ingress-http
5spec:
6  address: http://0.0.0.0:8081
7  services:
8    upstream:
9      address: http://backend
10      routes:
11        - config:
12            destinationPath: /anything/echo/
13  policies:
14    - policyRef:
15        name: masked-header-log-v1-0-impl # This value will be overriden during 'make run' execution
16        namespace: default
17      config:
18        header:
19          - x-secret

Als Nächstes werfen wir einen Blick auf die Datei docker-compose.yaml. Diese Datei legt fest, welche Version des Flex Gateways verwendet werden soll und welcher Service unter der Adresse http://backend läuft. In diesem Beispiel wird die Version 1.7.0 des Flex Gateways eingesetzt und der Service httpbin als Backend-Service angegeben. httpbin dient dazu, eine einfache Testumgebung für unsere Custom Policy bereitzustellen. Wenn das Flex Gateway Anfragen an den httpbin-Service weiterleitet, können wir in der Konsole die Header und die Antwort überprüfen, um sicherzustellen, dass die Policy korrekt funktioniert.

1version: "3.3"
2
3services:
4  local-flex:
5    image: mulesoft/flex-gateway:1.7.0
6    ports:
7      - 8081:8081
8    volumes:
9      - ./config:/usr/local/share/mulesoft/flex-gateway/conf.d/
10  backend:
11    image: kennethreitz/httpbin

Nun können wir den Playground mit dem Befehl make run starten und mit curl testen:

1curl "http://localhost:8081/some/route/" --header 'x-secret: mytoken'

Im Log erscheinen sowohl der Request- als auch der Response-Log. Dabei wurde das Log auf das Wesentliche reduziert um die Übersichtlichkeit zu verbessern. Der Header x-secret ist maskiert und alle anderen Header werden im Klartext angezeigt.

Beispiel für den Request-Log:

1local-flex-1  | [flex-gateway-envoy][info] wasm log ingress-http-masked-header-log-v1-0-impl-1.default.ingress-http.default.svc main: [policy: ingress-http-masked-header-log-v1-0-impl-1.default][api: ingress-http.default.svc][req: 78e0c44c-b3ee-4ee4-b278-1708911fcc6c] {":method":"GET", "x-secret":"*****", "x-forwarded-for":"172.18.0.1", ":path":"/some/route/"}

Beispiel für den Response-Log:

1local-flex-1  | [flex-gateway-envoy][info] wasm log ingress-http-masked-header-log-v1-0-impl-1.default.ingress-http.default.svc main: [policy: ingress-http-masked-header-log-v1-0-impl-1.default][api: ingress-http.default.svc][req: 78e0c44c-b3ee-4ee4-b278-1708911fcc6c] {"server":"gunicorn/19.9.0", ":status":"200", "x-forwarded-for":"172.18.0.1"}

Veröffentlichen der Custom Policy

Nachdem die Custom Policy lokal erfolgreich getestet wurde, kann sie auf die Anypoint Platform hochgeladen werden, um sie für die Business Group verfügbar zu machen, mit der man sich über die Anypoint CLI angemeldet hat. Dies geschieht mit dem folgenden Befehl:

1make publish

Nach dem Hochladen wird die Policy in der Anypoint Platform im Exchange sichtbar und kann im Auswahldialog für Policies des Flex Gateways im Connected Mode ausgewählt werden. Die Policy erhält zunächst das Kürzel DEV, und der Versionsname wird automatisch mit dem aktuellen Zeitstempel versehen. So wird signalisiert, dass die Policy sich noch in der Entwicklung befindet und nicht produktiv eingesetzt werden sollte.

Um die Policy in einer stabilen Version zu veröffentlichen, verwenden wir folgenden Befehl:

1make release

Sobald die Policy veröffentlicht ist, kann sie in einem Flex Gateway im Connected Mode angewendet und konfiguriert werden. Die Benutzeroberfläche (UI) für die Konfiguration hängt von der Definition der Policy ab.

Fazit

Nach der anfänglichen umfangreichen Konfiguration erhält man mit dem PDK (Policy Development Kit) ein ausgereiftes Werkzeug um eigene Policies zu erstellen, falls die Standard Policies nicht ausreichen. Durch die Programmiersprache Rust und das damit verbundene Typsystem schreibt man sicheren und verständlichen Code, der mit Unit- und Integrationstests überprüft werden kann. Das Testen der eigenen Policy im Local Mode ermöglicht schnelle Entwicklungszyklen bis zur fertigen stabilen eigenen Policy. Die komplette Implementierung dieser Policy findet Ihr in GitHub.

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.