diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..eef4bd20 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md \ No newline at end of file diff --git a/config.example.yaml b/config.example.yaml index 092ba926..6ebf74a4 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -143,7 +143,7 @@ routing: session-affinity-ttl: "1h" # When true, enable authentication for the WebSocket API (/v1/ws). -ws-auth: false +ws-auth: true # When true, enable Gemini CLI internal endpoints (/v1internal:*). # Default is false for safety. diff --git a/internal/api/server.go b/internal/api/server.go index 05bcd1cf..c8e92c8e 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -217,6 +217,9 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk // Create gin engine engine := gin.New() + if errSetTrustedProxies := engine.SetTrustedProxies(nil); errSetTrustedProxies != nil { + log.Warnf("failed to disable trusted proxy headers: %v", errSetTrustedProxies) + } if optionState.engineConfigurator != nil { optionState.engineConfigurator(engine) } diff --git a/internal/api/server_test.go b/internal/api/server_test.go index e503fe71..8f59752d 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -21,6 +21,10 @@ import ( ) func newTestServer(t *testing.T) *Server { + return newTestServerWithOptions(t) +} + +func newTestServerWithOptions(t *testing.T, opts ...ServerOption) *Server { t.Helper() gin.SetMode(gin.TestMode) @@ -46,7 +50,7 @@ func newTestServer(t *testing.T) *Server { accessManager := sdkaccess.NewManager() configPath := filepath.Join(tmpDir, "config.yaml") - return NewServer(cfg, authManager, accessManager, configPath) + return NewServer(cfg, authManager, accessManager, configPath, opts...) } func TestHealthz(t *testing.T) { @@ -148,6 +152,26 @@ func TestManagementUsageRequiresManagementAuthAndPopsArray(t *testing.T) { } } +func TestManagementLocalPasswordRejectsSpoofedForwardedFor(t *testing.T) { + t.Setenv("MANAGEMENT_PASSWORD", "") + + server := newTestServerWithOptions(t, WithLocalManagementPassword("test-local-key")) + + req := httptest.NewRequest(http.MethodGet, "/v0/management/config", nil) + req.RemoteAddr = "203.0.113.10:45678" + req.Header.Set("X-Forwarded-For", "127.0.0.1") + req.Header.Set("Authorization", "Bearer test-local-key") + + rr := httptest.NewRecorder() + server.engine.ServeHTTP(rr, req) + if rr.Code != http.StatusForbidden { + t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusForbidden, rr.Body.String()) + } + if body := rr.Body.String(); !strings.Contains(body, "remote management disabled") { + t.Fatalf("body = %q, want remote management disabled", body) + } +} + func TestHomeEnabledHidesManagementEndpointsAndControlPanel(t *testing.T) { t.Setenv("MANAGEMENT_PASSWORD", "test-management-key")