Kafbat – OAUTH With RBAC

I encountered a challenge with a Kafka management tool — it supports SSO, and I was able to get an OAUTH connection set up to control what users could see when logging in through the UI, but the API component didn’t extract information from the bearer token and there was nothing in the rbac mapping to allow the bearer-token client ID to access anything.

Updates to allow the /api components to be authenticated by simple bearer tokens and a client ID mapped into a role are at https://github.com/ljr55555/kafka-ui

AuthorizationController.java was updated to properly support non-browser, machine-principal auth.

Added/fixed:

  • avoids null failure when authentication.getName() is missing
  • resolves principal name from alternate attributes such as:
    • client_id
    • sub
    • username
  • updated displayed permissions logic so /api/authorization uses the same effective RBAC matching idea as the backend

Result

/api/authorization now works for bearer-token API callers and shows:

  • username = client ID
  • populated permissions list

AccessControlService.java

Added support for API bearer-token principals

Previously, getUser() only worked when the authenticated principal was a RbacUser, which covered the browser/user flow.

Now it can also derive an AuthenticatedUser from opaque-token authenticated principals by extracting:

  • principal name
  • group-like values from attributes/authorities if present

Updated role matching logic

Previously, role matching was only role name matches one of user.groups(). Now it also supports role name matches user.principal(). That enables RBAC binding directly to the API client ID.

Result

RBAC now works for:

  • normal browser users via groups
  • API bearer-token callers via client principal name

DynamicConfigMapper.java

Fixed a mapper bug.

Before

The method mapping resource server config created a populated OAuth2ResourceServerProperties result object but always returned null.

After

It now returns result.

Result

Dynamic/config mapping for resource-server settings no longer silently discards the mapped object.

Build/package note

To preserve the browser UI, the jar needs to be built with frontend included — which you know if you read the doc … or you take my route, start it all up, test the API successfully, and then get baffled that the user UI throws


        bash./gradlew clean assemble -Pinclude-frontend=true

application.yml

server:
  port: 8443
  ssl:
    enabled: true
    key-store: file:/etc/kafkaui/certs/kafbat.rushworth.us.p12
    key-store-password: ${KEYSTORE_PASSWORD}
    key-store-type: PKCS12
    key-alias: kafbat

auth:
  type: OAUTH2
  oauth2:
    client:
      pingfed:
        client-id: ${OAUTH_CLIENT_ID}
        client-secret: ${OAUTH_CLIENT_SECRET}
        scope:
          - openid
          - profile
          - email
        client-name: oauthclient
        provider: oauthclient
        redirect-uri: https://kafbat.rushworth.us:8443/login/oauth2/code/oauthclient
        authorization-grant-type: authorization_code
        issuer-uri: https://login.example.com
        jwk-set-uri: https://login.example.com/pf/JWKS
        authorization-uri: https://login.example.com/as/authorization.oauth2
        token-uri: https://login.example.com/as/token.oauth2
        user-info-uri: https://login.example.com/idp/userinfo.openid
        user-name-attribute: username
        custom-params:
          type: oauth
          roles-field: memberOf

    resource-server:
      opaque-token:
        client-id: ${OAUTH_CLIENT_ID}
        client-secret: ${OAUTH_CLIENT_SECRET}
        introspection-uri: https://login.example.com/as/introspect.oauth2

kafka:
  clusters:
    - name: test
      bootstrapServers: ${KAFKA_BOOTSTRAP_SERVERS} 

roles.yml

rbac:
  roles:
    - name: "admins"
      clusters:
        - test
      subjects:
        - provider: oauth
          type: role
          value: "CN=KafbatAdmins,OU=SecurityGroups,DC=example,DC=com"
      permissions:
        - resource: applicationconfig
          actions: all

        - resource: clusterconfig
          actions: all

        - resource: topic
          value: ".*"
          actions: all

        - resource: consumer
          value: ".*"
          actions: all

        - resource: schema
          value: ".*"
          actions: all

        - resource: connect
          value: ".*"
          actions: all

        - resource: ksql
          actions: all

        - resource: acl
          actions: [ view ]

    - name: "${OAUTH_CLIENT_ID}"
      clusters:
        - test
      subjects:
        - provider: oauth
          type: user
          value: "${OAUTH_CLIENT_ID}"
      permissions:
        - resource: applicationconfig
          actions: all

        - resource: clusterconfig
          actions: all

        - resource: topic
          value: ".*"
          actions: all

        - resource: consumer
          value: ".*"
          actions: all

        - resource: schema
          value: ".*"
 
        - resource: connect
          value: ".*"
          actions: all

        - resource: ksql
          actions: all

        - resource: acl
          actions: [ view ]
 

