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
136136 # upstream-url: "https://ampcode.com"
137137 # # Optional: Override API key for Amp upstream (otherwise uses env or file)
138138 # upstream-api-key: ""
139−# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (recommended)
140−# restrict-management-to-localhost: true
139+# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (default: false)
140+# restrict-management-to-localhost: false
141141 # # Force model mappings to run before checking local API keys (default: false)
142142 # force-model-mappings: false
143143 # # 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
137137 m.registerProviderAliases(ctx.Engine, ctx.BaseHandler, auth)
138138
139139 // 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)
141142
142143 // If no upstream URL, skip proxy routes but provider aliases are still available
143144 if upstreamURL == "" {
@@ -187,9 +188,6 @@ func (m *AmpModule) OnConfigUpdated(cfg *config.Config) error { · 43 unmodified lines
187188
188189 if oldSettings != nil && oldSettings.RestrictManagementToLocalhost != newSettings.RestrictManagementToLocalhost {
189190 m.setRestrictToLocalhost(newSettings.RestrictManagementToLocalhost)
190− if !newSettings.RestrictManagementToLocalhost {
191− log.Warnf("amp management routes now accessible from any IP - this is insecure!")
192− }
193191 }
194192
195193 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
6464 fields["cost"] = "amp_credits"
6565 fields["source"] = "ampcode.com"
6666 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)
6868
6969 case RouteTypeNoProvider:
7070 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
4141 originalDirector(req)
4242 req.Host = parsed.Host
4343
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+
4449 // Preserve correlation headers for debugging
4550 if req.Header.Get("X-Request-ID") == "" {
4651 // Could generate one here if needed
@@ -50,7 +55,7 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi · 3 unmodified lines
5055 // Users going through ampcode.com proxy are paying for the service and should get all features
5156 // including 1M context window (context-1m-2025-08-07)
5257
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)
5459 if key, err := secretSource.Get(req.Context()); err == nil && key != "" {
5560 req.Header.Set("X-Api-Key", key)
5661 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
9898 // registerManagementRoutes registers Amp management proxy routes
9999 // These routes proxy through to the Amp control plane for OAuth, user management, etc.
100100 // Uses dynamic middleware and proxy getter for hot-reload support.
101−func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler) {
101+// The auth middleware validates Authorization header against configured API keys.
102+func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler, auth gin.HandlerFunc) {
102103 ampAPI := engine.Group("/api")
103104
104105 // 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
107108 // Apply dynamic localhost-only restriction (hot-reloadable via m.IsRestrictedToLocalhost())
108109 ampAPI.Use(m.localhostOnlyMiddleware())
109110
110− if !m.IsRestrictedToLocalhost() {
111− log.Warn("amp management routes are NOT restricted to localhost - this is insecure!")
111+ // Apply authentication middleware - requires valid API key in Authorization header
112+ if auth != nil {
113+ ampAPI.Use(auth)
112114 }
113115
114116 // 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
154156 // Root-level routes that AMP CLI expects without /api prefix
155157 // These need the same security middleware as the /api/* routes (dynamic for hot-reload)
156158 rootMiddleware := []gin.HandlerFunc{m.managementAvailabilityMiddleware(), noCORSMiddleware(), m.localhostOnlyMiddleware()}
159+ if auth != nil {
160+ rootMiddleware = append(rootMiddleware, auth)
161+ }
157162 engine.GET("/threads/*path", append(rootMiddleware, proxyHandler)...)
158163 engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)
159164 engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...)
internal/config/config.go+2−2
@@ -139,7 +139,7 @@ type AmpCode struct { · 138 unmodified lines
139139
140140 // RestrictManagementToLocalhost restricts Amp management routes (/api/user, /api/threads, etc.)
141141 // 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).
143143 RestrictManagementToLocalhost bool `yaml:"restrict-management-to-localhost" json:"restrict-management-to-localhost"`
144144
145145 // ModelMappings defines model name mappings for Amp CLI requests.
@@ -327,7 +327,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { · 181 unmodified lines
327327 cfg.LoggingToFile = false
328328 cfg.UsageStatisticsEnabled = false
329329 cfg.DisableCooling = false
330− cfg.AmpCode.RestrictManagementToLocalhost = true // Default to secure: only localhost access
330+ cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
331331 if err = yaml.Unmarshal(data, &cfg); err != nil {
332332 if optional {
333333 // In cloud deploy mode, if YAML parsing fails, return empty config instead of error.