Categories
Security

The first in a new walkthrough series: Add a working classifier to the Appetizer and host it as a dark service!

If you’ve spent time with the OpenZiti Appetizer, you may have noticed this response:

you sent a message, but it can’t be qualified at this time for offensiveness

There’s a hate-speech classifier on the path every message takes, and it’s been waiting for a classifier to talk to. On the September 18 Ziti TV episode, I got curious about it and went down the “How do I get this thing to work?” rabbit hole. It turned out to be a nice tour of how OpenZiti authorization actually works!

Here’s the fun part. The first thing you notice is that the classifier URL has no instance prefix, while every other service name in the demo is scoped. That looked like the answer, but it wasn’t.Prepare tags the Appetizer’s own identity with an unprefixed classifier-clients attribute, deliberately, in a file where everything else goes through scopedName. The dial side was built with one shared classifier in mind rather than one per instance. The URL is right as written.

What’s left to build is the network side: the service and a dial policy. The application was always correct, and it’s been accurately reporting that it couldn’t reach something that wasn’t there yet.

In the end, the fix was a service and two policies, and (yay!) no changes to the code at all.

I’ve posted a preliminary (but complete) walkthrough as a repo here. It’ll be posted in a more official place (that is, a NetFoundry page) as an article and supporting scripts and files in the OpenZiti Test Kitchen on GitHub.

In the exercise, you add a real classifier (a RoBERTa model fine-tuned on OffensEval tweets) and host it as a dark service. By the end you have two processes talking by service name, with ss -ltn showing nothing listening inside the classifier container while the controller shows it actively hosting. Then you take access away with one policy change and watch the caller get “not found” for a service that’s running perfectly well three feet away.

The exercise shouldn’t take longer than a half hour, and it all runs in Docker containers, which means you don’t need Python, Go, or even the ziti CLI installed locally. I wrote it for people new to OpenZiti, with the diagnosis reasoning spelled out rather than just the commands, since that part transfers to debugging your own dials.

A few things I’d have liked to know going in, which I haven’t seen written down elsewhere:

  • Flask’s built-in dev server can’t host a ziti service. The bind succeeds and a terminator registers, but it never accepts. werkzeug polls the listening fd through selectors, and a ziti fd isn’t pollable that way. Callers see EOF and the Python side logs nothing. waitress works, and it’s what the SDK’s own Flask sample uses.
  • The SDK enumerates services at startup and doesn’t re-check per dial, so a service created after your process starts stays invisible until it refreshes.

Thanks to Clint for the Appetizer, which is a genuinely good teaching demo, and for co-hosting the episode this came from.

This is the first in a series! I’m turning Ziti TV episodes into standalone repos people can work through on their own. Episodes: https://www.youtube.com/@OpenZiti/streams

Issues and PRs welcome, especially if you hit something the troubleshooting section misses. Help make it better!

Once again, the repo is here: https://github.com/AccordionGuy/openziti-appetizer-classifier