diff --git a/config.example.yaml b/config.example.yaml index 31f16973..58e9e397 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -136,8 +136,8 @@ ws-auth: false # upstream-url: "https://ampcode.com" # # Optional: Override API key for Amp upstream (otherwise uses env or file) # upstream-api-key: "" -# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (recommended) -# restrict-management-to-localhost: true +# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (default: false) +# restrict-management-to-localhost: false # # Force model mappings to run before checking local API keys (default: false) # force-model-mappings: false # # Amp Model Mappings diff --git a/internal/api/modules/amp/amp.go b/internal/api/modules/amp/amp.go index 88319a78..c18657c9 100644 --- a/internal/api/modules/amp/amp.go +++ b/internal/api/modules/amp/amp.go @@ -137,7 +137,8 @@ func (m *AmpModule) Register(ctx modules.Context) error { m.registerProviderAliases(ctx.Engine, ctx.BaseHandler, auth) // Register management proxy routes once; middleware will gate access when upstream is unavailable. - m.registerManagementRoutes(ctx.Engine, ctx.BaseHandler) + // Pass auth middleware to require valid API key for all management routes. + m.registerManagementRoutes(ctx.Engine, ctx.BaseHandler, auth) // If no upstream URL, skip proxy routes but provider aliases are still available if upstreamURL == "" { @@ -187,9 +188,6 @@ func (m *AmpModule) OnConfigUpdated(cfg *config.Config) error { if oldSettings != nil && oldSettings.RestrictManagementToLocalhost != newSettings.RestrictManagementToLocalhost { m.setRestrictToLocalhost(newSettings.RestrictManagementToLocalhost) - if !newSettings.RestrictManagementToLocalhost { - log.Warnf("amp management routes now accessible from any IP - this is insecure!") - } } newUpstreamURL := strings.TrimSpace(newSettings.UpstreamURL) diff --git a/internal/api/modules/amp/fallback_handlers.go b/internal/api/modules/amp/fallback_handlers.go index c17f3f85..e7132b81 100644 --- a/internal/api/modules/amp/fallback_handlers.go +++ b/internal/api/modules/amp/fallback_handlers.go @@ -64,7 +64,7 @@ func logAmpRouting(routeType AmpRouteType, requestedModel, resolvedModel, provid fields["cost"] = "amp_credits" fields["source"] = "ampcode.com" fields["model_id"] = requestedModel // Explicit model_id for easy config reference - log.WithFields(fields).Warnf("forwarding to ampcode.com (uses amp credits) - model_id: %s | To use local proxy, add to config: amp-model-mappings: [{from: \"%s\", to: \"\"}]", requestedModel, requestedModel) + log.WithFields(fields).Warnf("forwarding to ampcode.com (uses amp credits) - model_id: %s | To use local provider, add to config: ampcode.model-mappings: [{from: \"%s\", to: \"\"}]", requestedModel, requestedModel) case RouteTypeNoProvider: fields["cost"] = "none" diff --git a/internal/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go index 33f32c28..3c4ef308 100644 --- a/internal/api/modules/amp/proxy.go +++ b/internal/api/modules/amp/proxy.go @@ -41,6 +41,11 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi originalDirector(req) req.Host = parsed.Host + // Remove client's Authorization header - it was only used for CLI Proxy API authentication + // We will set our own Authorization using the configured upstream-api-key + req.Header.Del("Authorization") + req.Header.Del("X-Api-Key") + // Preserve correlation headers for debugging if req.Header.Get("X-Request-ID") == "" { // Could generate one here if needed @@ -50,7 +55,7 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi // Users going through ampcode.com proxy are paying for the service and should get all features // including 1M context window (context-1m-2025-08-07) - // Inject API key from secret source (precedence: config > env > file) + // Inject API key from secret source (only uses upstream-api-key from config) if key, err := secretSource.Get(req.Context()); err == nil && key != "" { req.Header.Set("X-Api-Key", key) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key)) diff --git a/internal/api/modules/amp/routes.go b/internal/api/modules/amp/routes.go index 48fbbbb9..8d9ec8ae 100644 --- a/internal/api/modules/amp/routes.go +++ b/internal/api/modules/amp/routes.go @@ -98,7 +98,8 @@ func (m *AmpModule) managementAvailabilityMiddleware() gin.HandlerFunc { // registerManagementRoutes registers Amp management proxy routes // These routes proxy through to the Amp control plane for OAuth, user management, etc. // Uses dynamic middleware and proxy getter for hot-reload support. -func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler) { +// The auth middleware validates Authorization header against configured API keys. +func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler, auth gin.HandlerFunc) { ampAPI := engine.Group("/api") // Always disable CORS for management routes to prevent browser-based attacks @@ -107,8 +108,9 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha // Apply dynamic localhost-only restriction (hot-reloadable via m.IsRestrictedToLocalhost()) ampAPI.Use(m.localhostOnlyMiddleware()) - if !m.IsRestrictedToLocalhost() { - log.Warn("amp management routes are NOT restricted to localhost - this is insecure!") + // Apply authentication middleware - requires valid API key in Authorization header + if auth != nil { + ampAPI.Use(auth) } // Dynamic proxy handler that uses m.getProxy() for hot-reload support @@ -154,6 +156,9 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha // Root-level routes that AMP CLI expects without /api prefix // These need the same security middleware as the /api/* routes (dynamic for hot-reload) rootMiddleware := []gin.HandlerFunc{m.managementAvailabilityMiddleware(), noCORSMiddleware(), m.localhostOnlyMiddleware()} + if auth != nil { + rootMiddleware = append(rootMiddleware, auth) + } engine.GET("/threads/*path", append(rootMiddleware, proxyHandler)...) engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...) engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...) diff --git a/internal/config/config.go b/internal/config/config.go index 5af74b1b..a690d2ea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -139,7 +139,7 @@ type AmpCode struct { // RestrictManagementToLocalhost restricts Amp management routes (/api/user, /api/threads, etc.) // to only accept connections from localhost (127.0.0.1, ::1). When true, prevents drive-by - // browser attacks and remote access to management endpoints. Default: true (recommended). + // browser attacks and remote access to management endpoints. Default: false (API key auth is sufficient). RestrictManagementToLocalhost bool `yaml:"restrict-management-to-localhost" json:"restrict-management-to-localhost"` // ModelMappings defines model name mappings for Amp CLI requests. @@ -327,7 +327,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { cfg.LoggingToFile = false cfg.UsageStatisticsEnabled = false cfg.DisableCooling = false - cfg.AmpCode.RestrictManagementToLocalhost = true // Default to secure: only localhost access + cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient if err = yaml.Unmarshal(data, &cfg); err != nil { if optional { // In cloud deploy mode, if YAML parsing fails, return empty config instead of error.