Skip to content
Open

fix bug #3120

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
22 changes: 21 additions & 1 deletion gateway/configs/config-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@ shutdown_timeout = "15s"
gateway_id = '{{ env "APIP_GW_CONTROLLER_SERVER_GATEWAY_ID" "platform-gateway-id" }}'

[controller.admin_server]
# Dedicated admin/debug HTTP server for config dump and xDS sync endpoints
# Dedicated admin/debug HTTP server for config dump and xDS sync endpoints.
# Kept enabled by default because it also serves /health, used by Kubernetes
# liveness/readiness probes; see admin_server.config_dump below to gate the
# sensitive /config_dump route specifically.
enabled = true
port = 9092
allowed_ips = ["*"]

[controller.admin_server.config_dump]
# The /config_dump route returns a full snapshot of deployed APIs, policies,
# and resolved configuration. Off by default — enable only when needed for
# debugging, and prefer reaching it via `kubectl port-forward` over exposing
# the admin Service port.
enabled = false

[controller.admin_server.pprof]
# Go runtime profiling (net/http/pprof) served on the admin server, off by default.
# When profiling, also restrict admin_server.allowed_ips or reach it via port-forward.
Expand Down Expand Up @@ -312,10 +322,20 @@ max_decompressed_bytes = 10485760
extproc_port = 9001

[policy_engine.admin]
# Kept enabled by default because it also serves /health, used by Kubernetes
# liveness/readiness probes; see admin.config_dump below to gate the
# sensitive /config_dump route specifically.
enabled = true
port = 9002
allowed_ips = ["*", "127.0.0.1"]

[policy_engine.admin.config_dump]
# The /config_dump route returns the resolved policy chain and route
# configuration. Off by default — enable only when needed for debugging, and
# prefer reaching it via `kubectl port-forward` over exposing the admin
# Service port.
enabled = false

[policy_engine.admin.pprof]
# Go runtime profiling (net/http/pprof) served on the admin server, off by default.
# When profiling, also restrict admin.allowed_ips or reach it via port-forward.
Expand Down
10 changes: 10 additions & 0 deletions gateway/configs/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ path = '{{ env "APIP_GW_CONTROLLER_STORAGE_SQLITE_PATH" "./data/gateway.db" }}'
[policy_engine.logging]
level = "info"

[policy_engine.admin.config_dump]
# Enabled for local development convenience. Off by default in production
# (see config-template.toml) — /config_dump returns 404 when disabled.
enabled = true

[controller.logging]
level = '{{ env "APIP_GW_CONTROLLER_LOGGING_LEVEL" "info" }}'

Expand All @@ -37,6 +42,11 @@ apim_oauth2_client_secret = '{{ env "APIP_GW_CONTROLLER_CONTROLPLANE_APIM_OAUTH2
[controller.policies]
definitions_path = '{{ env "APIP_GW_CONTROLLER_POLICIES_DEFINITIONS_PATH" "./default-policies" }}'

[controller.admin_server.config_dump]
# Enabled for local development convenience. Off by default in production
# (see config-template.toml) — /config_dump returns 404 when disabled.
enabled = true

[controller.auth.basic]
enabled = true

