Skip to content

Instantly share code, notes, and snippets.

@Kenya-West
Last active September 9, 2026 14:00
Show Gist options
  • Select an option

  • Save Kenya-West/7f87b991028d904c59cc1332963c033a to your computer and use it in GitHub Desktop.

Select an option

Save Kenya-West/7f87b991028d904c59cc1332963c033a to your computer and use it in GitHub Desktop.
Wrapper script around Remnawave Node config retrieval command: `docker exec -i remna cli --dump-config`

remnawave-node-config-get

Extract and clean the XRay JSON config from a running Remnawave node Docker container.

Quick launch without saving a file

curl -fsSL 'https://gist.githubusercontent.com/Kenya-West/7f87b991028d904c59cc1332963c033a/raw/remnawave-node-config-get.sh' | bash -s -- -q -r

or, if you prefer wget:

wget -qO- https://gist.githubusercontent.com/Kenya-West/7f87b991028d904c59cc1332963c033a/raw/remnawave-node-config-get.sh | bash -s -- -q -r

This will extract XRay's config from the Remnawave container, redact secrets, and print the cleaned JSON to stdout without any info banner. Suitable for piping into less, jq, or other tools.

What it does

Remnawave node runs XRay inside a Docker container. The running config can be dumped with cli --dump-config, but the raw output contains ANSI escape codes, carriage returns, and Remnawave's own service entries (API inbound, stats, policy) that clutter the actual proxy configuration.

This script:

  1. Extracts the XRay config from the container via docker exec
  2. Cleans the raw output (strips ANSI codes, extracts the JSON object)
  3. Strips Remnawave internals (optional, on by default) — removes REMNAWAVE_API_INBOUND routing rule and inbound, plus api, stats, and policy top-level objects
  4. Summarizes serverNames (optional, on by default) — replaces SNI arrays with a count like ["<2 entries hidden>"] to reduce noise
  5. Redacts secrets (optional, off by default) — replaces private keys, passwords, certificates, client UUIDs, and emails with <REDACTED>
  6. Pretty-prints the result with jq

Requirements

  • docker — with access to the Remnawave container
  • jq >= 1.6
  • perl

Usage

./remnawave-node-config-get.sh [OPTIONS]

Options

Flag Description Default
-c, --container NAME Container name Auto-detect: remnanode then remna
-o, --output FILE Write JSON to file stdout
-R, --keep-remnawave Keep Remnawave service entities Strip them
-S, --keep-servernames Keep serverNames arrays as-is Summarize with count
-r, --redact Replace sensitive data with <REDACTED> Off
-q, --quiet Suppress info banner, output only JSON Show banner
-h, --help Show help
-V, --version Show version

Examples

# Auto-detect container, print cleaned config to stdout
./remnawave-node-config-get.sh

# Save to file with secrets redacted
./remnawave-node-config-get.sh -r -o config.json

# Keep everything, just extract and pretty-print
./remnawave-node-config-get.sh -R -S

# Pipe-friendly: quiet mode + redact
./remnawave-node-config-get.sh -q -r | less

# Specific container
./remnawave-node-config-get.sh -c my-remnawave-node -o config.json

Info banner

By default the script prints an info banner to stderr showing the active options:

remnawave-node-config-get v1.0.0
──────────────────────────────────────────────
  Container:          remnanode (auto-detected)
  Strip Remnawave:    yes   -R to keep
  Summarize SNI:      yes   -S to keep
  Redact secrets:     no    -r to enable
  Output:             stdout
──────────────────────────────────────────────

The banner goes to stderr, so it never pollutes the JSON output. Use -q to suppress it entirely.

What gets redacted (-r)

Secrets — replaced with <REDACTED>:

Field Scope Example
privateKey Reality settings "<REDACTED>"
password Reality settings "<REDACTED>"
key TLS certificate private keys ["<REDACTED>"]
certificate TLS certificates ["<REDACTED>"]
shortIds Reality short IDs ["<REDACTED>"]
id Client/user UUIDs "<REDACTED>"
email Client identifiers "<REDACTED>"

