Skip to content

Conversation

@danielrobbins
Copy link

Enable running multiple ZAP authenticators concurrently within a single process by allowing custom socket addresses in the start() method. This exposes functionality available in libzmq's C API that was previously inaccessible from Python.

Previously, all authenticators were hardcoded to bind to the single address "inproc://zeromq.zap.01", preventing multiple authenticators from coexisting. This limitation made it impossible to apply different authentication policies to different socket groups in the same process.

I've been using this patch privately for years, because my code needs it. Time to send it upstream.

Changes:

  • Add optional socket_addr parameter to Authenticator.start()
  • Add optional socket_addr parameter to AsyncioAuthenticator.start()
  • Add optional socket_addr parameter to ThreadAuthenticator.start()
  • All parameters default to "inproc://zeromq.zap.01" for backward compatibility
  • Add documentation with usage examples

This change is fully backward compatible - existing code continues to work without modification.

Enable running multiple ZAP authenticators concurrently within a single
process by allowing custom socket addresses in the start() method. This
exposes functionality available in libzmq's C API that was previously
inaccessible from Python.

Previously, all authenticators were hardcoded to bind to the single
address "inproc://zeromq.zap.01", preventing multiple authenticators
from coexisting. This limitation made it impossible to apply different
authentication policies to different socket groups in the same process.

I've been using this patch privately for years, because my code needs it.
Time to send it upstream.

Changes:
- Add optional socket_addr parameter to Authenticator.start()
- Add optional socket_addr parameter to AsyncioAuthenticator.start()
- Add optional socket_addr parameter to ThreadAuthenticator.start()
- All parameters default to "inproc://zeromq.zap.01" for backward
  compatibility
- Add documentation with usage examples

This change is fully backward compatible - existing code continues to
work without modification.
@minrk
Copy link
Member

minrk commented Nov 1, 2025

Interesting, thanks for the PR!

Can you give an example of how you've been using this? The spec says that ZAP SHALL always be on inproc://zeromq.zap.01, so how exactly have you been using a different URL?

This limitation made it impossible to apply different authentication policies to different socket groups in the same process.

Not per process, but rather per Context. inproc:// is really within a single Context, and you can have many sockets bound on the same inproc url as long as it's only one per Context. So I think the zeromq answer to your "socket group" situation would to use one Context per socket group, since that means you can have totally different auth for each.

Here's a script using two authenticators in one process, concurrently:

import zmq
from zmq.auth.thread import ThreadAuthenticator
ctx_1 = zmq.Context()

auth_1 = ThreadAuthenticator(ctx_1)
auth_1.start()
auth_1.allow("127.0.0.1")

ctx_2 = zmq.Context()
auth_2 = ThreadAuthenticator(ctx_2)
auth_2.start()
auth_2.allow("127.0.1.1")

auth_1.stop()
auth_2.stop()

ctx_1.term()
ctx_2.term()

Don't worry about the weird CI failures for now, I'll deal with that in another PR.

@danielrobbins
Copy link
Author

Here's how I have been using it. I have had a central "service hub", which has two ROUTER connections -- one internal-facing and one external-facing. The internal services (DEALER) which connect to the service hub use ZAP and use CURVE keys for authentication, on an internal (non-public) ROUTER connection. Then I also have clients/agents (DEALER) on the Internet which connect to the second ROUTER on the service hub, via a public-facing port, also using ZAP. Both services and clients use ZAP, but they each have their own separate CURVE key stores. They should not be intermingled, as the internal services and clients/agents have different security scopes and privileges. The service hub enforces a strict communications policy between clients/agents and services.

When implementing this model, I ran into an issue where I was unable to spin up these two ROUTER connections for the service hub, due to conflicting names. This patch allows me to specify a second, non-conflicting ZAP path which allows this model to work well. This allows me to have two Python classes, each implementing a ROUTER -- one internal, one external. For one, I manually specify a ZAP authenticator address for the second ROUTER, thus allowing them to both run simultaneously in-process ("inproc") without a namespace conflict. This allows trouble-free use of this pattern.

I have used this pattern for about a decade in production when operating the funtoo.org services, which has been using ZeroMQ as a communications fabric for integrating backend and frontend services.

@danielrobbins
Copy link
Author