Expand Down
3 changes: 3 additions & 0 deletions gateway/distribution/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ services:
format: raw
environment:
- GATEWAY_CONTROLLER_HOST=gateway-controller
# Envoy admin is disabled by default in the image; enabled here for local
# dev convenience since the port is already mapped to the host above.
- ROUTER_ADMIN_ENABLED=true
volumes:
- ./configs/config.toml:/etc/policy-engine/config.toml:ro
- ./configs/llm-pricing/model_prices.json:/etc/policy-engine/llm-pricing/model_prices.json:ro
Expand Down
3 changes: 3 additions & 0 deletions gateway/docker-compose-perf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ services:
environment:
- GATEWAY_CONTROLLER_HOST=gateway-controller
- LOG_LEVEL=info
# Envoy admin is disabled by default in the image; enabled here for local
# dev convenience since the port is already mapped to the host above.
- ROUTER_ADMIN_ENABLED=true
volumes:
- ./configs/config.toml:/etc/policy-engine/config.toml:ro
networks:
Expand Down
3 changes: 3 additions & 0 deletions gateway/docker-compose.debug.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ services:
environment:
- GATEWAY_CONTROLLER_HOST=gateway-controller
- LOG_LEVEL=info
# Envoy admin is disabled by default in the image; enabled here for local
# dev convenience since the port is already mapped to the host above.
- ROUTER_ADMIN_ENABLED=true
volumes:
- ./configs/config.toml:/etc/policy-engine/config.toml:ro
networks:
Expand Down
5 changes: 5 additions & 0 deletions gateway/gateway-controller/pkg/adminserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ func (s *Server) Stop(ctx context.Context) error {

// GetConfigDump implements adminapi.ServerInterface.
func (s *Server) GetConfigDump(w http.ResponseWriter, r *http.Request) {
if !s.cfg.ConfigDump.Enabled {
http.NotFound(w, r)
return
}

resp, err := s.apiServer.BuildConfigDumpResponse(s.logger)
if err != nil {
http.Error(w, "Failed to retrieve configuration dump", http.StatusInternalServerError)
Expand Down
40 changes: 36 additions & 4 deletions gateway/gateway-controller/pkg/adminserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ func TestAdminServer_ConfigDumpHandler(t *testing.T) {
stub := &stubAPIServer{
configDump: adminapi.ConfigDumpResponse{Status: &status},
}
s := NewServer(&config.AdminServerConfig{Port: 9092, AllowedIPs: []string{"*"}}, stub, nil, slog.Default())
s := NewServer(&config.AdminServerConfig{
Port: 9092,
AllowedIPs: []string{"*"},
ConfigDump: config.ConfigDumpConfig{Enabled: true},
}, stub, nil, slog.Default())

req := httptest.NewRequest(http.MethodGet, AdminAPIBasePath+"/config_dump", nil)
req.RemoteAddr = "127.0.0.1:12345"
Expand All @@ -100,6 +104,22 @@ func TestAdminServer_ConfigDumpHandler(t *testing.T) {
assert.Equal(t, "ok", *body.Status)
}

func TestAdminServer_ConfigDumpHandler_DisabledByDefault(t *testing.T) {
status := "ok"
stub := &stubAPIServer{
configDump: adminapi.ConfigDumpResponse{Status: &status},
}
// ConfigDump.Enabled left at its zero value (false) — matches the production default.
s := NewServer(&config.AdminServerConfig{Port: 9092, AllowedIPs: []string{"*"}}, stub, nil, slog.Default())

req := httptest.NewRequest(http.MethodGet, AdminAPIBasePath+"/config_dump", nil)
req.RemoteAddr = "127.0.0.1:12345"
rr := httptest.NewRecorder()

s.httpSrv.Handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusNotFound, rr.Code)
}

func TestAdminServer_XDSSyncStatusHandler(t *testing.T) {
component := "gateway-controller"
version := "12"
Expand Down Expand Up @@ -221,7 +241,11 @@ func TestAdminServer_LegacyConfigDump(t *testing.T) {
stub := &stubAPIServer{
configDump: adminapi.ConfigDumpResponse{Status: &status},
}
s := NewServer(&config.AdminServerConfig{Port: 9092, AllowedIPs: []string{"*"}}, stub, nil, slog.Default())
s := NewServer(&config.AdminServerConfig{
Port: 9092,
AllowedIPs: []string{"*"},
ConfigDump: config.ConfigDumpConfig{Enabled: true},
}, stub, nil, slog.Default())

req := httptest.NewRequest(http.MethodGet, "/config_dump", nil)
req.RemoteAddr = "127.0.0.1:12345"
Expand Down Expand Up @@ -307,7 +331,11 @@ func TestAdminServer_ConfigDump_WrongCredentials(t *testing.T) {
func TestAdminServer_ConfigDump_WithValidAuth(t *testing.T) {
status := "ok"
stub := &stubAPIServer{configDump: adminapi.ConfigDumpResponse{Status: &status}}
s := NewServer(&config.AdminServerConfig{Port: 9092, AllowedIPs: []string{"*"}}, stub, newBasicAuthMiddleware(t), slog.Default())
s := NewServer(&config.AdminServerConfig{
Port: 9092,
AllowedIPs: []string{"*"},
ConfigDump: config.ConfigDumpConfig{Enabled: true},
}, stub, newBasicAuthMiddleware(t), slog.Default())

req := httptest.NewRequest(http.MethodGet, AdminAPIBasePath+"/config_dump", nil)
req.SetBasicAuth(testAdminUser, testAdminPass)
Expand Down Expand Up @@ -384,7 +412,11 @@ func TestAdminServer_LegacyConfigDump_RequiresAuth(t *testing.T) {
func TestAdminServer_ConfigDump_AdminRoleAllowed(t *testing.T) {
status := "ok"
stub := &stubAPIServer{configDump: adminapi.ConfigDumpResponse{Status: &status}}
s := NewServer(&config.AdminServerConfig{Port: 9092, AllowedIPs: []string{"*"}}, stub,
s := NewServer(&config.AdminServerConfig{
Port: 9092,
AllowedIPs: []string{"*"},
ConfigDump: config.ConfigDumpConfig{Enabled: true},
}, stub,
newAdminProtectMiddleware(t, []string{"admin"}), slog.Default())

req := httptest.NewRequest(http.MethodGet, AdminAPIBasePath+"/config_dump", nil)
Expand Down
19 changes: 15 additions & 4 deletions gateway/gateway-controller/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,18 @@ type ServerConfig struct {

// AdminServerConfig holds controller admin HTTP server configuration.
type AdminServerConfig struct {
Enabled bool `koanf:"enabled"`
Port int `koanf:"port"`
AllowedIPs []string `koanf:"allowed_ips"`
Pprof PprofConfig `koanf:"pprof"`
Enabled bool `koanf:"enabled"`
Port int `koanf:"port"`
AllowedIPs []string `koanf:"allowed_ips"`
Pprof PprofConfig `koanf:"pprof"`
ConfigDump ConfigDumpConfig `koanf:"config_dump"`
}

// ConfigDumpConfig gates the /config_dump endpoint served on the admin HTTP
// server. Disabled by default — /health and other admin routes are unaffected
// by this flag; when disabled, /config_dump returns 404 rather than a payload.
type ConfigDumpConfig struct {
Enabled bool `koanf:"enabled"`
}

// PprofConfig gates the Go runtime profiling endpoints (net/http/pprof) served on
Expand Down Expand Up @@ -834,6 +842,9 @@ func defaultConfig() *Config {
BlockProfileRate: 0,
MutexProfileFraction: 0,
},
ConfigDump: ConfigDumpConfig{
Enabled: false,
},
},
PolicyServer: PolicyServerConfig{
Port: 18001,
Expand Down
33 changes: 30 additions & 3 deletions gateway/gateway-runtime/docker-entrypoint-debug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,19 @@ export ROUTER_CONCURRENCY="${ROUTER_CONCURRENCY:-0}"
export APIP_GW_POLICY_ENGINE_METRICS_ENABLED="${APIP_GW_POLICY_ENGINE_METRICS_ENABLED:-true}"
export APIP_GW_ROUTER_RE2_MAX_PROGRAM_SIZE="${APIP_GW_ROUTER_RE2_MAX_PROGRAM_SIZE:-400}"

# Router (Envoy) admin interface configuration (see docker-entrypoint.sh for details).
# Disabled by default — not defined in the static envoy-bootstrap.yaml at all. Set
# ROUTER_ADMIN_ENABLED=true to inject it at startup, bound to ROUTER_ADMIN_HOST (loopback
# by default).
export ROUTER_ADMIN_ENABLED="${ROUTER_ADMIN_ENABLED:-false}"
export ROUTER_ADMIN_HOST="${ROUTER_ADMIN_HOST:-127.0.0.1}"
export ROUTER_ADMIN_PORT="${ROUTER_ADMIN_PORT:-9901}"

# Graceful shutdown configuration (see docker-entrypoint.sh for details).
# On SIGTERM the Router (Envoy) is drained before processes are terminated so in-flight
# requests finish and keep-alive connections close cleanly instead of being reset.
# Requires ROUTER_ADMIN_ENABLED=true — skipped otherwise.
# Keep ROUTER_DRAIN_TIME_SECONDS < the pod terminationGracePeriodSeconds; 0 disables it.
export ROUTER_ADMIN_HOST="${ROUTER_ADMIN_HOST:-127.0.0.1}"
export ROUTER_ADMIN_PORT="${ROUTER_ADMIN_PORT:-9901}"
export ROUTER_DRAIN_TIME_SECONDS="${ROUTER_DRAIN_TIME_SECONDS:-15}"

# Derive Router (Envoy) xDS config — used by envsubst on config-override.yaml
Expand All @@ -121,6 +128,11 @@ log " GOMAXPROCS: ${GOMAXPROCS}"
log " Router Concurrency: ${ROUTER_CONCURRENCY}"
log " Router RE2 Max Program Size: ${APIP_GW_ROUTER_RE2_MAX_PROGRAM_SIZE}"
log " Policy Engine Metrics: ${APIP_GW_POLICY_ENGINE_METRICS_ENABLED}"
if [ "${ROUTER_ADMIN_ENABLED}" = "true" ]; then
log " Router Admin: enabled on ${ROUTER_ADMIN_HOST}:${ROUTER_ADMIN_PORT}"
else
log " Router Admin: disabled (set ROUTER_ADMIN_ENABLED=true to enable)"
fi
[[ ${#ROUTER_ARGS[@]} -gt 0 ]] && log " Router extra args: ${ROUTER_ARGS[*]}"
[[ ${#PE_ARGS[@]} -gt 0 ]] && log " Policy Engine extra args: ${PE_ARGS[*]}"

Expand All @@ -130,6 +142,18 @@ rm -f "${POLICY_ENGINE_SOCKET}"
# Generate Envoy config override by substituting environment variables
CONFIG_OVERRIDE=$(envsubst < /etc/envoy/config-override.yaml)

# The admin interface has no entry in the static bootstrap at all, so it only exists when
# explicitly opted into here — bound to ROUTER_ADMIN_HOST (loopback by default), never 0.0.0.0.
if [ "${ROUTER_ADMIN_ENABLED}" = "true" ]; then
CONFIG_OVERRIDE="${CONFIG_OVERRIDE}
admin:
address:
socket_address:
address: ${ROUTER_ADMIN_HOST}
port_value: ${ROUTER_ADMIN_PORT}
"
fi

# Track child PIDs
PE_PID=""
ENVOY_PID=""
Expand Down Expand Up @@ -167,7 +191,10 @@ shutdown() {

# Drain the Router first so in-flight requests finish and keep-alive connections are
# closed cleanly — prevents client-visible connection resets during rolling restarts.
if [ -n "$ENVOY_PID" ] && kill -0 "$ENVOY_PID" 2>/dev/null \
# Requires ROUTER_ADMIN_ENABLED=true; skipped otherwise since draining needs an admin call.
if [ "${ROUTER_ADMIN_ENABLED}" != "true" ]; then
log "Router admin disabled (ROUTER_ADMIN_ENABLED=false); skipping graceful drain"
elif [ -n "$ENVOY_PID" ] && kill -0 "$ENVOY_PID" 2>/dev/null \
&& [ "${ROUTER_DRAIN_TIME_SECONDS}" -gt 0 ] 2>/dev/null; then
log "Draining Router (Envoy); waiting up to ${ROUTER_DRAIN_TIME_SECONDS}s for in-flight requests..."
if drain_router; then
Expand Down
42 changes: 37 additions & 5 deletions gateway/gateway-runtime/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,26 @@ export PYTHON_POLICY_WORKERS="${PYTHON_POLICY_WORKERS:-4}"
export PYTHON_POLICY_MAX_CONCURRENT="${PYTHON_POLICY_MAX_CONCURRENT:-100}"
export PYTHON_POLICY_TIMEOUT="${PYTHON_POLICY_TIMEOUT:-30}"

# Router (Envoy) admin interface configuration
# The admin interface (config_dump, stats, /runtime_modify, etc.) is disabled by default —
# it is not defined in the static envoy-bootstrap.yaml at all. Set ROUTER_ADMIN_ENABLED=true
# to have it injected at startup via `envoy --config-yaml`, bound to ROUTER_ADMIN_HOST (loopback
# by default) so it is never reachable outside the pod's network namespace regardless of this
# flag. Enabling it also restores the graceful-drain-on-SIGTERM behavior below, since draining
# is triggered via an admin API call.
export ROUTER_ADMIN_ENABLED="${ROUTER_ADMIN_ENABLED:-false}"
export ROUTER_ADMIN_HOST="${ROUTER_ADMIN_HOST:-127.0.0.1}"
export ROUTER_ADMIN_PORT="${ROUTER_ADMIN_PORT:-9901}"

# Graceful shutdown configuration
# On SIGTERM the Router (Envoy) is drained before any process is terminated, so in-flight
# requests complete and keep-alive connections are closed cleanly (Connection: close)
# instead of being reset. This avoids connection-reset errors for clients during rolling
# restarts / pod evictions.
# ROUTER_ADMIN_HOST/PORT : Router (Envoy) admin endpoint used to trigger the drain
# restarts / pod evictions. Requires ROUTER_ADMIN_ENABLED=true — draining is skipped
# (with a clear log message, not a stalled attempt) when the admin interface is disabled.
# ROUTER_DRAIN_TIME_SECONDS : how long to wait for in-flight requests to finish before
# terminating. Keep this LESS than the pod's terminationGracePeriodSeconds (k8s
# default 30s) or the container is SIGKILLed mid-drain. Set to 0 to disable draining.
export ROUTER_ADMIN_HOST="${ROUTER_ADMIN_HOST:-127.0.0.1}"
export ROUTER_ADMIN_PORT="${ROUTER_ADMIN_PORT:-9901}"
export ROUTER_DRAIN_TIME_SECONDS="${ROUTER_DRAIN_TIME_SECONDS:-15}"

# Derive Router (Envoy) xDS config — used by envsubst on config-override.yaml
Expand All @@ -152,6 +161,11 @@ log " Policy Engine Socket: ${POLICY_ENGINE_SOCKET}"
log " GOMAXPROCS: ${GOMAXPROCS}"
log " Router Concurrency: ${ROUTER_CONCURRENCY}"
log " Router RE2 Max Program Size: ${APIP_GW_ROUTER_RE2_MAX_PROGRAM_SIZE}"
if [ "${ROUTER_ADMIN_ENABLED}" = "true" ]; then
log " Router Admin: enabled on ${ROUTER_ADMIN_HOST}:${ROUTER_ADMIN_PORT}"
else
log " Router Admin: disabled (set ROUTER_ADMIN_ENABLED=true to enable)"
fi
log " Policy Engine Metrics: ${APIP_GW_POLICY_ENGINE_METRICS_ENABLED}"
log " Python Workers: ${PYTHON_POLICY_WORKERS}"
log " Python Max Concurrent: ${PYTHON_POLICY_MAX_CONCURRENT}"
Expand All @@ -167,6 +181,19 @@ rm -f "${PYTHON_EXECUTOR_SOCKET}"
# Generate Envoy config override by substituting environment variables
CONFIG_OVERRIDE=$(envsubst < /etc/envoy/config-override.yaml)

# The admin interface has no entry in the static bootstrap (envoy-bootstrap.yaml) at all, so
# it only exists when explicitly opted into here — bound to ROUTER_ADMIN_HOST (loopback by
# default), never 0.0.0.0, so it is unreachable outside the pod's network namespace regardless.
if [ "${ROUTER_ADMIN_ENABLED}" = "true" ]; then
CONFIG_OVERRIDE="${CONFIG_OVERRIDE}
admin:
address:
socket_address:
address: ${ROUTER_ADMIN_HOST}
port_value: ${ROUTER_ADMIN_PORT}
Comment on lines +184 to +193

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^gateway/gateway-runtime/docker-entrypoint(.*\.sh)?$|^kubernetes/helm/gateway-helm-chart/README.md$|^gateway/it/docker-compose.*\.ya?ml$)' || true

echo
echo "== entrypoint relevant sections =="
for f in gateway/gateway-runtime/docker-entrypoint.sh gateway/gateway-runtime/docker-entrypoint-debug.sh; do
  if [ -f "$f" ]; then
    echo "--- $f defaults/usages ---"
    rg -n 'ROUTER_ADMIN_(ENABLED|HOST|PORT)|routerAdminEnabled|admin_server|admin:' "$f" -C 3 || true
    echo
  fi
done

echo
echo "== compose relevant admin exposure =="
for f in gateway/it/docker-compose.test.yaml gateway/it/docker-compose.test.vhosts-single.yaml; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n 'routerAdmin|9901|admin|host|ports:' "$f" -C 4 || true
  fi
done

echo
echo "== helm values/schema references =="
fd -i 'values.*\.ya?ml|schema\.json|README\.md' kubernetes/helm/gateway-helm-chart gateway -d 3 | xargs -r rg -n 'routerAdminEnabled|routerAdminHost|admin_server|9901|config_dump' -C 3 || true

echo
echo "== shell-like validation impact probe =="
python3 - <<'PY'
from pathlib import Path
for path in [
    Path("gateway/gateway-runtime/docker-entrypoint.sh"),
    Path("gateway/gateway-runtime/docker-entrypoint-debug.sh"),
]:
    text = path.read_text()
    snippet = text[text.find("if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ];"):text.find("fi", text.find("if [ \"${ROUTER_ADMIN_HOST}\"", text.find("if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ];")) if "if [ \"${ROUTER_ADMIN_HOST}\"" in text[text.find("if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ];"):] else -1)+1] if "if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ]; else " in text[text.find("if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ];"):] else text[text.find("if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ];"):text.find("\n\ncat", text.find("if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ];"))]]
    print(f"{path}:")
    print(text[text.find("if [ \"${ROUTER_ADMIN_ENABLED}\" = \"true\" ];"):].split("\n\n",1)[0][:800])
PY

Repository: wso2/api-platform

Length of output: 35046


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== admin host source/defaults =="
rg -n 'ROUTER_ADMIN_(ENABLED|HOST|PORT)|gateway\.gatewayRuntime\.deployment\.env\.routerAdmin|deployment\.env:' gateway kubernetes -C 4 || true

echo
echo "== compose explicit non-localhost exposure =="
python3 - <<'PY'
from pathlib import Path
for path in Path("gateway/it").glob("docker-compose*.yaml"):
    text = path.read_text()
    lines = text.splitlines()
    for i,l in enumerate(lines, 1):
        if "9901:9901" in l:
            print(path, i, l.strip())
    for i,l in enumerate(lines, 1):
        if "ROUTER_ADMIN_HOST" in l:
            print(path, i, l.strip())
PY

Repository: wso2/api-platform

Length of output: 31399


Security Misconfiguration (CWE-16)

Reachability: Internal

Reachability path
● Entry
  gateway/gateway-runtime/docker-entrypoint-debug.sh:220
  shutdown
│
▼
● Sink
  gateway/gateway-runtime/docker-entrypoint.sh

Enforce or accurately document the Router admin bind boundary.

ROUTER_ADMIN_HOST is documented as loopback-only but is accepted as-is when ROUTER_ADMIN_ENABLED=true; a non-loopback value makes the enabled Envoy admin listener reachable over any network path. Reject non-loopback values before writing admin.address in both gateway/gateway-runtime/docker-entrypoint.sh and gateway/gateway-runtime/docker-entrypoint-debug.sh; update gateway/gateway-runtime/docker-entrypoint-debug.sh’s loopback claim accordingly. Do not document Envoy admin in kubernetes/helm/gateway-helm-chart/README.md as loopback-only unless the entrypoints enforce that boundary.

📍 Affects 3 files
  • gateway/gateway-runtime/docker-entrypoint.sh#L184-L193 (this comment)
  • gateway/gateway-runtime/docker-entrypoint-debug.sh#L145-L153
  • kubernetes/helm/gateway-helm-chart/README.md#L168-L168
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-runtime/docker-entrypoint.sh` around lines 184 - 193, Enforce
loopback-only Router admin binding before generating the admin configuration in
the ROUTER_ADMIN_ENABLED branch of gateway/gateway-runtime/docker-entrypoint.sh
and the corresponding branch at
gateway/gateway-runtime/docker-entrypoint-debug.sh; reject non-loopback
ROUTER_ADMIN_HOST values. Update the debug entrypoint’s loopback documentation
to match the enforced behavior. In kubernetes/helm/gateway-helm-chart/README.md,
do not describe the admin interface as loopback-only unless both entrypoints
enforce that boundary.

"
fi

# Track child PIDs
PY_PID=""
PE_PID=""
Expand Down Expand Up @@ -207,7 +234,12 @@ shutdown() {

# Drain the Router first so in-flight requests finish and keep-alive connections are
# closed cleanly — prevents client-visible connection resets during rolling restarts.
if [ -n "$ENVOY_PID" ] && kill -0 "$ENVOY_PID" 2>/dev/null \
# Requires the admin interface (ROUTER_ADMIN_ENABLED=true); draining is unavailable
# without it, so skip straight to termination rather than attempting an admin call
# that can never succeed.
if [ "${ROUTER_ADMIN_ENABLED}" != "true" ]; then
log "Router admin disabled (ROUTER_ADMIN_ENABLED=false); skipping graceful drain"
elif [ -n "$ENVOY_PID" ] && kill -0 "$ENVOY_PID" 2>/dev/null \
&& [ "${ROUTER_DRAIN_TIME_SECONDS}" -gt 0 ] 2>/dev/null; then
log "Draining Router (Envoy); waiting up to ${ROUTER_DRAIN_TIME_SECONDS}s for in-flight requests..."
if drain_router; then
Expand Down
23 changes: 18 additions & 5 deletions gateway/gateway-runtime/health-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,27 @@
#
# Exit 0 = healthy, Exit 1 = unhealthy

ROUTER_ADMIN_ENABLED="${ROUTER_ADMIN_ENABLED:-false}"
ROUTER_ADMIN_PORT="${ROUTER_ADMIN_PORT:-9901}"
ROUTER_HTTP_PORT="${ROUTER_HTTP_PORT:-8080}"
POLICY_ENGINE_ADMIN_PORT="${POLICY_ENGINE_ADMIN_PORT:-9002}"

# Check Router (Envoy) readiness — expect HTTP 200
ROUTER_STATUS=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${ROUTER_ADMIN_PORT}/ready")
if [ "$ROUTER_STATUS" != "200" ]; then
echo "Router not ready (HTTP ${ROUTER_STATUS})"
exit 1
# Check Router (Envoy) readiness.
# The admin interface (/ready) is disabled by default (see docker-entrypoint.sh /
# ROUTER_ADMIN_ENABLED) — fall back to a raw TCP check against the main listener,
# which confirms Envoy is up and accepting connections.
if [ "${ROUTER_ADMIN_ENABLED}" = "true" ]; then
ROUTER_STATUS=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${ROUTER_ADMIN_PORT}/ready")
if [ "$ROUTER_STATUS" != "200" ]; then
echo "Router not ready (HTTP ${ROUTER_STATUS})"
exit 1
fi
else
if ! (exec 3<>"/dev/tcp/127.0.0.1/${ROUTER_HTTP_PORT}") 2>/dev/null; then
echo "Router not accepting connections on port ${ROUTER_HTTP_PORT}"
exit 1
fi
Comment on lines +32 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file header and relevant lines =="
sed -n '1,90p' gateway/gateway-runtime/health-check.sh 2>/dev/null || true

echo
echo "== references to health-check/router ports/admin =="
rg -n "health-check|ROUTER_ADMIN_ENABLED|ROUTER_ADMIN_PORT|ROUTER_HTTP_PORT|readiness|liveness|/dev/tcp|/ready|curl" gateway -S || true

echo
echo "== files mentioning envoy readiness/admin routes =="
rg -n "ready|admin|readiness|Envoy" -S gateway/gateway-runtime gateway 2>/dev/null | head -200 || true

Repository: wso2/api-platform

Length of output: 46070


Do not use the HTTP listener connection as a readiness probe.

health-check.sh is used by Kubernetes readiness probes, but the ROUTER_ADMIN_ENABLED=false path returns healthy after a TCP connection to 127.0.0.1:${ROUTER_HTTP_PORT}. A listening port does not prove Envoy has loaded usable routes, clusters, listeners, or the ext_proc policy engine. Use a real readiness check for readiness, or split liveness/readiness probes so this TCP fallback is not used as readiness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-runtime/health-check.sh` around lines 32 - 46, Remove the
HTTP listener TCP fallback from the ROUTER_ADMIN_ENABLED=false branch of
health-check.sh, since it cannot establish Envoy readiness. Use a genuine Envoy
readiness check instead, or separate the probe behavior so TCP is used only for
liveness and readiness validates loaded routes, clusters, listeners, and
ext_proc configuration.

exec 3<&- 3>&- 2>/dev/null || true
fi

# Check Policy Engine health — expect HTTP 200
Expand Down
Loading
Loading