From db11e11abefdb209dc242111c211374930e6c05e Mon Sep 17 00:00:00 2001 From: Jim Jagielski Date: Fri, 23 Oct 2026 18:57:56 +0000 Subject: [PATCH] mod_proxy_beacon: accept a random hex: key for ProxyBeaconSecret git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1939364 13f79535-47bb-0310-9956-ffa450edef68 --- docs/manual/mod/mod_proxy_beacon.xml | 21 ++++++++++------- modules/proxy/README.beacon | 7 +++--- modules/proxy/mod_proxy_beacon-guide.md | 6 ++--- modules/proxy/mod_proxy_beacon.c | 30 +++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/docs/manual/mod/mod_proxy_beacon.xml b/docs/manual/mod/mod_proxy_beacon.xml index c661a2cad8..185f78d2b2 100644 --- a/docs/manual/mod/mod_proxy_beacon.xml +++ b/docs/manual/mod/mod_proxy_beacon.xml @@ -107,7 +107,7 @@ to the reverse proxy over unicast UDP datagrams # Receive backend announcements on the cluster network interface (UDP). ProxyBeaconListen 0.0.0.0:5555 -ProxyBeaconSecret "a-long-random-shared-cluster-secret" +ProxyBeaconSecret "hex:00112233445566778899aabbccddeeff" ProxyBeaconBalancer cluster # A backend is dropped from rotation if it does not announce for 30 seconds. @@ -126,7 +126,7 @@ ProxyPassReverse "/" "balancer://cluster/" # Announce this backend's routable origin to the proxy every 10 seconds (UDP). ProxyBeaconAddress proxy.example.com:5555 ProxyBeaconAdvertise http://10.0.0.5:8080 -ProxyBeaconSecret "a-long-random-shared-cluster-secret" +ProxyBeaconSecret "hex:00112233445566778899aabbccddeeff" ProxyBeaconInterval 10 @@ -446,7 +446,7 @@ is taken out of rotation ProxyBeaconSecret Pre-shared secret used to authenticate announcements -ProxyBeaconSecret secret +ProxyBeaconSecret secret|hex:key server configvirtual host @@ -474,11 +474,16 @@ is taken out of rotation silently rejected (and logged), which appears as the backend never joining the balancer.

-

The secret must be at least 16 characters; shorter values are rejected - when the configuration is read. The MAC key is derived from it with a fast - hash and the authentication tag is 64 bits, so anyone who captures a single - announcement can test passphrase guesses offline at full speed: use a long - random value, not a memorable password.

+

Use a random key. Generate one with openssl rand -hex 16 + and configure it as hex: followed by the 32 hex digits; that + value is used directly as the 128-bit MAC key, so there is nothing to guess. + Any other value is treated as a passphrase: it must be at least 16 + characters, and the MAC key is derived from it with a fast hash. Because the + authentication tag is 64 bits, anyone who captures a single announcement can + test passphrase guesses offline at full speed, so a passphrase protects only + as well as it is random and long. A value that begins with hex: + is always read as a key and is rejected if it is not exactly 32 hex + digits.

The secret is a membership credential

