diff --git a/control-plane/cmd/af/main.go b/control-plane/cmd/af/main.go index 688b891c2..2f790bff7 100644 --- a/control-plane/cmd/af/main.go +++ b/control-plane/cmd/af/main.go @@ -216,7 +216,6 @@ func loadConfig(configFile string) (*config.Config, error) { // This is needed because Viper's AutomaticEnv only works for keys that exist in config _ = viper.BindEnv("api.auth.api_key", "AGENTFIELD_API_KEY") _ = viper.BindEnv("api.auth.api_key", "AGENTFIELD_API_AUTH_API_KEY") - _ = viper.BindEnv("api.auth.insecure_disable_auth", "AGENTFIELD_INSECURE_DISABLE_AUTH", "AGENTFIELD_API_AUTH_INSECURE_DISABLE_AUTH") // Get the directory where the binary is located for UI paths execPath, err := os.Executable() diff --git a/control-plane/cmd/agentfield-server/main.go b/control-plane/cmd/agentfield-server/main.go index 6bb96d992..090cdad59 100644 --- a/control-plane/cmd/agentfield-server/main.go +++ b/control-plane/cmd/agentfield-server/main.go @@ -245,7 +245,6 @@ func loadConfig(configFile string) (*config.Config, error) { // This is needed because Viper's AutomaticEnv only works for keys that exist in config _ = viper.BindEnv("api.auth.api_key", "AGENTFIELD_API_KEY") _ = viper.BindEnv("api.auth.api_key", "AGENTFIELD_API_AUTH_API_KEY") - _ = viper.BindEnv("api.auth.insecure_disable_auth", "AGENTFIELD_INSECURE_DISABLE_AUTH", "AGENTFIELD_API_AUTH_INSECURE_DISABLE_AUTH") // AutomaticEnv makes viper.IsSet("features.did.enabled") return true once // AGENTFIELD_FEATURES_DID_ENABLED is set, but Unmarshal won't actually // populate the struct field unless the key is bound. Without this, setting diff --git a/control-plane/config/agentfield.yaml b/control-plane/config/agentfield.yaml index f6e53f9d8..f05d9a29e 100644 --- a/control-plane/config/agentfield.yaml +++ b/control-plane/config/agentfield.yaml @@ -25,9 +25,6 @@ ui: dev_port: 5173 api: - auth: - # Local development only. Production deployments should configure api_key. - insecure_disable_auth: true cors: allowed_origins: - "http://localhost:3000" diff --git a/control-plane/config/docker-perf.yaml b/control-plane/config/docker-perf.yaml index da9ff1d1b..bd739649c 100644 --- a/control-plane/config/docker-perf.yaml +++ b/control-plane/config/docker-perf.yaml @@ -34,9 +34,6 @@ ui: backend_url: "" api: - auth: - # This benchmark configuration runs in an isolated local environment. - insecure_disable_auth: true cors: allowed_origins: - "*" diff --git a/control-plane/internal/config/config.go b/control-plane/internal/config/config.go index 933a104ec..f1bf17b18 100644 --- a/control-plane/internal/config/config.go +++ b/control-plane/internal/config/config.go @@ -307,12 +307,8 @@ type AuthorizationConfig struct { // DefaultApprovalDurationHours is the default duration for permission approvals DefaultApprovalDurationHours int `yaml:"default_approval_duration_hours" mapstructure:"default_approval_duration_hours" default:"720"` // AdminToken is a separate token required for admin operations (tag approval, - // policy management). When empty and InsecureDisableAdminAuth is false, admin - // routes will reject requests at startup or return 401. + // policy management). If empty, admin routes fall back to the standard API key. AdminToken string `yaml:"admin_token" mapstructure:"admin_token"` - // InsecureDisableAdminAuth explicitly permits running admin routes without - // an admin token. This should only be enabled for trusted local development. - InsecureDisableAdminAuth bool `yaml:"insecure_disable_admin_auth" mapstructure:"insecure_disable_admin_auth"` // InternalToken is sent as Authorization: Bearer header when the control plane // forwards execution requests to agents. Agents with RequireOriginAuth enabled // validate this token, preventing direct access to their HTTP ports. @@ -399,11 +395,8 @@ type CORSConfig struct { // AuthConfig holds API authentication configuration. type AuthConfig struct { - // APIKey is checked against headers or query params. + // APIKey is checked against headers or query params. Empty disables auth. APIKey string `yaml:"api_key" mapstructure:"api_key"` - // InsecureDisableAuth explicitly permits running without API-key authentication. - // This should only be enabled for trusted local development environments. - InsecureDisableAuth bool `yaml:"insecure_disable_auth" mapstructure:"insecure_disable_auth"` // SkipPaths allows bypassing auth for specific endpoints (e.g., health). SkipPaths []string `yaml:"skip_paths" mapstructure:"skip_paths"` } @@ -537,13 +530,6 @@ func ApplyEnvOverrides(cfg *Config) { if apiKey := os.Getenv("AGENTFIELD_API_AUTH_API_KEY"); apiKey != "" { cfg.API.Auth.APIKey = apiKey } - if val, ok := os.LookupEnv("AGENTFIELD_INSECURE_DISABLE_AUTH"); ok { - cfg.API.Auth.InsecureDisableAuth = parseEnvBool(val) - } - // Also support the nested path format for consistency. - if val, ok := os.LookupEnv("AGENTFIELD_API_AUTH_INSECURE_DISABLE_AUTH"); ok { - cfg.API.Auth.InsecureDisableAuth = parseEnvBool(val) - } if val := os.Getenv("AGENTFIELD_REGISTRATION_SERVERLESS_DISCOVERY_ALLOWED_HOSTS"); val != "" { parts := strings.Split(val, ",") @@ -662,13 +648,6 @@ func ApplyEnvOverrides(cfg *Config) { if val := os.Getenv("AGENTFIELD_AUTHORIZATION_ADMIN_TOKEN"); val != "" { cfg.Features.DID.Authorization.AdminToken = val } - if val, ok := os.LookupEnv("AGENTFIELD_INSECURE_ADMIN_NO_TOKEN"); ok { - cfg.Features.DID.Authorization.InsecureDisableAdminAuth = parseEnvBool(val) - } - // Also support the nested path format for consistency. - if val, ok := os.LookupEnv("AGENTFIELD_AUTHORIZATION_INSECURE_DISABLE_ADMIN_AUTH"); ok { - cfg.Features.DID.Authorization.InsecureDisableAdminAuth = parseEnvBool(val) - } if val := os.Getenv("AGENTFIELD_AUTHORIZATION_INTERNAL_TOKEN"); val != "" { cfg.Features.DID.Authorization.InternalToken = val } diff --git a/control-plane/internal/config/config_additional_test.go b/control-plane/internal/config/config_additional_test.go index 32b538255..bf01263e0 100644 --- a/control-plane/internal/config/config_additional_test.go +++ b/control-plane/internal/config/config_additional_test.go @@ -103,7 +103,7 @@ func TestLoadConfig(t *testing.T) { t.Run("loads explicit config path", func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "custom.yaml") - if err := os.WriteFile(path, []byte("agentfield:\n port: 7777\napi:\n auth:\n insecure_disable_auth: true\n"), 0o644); err != nil { + if err := os.WriteFile(path, []byte("agentfield:\n port: 7777\n"), 0o644); err != nil { t.Fatalf("write config: %v", err) } @@ -114,9 +114,6 @@ func TestLoadConfig(t *testing.T) { if cfg.AgentField.Port != 7777 { t.Fatalf("expected port 7777, got %d", cfg.AgentField.Port) } - if !cfg.API.Auth.InsecureDisableAuth { - t.Fatal("expected insecure_disable_auth to load from YAML") - } }) t.Run("falls back to config directory default path", func(t *testing.T) { @@ -295,7 +292,6 @@ func TestApplyEnvOverrides(t *testing.T) { "AGENTFIELD_AUTHORIZATION_DID_AUTH_ENABLED": "1", "AGENTFIELD_AUTHORIZATION_DOMAIN": "auth.local", "AGENTFIELD_AUTHORIZATION_ADMIN_TOKEN": "admin-token", - "AGENTFIELD_INSECURE_ADMIN_NO_TOKEN": "true", "AGENTFIELD_AUTHORIZATION_INTERNAL_TOKEN": "internal-token", "AGENTFIELD_AUTHORIZATION_DEFAULT_DENY": "true", "AGENTFIELD_NODE_LOG_PROXY_CONNECT_TIMEOUT": "21s", @@ -380,7 +376,6 @@ func TestApplyEnvOverrides(t *testing.T) { !cfg.Features.DID.Authorization.DIDAuthEnabled || cfg.Features.DID.Authorization.Domain != "auth.local" || cfg.Features.DID.Authorization.AdminToken != "admin-token" || - !cfg.Features.DID.Authorization.InsecureDisableAdminAuth || cfg.Features.DID.Authorization.InternalToken != "internal-token" || !cfg.Features.DID.Authorization.DefaultDeny { t.Fatalf("unexpected authorization overrides: %+v", cfg.Features.DID.Authorization) @@ -421,56 +416,6 @@ func TestApplyEnvOverrides(t *testing.T) { } } -func TestApplyEnvOverridesAPIAuthInsecureDisable(t *testing.T) { - t.Run("short environment name", func(t *testing.T) { - cfg := &Config{} - t.Setenv("AGENTFIELD_INSECURE_DISABLE_AUTH", "true") - - ApplyEnvOverrides(cfg) - - if !cfg.API.Auth.InsecureDisableAuth { - t.Fatal("expected insecure API auth disable from environment") - } - }) - - t.Run("nested environment name takes precedence", func(t *testing.T) { - cfg := &Config{} - t.Setenv("AGENTFIELD_INSECURE_DISABLE_AUTH", "true") - t.Setenv("AGENTFIELD_API_AUTH_INSECURE_DISABLE_AUTH", "false") - - ApplyEnvOverrides(cfg) - - if cfg.API.Auth.InsecureDisableAuth { - t.Fatal("expected nested insecure API auth setting to take precedence") - } - }) -} - -func TestApplyEnvOverridesInsecureAdminNoToken(t *testing.T) { - t.Run("short environment name", func(t *testing.T) { - cfg := &Config{} - t.Setenv("AGENTFIELD_INSECURE_ADMIN_NO_TOKEN", "true") - - ApplyEnvOverrides(cfg) - - if !cfg.Features.DID.Authorization.InsecureDisableAdminAuth { - t.Fatal("expected insecure admin auth disable from environment") - } - }) - - t.Run("nested environment name takes precedence", func(t *testing.T) { - cfg := &Config{} - t.Setenv("AGENTFIELD_INSECURE_ADMIN_NO_TOKEN", "true") - t.Setenv("AGENTFIELD_AUTHORIZATION_INSECURE_DISABLE_ADMIN_AUTH", "false") - - ApplyEnvOverrides(cfg) - - if cfg.Features.DID.Authorization.InsecureDisableAdminAuth { - t.Fatal("expected nested insecure admin auth setting to take precedence") - } - }) -} - func TestApplyEnvOverridesIgnoresInvalidValues(t *testing.T) { cfg := &Config{ AgentField: AgentFieldConfig{ diff --git a/control-plane/internal/server/middleware/auth.go b/control-plane/internal/server/middleware/auth.go index 19ecfbdb1..fe2c252a1 100644 --- a/control-plane/internal/server/middleware/auth.go +++ b/control-plane/internal/server/middleware/auth.go @@ -2,7 +2,6 @@ package middleware import ( "crypto/subtle" - "errors" "net/http" "strings" @@ -12,20 +11,11 @@ import ( // AuthConfig mirrors server configuration for HTTP authentication. type AuthConfig struct { APIKey string - InsecureDisableAuth bool SkipPaths []string SkipPrefixes []string QueryAPIKeyAllowedPaths []string } -// ValidateAPIKeyAuth rejects an implicit unauthenticated configuration. -func ValidateAPIKeyAuth(config AuthConfig) error { - if config.APIKey == "" && !config.InsecureDisableAuth { - return errors.New("API key is required; set AGENTFIELD_API_KEY or explicitly set AGENTFIELD_INSECURE_DISABLE_AUTH=true") - } - return nil -} - // APIKeyAuth enforces API key authentication via header or bearer token. func APIKeyAuth(config AuthConfig) gin.HandlerFunc { skipPathSet := make(map[string]struct{}, len(config.SkipPaths)) @@ -38,8 +28,8 @@ func APIKeyAuth(config AuthConfig) gin.HandlerFunc { } return func(c *gin.Context) { - // Unauthenticated operation must be explicitly enabled at startup. - if config.APIKey == "" && config.InsecureDisableAuth { + // No auth configured, allow everything. + if config.APIKey == "" { c.Next() return } @@ -103,15 +93,6 @@ func APIKeyAuth(config AuthConfig) gin.HandlerFunc { return } - if config.APIKey == "" { - c.Set("auth_level", "public") - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ - "error": "unauthorized", - "message": "authentication required but API key is not configured on the server", - }) - return - } - apiKey := "" // Preferred: X-API-Key header @@ -173,44 +154,20 @@ func queryAPIKeyAllowed(c *gin.Context, allowedPaths map[string]struct{}) bool { return false } -// AdminAuthConfig mirrors server configuration for admin token authentication. -type AdminAuthConfig struct { - AdminToken string - InsecureDisableAdminAuth bool -} - -// ValidateAdminTokenAuth rejects an implicit unauthenticated admin configuration. -func ValidateAdminTokenAuth(config AdminAuthConfig) error { - if config.AdminToken == "" && !config.InsecureDisableAdminAuth { - return errors.New("admin token is required when authorization is enabled; set AGENTFIELD_AUTHORIZATION_ADMIN_TOKEN or explicitly set AGENTFIELD_INSECURE_ADMIN_NO_TOKEN=true") - } - return nil -} - // AdminTokenAuth enforces a separate admin token for admin routes. +// If adminToken is empty, the middleware is a no-op (falls back to global API key auth). // Admin tokens must be sent via the X-Admin-Token header only (not Bearer) to avoid // collision with the API key Bearer token namespace. -func AdminTokenAuth(config AdminAuthConfig) gin.HandlerFunc { +func AdminTokenAuth(adminToken string) gin.HandlerFunc { return func(c *gin.Context) { - // Unauthenticated admin operation must be explicitly enabled at startup. - if config.AdminToken == "" && config.InsecureDisableAdminAuth { + if adminToken == "" { c.Next() return } - // Fail-closed: if the admin token is not configured and insecure mode - // was not explicitly enabled, reject all requests. - if config.AdminToken == "" { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ - "error": "unauthorized", - "message": "admin authentication required but admin token is not configured on the server", - }) - return - } - token := c.GetHeader("X-Admin-Token") - if subtle.ConstantTimeCompare([]byte(token), []byte(config.AdminToken)) != 1 { + if subtle.ConstantTimeCompare([]byte(token), []byte(adminToken)) != 1 { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "forbidden", "message": "admin token required for this operation (use X-Admin-Token header)", diff --git a/control-plane/internal/server/middleware/auth_admin_additional_test.go b/control-plane/internal/server/middleware/auth_admin_additional_test.go index c778a0b96..28e680707 100644 --- a/control-plane/internal/server/middleware/auth_admin_additional_test.go +++ b/control-plane/internal/server/middleware/auth_admin_additional_test.go @@ -1,7 +1,6 @@ package middleware import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -10,59 +9,34 @@ import ( "github.com/stretchr/testify/require" ) -func TestValidateAdminTokenAuth_RejectsImplicitDisable(t *testing.T) { - err := ValidateAdminTokenAuth(AdminAuthConfig{}) - require.Error(t, err) - require.Contains(t, err.Error(), "AGENTFIELD_INSECURE_ADMIN_NO_TOKEN=true") -} - -func TestValidateAdminTokenAuth_AcceptsExplicitInsecure(t *testing.T) { - err := ValidateAdminTokenAuth(AdminAuthConfig{InsecureDisableAdminAuth: true}) - require.NoError(t, err) -} - -func TestValidateAdminTokenAuth_AcceptsConfiguredToken(t *testing.T) { - err := ValidateAdminTokenAuth(AdminAuthConfig{AdminToken: "admin-secret"}) - require.NoError(t, err) -} - func TestAdminTokenAuth(t *testing.T) { tests := []struct { name string - config AdminAuthConfig + adminToken string headerToken string wantStatus int - wantBody string }{ { - name: "empty token with explicit insecure allows request through", - config: AdminAuthConfig{InsecureDisableAdminAuth: true}, + name: "disabled allows request through", + adminToken: "", wantStatus: http.StatusOK, }, - { - name: "empty token without insecure flag returns 401", - config: AdminAuthConfig{}, - wantStatus: http.StatusUnauthorized, - wantBody: "admin authentication required but admin token is not configured", - }, { name: "valid admin token", - config: AdminAuthConfig{AdminToken: "admin-secret"}, + adminToken: "admin-secret", headerToken: "admin-secret", wantStatus: http.StatusOK, }, { - name: "missing admin token header", - config: AdminAuthConfig{AdminToken: "admin-secret"}, - wantStatus: http.StatusForbidden, - wantBody: "admin token required", + name: "missing admin token header", + adminToken: "admin-secret", + wantStatus: http.StatusForbidden, }, { name: "invalid admin token header", - config: AdminAuthConfig{AdminToken: "admin-secret"}, + adminToken: "admin-secret", headerToken: "wrong-secret", wantStatus: http.StatusForbidden, - wantBody: "admin token required", }, } @@ -70,7 +44,7 @@ func TestAdminTokenAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() - router.Use(AdminTokenAuth(tt.config)) + router.Use(AdminTokenAuth(tt.adminToken)) router.GET("/admin", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) @@ -84,49 +58,9 @@ func TestAdminTokenAuth(t *testing.T) { router.ServeHTTP(recorder, req) require.Equal(t, tt.wantStatus, recorder.Code) - if tt.wantBody != "" { - require.Contains(t, recorder.Body.String(), tt.wantBody) + if tt.wantStatus == http.StatusForbidden { + require.Contains(t, recorder.Body.String(), "admin token required") } }) } } - -func TestAdminTokenAuth_EmptyTokenFailsClosed(t *testing.T) { - gin.SetMode(gin.TestMode) - router := gin.New() - router.Use(AdminTokenAuth(AdminAuthConfig{})) - router.GET("/admin/tags", func(c *gin.Context) { - c.String(http.StatusOK, "ok") - }) - - // Request with only a valid API key (simulated via no admin token header) - // should be rejected because admin token is not configured. - req := httptest.NewRequest(http.MethodGet, "/admin/tags", nil) - recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) - - require.Equal(t, http.StatusUnauthorized, recorder.Code) - - var resp map[string]string - require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) - require.Equal(t, "unauthorized", resp["error"]) - require.Contains(t, resp["message"], "admin token is not configured") -} - -func TestAdminTokenAuth_APIKeyOnlyReturns401WhenAdminTokenConfigured(t *testing.T) { - gin.SetMode(gin.TestMode) - router := gin.New() - router.Use(AdminTokenAuth(AdminAuthConfig{AdminToken: "admin-secret"})) - router.GET("/admin/policies", func(c *gin.Context) { - c.String(http.StatusOK, "ok") - }) - - // Request with API key header but NO admin token — should return 403 - req := httptest.NewRequest(http.MethodGet, "/admin/policies", nil) - req.Header.Set("X-API-Key", "some-api-key") - recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) - - require.Equal(t, http.StatusForbidden, recorder.Code) - require.Contains(t, recorder.Body.String(), "admin token required") -} diff --git a/control-plane/internal/server/middleware/auth_test.go b/control-plane/internal/server/middleware/auth_test.go index bef938e1e..e6b879a37 100644 --- a/control-plane/internal/server/middleware/auth_test.go +++ b/control-plane/internal/server/middleware/auth_test.go @@ -40,25 +40,9 @@ func setupRouter(config AuthConfig) *gin.Engine { return router } -func TestValidateAPIKeyAuth_RejectsImplicitDisable(t *testing.T) { - err := ValidateAPIKeyAuth(AuthConfig{}) - require.Error(t, err) - require.Contains(t, err.Error(), "AGENTFIELD_INSECURE_DISABLE_AUTH=true") -} - -func TestAPIKeyAuth_EmptyKeyFailsClosed(t *testing.T) { - router := setupRouter(AuthConfig{}) - - req := httptest.NewRequest(http.MethodGet, "/api/v1/test", nil) - w := httptest.NewRecorder() - - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusUnauthorized, w.Code) -} - -func TestAPIKeyAuth_ExplicitInsecureDisable(t *testing.T) { - router := setupRouter(AuthConfig{InsecureDisableAuth: true}) +func TestAPIKeyAuth_NoAuthConfigured(t *testing.T) { + // When no API key is configured, all requests should be allowed + router := setupRouter(AuthConfig{APIKey: ""}) req := httptest.NewRequest(http.MethodGet, "/api/v1/test", nil) w := httptest.NewRecorder() diff --git a/control-plane/internal/server/routes_admin.go b/control-plane/internal/server/routes_admin.go index cd22fc0ac..5ae109fd3 100644 --- a/control-plane/internal/server/routes_admin.go +++ b/control-plane/internal/server/routes_admin.go @@ -17,10 +17,7 @@ func (s *AgentFieldServer) registerAdminRoutes(agentAPI *gin.RouterGroup) { // Admin routes for tag approval and access policy management (VC-based authorization) if s.config.Features.DID.Authorization.Enabled { adminGroup := agentAPI.Group("") - adminGroup.Use(middleware.AdminTokenAuth(middleware.AdminAuthConfig{ - AdminToken: s.config.Features.DID.Authorization.AdminToken, - InsecureDisableAdminAuth: s.config.Features.DID.Authorization.InsecureDisableAdminAuth, - })) + adminGroup.Use(middleware.AdminTokenAuth(s.config.Features.DID.Authorization.AdminToken)) // Tag approval admin routes if s.tagApprovalService != nil { diff --git a/control-plane/internal/server/routes_middleware.go b/control-plane/internal/server/routes_middleware.go index 699d77ba6..ed48cc311 100644 --- a/control-plane/internal/server/routes_middleware.go +++ b/control-plane/internal/server/routes_middleware.go @@ -78,7 +78,6 @@ func (s *AgentFieldServer) applyGlobalMiddleware() { skipPaths = uniqueStrings(skipPaths) s.Router.Use(middleware.APIKeyAuth(middleware.AuthConfig{ APIKey: s.config.API.Auth.APIKey, - InsecureDisableAuth: s.config.API.Auth.InsecureDisableAuth, SkipPaths: skipPaths, SkipPrefixes: uniqueStrings(skipPrefixes), QueryAPIKeyAllowedPaths: streamingQueryAPIKeyAllowedPaths(), diff --git a/control-plane/internal/server/server.go b/control-plane/internal/server/server.go index f159b883d..926b85718 100644 --- a/control-plane/internal/server/server.go +++ b/control-plane/internal/server/server.go @@ -100,16 +100,6 @@ type AgentFieldServer struct { // NewAgentFieldServer creates a new instance of the AgentFieldServer. func NewAgentFieldServer(cfg *config.Config) (*AgentFieldServer, error) { - if err := validateAPIAuthConfig(cfg.API.Auth); err != nil { - return nil, fmt.Errorf("invalid API authentication configuration: %w", err) - } - - if cfg.Features.DID.Authorization.Enabled { - if err := validateAdminAuthConfig(cfg.Features.DID.Authorization); err != nil { - return nil, fmt.Errorf("invalid admin authentication configuration: %w", err) - } - } - // Define agentfieldHome at the very top agentfieldHome := os.Getenv("AGENTFIELD_HOME") if agentfieldHome == "" { @@ -291,6 +281,9 @@ func NewAgentFieldServer(cfg *config.Config) (*AgentFieldServer, error) { didWebService = services.NewDIDWebService(domain, didService, storageProvider) if cfg.Features.DID.Authorization.Enabled { + if cfg.Features.DID.Authorization.AdminToken == "" { + logger.Logger.Error().Msg("⚠️ SECURITY WARNING: Authorization is enabled but no admin_token is configured! Admin routes (tag approval, policy management) are unprotected. Set AGENTFIELD_AUTHORIZATION_ADMIN_TOKEN for production use.") + } if cfg.Features.DID.Authorization.TagApprovalRules.DefaultMode == "" || cfg.Features.DID.Authorization.TagApprovalRules.DefaultMode == "auto" { logger.Logger.Warn().Msg("⚠️ Tag approval default_mode is 'auto' — all agent tags will be auto-approved. Set tag_approval_rules.default_mode to 'manual' for production.") } @@ -537,38 +530,6 @@ func NewAgentFieldServer(cfg *config.Config) (*AgentFieldServer, error) { }, nil } -func validateAPIAuthConfig(auth config.AuthConfig) error { - middlewareConfig := middleware.AuthConfig{ - APIKey: auth.APIKey, - InsecureDisableAuth: auth.InsecureDisableAuth, - } - if err := middleware.ValidateAPIKeyAuth(middlewareConfig); err != nil { - return err - } - if auth.APIKey == "" { - logger.Logger.Warn(). - Bool("insecure_disable_auth", true). - Msg("SECURITY WARNING: API key authentication is explicitly disabled; all API routes relying on API-key authentication are unauthenticated") - } - return nil -} - -func validateAdminAuthConfig(authz config.AuthorizationConfig) error { - adminConfig := middleware.AdminAuthConfig{ - AdminToken: authz.AdminToken, - InsecureDisableAdminAuth: authz.InsecureDisableAdminAuth, - } - if err := middleware.ValidateAdminTokenAuth(adminConfig); err != nil { - return err - } - if authz.AdminToken == "" { - logger.Logger.Warn(). - Bool("insecure_disable_admin_auth", true). - Msg("SECURITY WARNING: Admin token authentication is explicitly disabled; admin routes (tag approval, policy management) are unauthenticated") - } - return nil -} - // configReloadFn returns a function that reloads config from the database, // or nil if AGENTFIELD_CONFIG_SOURCE is not set to "db". // The returned function acquires configMu to prevent data races with diff --git a/control-plane/internal/server/server_additional_test.go b/control-plane/internal/server/server_additional_test.go index dfcfda505..09a8656d1 100644 --- a/control-plane/internal/server/server_additional_test.go +++ b/control-plane/internal/server/server_additional_test.go @@ -1,7 +1,6 @@ package server import ( - "bytes" "context" "errors" "fmt" @@ -15,13 +14,11 @@ import ( "time" "github.com/Agent-Field/agentfield/control-plane/internal/config" - "github.com/Agent-Field/agentfield/control-plane/internal/logger" "github.com/Agent-Field/agentfield/control-plane/internal/services" "github.com/Agent-Field/agentfield/control-plane/internal/storage" "github.com/Agent-Field/agentfield/control-plane/pkg/adminpb" "github.com/Agent-Field/agentfield/control-plane/pkg/types" "github.com/gin-gonic/gin" - "github.com/rs/zerolog" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -72,15 +69,6 @@ func TestConfigReloadFn(t *testing.T) { } func TestNewAgentFieldServer(t *testing.T) { - t.Run("rejects an empty API key without explicit insecure mode", func(t *testing.T) { - cfg := baseConfigForDBTests() - cfg.API.Auth.APIKey = "" - - srv, err := NewAgentFieldServer(&cfg) - require.Nil(t, srv) - require.ErrorContains(t, err, "AGENTFIELD_INSECURE_DISABLE_AUTH=true") - }) - t.Run("uses env home and explicit admin port", func(t *testing.T) { cfg := baseConfigForDBTests() cfg.UI.Enabled = false @@ -132,14 +120,12 @@ func TestNewAgentFieldServer(t *testing.T) { cfg := baseConfigForDBTests() cfg.UI.Enabled = false cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.Features.DID.Enabled = true cfg.Features.DID.KeyAlgorithm = "Ed25519" cfg.Features.DID.Authorization.Enabled = true cfg.Features.DID.Authorization.DIDAuthEnabled = true cfg.Features.DID.Authorization.Domain = "example.com" cfg.Features.DID.Authorization.InternalToken = "internal-token" - cfg.Features.DID.Authorization.InsecureDisableAdminAuth = true cfg.Features.DID.Authorization.TagApprovalRules.DefaultMode = "manual" cfg.Features.Connector.Enabled = false @@ -187,43 +173,6 @@ func TestStartAdminGRPCServer(t *testing.T) { } } -func TestValidateAPIAuthConfigWarnsWhenExplicitlyDisabled(t *testing.T) { - previousLogger := logger.Logger - var output bytes.Buffer - logger.Logger = zerolog.New(&output) - t.Cleanup(func() { logger.Logger = previousLogger }) - - err := validateAPIAuthConfig(config.AuthConfig{InsecureDisableAuth: true}) - require.NoError(t, err) - require.Contains(t, output.String(), `"level":"warn"`) - require.Contains(t, output.String(), "API key authentication is explicitly disabled") -} - -func TestValidateAdminAuthConfigWarnsWhenExplicitlyDisabled(t *testing.T) { - previousLogger := logger.Logger - var output bytes.Buffer - logger.Logger = zerolog.New(&output) - t.Cleanup(func() { logger.Logger = previousLogger }) - - err := validateAdminAuthConfig(config.AuthorizationConfig{InsecureDisableAdminAuth: true}) - require.NoError(t, err) - require.Contains(t, output.String(), `"level":"warn"`) - require.Contains(t, output.String(), "Admin token authentication is explicitly disabled") -} - -func TestNewAgentFieldServerRejectsEmptyAdminToken(t *testing.T) { - cfg := baseConfigForDBTests() - cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true - cfg.Features.DID.Enabled = false - cfg.Features.DID.Authorization.Enabled = true - cfg.Features.DID.Authorization.AdminToken = "" - - srv, err := NewAgentFieldServer(&cfg) - require.Nil(t, srv) - require.ErrorContains(t, err, "AGENTFIELD_INSECURE_ADMIN_NO_TOKEN=true") -} - func TestStartAndStop(t *testing.T) { cfg := baseConfigForDBTests() cfg.UI.Enabled = false @@ -302,7 +251,6 @@ func TestSetupRoutesFilesystemUIAndNoRouteFallback(t *testing.T) { cfg.UI.Mode = "filesystem" cfg.UI.DistPath = distDir cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.API.Auth.SkipPaths = nil cfg.API.CORS = config.CORSConfig{} cfg.Features.DID.Enabled = false @@ -351,7 +299,6 @@ func TestSetupRoutesRegistersAuthorizationAndConnectorBranches(t *testing.T) { cfg := baseConfigForDBTests() cfg.UI.Enabled = false cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.Features.DID.Enabled = true cfg.Features.DID.Authorization.Enabled = true cfg.Features.DID.Authorization.DIDAuthEnabled = true @@ -379,14 +326,12 @@ func TestSetupRoutesWithDIDServices(t *testing.T) { cfg := baseConfigForDBTests() cfg.UI.Enabled = false cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.Features.DID.Enabled = true cfg.Features.DID.KeyAlgorithm = "Ed25519" cfg.Features.DID.Authorization.Enabled = true cfg.Features.DID.Authorization.DIDAuthEnabled = true cfg.Features.DID.Authorization.Domain = "example.com" cfg.Features.DID.Authorization.InternalToken = "internal-token" - cfg.Features.DID.Authorization.InsecureDisableAdminAuth = true cfg.Features.DID.Authorization.TagApprovalRules.DefaultMode = "manual" cfg.Features.Connector.Enabled = false diff --git a/control-plane/internal/server/server_coverage_additional_test.go b/control-plane/internal/server/server_coverage_additional_test.go index 5e6757869..b87417613 100644 --- a/control-plane/internal/server/server_coverage_additional_test.go +++ b/control-plane/internal/server/server_coverage_additional_test.go @@ -40,12 +40,10 @@ func TestNewAgentFieldServerCoversFallbacksAndOptionalServices(t *testing.T) { cfg := baseConfigForDBTests() cfg.UI.Enabled = false cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.Features.Connector.Enabled = false cfg.Features.DID.Enabled = true cfg.Features.DID.KeyAlgorithm = "Ed25519" cfg.Features.DID.Authorization.Enabled = true - cfg.Features.DID.Authorization.InsecureDisableAdminAuth = true cfg.Features.DID.Authorization.Domain = "" cfg.Features.DID.Authorization.TagApprovalRules.DefaultMode = "auto" cfg.Features.DID.Authorization.AccessPolicies = []config.AccessPolicyConfig{ @@ -119,7 +117,6 @@ func TestStartAndStopCoverAdditionalBranches(t *testing.T) { cfg.Features.DID.Enabled = false cfg.Features.Connector.Enabled = false cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.Features.Tracing.Enabled = true cfg.Features.Tracing.Insecure = true cfg.AgentField.LLMHealth.Enabled = true @@ -166,7 +163,6 @@ func TestStartCoversRecoveryErrorBranches(t *testing.T) { cfg.Features.DID.Enabled = false cfg.Features.Connector.Enabled = false cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true errStorage := &listAgentsStorage{stubStorage: newStubStorage(), err: errors.New("list failed")} statusManager := services.NewStatusManager(errStorage, services.StatusManagerConfig{ @@ -222,7 +218,6 @@ func TestSetupRoutesFilesystemFallbackDistPath(t *testing.T) { cfg.UI.Mode = "filesystem" cfg.UI.DistPath = "" cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.API.CORS = config.CORSConfig{} cfg.Features.DID.Enabled = false cfg.Features.Connector.Enabled = false @@ -384,7 +379,6 @@ func TestSetupRoutesAgentFieldDIDRouteHandlesUninitializedService(t *testing.T) cfg := baseConfigForDBTests() cfg.UI.Enabled = false cfg.API.Auth.APIKey = "" - cfg.API.Auth.InsecureDisableAuth = true cfg.Features.DID.Enabled = true cfg.Features.Connector.Enabled = false diff --git a/control-plane/internal/server/server_routes_test.go b/control-plane/internal/server/server_routes_test.go index 93346909e..dac0ea7e3 100644 --- a/control-plane/internal/server/server_routes_test.go +++ b/control-plane/internal/server/server_routes_test.go @@ -508,7 +508,7 @@ func TestSetupRoutesRegistersMetricsAndUI(t *testing.T) { webhookDispatcher: &stubWebhookDispatcher{}, config: &config.Config{ UI: config.UIConfig{Enabled: true, Mode: "embedded"}, - API: config.APIConfig{Auth: config.AuthConfig{InsecureDisableAuth: true}}, + API: config.APIConfig{}, }, } @@ -544,7 +544,7 @@ func TestSetupRoutesRegistersWorkflowCleanupUIRoute(t *testing.T) { webhookDispatcher: &stubWebhookDispatcher{}, config: &config.Config{ UI: config.UIConfig{Enabled: true, Mode: "embedded"}, - API: config.APIConfig{Auth: config.AuthConfig{InsecureDisableAuth: true}}, + API: config.APIConfig{}, }, } @@ -572,7 +572,7 @@ func TestSetupRoutesRegistersHealthEndpoint(t *testing.T) { webhookDispatcher: &stubWebhookDispatcher{}, config: &config.Config{ UI: config.UIConfig{Enabled: false}, - API: config.APIConfig{Auth: config.AuthConfig{InsecureDisableAuth: true}}, + API: config.APIConfig{}, }, storageHealthOverride: func(context.Context) gin.H { return gin.H{"status": "healthy"} }, } @@ -622,7 +622,7 @@ func TestSetupRoutesRegistersHealthEndpoint(t *testing.T) { webhookDispatcher: &stubWebhookDispatcher{}, config: &config.Config{ UI: config.UIConfig{Enabled: false}, - API: config.APIConfig{Auth: config.AuthConfig{InsecureDisableAuth: true}}, + API: config.APIConfig{}, }, storageHealthOverride: func(context.Context) gin.H { return gin.H{"status": "healthy"} }, } diff --git a/deployments/helm/agentfield/templates/control-plane-deployment.yaml b/deployments/helm/agentfield/templates/control-plane-deployment.yaml index d4252f905..bcac3a872 100644 --- a/deployments/helm/agentfield/templates/control-plane-deployment.yaml +++ b/deployments/helm/agentfield/templates/control-plane-deployment.yaml @@ -86,9 +86,6 @@ spec: secretKeyRef: name: {{ include "agentfield.apiAuth.secretName" . }} key: {{ .Values.apiAuth.existingSecretKey }} - {{- else }} - - name: AGENTFIELD_INSECURE_DISABLE_AUTH - value: "true" {{- end }} {{- with .Values.controlPlane.extraEnv }} {{- toYaml . | nindent 12 }} diff --git a/docs/ENVIRONMENT_VARIABLES.md b/docs/ENVIRONMENT_VARIABLES.md index 2c565ea7d..de7192c9a 100644 --- a/docs/ENVIRONMENT_VARIABLES.md +++ b/docs/ENVIRONMENT_VARIABLES.md @@ -39,17 +39,11 @@ Example DSNs: - `postgres://agentfield:agentfield@postgres:5432/agentfield?sslmode=disable` - `postgresql://agentfield:agentfield@postgres:5432/agentfield?sslmode=disable` -### API Authentication +### API Authentication (optional) -The control plane requires an API key for most endpoints. To run without API-key -authentication, insecure mode must be explicitly enabled and should only be used -in trusted local development environments. +If set, the control plane requires an API key for most endpoints. - `AGENTFIELD_API_KEY` or `AGENTFIELD_API_AUTH_API_KEY`: API key checked by the control plane. -- `AGENTFIELD_INSECURE_DISABLE_AUTH` or `AGENTFIELD_API_AUTH_INSECURE_DISABLE_AUTH`: explicitly allow startup without an API key. - -The equivalent YAML configuration is `api.auth.api_key` or -`api.auth.insecure_disable_auth: true`. ### UI diff --git a/tests/functional/docker/agentfield-test.yaml b/tests/functional/docker/agentfield-test.yaml index 8a429e5f3..3f41b9be9 100644 --- a/tests/functional/docker/agentfield-test.yaml +++ b/tests/functional/docker/agentfield-test.yaml @@ -63,9 +63,6 @@ ui: mode: "embedded" api: - auth: - # Functional tests run on an isolated Docker network. - insecure_disable_auth: true cors: allowed_origins: - "*"