docker-compose.yml

services:
  redpanda:
    image: redpandadata/redpanda:v25.1.2
    container_name: redpanda
    command:
      - redpanda
      - start
      - --overprovisioned
      - --smp=1
      - --memory=1G
      - --reserve-memory=0M
      - --node-id=0
      - --check=false
      - --kafka-addr=PLAINTEXT://0.0.0.0:9092
      - --advertise-kafka-addr=PLAINTEXT://redpanda:9092
    ports:
      - "9092:9092"

  kafbat-ui:
    image: ghcr.io/kafbat/kafka-ui:latest
    container_name: kafbat-ui
    restart: unless-stopped
    depends_on:
      - redpanda
    ports:
      - "8443:8443"
    volumes:
      - ./config/application.yml:/etc/kafkaui/application.yml:ro
      - ./config/roles.yml:/etc/kafkaui/roles.yml:ro
      - ./certs:/etc/kafkaui/certs:ro
    environment:
      SPRING_CONFIG_LOCATION: file:/etc/kafkaui/application.yml
      SPRING_CONFIG_ADDITIONAL_LOCATION: file:/etc/kafkaui/roles.yml
      KEYSTORE_PASSWORD: REDACTED
      OAUTH_CLIENT_ID: REDACTED
      OAUTH_CLIENT_SECRET: REDACTED
      KAFKA_BOOTSTRAP_SERVERS: redpanda:9092

Docker for Java Builds

Instead of flipping back and forth between java versions for various builds, you can just use a docker container for the proper Java version to run the build.

[lisa@docker kafka-ui]# docker run --rm -it   --user $(id -u):$(id -g)   -v "$PWD":/workspace   -w /workspace   eclipse-temurin:25   bash -lc './gradlew clean build'
Downloading https://services.gradle.org/distributions/gradle-9.2.0-bin.zip
............10%.............20%.............30%.............40%.............50%.............60%.............70%.............80%.............90%.............100%

Welcome to Gradle 9.2.0!

Here are the highlights of this release:
 - Windows ARM support
 - Improved publishing APIs
 - Better guidance for dependency verification failures

For more details see https://docs.gradle.org/9.2.0/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)
<=============> 100% CONFIGURING [1m 46s]
> Resolve dependencies of :api:detachedConfiguration273

Ash Flash

Sept 5, 2022 – May 13, 2026

When Patches and the four kittens would come to eat and play on our back patio, we noticed that they never really meowed. And then, on October 28, 2022 — we heard a little kitten meow. The little gray kitten had been left behind – the cat Scott had decided was the coolest one. We got a little trap and baited it with cat food and tuna bits. And caught our little Ash.

We petted him and fed him, and he would curl up on one of us to be extra warm to take a nap. He had the loudest purr I’ve ever heard. You could hear from across the room whenever he was happy. And he was so happy. He loved to be scratched at – ruffling up his fur, then smoothing it back down so he was a sleek, tiny panther again.

He was a spooky cat, very skittish until he got to know and love you. I’d hang out with Anya in her room and play with her while Ash was in her room. One day, we were laying on the bed and he climbed up on my leg. Froze when I looked at him, so I looked away. He walked up my leg and onto my back. Then there was a little kitten head poking over my shoulder. He learned to trust me. Inside! I could pick him up, pet him, and play with him. It took a while before he would come up to me outside. I was cleaning a spot to plant some comfrey and asparagus over by the orchard. Crawling around on the ground, picking out all the weeds. He slowly made his way over to me, creeping closer and closer, and eventually wanted to be petted. I took a long break to talk to him, pet him, and snuggle him. Since then, when I’d go outside, he would usually stop whatever he was doing to run over and greet me.