A valid MAC proves only that the sender holds the secret. It does not diff --git a/modules/proxy/README.beacon b/modules/proxy/README.beacon index e6a2fe3db6..a3d57b69d6 100644 --- a/modules/proxy/README.beacon +++ b/modules/proxy/README.beacon @@ -208,9 +208,10 @@ Authentication (ProxyBeaconSecret, REQUIRED): genuine sub-second announcements always advance. seq= is NOT used for replay (it resets when a backend restarts); the wall-clock ts= does not. - The secret must be at least 16 characters. The key is MD5(passphrase) and the - tag is 64 bits: a captured announcement permits offline guessing at full speed, - so the secret should be long and random. A valid MAC shows possession of the + "hex:<32 hex digits>" is the 128-bit SipHash key itself (openssl rand -hex 16): + no passphrase, nothing to guess. Any other value is a passphrase of at least 16 + characters; the key is then MD5(passphrase) and the tag is 64 bits, so a + captured announcement permits offline guessing at full speed. A valid MAC shows possession of the secret, nothing more: every holder can register any reachable origin, speak for other members and consume slots/records. The secret is a membership- administrator credential, not a boundary between partially trusted workloads. diff --git a/modules/proxy/mod_proxy_beacon-guide.md b/modules/proxy/mod_proxy_beacon-guide.md index 458ffd2c6f..1c04b97302 100644 --- a/modules/proxy/mod_proxy_beacon-guide.md +++ b/modules/proxy/mod_proxy_beacon-guide.md @@ -80,7 +80,7 @@ httpd -l | grep mod_proxy_beacon # static build ```apache # Receive backend announcements on the cluster-facing interface (UDP/5555). ProxyBeaconListen 0.0.0.0:5555 -ProxyBeaconSecret "a-long-random-shared-cluster-secret" +ProxyBeaconSecret "hex:00112233445566778899aabbccddeeff" ProxyBeaconBalancer cluster # Drop a backend from rotation if it goes silent for 30s; re-add on next beacon. @@ -101,7 +101,7 @@ ProxyPassReverse "/" "balancer://cluster/" # Announce this backend's routable origin to the proxy every 10s (UDP). ProxyBeaconAddress proxy.example.com:5555 ProxyBeaconAdvertise http://10.0.0.5:8080 -ProxyBeaconSecret "a-long-random-shared-cluster-secret" +ProxyBeaconSecret "hex:00112233445566778899aabbccddeeff" ProxyBeaconInterval 10 ``` @@ -125,7 +125,7 @@ as an enabled member of `balancer://cluster` on the proxy. Stop one, and after | `ProxyBeaconListen [addr][:port]` | — | **Marks this server as the receiver.** Binds a UDP socket. `addr`/`port` are optional and inherited from the server's own `Listen`/`ServerName` when omitted (see note below). | | `ProxyBeaconBalancer name` | — | Balancer that announced backends are added to. Bare name (`cluster`); a leading `balancer://` is stripped. Must already exist with spare `growth`. | | `ProxyBeaconTimeout interval` | `0` (no eviction) | Seconds of silence before a member is disabled. `0` = add-only, never auto-remove. Set to a small multiple of the backends' interval to get self-healing. | -| `ProxyBeaconSecret secret` | — (**required**) | Shared cluster secret; **same value on proxy and all backends**, at least 16 characters (long and random). The server fails to start if a participating server omits it or it is too short. | +| `ProxyBeaconSecret secret` | — (**required**) | Shared cluster secret; **same value on proxy and all backends**, either `hex:` + 32 hex digits (a random key: `openssl rand -hex 16`) or a passphrase of at least 16 characters (weaker: guessable offline from one captured packet). The server fails to start if a participating server omits it or it is invalid. | | `ProxyBeaconMaxSkew interval` | `30` | Anti-replay freshness window: reject announcements whose signed timestamp is more than this far from now (either direction). Sub-second values (`500ms`) are honoured. | **Inheriting the listen address:** because UDP and TCP are separate port spaces, diff --git a/modules/proxy/mod_proxy_beacon.c b/modules/proxy/mod_proxy_beacon.c index 0ee0785839..f4a986aa13 100644 --- a/modules/proxy/mod_proxy_beacon.c +++ b/modules/proxy/mod_proxy_beacon.c @@ -168,6 +168,8 @@ typedef struct { #define BEACON_MAC_HEXLEN (APR_SIPHASH_DSIZE * 2) /* Shortest ProxyBeaconSecret accepted. */ #define BEACON_MIN_SECRET_LEN 16 +/* A secret of this form is used directly as the MAC key (no passphrase). */ +#define BEACON_HEXKEY_PREFIX "hex:" #define BEACON_MAXSKEW_DEFAULT apr_time_from_sec(30) /* Max announcement datagram we accept. Our messages are a few hundred bytes; @@ -409,6 +411,11 @@ static const char *beacon_set_timeout(cmd_parms *cmd, void *dummy, return NULL; } +static int beacon_hexval(char c) +{ + return apr_isdigit(c) ? c - '0' : apr_tolower(c) - 'a' + 10; +} + static const char *beacon_set_secret(cmd_parms *cmd, void *dummy, const char *arg) { beacon_ctx_t *ctx = ap_get_module_config(cmd->server->module_config, @@ -420,6 +427,29 @@ static const char *beacon_set_secret(cmd_parms *cmd, void *dummy, const char *ar if (!*arg) { return "ProxyBeaconSecret: empty secret"; } + /* hex:<32 hex digits> is the key itself: a random 128-bit key has no + * passphrase to guess, so it needs no key derivation. */ + if (!strncmp(arg, BEACON_HEXKEY_PREFIX, sizeof(BEACON_HEXKEY_PREFIX) - 1)) { + const char *h = arg + sizeof(BEACON_HEXKEY_PREFIX) - 1; + int i; + + if (strlen(h) != 2 * sizeof(ctx->mac_key)) { + return apr_psprintf(cmd->pool, + "ProxyBeaconSecret: hex: needs exactly %d hex " + "digits (e.g. from 'openssl rand -hex %d')", + (int)(2 * sizeof(ctx->mac_key)), + (int)sizeof(ctx->mac_key)); + } + for (i = 0; i < (int)sizeof(ctx->mac_key); i++) { + if (!apr_isxdigit(h[2 * i]) || !apr_isxdigit(h[2 * i + 1])) { + return "ProxyBeaconSecret: hex: value has a non-hex digit"; + } + ctx->mac_key[i] = (unsigned char) + ((beacon_hexval(h[2 * i]) << 4) | beacon_hexval(h[2 * i + 1])); + } + ctx->has_secret = 1; + return NULL; + } /* The MAC key is derived from the passphrase with a fast hash, so anyone who * captures one announcement can test guesses offline at full speed; only a * long random secret resists that. */