danielrobbins commented Nov 1, 2025

@minrk regarding your recommended use of contexts -- maybe this will work and make my patch unnecessary? It depends on a few things:

Is configure_curve_callback() context-specific?

  1. When you call auth_1.configure_curve_callback(callback_A), do only sockets in ctx_1 use callback_A?
  2. Can auth_2.configure_curve_callback(callback_B) use a completely different callback without interference?

Are the callback invocations truly isolated?

  1. If a socket in ctx_1 connects, does it only trigger callback_A (not callback_B)?
  2. Can each callback maintain separate state - mapping public keys their own authentication database?

Can file-based and callback-based auth coexist?

Can auth_1.configure_curve(domain='*', location='/path/to/keys') work in one context while auth_2.configure_curve_callback() works in another?

Basically, does the Context fully disentangle multiple ZAP authenticators from another even if they are using the same endpoint?

Example code demonstrating what would need to be supported with contexts for the multi-zap patch to not be needed:

import zmq
from zmq.auth.asyncio import AsyncioAuthenticator

# Context 1: Device registry with callback-based authentication
ctx_1 = zmq.asyncio.Context()
auth_1 = AsyncioAuthenticator(ctx_1)
auth_1.start()
auth_1.allow("127.0.0.1")

def device_registry_callback(domain, z85_public_key):
    print(f"Auth1 callback: {z85_public_key}")
    # Query device registry, return True/False
    return True

auth_1.configure_curve_callback(callback=device_registry_callback)

# Context 2: Legacy file-based authentication
ctx_2 = zmq.asyncio.Context()
auth_2 = AsyncioAuthenticator(ctx_2)
auth_2.start()
auth_2.allow("127.0.0.1")
auth_2.configure_curve(domain='*', location='/path/to/authorized_keys')

I have not tried this myself. Let me know if it should work and if there are any concerns with doing this. If this is fully supported and doesn't create any potential security concerns with inter-mingling of two separate security contexts, then my patch is probably not needed and I can simply update my code :)

@minrk
Copy link
Member

minrk commented Dec 5, 2025

Sorry for the delay, November was a busy month. I'm still curious about how this PR could work, since sockets won't use any endpoint other than inproc://zeromq.zap.01 for ZAP, which is hardcoded and specified in the spec as the only valid value.

So if you've done something like what would be enabled by this PR:

Authenticator(ctx, url="inproc://zeromq.zap.02")

the effect would be that auth is not enabled at all.

But I think you're right that this PR is not necessary - you can have any number of ZAP configurations as long as they are one per context. They are perfectly isolated from each other and have no interactions.

Is configure_curve_callback() context-specific?

Yes, an Authenticator only affects sockets on its own Context (inproc cannot interact with sockets from other contexts). Sockets from other contexts behave as if the Authenticator doesn't exist.

Are the callback invocations truly isolated?

Yes, state and configuration are instance attributes, nothing is process-wide or shared between Authenticator objects.

Can file-based and callback-based auth coexist?

Yes, they have no relationship to each other, so they can do whatever they want. There are no interactions between contexts or authenticators.

@danielrobbins
Copy link
Author

danielrobbins commented Dec 5, 2025

OK, thanks for the clarification.

I have definitely used this patch and it did indeed have ZAP working on two separate ROUTER connections with no use of Context.

But based on your response, this patch is not needed. But there may be something useful to glean from this bug report -- which is a better exception message.

I believe I implemented this patch because pyzmq "hides" Context by default, so I had some code that did not use Context(), and worked, and with this code in a python Class, two instances of the object both used ZAP with an implicit shared context. This gave me an exception of some kind telling me that inproc://zeromq.zap.1 was already in use. I addressed this issue by eliminating the conflict with my patch, which seemed like the obvious and correct fix. Really, the proper fix was to update the class so each instance instantiates its own Context and has its own inproc namespace.

It may be good for me to attempt to reproduce this original traceback, and submit a patch with a more helpful exception that essentially says "If you are seeing this when managing independent ZMQ connections, you probably need to use Context() to for each ZeroMQ connection so they have their own inproc namespace.".

That would give developers better guidance if they hit some issue related to the built-in implicit instantiation of Context via the zmq.Context.instance() singleton.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants