Skip to content
Merged
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
12 changes: 10 additions & 2 deletions charts/mcp-stack/templates/configmap-nginx-proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ data:
proxy_cache_path {{ .Values.nginxProxy.config.cache.path }} levels=1:2 keys_zone=mcp_cache:100m max_size={{ .Values.nginxProxy.config.cache.maxSize }} inactive={{ .Values.nginxProxy.config.cache.inactive }} use_temp_path=off;
{{- end }}

# Preserve X-Forwarded-Proto from upstream proxy (e.g. ALB, ingress
# controller); fall back to $scheme when nginx is the outermost proxy.
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
"" $scheme;
}

upstream gateway_upstream {
server {{ $gatewayServiceName }}:{{ $gatewayServicePort }};
keepalive 32;
Expand All @@ -51,10 +58,11 @@ data:

location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";

proxy_connect_timeout 30s;
Expand Down
40 changes: 29 additions & 11 deletions infra/nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ http {
# # proxy_ssl_trusted_certificate /app/certs/cert.pem;
# proxy_ssl_session_reuse on;

# Preserve X-Forwarded-Proto from upstream proxy (e.g. ALB, ingress
# controller); fall back to $scheme when nginx is the outermost proxy.
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
"" $scheme;
}

# Cache bypass conditions
map $request_method $skip_cache {
default 0;
Expand Down Expand Up @@ -285,7 +292,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";
proxy_http_version 1.1;
}
Expand Down Expand Up @@ -313,7 +321,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";
proxy_http_version 1.1;
}
Expand Down Expand Up @@ -361,7 +370,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";
proxy_http_version 1.1;

Expand All @@ -384,7 +394,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";
proxy_http_version 1.1;

Expand Down Expand Up @@ -450,7 +461,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";
proxy_http_version 1.1;

Expand All @@ -477,7 +489,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;

# Extended timeouts for SSE
proxy_connect_timeout 1h;
Expand All @@ -500,7 +513,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;

# Extended timeouts for SSE
proxy_connect_timeout 1h;
Expand All @@ -518,7 +532,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
}

location ~ ^/servers/.*/ws$ {
Expand All @@ -534,7 +549,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;

# Extended timeouts for WebSocket
proxy_connect_timeout 1h;
Expand Down Expand Up @@ -564,7 +580,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";
proxy_http_version 1.1;

Expand Down Expand Up @@ -596,7 +613,8 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Connection "";
proxy_http_version 1.1;

Expand Down
40 changes: 33 additions & 7 deletions mcpgateway/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,16 +1080,20 @@ def _normalize_origin_parts(scheme: str, netloc: str) -> tuple[str, str, int]:
def _request_origin_matches(request: Request) -> bool:
"""Return ``True`` when Origin/Referer matches this request origin.

The function first performs an exact same-origin comparison using the
request's forwarded headers (``X-Forwarded-Proto`` / ``X-Forwarded-Host``).
When that fails — common behind layered reverse proxies where forwarded
headers reflect internal hops rather than the external scheme — it falls
back to checking whether the candidate origin is explicitly listed in
``settings.allowed_origins``. Wildcard entries (``*``, ``null``, ``""``)
are excluded from the fallback to preserve fail-closed behavior.

Args:
request: Incoming request carrying Origin/Referer and host headers.

Returns:
``True`` when candidate origin exactly matches request origin; otherwise ``False``.

Note:
When the service is deployed behind a reverse proxy, this check relies on
``X-Forwarded-Proto`` / ``X-Forwarded-Host`` values emitted by that proxy.
The deployment boundary must sanitize and overwrite forwarded headers.
``True`` when candidate origin matches either the request origin or an
entry in ``settings.allowed_origins``; otherwise ``False``.
"""
origin = request.headers.get("origin")
referer = request.headers.get("referer")
Expand Down Expand Up @@ -1117,7 +1121,29 @@ def _request_origin_matches(request: Request) -> bool:

candidate_parts = _normalize_origin_parts(parsed_candidate.scheme, parsed_candidate.netloc)
request_parts = _normalize_origin_parts(request_scheme, request_netloc)
return candidate_parts == request_parts
if candidate_parts == request_parts:
return True

# Fallback: accept origins explicitly listed in settings.allowed_origins.
# Handles reverse-proxy deployments where forwarded headers may not
# accurately reflect the external scheme/host.
for allowed in settings.allowed_origins:
# Normalize each allowed origin to avoid config surprises such as
# ["https://a.com "] or [" null "], which could otherwise be
# mis-parsed or skipped.
allowed_normalized = str(allowed).strip()
if not allowed_normalized or allowed_normalized == "*" or allowed_normalized.casefold() == "null":
continue
try:
allowed_parsed = urllib.parse.urlparse(allowed_normalized if "://" in allowed_normalized else f"https://{allowed_normalized}")
if not allowed_parsed.scheme or not allowed_parsed.netloc:
continue
if candidate_parts == _normalize_origin_parts(allowed_parsed.scheme, allowed_parsed.netloc):
return True
except Exception: # nosec B112 - malformed allowed_origins entry should not crash
continue

return False


def _set_admin_csrf_cookie(request: Request, response: Response) -> str:
Expand Down
Loading
Loading