diff --git a/cmd/tx_indexer/main.go b/cmd/tx_indexer/main.go index f49b8149..a9a311f7 100644 --- a/cmd/tx_indexer/main.go +++ b/cmd/tx_indexer/main.go @@ -50,6 +50,7 @@ func main() { Enabled: true, Host: cfg.Metrics.Host, Port: cfg.Metrics.Port, + Token: cfg.Metrics.Token, } _ = internalMetrics.StartMetricsServer(metricsConfig, []string{internalMetrics.ServiceTxIndexer}, logger) diff --git a/cmd/verifier/main.go b/cmd/verifier/main.go index 7fc8c797..314c3a65 100644 --- a/cmd/verifier/main.go +++ b/cmd/verifier/main.go @@ -83,7 +83,6 @@ func main() { supportedChains, ) - // Initialize metrics based on configuration var httpMetrics *internalMetrics.HTTPMetrics var appStoreCollector *internalMetrics.AppStoreCollector if cfg.Metrics.Enabled { @@ -94,9 +93,9 @@ func main() { Enabled: true, Host: cfg.Metrics.Host, Port: cfg.Metrics.Port, + Token: cfg.Metrics.Token, }, services, logger) - // Create HTTP metrics implementation httpMetrics = internalMetrics.NewHTTPMetrics() appStoreMetrics := internalMetrics.NewAppStoreMetrics() diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 4c15ef9a..f0642ae2 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -129,20 +129,18 @@ func main() { vaultMgmService, ) - // Initialize metrics based on configuration var workerMetrics internalMetrics.WorkerMetricsInterface if cfg.Metrics.Enabled { logger.Info("Metrics enabled, setting up Prometheus metrics") - // Start metrics HTTP server with worker metrics services := []string{internalMetrics.ServiceWorker} _ = internalMetrics.StartMetricsServer(internalMetrics.Config{ Enabled: true, Host: cfg.Metrics.Host, Port: cfg.Metrics.Port, + Token: cfg.Metrics.Token, }, services, logger) - // Create worker metrics instance workerMetrics = internalMetrics.NewWorkerMetrics() } else { logger.Info("Worker metrics disabled") diff --git a/config/config.go b/config/config.go index 6d63c679..564c19a5 100644 --- a/config/config.go +++ b/config/config.go @@ -56,6 +56,7 @@ type MetricsConfig struct { Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` Host string `mapstructure:"host" json:"host,omitempty"` Port int `mapstructure:"port" json:"port,omitempty"` + Token string `mapstructure:"token" json:"token,omitempty"` } type PluginAssetsConfig struct { diff --git a/internal/api/policy.go b/internal/api/policy.go index 103b6e09..220e7e37 100644 --- a/internal/api/policy.go +++ b/internal/api/policy.go @@ -71,20 +71,13 @@ func (s *Server) CreatePluginPolicy(c echo.Context) error { if policy.ID == uuid.Nil { policy.ID = uuid.New() } - publicKey, ok := c.Get("vault_public_key").(string) - if !ok || publicKey == "" { - return c.JSON(http.StatusInternalServerError, NewErrorResponseWithMessage(msgVaultPublicKeyGetFailed)) - } - if policy.PublicKey != publicKey { - return c.JSON(http.StatusForbidden, NewErrorResponseWithMessage(msgPublicKeyMismatch)) - } var ( isTrialActive bool err error ) err = s.db.WithTransaction(c.Request().Context(), func(ctx context.Context, tx pgx.Tx) error { - isTrialActive, _, err = s.db.IsTrialActive(ctx, tx, publicKey) + isTrialActive, _, err = s.db.IsTrialActive(ctx, tx, policy.PublicKey) return err }) if err != nil { @@ -92,7 +85,7 @@ func (s *Server) CreatePluginPolicy(c echo.Context) error { } if !isTrialActive { - filePathName := common.GetVaultBackupFilename(publicKey, vtypes.PluginVultisigFees_feee.String()) + filePathName := common.GetVaultBackupFilename(policy.PublicKey, vtypes.PluginVultisigFees_feee.String()) exist, err := s.vaultStorage.Exist(filePathName) if err != nil { s.logger.WithError(err).Error("failed to check vault existence") @@ -197,18 +190,13 @@ func (s *Server) UpdatePluginPolicyById(c echo.Context) error { return c.JSON(http.StatusBadRequest, NewErrorResponseWithMessage(msgRequestParseFailed)) } - publicKey, ok := c.Get("vault_public_key").(string) - if !ok || publicKey == "" { - return c.JSON(http.StatusInternalServerError, NewErrorResponseWithMessage(msgVaultPublicKeyGetFailed)) - } - oldPolicy, err := s.policyService.GetPluginPolicy(c.Request().Context(), policy.ID) if err != nil { s.logger.WithError(err).Errorf("failed to get plugin policy, id:%s", policy.ID) return c.JSON(http.StatusInternalServerError, NewErrorResponseWithMessage(msgPolicyGetFailed)) } - if oldPolicy.PublicKey != publicKey || policy.PublicKey != publicKey { + if oldPolicy.PublicKey != policy.PublicKey { return c.JSON(http.StatusForbidden, NewErrorResponseWithMessage(msgPublicKeyMismatch)) } diff --git a/internal/api/server.go b/internal/api/server.go index 6aa5b516..f944675b 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -160,27 +160,27 @@ func (s *Server) StartServer() error { tokenGroup.GET("", s.GetActiveTokens) // Protected vault endpoints - vaultGroup := e.Group("/vault", s.VaultAuthMiddleware) - // Reshare vault endpoint, only user who already log in can request resharing + vaultGroup := e.Group("/vault") + // Reshare vault endpoint vaultGroup.POST("/reshare", s.ReshareVault) - vaultGroup.GET("/get/:pluginId/:publicKeyECDSA", s.GetVault) // Get Vault Data - vaultGroup.GET("/exist/:pluginId/:publicKeyECDSA", s.ExistVault) // Check if Vault exists + vaultGroup.GET("/get/:pluginId/:publicKeyECDSA", s.GetVault, s.VaultAuthMiddleware) // Get Vault Data (secured) + vaultGroup.GET("/exist/:pluginId/:publicKeyECDSA", s.ExistVault) // Check if Vault exists // Sign endpoint, plugin should authenticate themselves using the API Key issued by the Verifier pluginSigner := e.Group("/plugin-signer", s.PluginAuthMiddleware) pluginSigner.POST("/sign", s.SignPluginMessages) // Sign messages pluginSigner.GET("/sign/response/:taskId", s.GetKeysignResult) // Get keysign result - pluginGroup := e.Group("/plugin", s.VaultAuthMiddleware) - pluginGroup.DELETE("/:pluginId", s.DeletePlugin) // Delete plugin - pluginGroup.POST("/policy", s.CreatePluginPolicy) + pluginGroup := e.Group("/plugin") + pluginGroup.DELETE("/:pluginId", s.DeletePlugin, s.VaultAuthMiddleware) + pluginGroup.POST("/policy", s.CreatePluginPolicy) // Every valid request should be signed pluginGroup.PUT("/policy", s.UpdatePluginPolicyById) - pluginGroup.GET("/policies/:pluginId", s.GetAllPluginPolicies) - pluginGroup.GET("/policy/:policyId", s.GetPluginPolicyById) - pluginGroup.GET("/policy/:pluginId/total-count", s.GetPluginInstallationsCountByID) - pluginGroup.DELETE("/policy/:policyId", s.DeletePluginPolicyById) - pluginGroup.GET("/policies/:policyId/history", s.GetPluginPolicyTransactionHistory) - pluginGroup.GET("/transactions", s.GetPluginTransactionHistory) + pluginGroup.GET("/policies/:pluginId", s.GetAllPluginPolicies, s.VaultAuthMiddleware) + pluginGroup.GET("/policy/:policyId", s.GetPluginPolicyById, s.VaultAuthMiddleware) + pluginGroup.GET("/policy/:pluginId/total-count", s.GetPluginInstallationsCountByID, s.VaultAuthMiddleware) + pluginGroup.DELETE("/policy/:policyId", s.DeletePluginPolicyById, s.VaultAuthMiddleware) + pluginGroup.GET("/policies/:policyId/history", s.GetPluginPolicyTransactionHistory, s.VaultAuthMiddleware) + pluginGroup.GET("/transactions", s.GetPluginTransactionHistory, s.VaultAuthMiddleware) // fee group. These should only be accessible by the plugin server feeGroup := e.Group("/fees", s.PluginAuthMiddleware) diff --git a/internal/metrics/appstore_collector.go b/internal/metrics/appstore_collector.go index ba74bf9b..72f118d8 100644 --- a/internal/metrics/appstore_collector.go +++ b/internal/metrics/appstore_collector.go @@ -2,6 +2,7 @@ package metrics import ( "context" + "sync" "time" "github.com/sirupsen/logrus" @@ -20,6 +21,7 @@ type AppStoreCollector struct { interval time.Duration stopCh chan struct{} doneCh chan struct{} + stopOnce sync.Once } func NewAppStoreCollector(db DatabaseQuerier, metrics *AppStoreMetrics, logger *logrus.Logger, interval time.Duration) *AppStoreCollector { @@ -57,12 +59,15 @@ func (c *AppStoreCollector) Start() { } func (c *AppStoreCollector) Stop() { - close(c.stopCh) - <-c.doneCh + c.stopOnce.Do(func() { + close(c.stopCh) + <-c.doneCh + }) } func (c *AppStoreCollector) collect() { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), c.interval*9/10) + defer cancel() installations, err := c.db.GetInstallationsByPlugin(ctx) if err != nil { diff --git a/internal/metrics/server.go b/internal/metrics/server.go index dfb223af..813c9bb6 100644 --- a/internal/metrics/server.go +++ b/internal/metrics/server.go @@ -2,8 +2,10 @@ package metrics import ( "context" + "crypto/subtle" "fmt" "net/http" + "strings" "time" "github.com/prometheus/client_golang/prometheus" @@ -16,6 +18,7 @@ type Config struct { Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` Host string `mapstructure:"host" json:"host,omitempty"` Port int `mapstructure:"port" json:"port,omitempty"` + Token string `mapstructure:"token" json:"token,omitempty"` } // DefaultConfig returns default metrics configuration @@ -31,20 +34,33 @@ func DefaultConfig() Config { type Server struct { server *http.Server logger *logrus.Logger + token string } // NewServer creates a new metrics server with a custom registry -func NewServer(host string, port int, logger *logrus.Logger, registry *prometheus.Registry) *Server { +func NewServer(host string, port int, token string, logger *logrus.Logger, registry *prometheus.Registry) *Server { + s := &Server{ + logger: logger, + token: token, + } + mux := http.NewServeMux() - // Register the Prometheus metrics handler with custom registry + var metricsHandler http.Handler if registry != nil { - mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) + metricsHandler = promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) + } else { + metricsHandler = promhttp.Handler() + } + + if token != "" { + logger.Info("Metrics endpoint protected with Bearer token authentication") + mux.Handle("/metrics", s.authMiddleware(metricsHandler)) } else { - mux.Handle("/metrics", promhttp.Handler()) + logger.Warn("Metrics endpoint is NOT protected - consider setting metrics.token in config") + mux.Handle("/metrics", metricsHandler) } - // Add a health check endpoint mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) @@ -70,10 +86,42 @@ func NewServer(host string, port int, logger *logrus.Logger, registry *prometheu IdleTimeout: 15 * time.Second, } - return &Server{ - server: server, - logger: logger, - } + s.server = server + return s +} + +func (s *Server) authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "missing authorization header", http.StatusUnauthorized) + return + } + + const prefix = "Bearer " + if !strings.HasPrefix(authHeader, prefix) { + http.Error(w, "invalid authorization header format", http.StatusUnauthorized) + return + } + + token := authHeader[len(prefix):] + if token == "" { + http.Error(w, "missing token", http.StatusUnauthorized) + return + } + + if !s.validateToken(token) { + http.Error(w, "invalid token", http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, r) + }) +} + +func (s *Server) validateToken(token string) bool { + tokenBytes := []byte(token) + return subtle.ConstantTimeCompare([]byte(s.token), tokenBytes) == 1 } // Start starts the metrics server in a goroutine @@ -99,11 +147,10 @@ func StartMetricsServer(cfg Config, services []string, logger *logrus.Logger) *S return nil } - // Create registry and register metrics for specified services registry := prometheus.NewRegistry() RegisterMetrics(services, registry, logger) - server := NewServer(cfg.Host, cfg.Port, logger, registry) + server := NewServer(cfg.Host, cfg.Port, cfg.Token, logger, registry) server.Start() return server } @@ -115,7 +162,7 @@ func StartMetricsServerWithRegistry(cfg Config, registry *prometheus.Registry, l return nil } - server := NewServer(cfg.Host, cfg.Port, logger, registry) + server := NewServer(cfg.Host, cfg.Port, cfg.Token, logger, registry) server.Start() return server } diff --git a/plugin/server/server.go b/plugin/server/server.go index ca1ffe7a..63617b04 100644 --- a/plugin/server/server.go +++ b/plugin/server/server.go @@ -116,15 +116,15 @@ func (s *Server) GetRouter() *echo.Echo { e.GET("/healthz", s.handleHealthz) vlt := e.Group("/vault") - vlt.POST("/reshare", s.handleReshareVault, s.VerifierAuthMiddleware) + vlt.POST("/reshare", s.handleReshareVault) vlt.GET("/get/:pluginId/:publicKeyECDSA", s.handleGetVault) vlt.GET("/exist/:pluginId/:publicKeyECDSA", s.handleExistVault) vlt.GET("/sign/response/:taskId", s.handleGetKeysignResult) vlt.DELETE("/:pluginId/:publicKeyECDSA", s.handleDeleteVault, s.VerifierAuthMiddleware) plg := e.Group("/plugin") - plg.POST("/policy", s.handleCreatePluginPolicy, s.VerifierAuthMiddleware) - plg.PUT("/policy", s.handleUpdatePluginPolicyById, s.VerifierAuthMiddleware) + plg.POST("/policy", s.handleCreatePluginPolicy) + plg.PUT("/policy", s.handleUpdatePluginPolicyById) plg.GET("/recipe-specification", s.handleGetRecipeSpecification) plg.POST("/recipe-specification/suggest", s.handleGetRecipeSpecificationSuggest) plg.DELETE("/policy/:policyId", s.handleDeletePluginPolicyById, s.VerifierAuthMiddleware) diff --git a/plugin/tx_indexer/pkg/config/config.go b/plugin/tx_indexer/pkg/config/config.go index 8f2fa18d..1b4049fe 100644 --- a/plugin/tx_indexer/pkg/config/config.go +++ b/plugin/tx_indexer/pkg/config/config.go @@ -53,4 +53,5 @@ type MetricsConfig struct { Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` Host string `mapstructure:"host" json:"host,omitempty"` Port int `mapstructure:"port" json:"port,omitempty"` + Token string `mapstructure:"token" json:"token,omitempty"` }