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 :)

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