Skip to content

Instantly share code, notes, and snippets.

@Kenya-West
Last active March 31, 2026 14:34
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.

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