Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 138 additions & 7 deletions ZeekControl/plugins/cluster_backend_zeromq.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import json
import os.path
import subprocess
from pathlib import Path

import ZeekControl.config
import ZeekControl.plugin
from ZeekControl.exceptions import ConfigurationError


class ClusterBackendZeroMQ(ZeekControl.plugin.Plugin):
Expand All @@ -19,8 +25,52 @@
"bool",
False,
"Disable the multi-node unencrypted warning.",
)
),
(
"use_curve_encryption",
"string",
"auto",
"Whether to enable ZeroMQ CURVE-based encryption for cluster communication (auto, 0 or 1)",
),
(
"curve_dir",
"string",
"${SpoolDir}/zeromq/curve",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to make this ${CfgDir}? At least historically the spool is a transient storage place for data awaiting processing, so not a great fit for fixed configs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used SpoolDir because...

  • didn't want to auto-generate directories/files into CfgDir (this would be next to etc/node.cfg or so)
  • broker stores end/ended up in SpoolDir/stores and they already aren't transient.
  • ${SpoolDir}/state.db is also located there.
  • unless there's non-Zeekctl managed processes connecting: A re-deploy after deleting the keys will re-generate and re-distribute them to all cluster nodes, so they could be seen as somewhat transient.

I'll keep it there. The CfgDir seems less fitting (but certainly debatable and I see the point).

"Directory to persistently store client and server curve keys on the manager",
),
]

def generate_keypair(self, publickey: Path, secretkey: Path):
"""
Generate the ZeroMQ CURVE keypair in Z85 encoded format and write
them to the files pointed at by publickey and secretkey. The keys
themselves are generated using zeek and calling the appropriate BiF.
"""
zeek = self.getGlobalOption("zeek")
if not os.path.lexists(zeek):
raise ConfigurationError(f"cannot find Zeek binary: {zeek}")

args = [
zeek,
"-b",
"-e",
"print to_json(Cluster::Backend::ZeroMQ::generate_keypair())",
]
output = subprocess.check_output(args)
loaded = json.loads(output)
public, secret = loaded["public"], loaded["secret"]
if len(public) != 40 or len(secret) != 40:
raise ConfigurationError(
f"failed to create ZeroMQ CURVE keypair {loaded!r}"
)

publickey.touch(exist_ok=True)
publickey.chmod(0o600)
publickey.write_text(public + "\n")

secretkey.touch(exist_ok=True)
secretkey.chmod(0o600)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was updated from 0o400 to 0o600 otherwise can't write, I didn't realize the tests were failing.

secretkey.write_text(secret + "\n")
Comment thread
awelzel marked this conversation as resolved.