He had a great smile and always looked thrilled when one of his “people” came outside to hang out. He’d run up with a happy murr-ur. Follow me and ‘help’. He’d even brave the giant growly lawnmower monster to follow me around the yard when I’d use the push-mower. We’d walk over to the chicken coop to let the chickens and turkeys out, he’d come back to the house with me, and then he’d go do cat things for the day. In the winter, he’d carefully follow behind me in the trail I broke through the snow, while his brother would bound across the foot-deep snow into the woods. He’d come with Anya and I as we gathered maple syrup – going from the back woods to the farm, and back to the house. He ducked into the covered cub cadet with us when it started raining, walked along the river as Anya looked for rocks, and was always on the look for chipmunks to pounce on. He’d follow me a lot inside, too. Walking up to the laundry room to get more cat food, bring laundry from the drier into my bedroom and fold it, then back downstairs. Or back and forth into the pantry while I gathered ingredients for dinner.

He was the snuggliest cat I’ve ever met. He would weave between my legs while I was walking outside; I frequently picked him up, held him, and petted him as I walked somewhere. He’d nuzzle his forehead under my chin or scan the yard for prey. If he saw a bird, he’d leap from my arms to try and get it. He loved getting his belly petted. His belly had the softest, silkiest fur. And he was growing two white patches – one on his chest and one farther down on his abdomen. They were getting bigger as he got older, and I thought he might turn into Ash the White like Gandolf.

He was such a smart cat. He learned how to open the garage door to follow me inside when I needed to grab some tools. As I was going through the drawers, trying to find something, I heard a creaky noise behind me. Turning around, the garage door was open and he came trotting over purring and asking to be petted. He would open the laundry room door, so we had to lock it at bedtime.

We used to have trouble rounding the cats up at night, but I got canned cat food. We started bringing them inside at night and giving them canned food for “dinner”. He loved kitten dinner time. Rarely had trouble getting Ash to come in at night. He’d come with us to round up the birds and happily run into the house for dinner.

He loved butchering day – the best farm cat day of the year. He would get excited if I picked up the big, white cutting boards and carry them toward the family room door. It’s hard to walk when you’ve got two 18×30 pieces of plastic in your hands and a big, strong cat weaving between your legs and purring at you. The first butchering day, Ash and Dumplin were so hyper after eating the trim meat I’d throw for them. Zooming around the yard and running up trees. Ash climbed so high on one of the big trees that Anya was worried he wouldn’t get down safely.

He’d climb the ornamental pine at the corner of the garage and hop onto the roof. Looking from the driveway, there’d be a cat walking around on the roof.

As he got a little older, he liked to come into the family room for a nap during the day. He’d sleep on the sofa, either in Anya’s spot while she was at school or snuggled up next to me. When the weather was nice, he would have cat adventures then come back to nap on one of the benches on the back patio or some giant slice of a tree. If he wanted to come inside and no one noticed him, he’d crawl up the screen door (which irritated Scott endlessly) – was very noticeable!

He started having seizures three weeks ago. Trips to the emergency vet, medicine and more medicine. I thought the medicine was working since he had three seizure free days. He was snoozy from the drugs, and spent a lot of time being held while he slept. Petted, brushed, and loved. Comforted after each seizure. He knew how much we loved him, and he was with his people until the end.

May Starclan light your path. May you find swift running, good hunting, and shelter where you sleep.

Using polkit to allow non-priv user to restart service

As I work through automating certificate installation, most applications have a “service account” user that has write access to the SSL certificate files. However, that user does not generally have permission to restart the application service.

We could get the ID added to sudoers with specific rights to manage the service … but it seemed more straightforward to use Polkit for very granular control permitting the service account to run specific verbs with systemctl.

The following rule allows the “tomcatadmin” user to run systemctl start, stop, or restart with the apache-tomcat.service unit.

