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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@ MCPGATEWAY_A2A_METRICS_ENABLED=true
# which headers should be passed through to backing MCP servers.
# ENABLE_HEADER_PASSTHROUGH=false

# Enable overwriting of base headers (advanced usage only)
# When disabled, passthrough headers cannot override gateway headers like Content-Type, Authorization
# ENABLE_OVERWRITE_BASE_HEADERS=false

# Default headers to pass through (when feature is enabled)
# JSON array format recommended: ["X-Tenant-Id", "X-Trace-Id"]
# Comma-separated also supported: X-Tenant-Id,X-Trace-Id
Expand Down Expand Up @@ -709,6 +713,7 @@ DEBUG=false

# Header Passthrough (WARNING: Security implications)
ENABLE_HEADER_PASSTHROUGH=false
ENABLE_OVERWRITE_BASE_HEADERS=false
DEFAULT_PASSTHROUGH_HEADERS=["X-Tenant-Id", "X-Trace-Id"]

# Authorization Header Conflict Resolution:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,7 @@ MCP Gateway uses Alembic for database migrations. Common commands:
| Setting | Description | Default | Options |
| ------------------------------ | ------------------------------------------------ | --------------------- | ------- |
| `ENABLE_HEADER_PASSTHROUGH` | Enable HTTP header passthrough feature (⚠️ Security implications) | `false` | bool |
| `ENABLE_OVERWRITE_BASE_HEADERS` | Enable overwriting of base headers (⚠️ Advanced usage) | `false` | bool |
| `DEFAULT_PASSTHROUGH_HEADERS` | Default headers to pass through (JSON array) | `["X-Tenant-Id", "X-Trace-Id"]` | JSON array |

> ⚠️ **Security Warning**: Header passthrough is disabled by default for security. Only enable if you understand the implications and have reviewed which headers should be passed through to backing MCP servers. Authorization headers are not included in defaults.
Expand Down
1 change: 1 addition & 0 deletions charts/mcp-stack/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ mcpContextForge:

# ─ Header Passthrough (Security Warning) ─
ENABLE_HEADER_PASSTHROUGH: "false" # enable HTTP header passthrough (security implications)
ENABLE_OVERWRITE_BASE_HEADERS: "false" # enable overwriting of base headers (advanced usage)
DEFAULT_PASSTHROUGH_HEADERS: '["X-Tenant-Id", "X-Trace-Id"]' # default headers to pass through (JSON array)

####################################################################
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/deployment/proxy-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ When using proxy authentication, you may want to pass additional headers to down
# Enable header passthrough
ENABLE_HEADER_PASSTHROUGH=true

# Optional: Enable overwriting of base headers (advanced usage)
ENABLE_OVERWRITE_BASE_HEADERS=false