def init(self):
"""
Expand Down Expand Up @@ -55,21 +105,66 @@
# Check if this is a multi-node cluster (multiple IP addresses) and
# tell the user about it.
addrs = {n.addr for n in self.nodes()}
if len(addrs) > 1 and not self.getOption("disable_unencrypted_warning"):

# If use_curve_encryption is "auto", determine 0 or 1 based on
# the number of different node addresses available. If it is
# already "0" or "1", just make it the integer value.
self.use_curve_encryption = (
self.getOption("use_curve_encryption").lower().strip()
)
if len(addrs) > 1 and self.use_curve_encryption == "auto":
self.use_curve_encryption = 1
elif len(addrs) == 1 and self.use_curve_encryption == "auto":
self.use_curve_encryption = 0
elif self.use_curve_encryption in ["0", "1"]:
self.use_curve_encryption = int(self.use_curve_encryption)
elif self.use_curve_encryption in ["true", "false"]:
self.use_curve_encryption = 1 if self.use_curve_encryption == "true" else 0
else:
raise ConfigurationError(
f"invalid UseCurveEncryption value: {self.use_curve_encryption}"
)

# Store public and secret keys in in spool/zeromq/curve by default.
curve_dir = self.getOption("curve_dir")
self.server_publickey = Path(curve_dir) / "server_publickey"
self.server_secretkey = Path(curve_dir) / "server_secretkey"
self.client_publickey = Path(curve_dir) / "client_publickey"
self.client_secretkey = Path(curve_dir) / "client_secretkey"

# If encryption is enabled, create the spool directory for
# the server and client keypairs and generate them if needed.
if self.use_curve_encryption:
os.makedirs(curve_dir, exist_ok=True)
os.chmod(curve_dir, 0o700)

if not self.server_publickey.exists() or not self.server_secretkey.exists():
self.message("Generating ZeroMQ CURVE server keypair...")
self.generate_keypair(self.server_publickey, self.server_secretkey)

if not self.client_publickey.exists() or not self.client_secretkey.exists():
self.message("Generating ZeroMQ CURVE client keypair...")
self.generate_keypair(self.client_publickey, self.client_secretkey)

if (
not self.use_curve_encryption
and len(addrs) > 1
and not self.getOption("disable_unencrypted_warning")
):
self.message(
f'Warning: ZeroMQ cluster backend enabled and multi-node cluster detected (IPs {", ".join(addrs)}).'
f'Warning: ZeroMQ encryption disabled, but multi-node cluster detected (IPs {", ".join(addrs)}).'
)
self.message(
"Communication between Zeek nodes using ZeroMQ is currently unencrypted. Use Broker with TLS if this"
"\nYou may disable this warning by setting the following option in zeekctl.cfg:"
)
self.message(
"is concerning to you. ZeroMQ encryption is tracked at https://github.com/zeek/zeek/issues/4432"
"\n cluster_backend_zeromq.disable_unencrypted_warning = 1\n"
)
self.message(
"\nYou may disable this warning by setting the following option in zeekctl.cfg:"
"\nYou may enable encryption by setting the following option.cfg:"
)
self.message(
"\n cluster_backend_zeromq.disable_unencrypted_warning = 1\n"
"\n cluster_backend_zeromq.use_curve_encryption = 1 (or auto)\n"
)

# If any of the addresses used by nodes looks like an IPv6 address,
Expand Down Expand Up @@ -103,6 +198,42 @@
]
)

# If CURVE encryption is enabled, redef the server and client
# keys into the zeekctl-config file.
if self.use_curve_encryption:

def render_redef(path: Path) -> str:
"""
Small helper to render a redef line given the path to
the keypair files stored in curve_dir.
"""
what = path.parts[-1]
value = path.read_text().strip()
if len(value) != 40:
raise ConfigurationError(f"CURVE key {what} not 40 bytes long")

return "\n".join(
[
f"@if ( |Cluster::Backend::ZeroMQ::curve_{what}| == 0 )",
f'redef Cluster::Backend::ZeroMQ::curve_{what} = "{value}";',
"@endif",
]
)

script += "\n".join(
[
"",
"# Public and secret server keys for ZeroMQ CURVE encryption.",
render_redef(self.server_publickey),
render_redef(self.server_secretkey),
"",
"# Public and secret client keys for ZeroMQ CURVE encryption.",
render_redef(self.client_publickey),
render_redef(self.client_secretkey),
"",
]
)

# Usually this runs automatically on the manager, but Zeectl supports
# standalone mode and the node doesn't know it should run the proxy
# thread for WebSocket functionality.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
redef Cluster::Backend::ZeroMQ::curve_client_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
redef Cluster::Backend::ZeroMQ::curve_client_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
redef Cluster::Backend::ZeroMQ::curve_server_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
redef Cluster::Backend::ZeroMQ::curve_server_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
@endif

redef Cluster::Backend::ZeroMQ::listen_xpub_endpoint = "tcp://127.0.0.1:27760";
redef Cluster::Backend::ZeroMQ::listen_xsub_endpoint = "tcp://127.0.0.1:27761";
redef Cluster::Backend::ZeroMQ::connect_xpub_endpoint = "tcp://127.0.0.1:27761";
redef Cluster::Backend::ZeroMQ::connect_xsub_endpoint = "tcp://127.0.0.1:27760";

redef Cluster::Backend::ZeroMQ::ipv6 = F;

# Public and secret server keys for ZeroMQ CURVE encryption.
@if ( |Cluster::Backend::ZeroMQ::curve_server_publickey| == 0 )
redef Cluster::Backend::ZeroMQ::curve_server_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@endif
@if ( |Cluster::Backend::ZeroMQ::curve_server_secretkey| == 0 )
redef Cluster::Backend::ZeroMQ::curve_server_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@endif

# Public and secret client keys for ZeroMQ CURVE encryption.
@if ( |Cluster::Backend::ZeroMQ::curve_client_publickey| == 0 )
redef Cluster::Backend::ZeroMQ::curve_client_publickey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@endif
@if ( |Cluster::Backend::ZeroMQ::curve_client_secretkey| == 0 )
redef Cluster::Backend::ZeroMQ::curve_client_secretkey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@endif

26 changes: 26 additions & 0 deletions testing/command/install-cluster-backend-zeromq-encryption.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# @TEST-DOC: Test enabling cluster_backend_zeromq.use_curve_encryption and observe the generated keys and redefs in the configuration file.
#
# @TEST-EXEC: PATH=$(pwd)/bin:$PATH bash %INPUT
#
# @TEST-EXEC: TEST_DIFF_CANONIFIER="sed -E -e 's/\".{40}\";/\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"/g'" btest-diff found-keys
# @TEST-EXEC: TEST_DIFF_CANONIFIER="sed -E -e 's/\".{40}\";/\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"/g'" btest-diff zeekctl-config-redefs

. zeekctl-test-setup

config=$ZEEKCTL_INSTALL_PREFIX/spool/installed-scripts-do-not-touch/auto/zeekctl-config.zeek

installfile etc/node.cfg__cluster
# Enable CURVE encryption
echo "cluster_backend_zeromq.use_curve_encryption = 1" >> $ZEEKCTL_INSTALL_PREFIX/etc/zeekctl.cfg
zeekctl install

# Check if the keys generated into spool/zeromq/curve appear in the
# generated configuration file.
for f in $ZEEKCTL_INSTALL_PREFIX/spool/zeromq/curve/* ; do
if ! grep -F -f $f $config >> found-keys; then
echo "Could not find key $f in $config" >&2
exit 1
fi
done

grep -C2 'redef.*Cluster::Backend' $config > zeekctl-config-redefs
Loading