1+ import json
2+ import os .path
3+ import subprocess
4+ from pathlib import Path
5+
16import ZeekControl .config
27import ZeekControl .plugin
8+ from ZeekControl .exceptions import ConfigurationError
39
410
511class ClusterBackendZeroMQ (ZeekControl .plugin .Plugin ):
@@ -19,8 +25,52 @@ def options(self):
1925 "bool" ,
2026 False ,
2127 "Disable the multi-node unencrypted warning." ,
22- )
28+ ),
29+ (
30+ "use_curve_encryption" ,
31+ "string" ,
32+ "auto" ,
33+ "Whether to enable ZeroMQ CURVE-based encryption for cluster communication (auto, 0 or 1)" ,
34+ ),
35+ (
36+ "curve_dir" ,
37+ "string" ,
38+ "${SpoolDir}/zeromq/curve" ,
39+ "Directory to persistently store client and server curve keys on the manager" ,
40+ ),
41+ ]
42+
43+ def generate_keypair (self , publickey : Path , secretkey : Path ):
44+ """
45+ Generate the ZeroMQ CURVE keypair in Z85 encoded format and write
46+ them to the files pointed at by publickey and secretkey. The keys
47+ themselves are generated using zeek and calling the appropriate BiF.
48+ """
49+ zeek = self .getGlobalOption ("zeek" )
50+ if not os .path .lexists (zeek ):
51+ raise ConfigurationError (f"cannot find Zeek binary: { zeek } " )
52+
53+ args = [
54+ zeek ,
55+ "-b" ,
56+ "-e" ,
57+ "print to_json(Cluster::Backend::ZeroMQ::generate_keypair())" ,
2358 ]
59+ output = subprocess .check_output (args )
60+ loaded = json .loads (output )
61+ public , secret = loaded ["public" ], loaded ["secret" ]
62+ if len (public ) != 40 or len (secret ) != 40 :
63+ raise ConfigurationError (
64+ f"failed to create ZeroMQ CURVE keypair { loaded !r} "
65+ )
66+
67+ publickey .touch (exist_ok = True )
68+ publickey .chmod (0o400 )
69+ publickey .write_text (public + "\n " )
70+
71+ secretkey .touch (exist_ok = True )
72+ secretkey .chmod (0o400 )
73+ secretkey .write_text (secret + "\n " )
2474
2575 def init (self ):
2676 """
@@ -55,21 +105,66 @@ def init(self):
55105 # Check if this is a multi-node cluster (multiple IP addresses) and
56106 # tell the user about it.
57107 addrs = {n .addr for n in self .nodes ()}
58- if len (addrs ) > 1 and not self .getOption ("disable_unencrypted_warning" ):
108+
109+ # If use_curve_encryption is "auto", determine 0 or 1 based on
110+ # the number of different node addresses available. If it is
111+ # already "0" or "1", just make it the integer value.
112+ self .use_curve_encryption = (
113+ self .getOption ("use_curve_encryption" ).lower ().strip ()
114+ )
115+ if len (addrs ) > 1 and self .use_curve_encryption == "auto" :
116+ self .use_curve_encryption = 1
117+ elif len (addrs ) == 1 and self .use_curve_encryption == "auto" :
118+ self .use_curve_encryption = 0
119+ elif self .use_curve_encryption in ["0" , "1" ]:
120+ self .use_curve_encryption = int (self .use_curve_encryption )
121+ elif self .use_curve_encryption in ["true" , "false" ]:
122+ self .use_curve_encryption = 1 if self .use_curve_encryption == "true" else 0
123+ else :
124+ raise ConfigurationError (
125+ f"invalid UseCurveEncryption value: { self .use_curve_encryption } "
126+ )
127+
128+ # Store public and secret keys in in spool/zeromq/curve by default.
129+ curve_dir = self .getOption ("curve_dir" )
130+ self .server_publickey = Path (curve_dir ) / "server_publickey"
131+ self .server_secretkey = Path (curve_dir ) / "server_secretkey"
132+ self .client_publickey = Path (curve_dir ) / "client_publickey"
133+ self .client_secretkey = Path (curve_dir ) / "client_secretkey"
134+
135+ # If encryption is enabled, create the spool directory for
136+ # the server and client keypairs and generate them if needed.
137+ if self .use_curve_encryption :
138+ os .makedirs (curve_dir , exist_ok = True )
139+ os .chmod (curve_dir , 0o700 )
140+
141+ if not self .server_publickey .exists () or not self .server_secretkey .exists ():
142+ self .message ("Generating ZeroMQ CURVE server keypair..." )
143+ self .generate_keypair (self .server_publickey , self .server_secretkey )
144+
145+ if not self .client_publickey .exists () or not self .client_secretkey .exists ():
146+ self .message ("Generating ZeroMQ CURVE client keypair..." )
147+ self .generate_keypair (self .client_publickey , self .client_secretkey )
148+
149+ if (
150+ not self .use_curve_encryption
151+ and len (addrs ) > 1
152+ and not self .getOption ("disable_unencrypted_warning" )
153+ ):
59154 self .message (
60- f'Warning: ZeroMQ cluster backend enabled and multi-node cluster detected (IPs { ", " .join (addrs )} ).'
155+ f'Warning: ZeroMQ encryption disabled, but multi-node cluster detected (IPs { ", " .join (addrs )} ).'
61156 )
62157 self .message (
63- "Communication between Zeek nodes using ZeroMQ is currently unencrypted. Use Broker with TLS if this "
158+ "\n You may disable this warning by setting the following option in zeekctl.cfg: "
64159 )
65160 self .message (
66- "is concerning to you. ZeroMQ encryption is tracked at https://github.com/zeek/zeek/issues/4432 "
161+ "\n cluster_backend_zeromq.disable_unencrypted_warning = 1 \n "
67162 )
68163 self .message (
69- "\n You may disable this warning by setting the following option in zeekctl .cfg:"
164+ "\n You may enable encryption by setting the following option.cfg:"
70165 )
71166 self .message (
72- "\n cluster_backend_zeromq.disable_unencrypted_warning = 1\n "
167+ "\n cluster_backend_zeromq.use_curve_encryption = 1 (or auto) \n "
73168 )
74169
75170 # If any of the addresses used by nodes looks like an IPv6 address,
@@ -103,6 +198,42 @@ def zeekctl_config(self):
103198 ]
104199 )
105200
201+ # If CURVE encryption is enabled, redef the server and client
202+ # keys into the zeekctl-config file.
203+ if self .use_curve_encryption :
204+
205+ def render_redef (path : Path ) -> str :
206+ """
207+ Small helper to render a redef line given the path to
208+ the keypair files stored in curve_dir.
209+ """
210+ what = path .parts [- 1 ]
211+ value = path .read_text ().strip ()
212+ if len (value ) != 40 :
213+ raise ConfigurationError (f"CURVE key { what } not 40 bytes long" )
214+
215+ return "\n " .join (
216+ [
217+ f"@if ( |Cluster::Backend::ZeroMQ::curve_{ what } | == 0 )" ,
218+ f'redef Cluster::Backend::ZeroMQ::curve_{ what } = "{ value } ";' ,
219+ "@endif" ,
220+ ]
221+ )
222+
223+ script += "\n " .join (
224+ [
225+ "" ,
226+ "# Public and secret server keys for ZeroMQ CURVE encryption." ,
227+ render_redef (self .server_publickey ),
228+ render_redef (self .server_secretkey ),
229+ "" ,
230+ "# Public and secret client keys for ZeroMQ CURVE encryption." ,
231+ render_redef (self .client_publickey ),
232+ render_redef (self .client_secretkey ),
233+ "" ,
234+ ]
235+ )
236+
106237 # Usually this runs automatically on the manager, but Zeectl supports
107238 # standalone mode and the node doesn't know it should run the proxy
108239 # thread for WebSocket functionality.
0 commit comments