cat > /etc/polkit-1/rules.d/60-apache-tomcat-tomcatadmin.rules <<'EOF'
polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.systemd1.manage-units") {
        var unit = action.lookup("unit");
        var verb = action.lookup("verb");

        if (subject.user == "tomcatadmin" &&
            unit == "apache-tomcat.service" &&
            (verb == "start" || verb == "stop" || verb == "restart")) {
            return polkit.Result.YES;
        }
    }
});
EOF

Polymarket and PCI Standards

Somewhat disparagingly, I’ve often thought the payment card companies came up with the PCI standards as an effort to avoid a legislative solution. Significant, public issues made people “demand answers” – and government regulation is an obvious answer. Except it’s outside of your control. If you can convince everyone that you’ve come up with rules to solve the problem … no need for those government types to waste their time and create more red tape! We’re all good here.

I thought of that when reading about the soldier arrested for using insider knowledge in Polymarket trades. “Insider trading has no place on Polymarket,” the company wrote. “Today’s arrest is proof the system works.” … an unregulated market were people see spikes in “trades” that appear to be driven on insider knowledge is bad for business. If you want to go and bet on which team ends up in the basketball championship, you aren’t betting against people with special knowledge that put you at a disadvantage. But bets like this? Who wants to put their money on “US invades Iran in 7 days” against someone sitting in the situation room delaying the troops movement because his bet was exactly 9:57PM UTC today.

Of course dude wants to show “the system works” — last thing he wants is to fall under securities regulations!

Vexing RDPSign Issue

With recent Windows updates, users now get a big message saying “Caution: Unknown remote connection” when launching RDP sessions from our CyberArk server. Easy enough – I have an internal CA, I can generate a code signing certificate, so I can sign these RDP files.

Except, in testing, I continually got an error indicating rdpsign cannot find the certificate. It’s there. I have a private key. It’s a code signing certificate. An hour or so later, I realize the “sha256” value is actually the SHA-1 thumbprint. Which … not my first guess and really more of a “out of reasonable options, start trying silly things” guess.

“$env:SystemRoot\System32\rdpsign.exe” /v /sha256 $hash256 $rdp

Voila, “All rdp file(s) have been successfully signed.”

Sigh — and, after all this work? I go from the red “unknown publisher” error to a yellow “yeah, you should think about this” banner.

Blender Script: Distance Between Two Points

We were having a lot of trouble using the measure tool in Blender — after discovering that you can hold the Ctrl key and “snap” your selection to a vertex, it actually worked in the way we wanted it to. But, before that discovery, my thought was to manually select two vertices and use a Python script to measure the distance between those points. That is “blender units” … and the metric or imperial units would be more meaningful. So I converted based on the scene configuration.

import bpy
import bmesh


def format_metric(meters: float) -> str:
    a = abs(meters)

    if a >= 1.0:
        return f"{meters:.6f} m"
    elif a >= 0.01:
        return f"{meters * 100.0:.3f} cm"
    else:
        return f"{meters * 1000.0:.3f} mm"