# Headers to pass through (JSON array)
DEFAULT_PASSTHROUGH_HEADERS='["X-Tenant-Id", "X-Request-Id", "X-Authenticated-User"]'
```
Expand Down
1 change: 1 addition & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,7 @@ MCP Gateway uses Alembic for database migrations. Common commands:
| Setting | Description | Default | Options |
| ------------------------------ | ------------------------------------------------ | --------------------- | ------- |
| `ENABLE_HEADER_PASSTHROUGH` | Enable HTTP header passthrough feature (⚠️ Security implications) | `false` | bool |
| `ENABLE_OVERWRITE_BASE_HEADERS` | Enable overwriting of base headers (⚠️ Advanced usage) | `false` | bool |
| `DEFAULT_PASSTHROUGH_HEADERS` | Default headers to pass through (JSON array) | `["X-Tenant-Id", "X-Trace-Id"]` | JSON array |

> ⚠️ **Security Warning**: Header passthrough is disabled by default for security. Only enable if you understand the implications and have reviewed which headers should be passed through to backing MCP servers. Authorization headers are not included in defaults.
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/manage/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ When using proxy authentication, you often need to pass additional headers to do
# Enable header passthrough
ENABLE_HEADER_PASSTHROUGH=true

# Optional: Enable overwriting of base headers (advanced usage)
ENABLE_OVERWRITE_BASE_HEADERS=false

# Headers to pass through (JSON array)
DEFAULT_PASSTHROUGH_HEADERS='["X-Tenant-Id", "X-Request-Id", "X-Authenticated-User", "X-Groups"]'
```
Expand Down
27 changes: 27 additions & 0 deletions docs/docs/overview/passthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ When clients make requests through the MCP Gateway, certain headers (like authen
# Enable the feature (disabled by default)
ENABLE_HEADER_PASSTHROUGH=true

# Optional: Enable overwriting of base headers (disabled by default)
ENABLE_OVERWRITE_BASE_HEADERS=false

# Or in .env file
ENABLE_HEADER_PASSTHROUGH=true
ENABLE_OVERWRITE_BASE_HEADERS=false
```

**Warning**: Only enable this feature if you:
Expand Down Expand Up @@ -61,6 +65,29 @@ DEFAULT_PASSTHROUGH_HEADERS=["X-Tenant-Id", "X-Trace-Id"]
- Header names are validated against pattern: `^[A-Za-z0-9-]+$`
- Header values are sanitized (newlines removed, length limited to 4KB)

### Base Headers Override (Advanced)

By default, passthrough headers **cannot override** existing base headers set by the gateway (like `Content-Type`, `Authorization`, etc.). This prevents conflicts with essential gateway functionality.

```bash
# Enable overwriting of base headers (⚠️ Advanced usage only)
ENABLE_OVERWRITE_BASE_HEADERS=true
```

**⚠️ Warning**: Only enable this if you:
- Understand the implications of overriding gateway headers
- Need specific headers from client requests to take precedence
- Have thoroughly tested the impact on gateway functionality

**Use Cases**:
- Custom authentication schemes that require client-provided `Authorization` headers
- Specialized content negotiation requiring client `Content-Type` override
- Advanced proxy scenarios with specific header requirements

**Conflicts Still Prevented**:
- Gateway authentication conflicts are still detected and logged
- Invalid headers are still rejected and sanitized

### Admin UI Configuration

**Prerequisites**:
Expand Down
1 change: 1 addition & 0 deletions mcpgateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,7 @@ def validate_database(self) -> None:

# Header passthrough feature (disabled by default for security)
enable_header_passthrough: bool = Field(default=False, description="Enable HTTP header passthrough feature (WARNING: Security implications - only enable if needed)")
enable_overwrite_base_headers: bool = Field(default=False, description="Enable overwriting of base headers")

# Passthrough headers configuration
default_passthrough_headers: List[str] = Field(default_factory=list)
Expand Down
7 changes: 7 additions & 0 deletions mcpgateway/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,19 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
logger.info(f"Plugin manager initialized with {plugin_manager.plugin_count} plugins")

if settings.enable_header_passthrough:
logger.info(f"🔄 Header Passthrough: ENABLED (default headers: {settings.default_passthrough_headers})")
if settings.enable_overwrite_base_headers:
logger.warning("⚠️ Base Header Override: ENABLED - Client headers can override gateway headers")
else:
logger.info("🔒 Base Header Override: DISABLED - Gateway headers take precedence")
db_gen = get_db()
db = next(db_gen) # pylint: disable=stop-iteration-return
try:
await set_global_passthrough_headers(db)
finally:
db.close()
else:
logger.info("🔒 Header Passthrough: DISABLED")

await tool_service.initialize()
await resource_service.initialize()
Expand Down
5 changes: 4 additions & 1 deletion mcpgateway/utils/passthrough_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ def get_passthrough_headers(request_headers: Dict[str, str], base_headers: Dict[
logger.debug("Header passthrough is disabled via ENABLE_HEADER_PASSTHROUGH flag")
return passthrough_headers

if settings.enable_overwrite_base_headers:
logger.debug("Overwriting base headers is enabled via ENABLE_OVERWRITE_BASE_HEADERS flag")

# Get global passthrough headers first
global_config = db.query(GlobalConfig).first()
allowed_headers = global_config.passthrough_headers if global_config else settings.default_passthrough_headers
Expand Down Expand Up @@ -278,7 +281,7 @@ def get_passthrough_headers(request_headers: Dict[str, str], base_headers: Dict[
continue

# Skip if header would conflict with existing auth headers
if header_lower in base_headers_keys:
if header_lower in base_headers_keys and not settings.enable_overwrite_base_headers:
logger.warning(f"Skipping {header_name} header passthrough as it conflicts with pre-defined headers")
continue

Expand Down
48 changes: 48 additions & 0 deletions tests/unit/mcpgateway/utils/test_passthrough_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def test_authorization_conflict_bearer_auth(self, mock_settings, caplog):
def test_base_header_conflict_prevention(self, mock_settings, caplog):
"""Test that request headers don't override base headers."""
mock_settings.enable_header_passthrough = True
mock_settings.enable_overwrite_base_headers = False

mock_db = Mock()
mock_global_config = Mock(spec=GlobalConfig)
Expand Down Expand Up @@ -412,6 +413,7 @@ def test_database_query_called_correctly(self, mock_settings):
def test_logging_levels(self, mock_settings, caplog):
"""Test that appropriate log levels are used for different scenarios."""
mock_settings.enable_header_passthrough = True
mock_settings.enable_overwrite_base_headers = False

mock_db = Mock()
mock_global_config = Mock(spec=GlobalConfig)
Expand All @@ -435,3 +437,49 @@ def test_logging_levels(self, mock_settings, caplog):
assert len(warning_messages) == 2 # Only auth conflict and base header conflict
assert any("due to basic auth" in msg for msg in warning_messages)
assert any("conflicts with pre-defined headers" in msg for msg in warning_messages)

@patch("mcpgateway.utils.passthrough_headers.settings")
def test_enable_overwrite_base_headers(self, mock_settings):
"""Test that enable_overwrite_base_headers allows overriding base headers."""
mock_settings.enable_header_passthrough = True
mock_settings.enable_overwrite_base_headers = True # Enable override
mock_settings.default_passthrough_headers = ["Content-Type", "X-Tenant-Id"]

mock_db = Mock()
mock_db.query.return_value.first.return_value = None

request_headers = {"content-type": "text/plain", "x-tenant-id": "acme-corp"}
base_headers = {"Content-Type": "application/json", "User-Agent": "MCPGateway"}

with patch("mcpgateway.utils.passthrough_headers.logger") as mock_logger:
result = get_passthrough_headers(request_headers, base_headers, mock_db)

# Should override Content-Type and add X-Tenant-Id
expected = {"Content-Type": "text/plain", "User-Agent": "MCPGateway", "X-Tenant-Id": "acme-corp"}
assert result == expected

# Should log debug message about override being enabled
mock_logger.debug.assert_any_call("Overwriting base headers is enabled via ENABLE_OVERWRITE_BASE_HEADERS flag")

@patch("mcpgateway.utils.passthrough_headers.settings")
def test_disable_overwrite_base_headers_prevents_conflicts(self, mock_settings, caplog):
"""Test that when overwrite is disabled, base header conflicts are prevented."""
mock_settings.enable_header_passthrough = True
mock_settings.enable_overwrite_base_headers = False # Disable override (default)
mock_settings.default_passthrough_headers = ["Content-Type", "X-Tenant-Id"]

mock_db = Mock()
mock_db.query.return_value.first.return_value = None

request_headers = {"content-type": "text/plain", "x-tenant-id": "acme-corp"}
base_headers = {"Content-Type": "application/json", "User-Agent": "MCPGateway"}

with caplog.at_level(logging.WARNING):
result = get_passthrough_headers(request_headers, base_headers, mock_db)

# Should preserve base Content-Type and add X-Tenant-Id
expected = {"Content-Type": "application/json", "User-Agent": "MCPGateway", "X-Tenant-Id": "acme-corp"}
assert result == expected

# Should log warning about conflict
assert any("conflicts with pre-defined headers" in record.message for record in caplog.records)
Loading