mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
feat(amp): require API key authentication for management routes
All Amp management endpoints (e.g., /api/user, /threads) are now protected by the standard API key authentication middleware. This ensures that all management operations require a valid API key, significantly improving security. As a result of this change: - The `restrict-management-to-localhost` setting now defaults to `false`. API key authentication provides a stronger and more flexible security control than IP-based restrictions, improving usability in containerized environments. - The reverse proxy logic now strips the client's `Authorization` header after authenticating the initial request. It then injects the configured `upstream-api-key` for the request to the upstream Amp service. BREAKING CHANGE: Amp management endpoints now require a valid API key for authentication. Requests without a valid API key in the `Authorization` header will be rejected with a 401 Unauthorized error.
This commit is contained in:
@@ -136,8 +136,8 @@ ws-auth: false
|
|||||||
# upstream-url: "https://ampcode.com"
|
# upstream-url: "https://ampcode.com"
|
||||||
# # Optional: Override API key for Amp upstream (otherwise uses env or file)
|
# # Optional: Override API key for Amp upstream (otherwise uses env or file)
|
||||||
# upstream-api-key: ""
|
# upstream-api-key: ""
|
||||||
# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (recommended)
|
# # Restrict Amp management routes (/api/auth, /api/user, etc.) to localhost only (default: false)
|
||||||
# restrict-management-to-localhost: true
|
# restrict-management-to-localhost: false
|
||||||
# # Force model mappings to run before checking local API keys (default: false)
|
# # Force model mappings to run before checking local API keys (default: false)
|
||||||
# force-model-mappings: false
|
# force-model-mappings: false
|
||||||
# # Amp Model Mappings
|
# # Amp Model Mappings
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ func (m *AmpModule) Register(ctx modules.Context) error {
|
|||||||
m.registerProviderAliases(ctx.Engine, ctx.BaseHandler, auth)
|
m.registerProviderAliases(ctx.Engine, ctx.BaseHandler, auth)
|
||||||
|
|
||||||
// Register management proxy routes once; middleware will gate access when upstream is unavailable.
|
// 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 no upstream URL, skip proxy routes but provider aliases are still available
|
||||||
if upstreamURL == "" {
|
if upstreamURL == "" {
|
||||||
@@ -187,9 +188,6 @@ func (m *AmpModule) OnConfigUpdated(cfg *config.Config) error {
|
|||||||
|
|
||||||
if oldSettings != nil && oldSettings.RestrictManagementToLocalhost != newSettings.RestrictManagementToLocalhost {
|
if oldSettings != nil && oldSettings.RestrictManagementToLocalhost != newSettings.RestrictManagementToLocalhost {
|
||||||
m.setRestrictToLocalhost(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)
|
newUpstreamURL := strings.TrimSpace(newSettings.UpstreamURL)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func logAmpRouting(routeType AmpRouteType, requestedModel, resolvedModel, provid
|
|||||||
fields["cost"] = "amp_credits"
|
fields["cost"] = "amp_credits"
|
||||||
fields["source"] = "ampcode.com"
|
fields["source"] = "ampcode.com"
|
||||||
fields["model_id"] = requestedModel // Explicit model_id for easy config reference
|
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: \"<your-local-model>\"}]", 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: \"<your-local-model>\"}]", requestedModel, requestedModel)
|
||||||
|
|
||||||
case RouteTypeNoProvider:
|
case RouteTypeNoProvider:
|
||||||
fields["cost"] = "none"
|
fields["cost"] = "none"
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi
|
|||||||
originalDirector(req)
|
originalDirector(req)
|
||||||
req.Host = parsed.Host
|
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
|
// Preserve correlation headers for debugging
|
||||||
if req.Header.Get("X-Request-ID") == "" {
|
if req.Header.Get("X-Request-ID") == "" {
|
||||||
// Could generate one here if needed
|
// 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
|
// 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)
|
// 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 != "" {
|
if key, err := secretSource.Get(req.Context()); err == nil && key != "" {
|
||||||
req.Header.Set("X-Api-Key", key)
|
req.Header.Set("X-Api-Key", key)
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ func (m *AmpModule) managementAvailabilityMiddleware() gin.HandlerFunc {
|
|||||||
// registerManagementRoutes registers Amp management proxy routes
|
// registerManagementRoutes registers Amp management proxy routes
|
||||||
// These routes proxy through to the Amp control plane for OAuth, user management, etc.
|
// These routes proxy through to the Amp control plane for OAuth, user management, etc.
|
||||||
// Uses dynamic middleware and proxy getter for hot-reload support.
|
// 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")
|
ampAPI := engine.Group("/api")
|
||||||
|
|
||||||
// Always disable CORS for management routes to prevent browser-based attacks
|
// 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())
|
// Apply dynamic localhost-only restriction (hot-reloadable via m.IsRestrictedToLocalhost())
|
||||||
ampAPI.Use(m.localhostOnlyMiddleware())
|
ampAPI.Use(m.localhostOnlyMiddleware())
|
||||||
|
|
||||||
if !m.IsRestrictedToLocalhost() {
|
// Apply authentication middleware - requires valid API key in Authorization header
|
||||||
log.Warn("amp management routes are NOT restricted to localhost - this is insecure!")
|
if auth != nil {
|
||||||
|
ampAPI.Use(auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic proxy handler that uses m.getProxy() for hot-reload support
|
// 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
|
// Root-level routes that AMP CLI expects without /api prefix
|
||||||
// These need the same security middleware as the /api/* routes (dynamic for hot-reload)
|
// These need the same security middleware as the /api/* routes (dynamic for hot-reload)
|
||||||
rootMiddleware := []gin.HandlerFunc{m.managementAvailabilityMiddleware(), noCORSMiddleware(), m.localhostOnlyMiddleware()}
|
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/*path", append(rootMiddleware, proxyHandler)...)
|
||||||
engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)
|
engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)
|
||||||
engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...)
|
engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...)
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ type AmpCode struct {
|
|||||||
|
|
||||||
// RestrictManagementToLocalhost restricts Amp management routes (/api/user, /api/threads, etc.)
|
// 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
|
// 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"`
|
RestrictManagementToLocalhost bool `yaml:"restrict-management-to-localhost" json:"restrict-management-to-localhost"`
|
||||||
|
|
||||||
// ModelMappings defines model name mappings for Amp CLI requests.
|
// 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.LoggingToFile = false
|
||||||
cfg.UsageStatisticsEnabled = false
|
cfg.UsageStatisticsEnabled = false
|
||||||
cfg.DisableCooling = 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 err = yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
if optional {
|
if optional {
|
||||||
// In cloud deploy mode, if YAML parsing fails, return empty config instead of error.
|
// In cloud deploy mode, if YAML parsing fails, return empty config instead of error.
|
||||||
|
|||||||
Reference in New Issue
Block a user