Hostnames & IPs — masked in address, serverName, host, target, and serverNames entries:

Input Output Rule
node-eu.real-domain.com node-eu.example.tld Domains: keep subdomains, replace last two levels
real-domain.com example.tld Two-part domains: fully replaced
203.0.113.42 203.0.XXX.XXX IPs: mask last two octets
10.0.0.1:8443 10.0.XXX.XXX:8443 Port is preserved
0.0.0.0, 127.0.0.1 unchanged Bind/loopback addresses are kept
caddy:443 unchanged Non-dotted hostnames (e.g. Docker services) are kept

What gets stripped (-R to keep)

  • Routing rule where inboundTag contains REMNAWAVE_API_INBOUND
  • Inbound with tag === REMNAWAVE_API_INBOUND
  • Top-level objects: api, stats, policy

Exit codes

Code Meaning
0 Success
1 Invalid arguments or missing dependencies
2 Container not found or not running
3 Config extraction or JSON parsing failed
4 jq processing failed
5 File write failed

License

MIT

#!/usr/bin/env bash
# remnawave-node-config-get — Extract and clean XRay config from a Remnawave node container
# https://gist.github.com/Kenya-West/7f87b991028d904c59cc1332963c033a…
set -euo pipefail
VERSION="1.0.0"
# ── Colors (stderr only, disabled when not a terminal) ───────────────────────
if [[ -t 2 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
else
RED='' GREEN='' CYAN='' BOLD='' DIM='' NC=''
fi
# ── Defaults ─────────────────────────────────────────────────────────────────
CONTAINER=""
OUTPUT=""
STRIP_REMNAWAVE=true
SUMMARIZE_SNI=true
SUMMARIZE_CLIENTS=true
REDACT=false
QUIET=false
# ── Functions ────────────────────────────────────────────────────────────────
err() { printf "${RED}error:${NC} %s\n" "$*" >&2; }
info() { printf "${CYAN}%s${NC}\n" "$*" >&2; }
usage() {
cat >&2 <<EOF
${BOLD}remnawave-node-config-get${NC} v${VERSION}
Extract and clean XRay config from a Remnawave node Docker container.
${BOLD}USAGE${NC}
remnawave-node-config-get.sh [OPTIONS]
${BOLD}OPTIONS${NC}
-c, --container NAME Container name (default: auto-detect remnanode → remna)
-o, --output FILE Write JSON to FILE instead of stdout
-R, --keep-remnawave Keep Remnawave service entities (api, stats, policy,
REMNAWAVE_API_INBOUND inbound & routing rule)
-S, --keep-servernames Keep serverNames arrays as-is (default: summarize)
-C, --keep-clients Keep clients arrays as-is (default: summarize)
-r, --redact Replace sensitive values with <REDACTED>
-q, --quiet Output only the JSON, suppress info banner
-h, --help Show this help
-V, --version Show version
${BOLD}EXAMPLES${NC}
# Auto-detect container, print cleaned config to stdout
./remnawave-node-config-get.sh
# Save to file, redact secrets
./remnawave-node-config-get.sh -r -o config.json
# Use specific container, keep everything, quiet mode
./remnawave-node-config-get.sh -c mynode -R -S -C -q
# Pipe-friendly: quiet + redact, pipe to another tool
./remnawave-node-config-get.sh -q -r | less
EOF
}
# ── Argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--container)
[[ -z "${2:-}" ]] && { err "Option $1 requires a value"; exit 1; }
CONTAINER="$2"; shift 2 ;;
-o|--output)
[[ -z "${2:-}" ]] && { err "Option $1 requires a value"; exit 1; }
OUTPUT="$2"; shift 2 ;;
-R|--keep-remnawave) STRIP_REMNAWAVE=false; shift ;;
-S|--keep-servernames) SUMMARIZE_SNI=false; shift ;;
-C|--keep-clients) SUMMARIZE_CLIENTS=false; shift ;;
-r|--redact) REDACT=true; shift ;;
-q|--quiet) QUIET=true; shift ;;
-h|--help) usage; exit 0 ;;
-V|--version) echo "$VERSION"; exit 0 ;;
-*) err "Unknown option: $1"; usage; exit 1 ;;
*) err "Unexpected argument: $1"; usage; exit 1 ;;
esac
done
# ── Dependency check ─────────────────────────────────────────────────────────
missing=()
for cmd in docker jq perl; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [[ ${#missing[@]} -gt 0 ]]; then
err "Missing required tools: ${missing[*]}"
exit 1
fi
# ── Container detection ──────────────────────────────────────────────────────
CONTAINER_SOURCE="specified"
if [[ -z "$CONTAINER" ]]; then
CONTAINER_SOURCE="auto-detected"
if docker inspect --format='{{.State.Running}}' remnanode 2>/dev/null | grep -q true; then
CONTAINER="remnanode"
elif docker inspect --format='{{.State.Running}}' remna 2>/dev/null | grep -q true; then
CONTAINER="remna"
else
err "No running Remnawave container found (tried 'remnanode', 'remna')"
err "Specify one with: -c <container-name>"
exit 2
fi
else
if ! docker inspect --format='{{.State.Running}}' "$CONTAINER" 2>/dev/null | grep -q true; then
err "Container '$CONTAINER' is not running or does not exist"
exit 2
fi
fi
# ── Banner ───────────────────────────────────────────────────────────────────
print_banner() {
local yn_strip yn_sni yn_clients yn_redact out_target
yn_strip=$( [[ "$STRIP_REMNAWAVE" == true ]] && echo "yes" || echo "no")
yn_sni=$( [[ "$SUMMARIZE_SNI" == true ]] && echo "yes" || echo "no")
yn_clients=$( [[ "$SUMMARIZE_CLIENTS" == true ]] && echo "yes" || echo "no")
yn_redact=$([[ "$REDACT" == true ]] && echo "yes" || echo "no")
out_target=$([[ -n "$OUTPUT" ]] && echo "$OUTPUT" || echo "stdout")
printf >&2 "\n"
printf >&2 "${BOLD}remnawave-node-config-get${NC} v%s\n" "$VERSION"
printf >&2 "${DIM}──────────────────────────────────────────────${NC}\n"
printf >&2 " Container: ${BOLD}%s${NC} (%s)\n" "$CONTAINER" "$CONTAINER_SOURCE"
printf >&2 " Strip Remnawave: ${BOLD}%-3s${NC} ${DIM}-R to keep${NC}\n" "$yn_strip"
printf >&2 " Summarize SNI: ${BOLD}%-3s${NC} ${DIM}-S to keep${NC}\n" "$yn_sni"
printf >&2 " Summarize Clients: ${BOLD}%-3s${NC} ${DIM}-C to keep${NC}\n" "$yn_clients"
printf >&2 " Redact secrets: ${BOLD}%-3s${NC} ${DIM}-r to enable${NC}\n" "$yn_redact"
printf >&2 " Output: ${BOLD}%s${NC}${DIM}%s${NC}\n" "$out_target" "${OUTPUT:+ -o <file>}"
printf >&2 "${DIM}──────────────────────────────────────────────${NC}\n"
}
if [[ "$QUIET" != true ]]; then
print_banner
fi
# ── Extract raw config ───────────────────────────────────────────────────────
RAW_OUTPUT=$(docker exec -e NO_COLOR=1 "$CONTAINER" cli --dump-config 2>/dev/null </dev/null) || {
err "Failed to run 'cli --dump-config' in container '$CONTAINER'"
exit 3
}
JSON=$(printf '%s' "$RAW_OUTPUT" \
| perl -CSDA -pe 's/\r/\n/g; s/\e\[[0-9;?]*[ -\/]*[@-~]//g' \
| perl -0777 -ne 'if (/(\{.*\})/s) { print $1 }')
if [[ -z "$JSON" ]]; then
err "No JSON object found in command output"
exit 3
fi
if ! printf '%s' "$JSON" | jq empty 2>/dev/null; then
err "Extracted data is not valid JSON"
exit 3
fi
# ── Build jq filter ──────────────────────────────────────────────────────────
JQ_FILTER="."
if [[ "$STRIP_REMNAWAVE" == true ]]; then
JQ_FILTER+='
| (if .routing.rules then
.routing.rules |= [.[] | select(
(.inboundTag // []) | index("REMNAWAVE_API_INBOUND") | not
)]
else . end)
| (if .inbounds then
.inbounds |= [.[] | select(.tag != "REMNAWAVE_API_INBOUND")]
else . end)
| del(.api) | del(.stats) | del(.policy)'
fi
if [[ "$SUMMARIZE_SNI" == true ]]; then
JQ_FILTER+='
| walk(
if type == "object" and has("serverNames") and (.serverNames | type) == "array"
then .serverNames = ["<\(.serverNames | length) entries hidden>"]
else . end
)'
fi
if [[ "$SUMMARIZE_CLIENTS" == true ]]; then
JQ_FILTER+='
| (if .inbounds then
.inbounds |= [.[] |
if .settings.clients and (.settings.clients | type) == "array"
then .settings.clients = ["<\(.settings.clients | length) clients>"]
else . end
]
else . end)'
fi
if [[ "$REDACT" == true ]]; then
JQ_FILTER+='
| def redact_host:
if type != "string" then .
elif . == "" or . == "0.0.0.0" or . == "127.0.0.1" or . == "localhost" then .
elif test("^[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+(:[0-9]+)?$") then
split(":") as $p |
($p[0] | split(".") | .[0:2] + ["XXX","XXX"] | join("."))
+ (if ($p | length) > 1 then ":" + $p[1] else "" end)
elif test("[.]") then
split(":") as $p |
($p[0] | split(".")
| if length <= 2 then ["example","tld"]
else .[:-2] + ["example","tld"] end
| join("."))
+ (if ($p | length) > 1 then ":" + $p[1] else "" end)
else . end;
walk(
if type == "object" then
(if has("privateKey") and (.privateKey | type) == "string"
then .privateKey = "<REDACTED>" else . end)
| (if has("password") and (.password | type) == "string"
then .password = "<REDACTED>" else . end)
| (if has("key") and (.key | type) == "array"
then .key = ["<REDACTED>"] else . end)
| (if has("certificate") and (.certificate | type) == "array"
then .certificate = ["<REDACTED>"] else . end)
| (if has("shortIds") and (.shortIds | type) == "array"
then .shortIds = ["<REDACTED>"] else . end)
| (if has("id") and (.id | type) == "string" and (.id | test("^[0-9a-f]{8}-[0-9a-f]{4}-"))
then .id = "<REDACTED>" else . end)
| (if has("email") and (.email | type) == "string"
then .email = "<REDACTED>" else . end)
| (if has("address") and (.address | type) == "string"
then .address |= redact_host else . end)
| (if has("serverName") and (.serverName | type) == "string"
then .serverName |= redact_host else . end)
| (if has("host") and (.host | type) == "string"
then .host |= redact_host else . end)
| (if has("target") and (.target | type) == "string"
then .target |= redact_host else . end)
| (if has("serverNames") and (.serverNames | type) == "array"
then .serverNames |= map(if type == "string" then redact_host else . end)
else . end)
else . end
)'
fi
# ── Apply filter ─────────────────────────────────────────────────────────────
RESULT=$(printf '%s' "$JSON" | jq "$JQ_FILTER") || {
err "jq processing failed"
exit 4
}
# ── Output ───────────────────────────────────────────────────────────────────
if [[ -n "$OUTPUT" ]]; then
printf '%s\n' "$RESULT" > "$OUTPUT" || {
err "Failed to write to '$OUTPUT'"
exit 5
}
if [[ "$QUIET" != true ]]; then
printf >&2 "\n${GREEN}Config written to ${BOLD}%s${NC}\n" "$OUTPUT"
fi
else
printf '%s\n' "$RESULT"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment