-
Notifications
You must be signed in to change notification settings - Fork 656
auth: Add support for multiple ZAP authenticators in same process #2139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
b2459ad to
50290c7
Compare
|
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
Not per process, but rather per Context. 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. |
|
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. |
|
@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?
Are the callback invocations truly isolated?
Can file-based and callback-based auth coexist? Can 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 :) |
|
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 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.
Yes, an Authenticator only affects sockets on its own Context (
Yes, state and configuration are instance attributes, nothing is process-wide or shared between Authenticator objects.
Yes, they have no relationship to each other, so they can do whatever they want. There are no interactions between contexts or authenticators. |
|
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. |
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:
This change is fully backward compatible - existing code continues to work without modification.