History

Merge pull request #547 from router-for-me/amp

feat(amp): require API key authentication for management routes

Luis Pater committed 11 months agov6.6.15

Commit f6720f82 parents e19ab3a 8f1dd696 files · +21 −13Browse files

Unified · Split

6 files
  1. config.example.yaml +2 −2
  2. internal/api/modules/amp/amp.go +2 −4
  3. internal/api/modules/amp/fallback_handlers.go +1 −1
  4. internal/api/modules/amp/proxy.go +6 −1
  5. internal/api/modules/amp/routes.go +8 −3
  6. internal/config/config.go +2 −2
config.example.yaml+2−2
@@ -136,8 +136,8 @@ ws-auth: false · 135 unmodified lines
136# upstream-url: "https://ampcode.com"136# upstream-url: "https://ampcode.com"
137# # Optional: Override API key for Amp upstream (otherwise uses env or file)137# # Optional: Override API key for Amp upstream (otherwise uses env or file)
138# upstream-api-key: ""138# upstream-api-key: ""
139# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (recommended)139# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (default: false)
140# restrict-management-to-localhost: true140# restrict-management-to-localhost: false
141# # Force model mappings to run before checking local API keys (default: false)141# # Force model mappings to run before checking local API keys (default: false)
142# force-model-mappings: false142# force-model-mappings: false
143# # Amp Model Mappings143# # Amp Model Mappings
internal/api/modules/amp/amp.go+2−4
@@ -137,7 +137,8 @@ func (m *AmpModule) Register(ctx modules.Context) error { · 136 unmodified lines
137 m.registerProviderAliases(ctx.Engine, ctx.BaseHandler, auth)137 m.registerProviderAliases(ctx.Engine, ctx.BaseHandler, auth)
138 138
139 // Register management proxy routes once; middleware will gate access when upstream is unavailable.139 // Register management proxy routes once; middleware will gate access when upstream is unavailable.
140 m.registerManagementRoutes(ctx.Engine, ctx.BaseHandler)140 // Pass auth middleware to require valid API key for all management routes.
141 m.registerManagementRoutes(ctx.Engine, ctx.BaseHandler, auth)
141 142
142 // If no upstream URL, skip proxy routes but provider aliases are still available143 // If no upstream URL, skip proxy routes but provider aliases are still available
143 if upstreamURL == "" {144 if upstreamURL == "" {
@@ -187,9 +188,6 @@ func (m *AmpModule) OnConfigUpdated(cfg *config.Config) error { · 43 unmodified lines
187 188
188 if oldSettings != nil && oldSettings.RestrictManagementToLocalhost != newSettings.RestrictManagementToLocalhost {189 if oldSettings != nil && oldSettings.RestrictManagementToLocalhost != newSettings.RestrictManagementToLocalhost {
189 m.setRestrictToLocalhost(newSettings.RestrictManagementToLocalhost)190 m.setRestrictToLocalhost(newSettings.RestrictManagementToLocalhost)
190 if !newSettings.RestrictManagementToLocalhost {
191 log.Warnf("amp management routes now accessible from any IP - this is insecure!")
192 }
193 }191 }
194 192
195 newUpstreamURL := strings.TrimSpace(newSettings.UpstreamURL)193 newUpstreamURL := strings.TrimSpace(newSettings.UpstreamURL)
internal/api/modules/amp/fallback_handlers.go+1−1
@@ -64,7 +64,7 @@ func logAmpRouting(routeType AmpRouteType, requestedModel, resolvedModel, provid · 63 unmodified lines
64 fields["cost"] = "amp_credits"64 fields["cost"] = "amp_credits"
65 fields["source"] = "ampcode.com"65 fields["source"] = "ampcode.com"
66 fields["model_id"] = requestedModel // Explicit model_id for easy config reference66 fields["model_id"] = requestedModel // Explicit model_id for easy config reference
67 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: \"<your-local-model>\"}]", requestedModel, requestedModel)67 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: \"<your-local-model>\"}]", requestedModel, requestedModel)
68 68
69 case RouteTypeNoProvider:69 case RouteTypeNoProvider:
70 fields["cost"] = "none"70 fields["cost"] = "none"
internal/api/modules/amp/proxy.go+6−1
@@ -41,6 +41,11 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi · 40 unmodified lines
41 originalDirector(req)41 originalDirector(req)
42 req.Host = parsed.Host42 req.Host = parsed.Host
43 43
44 // Remove client's Authorization header - it was only used for CLI Proxy API authentication
45 // We will set our own Authorization using the configured upstream-api-key
46 req.Header.Del("Authorization")
47 req.Header.Del("X-Api-Key")
48
44 // Preserve correlation headers for debugging49 // Preserve correlation headers for debugging
45 if req.Header.Get("X-Request-ID") == "" {50 if req.Header.Get("X-Request-ID") == "" {
46 // Could generate one here if needed51 // Could generate one here if needed
@@ -50,7 +55,7 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi · 3 unmodified lines
50 // Users going through ampcode.com proxy are paying for the service and should get all features55 // Users going through ampcode.com proxy are paying for the service and should get all features
51 // including 1M context window (context-1m-2025-08-07)56 // including 1M context window (context-1m-2025-08-07)
52 57
53 // Inject API key from secret source (precedence: config > env > file)58 // Inject API key from secret source (only uses upstream-api-key from config)
54 if key, err := secretSource.Get(req.Context()); err == nil && key != "" {59 if key, err := secretSource.Get(req.Context()); err == nil && key != "" {
55 req.Header.Set("X-Api-Key", key)60 req.Header.Set("X-Api-Key", key)
56 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))61 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
internal/api/modules/amp/routes.go+8−3
@@ -98,7 +98,8 @@ func (m *AmpModule) managementAvailabilityMiddleware() gin.HandlerFunc { · 97 unmodified lines
98// registerManagementRoutes registers Amp management proxy routes98// registerManagementRoutes registers Amp management proxy routes
99// These routes proxy through to the Amp control plane for OAuth, user management, etc.99// These routes proxy through to the Amp control plane for OAuth, user management, etc.
100// Uses dynamic middleware and proxy getter for hot-reload support.100// Uses dynamic middleware and proxy getter for hot-reload support.
101func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler) {101// The auth middleware validates Authorization header against configured API keys.
102func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler, auth gin.HandlerFunc) {
102 ampAPI := engine.Group("/api")103 ampAPI := engine.Group("/api")
103 104
104 // Always disable CORS for management routes to prevent browser-based attacks105 // Always disable CORS for management routes to prevent browser-based attacks
@@ -107,8 +108,9 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha · 2 unmodified lines
107 // Apply dynamic localhost-only restriction (hot-reloadable via m.IsRestrictedToLocalhost())108 // Apply dynamic localhost-only restriction (hot-reloadable via m.IsRestrictedToLocalhost())
108 ampAPI.Use(m.localhostOnlyMiddleware())109 ampAPI.Use(m.localhostOnlyMiddleware())
109 110
110 if !m.IsRestrictedToLocalhost() {111 // Apply authentication middleware - requires valid API key in Authorization header
111 log.Warn("amp management routes are NOT restricted to localhost - this is insecure!")112 if auth != nil {
113 ampAPI.Use(auth)
112 }114 }
113 115
114 // Dynamic proxy handler that uses m.getProxy() for hot-reload support116 // Dynamic proxy handler that uses m.getProxy() for hot-reload support
@@ -154,6 +156,9 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha · 39 unmodified lines
154 // Root-level routes that AMP CLI expects without /api prefix156 // Root-level routes that AMP CLI expects without /api prefix
155 // These need the same security middleware as the /api/* routes (dynamic for hot-reload)157 // These need the same security middleware as the /api/* routes (dynamic for hot-reload)
156 rootMiddleware := []gin.HandlerFunc{m.managementAvailabilityMiddleware(), noCORSMiddleware(), m.localhostOnlyMiddleware()}158 rootMiddleware := []gin.HandlerFunc{m.managementAvailabilityMiddleware(), noCORSMiddleware(), m.localhostOnlyMiddleware()}
159 if auth != nil {
160 rootMiddleware = append(rootMiddleware, auth)
161 }
157 engine.GET("/threads/*path", append(rootMiddleware, proxyHandler)...)162 engine.GET("/threads/*path", append(rootMiddleware, proxyHandler)...)
158 engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)163 engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)
159 engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...)164 engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...)
internal/config/config.go+2−2
@@ -139,7 +139,7 @@ type AmpCode struct { · 138 unmodified lines
139 139
140 // RestrictManagementToLocalhost restricts Amp management routes (/api/user, /api/threads, etc.)140 // RestrictManagementToLocalhost restricts Amp management routes (/api/user, /api/threads, etc.)
141 // to only accept connections from localhost (127.0.0.1, ::1). When true, prevents drive-by141 // to only accept connections from localhost (127.0.0.1, ::1). When true, prevents drive-by
142 // browser attacks and remote access to management endpoints. Default: true (recommended).142 // browser attacks and remote access to management endpoints. Default: false (API key auth is sufficient).
143 RestrictManagementToLocalhost bool `yaml:"restrict-management-to-localhost" json:"restrict-management-to-localhost"`143 RestrictManagementToLocalhost bool `yaml:"restrict-management-to-localhost" json:"restrict-management-to-localhost"`
144 144
145 // ModelMappings defines model name mappings for Amp CLI requests.145 // ModelMappings defines model name mappings for Amp CLI requests.
@@ -327,7 +327,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { · 181 unmodified lines
327 cfg.LoggingToFile = false327 cfg.LoggingToFile = false
328 cfg.UsageStatisticsEnabled = false328 cfg.UsageStatisticsEnabled = false
329 cfg.DisableCooling = false329 cfg.DisableCooling = false
330 cfg.AmpCode.RestrictManagementToLocalhost = true // Default to secure: only localhost access330 cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
331 if err = yaml.Unmarshal(data, &cfg); err != nil {331 if err = yaml.Unmarshal(data, &cfg); err != nil {
332 if optional {332 if optional {
333 // In cloud deploy mode, if YAML parsing fails, return empty config instead of error.333 // In cloud deploy mode, if YAML parsing fails, return empty config instead of error.