def format_imperial(meters: float) -> str:
    total_inches = meters / 0.0254
    sign = "-" if total_inches < 0 else ""
    total_inches = abs(total_inches)

    feet = int(total_inches // 12)
    inches = total_inches - (feet * 12)

    if feet > 0:
        return f"{sign}{feet} ft {inches:.3f} in"
    else:
        return f"{sign}{total_inches:.3f} in"


def measure_selected_vertices():
    ctx = bpy.context
    obj = ctx.edit_object

    if obj is None or obj.type != 'MESH':
        raise RuntimeError("Go into Edit Mode on a mesh and select exactly 2 vertices.")

    bm = bmesh.from_edit_mesh(obj.data)
    selected_verts = [v for v in bm.verts if v.select]

    if len(selected_verts) != 2:
        raise RuntimeError(f"Expected exactly 2 selected vertices, found {len(selected_verts)}.")

    v1, v2 = selected_verts

    # Convert local vertex coordinates to world-space coordinates
    p1 = obj.matrix_world @ v1.co
    p2 = obj.matrix_world @ v2.co

    # Raw world-space distance in Blender units
    raw_distance = (p2 - p1).length

    scene = ctx.scene
    units = scene.unit_settings
    scale_length = units.scale_length if units.scale_length != 0 else 1.0

    # Convert Blender units to meters according to the scene unit scale
    distance_meters = raw_distance * scale_length

    print("\n----- Vertex Distance -----")
    print(f"Object: {obj.name}")
    print(f"Raw distance (Blender units): {raw_distance:.6f}")
    print(f"Scene unit system: {units.system}")
    print(f"Scene unit scale: {scale_length}")

    if units.system == 'METRIC':
        print(f"Formatted distance: {format_metric(distance_meters)}")

    elif units.system == 'IMPERIAL':
        print(f"Formatted distance: {format_imperial(distance_meters)}")

    else:
        print("Formatted distance: Scene unit system is 'NONE'")
        print(f"Interpreted using current unit scale: {format_metric(distance_meters)}")


measure_selected_vertices()

Signing PowerShell Scripts

A quick PowerShell script to report on its own signature data:

$scriptPath = $PSCommandPath

if (-not $scriptPath) {
    throw 'This script must be run from a .ps1 file so $PSCommandPath is available.'
}

$sig = Get-AuthenticodeSignature -FilePath $scriptPath

Write-Host "Script path: $scriptPath`n" -ForegroundColor Cyan

[PSCustomObject]@{
    Status                  = $sig.Status
    StatusMessage           = $sig.StatusMessage
    SignatureType           = $sig.SignatureType
    IsOSBinary              = $sig.IsOSBinary
    SignerSubject           = $sig.SignerCertificate.Subject
    SignerThumbprint        = $sig.SignerCertificate.Thumbprint
    SignerNotBefore         = $sig.SignerCertificate.NotBefore
    SignerNotAfter          = $sig.SignerCertificate.NotAfter
    TimeStamperSubject      = $sig.TimeStamperCertificate.Subject
    TimeStamperThumbprint   = $sig.TimeStamperCertificate.Thumbprint
} | Format-List

To sign the script:

$thumb = '87E4C1F40D1DB8486F1E9093A76626AB1DFDEA30'
$scriptPath = "$env:USERPROFILE\git\CyberSecurity\misc\CheckPSSignature.ps1"

$cert = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My |
    Where-Object {
        $_.Thumbprint -eq $thumb -and
        $_.HasPrivateKey -and
        ($_.EnhancedKeyUsageList | Where-Object {
            $_.ObjectId -eq '1.3.6.1.5.5.7.3.3' -or $_.FriendlyName -eq 'Code Signing'
        })
    } |
    Select-Object -First 1

if (-not $cert) {
    throw "Code signing certificate $thumb not found."
}

Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $cert
Get-AuthenticodeSignature -FilePath $scriptPath | Format-List *

And now the script is signed:

Venafi Trust Protect and Azure Key Vault Integration

Entra App Registration

Add a new Entra App registration for Venafi

There is no redirect URI needed for this registration

In this example, my App ID is 05151153-f5d5-4ce8-94cb-9086d70d3c05

On app, go to certificates & secrets. Upload PUBLIC key of a Digital Signature certificate.

Confirm the public key has been added

Key Vault Configuration

In the Azure Portal, navigate to the Key Vaults

Confirm you have an appropriate key vault, or create one. In this example, I am creating a new key vault.

Select the subscription and resource group to be used, provide a name for the vault.

In this example, my key vault is LJRVenafiTestKeyVault

This vault uses RBAC access. Click on “Access control (IAM)” to add rights for the Entra app to use this key vault

Select “Add role assignment”

Select the “Key Vault Certificates Officer” role

Add the application name to this role:

Venafi Configuration

Now, in Venafi, we can add an Azure Key Vault installation to a certificate.

First, we need to create a new certificate type credential to hold the private key for the certificate used in the app registration

Upload the certificate pfx file and supply the pfx password

Navigate to the certificate you want published into the Azure Key Vault. From the “Actions” menu, select “Add Installation”

Select “Track, validate, and automate installation of this certificate”

Select a device and chose the “Azure Key Vault” installation type:

The first half of the form does not need to be changed, although you can add a description explaining what the deployment is.

Select the device credential for the host. The “Application ID” is the Azure App ID from the registered application. The Certificate Credential is the Digital Signature private key uploaded for application authentication.

The Azure Key Vault Name is the name of the key vault created in Azure, and Certificate Name is the “friendly” name to be used in the certificate file deployed to the server. This often needs to be included in the application configuration (use this keystore file and use this certificate from the keystore). Because I am using this key in a release pipeline, I do not want to bind the certificate to a web service

The cert will be queued for installation into the Azure Key Vault

Once the installation has completed, return to the Azure Portal to confirm that the certificate is now present in the key vault.

Using the Key in a Pipeline

This document assumes:

Azure CLI is installed (https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux)

You already have an agent pool with online agent in a deployment pool

And, finally, that you have a pipeline deployment that uses a static keystore. We will be replacing that static keystore file with one obtained from the Azure Key Vault.

First, ensure the Azure DevOps service connection used by the pipeline has access to LJRVenafiTestKeyVault with at least:

  • Secrets: Get, List

From the Azure command line, e.g.

az role assignment create –assignee-object-id 107d2d9a-4d1b-4d8b-9cd6-0f95587eb9ae –assignee-principal-type ServicePrincipal –role “Key Vault Secrets User” –scope “/subscriptions/dede429d-a340-4e90-8f76-05aa5280a1f5/resourceGroups/ljr-keyvault-demo/providers/Microsoft.KeyVault/vaults/LJRVenafiTestKeyVault”

If you do not know which service connection is being used, update and run the pipeline. It will fail with a permission error, but the service connection’s usage history will reflect the release pipeline’s use:

Update your pipeline to retrieve the certificate from the Azure KeyVault. Add an Azure CLI task using an inline script

set -euo pipefail

PFX_FILE=”$AGENT_TEMPDIRECTORY/VenafiDeployedCertificate.pfx”

az keyvault secret download \

–vault-name LJRVenafiTestKeyVault \

–name VenafiDeployedCertificate \

–file “$PFX_FILE” \

–encoding base64

echo “Downloaded PFX to $PFX_FILE”

echo “##vso[task.setvariable variable=PFX_PATH]$PFX_FILE”

If you need a JKS file, add an additional bash task with an inline script

set -euo pipefail

JKS_FILE=”$AGENT_TEMPDIRECTORY/VenafiDeployedCertificate.jks”

# Verify keytool exists

command -v keytool >/dev/null 2>&1 || { echo “keytool not found on agent”; exit 1; }

keytool -importkeystore \

-srckeystore “$(PFX_PATH)” \

-srcstoretype PKCS12 \

-srcstorepass “” \

-destkeystore “$JKS_FILE” \

-deststoretype JKS \

-deststorepass “$(JksPassword)” \

-destkeypass “$(JksPassword)” \

-noprompt

echo “Created JKS at $JKS_FILE”

echo “##vso[task.setvariable variable=JKS_PATH]$JKS_FILE”

Add a pipeline variable for the JKS Password – make sure to click the lock icon to protect the password

And, finally, add a bash task task to copy the JKS or PFX file to the proper place on the server

set -euo pipefail

# Copy JKS to location on server used in app config

TARGET_DIR=”/opt/credential-injection/certs”

TARGET_JKS=”$TARGET_DIR/VenafiDeployedCertificate.jks”

cp “$(JKS_PATH)” “$TARGET_JKS”

chmod 600 “$TARGET_JKS”

echo “JKS copied to $TARGET_JKS”

# Or copy pfx to location on server used in app config

TARGET_PFX=”$TARGET_DIR/VenafiDeployedCertificate.pfx”

cp “$(PFX_PATH)” “$TARGET_PFX”

chmod 600 “$TARGET_PFX”

Create a release to run the pipeline. Looking at the logs, you should see a confirmation that the pfx file was created

And, if you are creating a JKS file, a confirmation that it was created as well

You should also see the certificate file(s) on the server: