Skip to content

Commit bf1f8c5

Browse files
committed
cluster_backend_zeromq: Automatically enable encryption for multi-node clusters
This change introduces two new options for the ZeroMQ cluster backend: cluster_backend_zeromq.use_curve_encryption = auto|0|1 cluster_backend_zeromq.curve_dir = ${SpoolDir}/zeromq/curve The directory is used to persistently store the generated keys. Keys are generated by invoking Zeek and calling the ZeroMQ specific BiF generate_keypair(). To pre-provision keys, it's possible to populate the directory before invoking zeekctl. By default, when a multi-node cluster is detected, encryption and key generation is implicitly enabled. Setting use_curve_encryption to "0" or "1" allows for explicit configuration. The default is "auto". The keys are rendered verbatim into zeekctl-config.zeek. The assumption here is that if anyone manages to get access to the zeekctl-config.zeek file, they'll likely have a way to get to the keys in a different way already.
1 parent 822b7a6 commit bf1f8c5

4 files changed

Lines changed: 195 additions & 7 deletions

File tree

ZeekControl/plugins/cluster_backend_zeromq.py

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import json
2+
import os.path
3+
import subprocess
4+
from pathlib import Path
5+
16
import ZeekControl.config
27
import ZeekControl.plugin
8+
from ZeekControl.exceptions import ConfigurationError
39

410

511
class 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+
"\nYou 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-
"\nYou may disable this warning by setting the following option in zeekctl.cfg:"
164+
"\nYou 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.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
2+
redef Cluster::Backend::ZeroMQ::curve_client_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
3+
redef Cluster::Backend::ZeroMQ::curve_client_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
4+
redef Cluster::Backend::ZeroMQ::curve_server_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
5+
redef Cluster::Backend::ZeroMQ::curve_server_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
2+
@endif
3+
4+
redef Cluster::Backend::ZeroMQ::listen_xpub_endpoint = "tcp://127.0.0.1:27760";
5+
redef Cluster::Backend::ZeroMQ::listen_xsub_endpoint = "tcp://127.0.0.1:27761";
6+
redef Cluster::Backend::ZeroMQ::connect_xpub_endpoint = "tcp://127.0.0.1:27761";
7+
redef Cluster::Backend::ZeroMQ::connect_xsub_endpoint = "tcp://127.0.0.1:27760";
8+
9+
redef Cluster::Backend::ZeroMQ::ipv6 = F;
10+
11+
# Public and secret server keys for ZeroMQ CURVE encryption.
12+
@if ( |Cluster::Backend::ZeroMQ::curve_server_publickey| == 0 )
13+
redef Cluster::Backend::ZeroMQ::curve_server_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
14+
@endif
15+
@if ( |Cluster::Backend::ZeroMQ::curve_server_secretkey| == 0 )
16+
redef Cluster::Backend::ZeroMQ::curve_server_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
17+
@endif
18+
19+
# Public and secret client keys for ZeroMQ CURVE encryption.
20+
@if ( |Cluster::Backend::ZeroMQ::curve_client_publickey| == 0 )
21+
redef Cluster::Backend::ZeroMQ::curve_client_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
22+
@endif
23+
@if ( |Cluster::Backend::ZeroMQ::curve_client_secretkey| == 0 )
24+
redef Cluster::Backend::ZeroMQ::curve_client_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
25+
@endif
26+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# @TEST-DOC: Test enabling cluster_backend_zeromq.use_curve_encryption and observe the generated keys and redefs in the configuration file.
2+
#
3+
# @TEST-EXEC: PATH=$(pwd)/bin:$PATH bash %INPUT
4+
#
5+
# @TEST-EXEC: TEST_DIFF_CANONIFIER="sed -E -e 's/\".{40}\";/\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"/g'" btest-diff found-keys
6+
# @TEST-EXEC: TEST_DIFF_CANONIFIER="sed -E -e 's/\".{40}\";/\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"/g'" btest-diff zeekctl-config-redefs
7+
8+
. zeekctl-test-setup
9+
10+
config=$ZEEKCTL_INSTALL_PREFIX/spool/installed-scripts-do-not-touch/auto/zeekctl-config.zeek
11+
12+
installfile etc/node.cfg__cluster
13+
# Enable CURVE encryption
14+
echo "cluster_backend_zeromq.use_curve_encryption = 1" >> $ZEEKCTL_INSTALL_PREFIX/etc/zeekctl.cfg
15+
zeekctl install
16+
17+
# Check if the keys generated into spool/zeromq/curve appear in the
18+
# generated configuration file.
19+
for f in $ZEEKCTL_INSTALL_PREFIX/spool/zeromq/curve/* ; do
20+
if ! grep -F -f $f $config >> found-keys; then
21+
echo "Could not find key $f in $config" >&2
22+
exit 1
23+
fi
24+
done
25+
26+
grep -C2 'redef.*Cluster::Backend' $config > zeekctl-config-redefs

0 commit comments

Comments
 (0)