From 343d65b048197b5326a7261ad7d7dc70b155af8f Mon Sep 17 00:00:00 2001 From: Homer Quan Date: Mon, 18 May 2026 14:28:32 -0400 Subject: [PATCH] Require auth for Redis HA helper --- README.md | 6 +- scripts/redis_ha.sh | 47 +++++++++-- scripts/start_cluster_node.sh | 3 + tests/unit/redis_ha_scripts_test.exs | 114 +++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2569174..aa4c51b 100644 --- a/README.md +++ b/README.md @@ -175,13 +175,13 @@ Runtime configuration is read from environment variables in `config/runtime.exs` | `MN_REDIS_NAMESPACE` | `mirror_neuron` | Prefix/namespace for persisted runtime data. | | `MN_REDIS_DB` | `0` | Redis database number. | | `MN_REDIS_USERNAME` | Empty | Redis username. | -| `MN_REDIS_PASSWORD` | Empty | Redis password. | +| `MN_REDIS_PASSWORD` | Empty | Redis password. Required by `scripts/redis_ha.sh join` because HA autoconfiguration exposes Redis to cluster peers. | | `MN_REDIS_HA_MODE` | `single` | Redis mode, currently `single` or Sentinel-related configuration. | | `MN_REDIS_SENTINELS` | Empty | Comma-separated Sentinel endpoints. | | `MN_REDIS_SENTINEL_MASTER` | `mirror-neuron` | Sentinel master name. | | `MN_REDIS_SENTINEL_HOST_MAP` | Empty | Optional host mapping used when resolving Sentinel primary hosts. | | `MN_REDIS_SENTINEL_USERNAME` | Empty | Sentinel username. | -| `MN_REDIS_SENTINEL_PASSWORD` | Empty | Sentinel password. | +| `MN_REDIS_SENTINEL_PASSWORD` | Empty | Sentinel password. The HA helper defaults it to `MN_REDIS_PASSWORD` when unset. | | `MN_REDIS_WAIT_REPLICAS` | `0` | Redis write durability wait replica count. | | `MN_REDIS_WAIT_TIMEOUT_MS` | `100` | Redis wait timeout in milliseconds. | | `MN_REDIS_RECONNECT_ATTEMPTS` | `10` | Redis reconnect attempt count. | @@ -281,7 +281,7 @@ MirrorNeuron.cancel("job-id") ### Cluster Helpers -Development and smoke-test scripts are available under `scripts/`. +Development and smoke-test scripts are available under `scripts/`. Redis HA autoconfiguration requires `MN_REDIS_PASSWORD` so Redis and Sentinel do not start as unauthenticated network listeners. ```bash bash scripts/cluster_cli.sh --help diff --git a/scripts/redis_ha.sh b/scripts/redis_ha.sh index 40dc44a..9a0707a 100755 --- a/scripts/redis_ha.sh +++ b/scripts/redis_ha.sh @@ -22,7 +22,7 @@ REDIS_CLI="${MN_REDIS_CLI_BIN:-redis-cli}" REDIS_USERNAME="${MN_REDIS_USERNAME:-}" REDIS_PASSWORD="${MN_REDIS_PASSWORD:-}" SENTINEL_USERNAME="${MN_REDIS_SENTINEL_USERNAME:-}" -SENTINEL_PASSWORD="${MN_REDIS_SENTINEL_PASSWORD:-}" +SENTINEL_PASSWORD="${MN_REDIS_SENTINEL_PASSWORD:-$REDIS_PASSWORD}" PURGE_LOCAL="0" usage() { @@ -43,8 +43,11 @@ options: Environment: MN_REDIS_USERNAME / MN_REDIS_PASSWORD - MN_REDIS_SENTINEL_USERNAME / MN_REDIS_SENTINEL_PASSWORD + MN_REDIS_SENTINEL_USERNAME / MN_REDIS_SENTINEL_PASSWORD (defaults to MN_REDIS_PASSWORD) MN_REDIS_SERVER_BIN / MN_REDIS_CLI_BIN + +Security: + join requires MN_REDIS_PASSWORD so Redis and Sentinel are not exposed without authentication. EOF } @@ -118,6 +121,32 @@ need_bins() { fi } +redis_conf_quote() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf '"%s"' "$value" +} + +require_join_credentials() { + if [ -z "$REDIS_PASSWORD" ]; then + cat >&2 <&2 + exit 1 + fi +} + redis_auth_args() { if [ -n "$REDIS_USERNAME" ]; then printf '%s\0%s\0' --user "$REDIS_USERNAME" @@ -188,9 +217,7 @@ ensure_local_redis() { --appendonly yes ) - if [ -n "$REDIS_PASSWORD" ]; then - args+=(--requirepass "$REDIS_PASSWORD" --masterauth "$REDIS_PASSWORD") - fi + args+=(--requirepass "$REDIS_PASSWORD" --masterauth "$REDIS_PASSWORD") "$REDIS_SERVER" "${args[@]}" } @@ -203,6 +230,9 @@ ensure_local_sentinel() { mkdir -p "$DATA_DIR/sentinel" local conf="$DATA_DIR/sentinel/sentinel.conf" + local sentinel_password_conf + sentinel_password_conf="$(redis_conf_quote "$SENTINEL_PASSWORD")" + cat >"$conf" <> #{Path.join(tmp_dir, "redis-server.log")} + exit 0 + """) + + File.chmod!(path, 0o755) + path + end + + defp fake_redis_cli(tmp_dir) do + File.mkdir_p!(tmp_dir) + + path = Path.join(tmp_dir, "fake_redis_cli.sh") + + File.write!(path, """ + #!/usr/bin/env bash + args=("$@") + command=() + i=0 + while [ "$i" -lt "${#args[@]}" ]; do + arg="${args[$i]}" + case "$arg" in + -h|-p|-a|--user) + i=$((i + 2)) + ;; + --no-auth-warning) + i=$((i + 1)) + ;; + *) + command=("${args[@]:$i}") + break + ;; + esac + done + + printf '%s\n' "${command[*]}" >> #{Path.join(tmp_dir, "redis-cli.log")} + + case "${command[*]}" in + "PING") + exit 1 + ;; + "INFO replication") + printf 'role:master\n' + ;; + "SENTINEL get-master-addr-by-name mirror-neuron") + ;; + esac + + exit 0 + """) + + File.chmod!(path, 0o755) + path + end end