aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-20 16:15:50 -0800
committerFuwn <[email protected]>2026-01-20 16:15:50 -0800
commitb147654b8aed7005760017f604aae58637ac7767 (patch)
tree77cad60fc8eff8bd6c18a9846307e02bf1d6ba32
parentfeat: Add all remaining monitor options to group defaults (diff)
downloadkaze-b147654b8aed7005760017f604aae58637ac7767.tar.xz
kaze-b147654b8aed7005760017f604aae58637ac7767.zip
feat: Add API access control (public/private/authenticated)
Add configurable access control for /api/* endpoints: - public: Anyone can access (default, backwards compatible) - private: API endpoints return 403 Forbidden - authenticated: Requires API key via X-API-Key header or ?api_key= param Config example: api: access: authenticated keys: - "secret-key-1" - "secret-key-2"
-rw-r--r--config.example.yaml26
-rw-r--r--internal/config/config.go29
-rw-r--r--internal/server/server.go51
3 files changed, 103 insertions, 3 deletions
diff --git a/config.example.yaml b/config.example.yaml
index 93236df..228aecd 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -23,6 +23,18 @@ server:
host: "0.0.0.0"
port: 8080
+# API settings
+api:
+ # Access control for /api/* endpoints:
+ # "public" - Anyone can access (default)
+ # "private" - API endpoints return 403 Forbidden
+ # "authenticated" - Requires API key via X-API-Key header or ?api_key= query param
+ access: public
+ # API keys (only used when access is "authenticated")
+ # keys:
+ # - "your-secret-api-key-1"
+ # - "your-secret-api-key-2"
+
# Storage settings
storage:
path: "./kaze.db" # For Docker single-volume: use "./data/kaze.db" to store in mounted volume
@@ -340,3 +352,17 @@ incidents:
# identified - Root cause found
# monitoring - Fix applied, monitoring
# resolved - Issue resolved
+#
+# API access control:
+# api:
+# access: string - "public" (default), "private", or "authenticated"
+# keys: []string - List of valid API keys (for "authenticated" mode)
+#
+# Endpoints:
+# GET /api/status - All monitors status JSON
+# GET /api/monitor/{name} - Single monitor status JSON
+# GET /api/history/{name} - Monitor history (supports ?mode=ping|minute|hour|day&count=N)
+#
+# Authentication (when access is "authenticated"):
+# - Header: X-API-Key: your-secret-key
+# - Query param: ?api_key=your-secret-key
diff --git a/internal/config/config.go b/internal/config/config.go
index d82fee0..5f05b96 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -15,10 +15,22 @@ type Config struct {
Server ServerConfig `yaml:"server"`
Storage StorageConfig `yaml:"storage"`
Display DisplayConfig `yaml:"display"`
+ API APIConfig `yaml:"api"`
Groups []GroupConfig `yaml:"groups"`
Incidents []IncidentConfig `yaml:"incidents"`
}
+// APIConfig contains settings for the JSON API endpoints
+type APIConfig struct {
+ // Access controls who can access /api/* endpoints:
+ // "public" - Anyone can access (default)
+ // "private" - API endpoints are disabled (return 403)
+ // "authenticated" - Requires API key via X-API-Key header or ?api_key= query param
+ Access string `yaml:"access"`
+ // Keys is a list of valid API keys (only used when access is "authenticated")
+ Keys []string `yaml:"keys"`
+}
+
// DisplayConfig contains display/UI settings
type DisplayConfig struct {
// TickMode controls how history is aggregated: ping, minute, hour, day
@@ -241,6 +253,11 @@ func (c *Config) applyDefaults() {
c.Display.Scale = 2.0
}
+ // Apply API defaults
+ if c.API.Access == "" {
+ c.API.Access = "public"
+ }
+
// Apply group defaults
for i := range c.Groups {
grp := &c.Groups[i]
@@ -456,6 +473,18 @@ func (c *Config) validate() error {
return fmt.Errorf("tick_count must be between 1 and 200, got %d", c.Display.TickCount)
}
+ // Validate API config
+ switch c.API.Access {
+ case "", "public", "private", "authenticated":
+ // Valid modes ("" defaults to "public")
+ default:
+ return fmt.Errorf("invalid api.access %q (must be public, private, or authenticated)", c.API.Access)
+ }
+
+ if c.API.Access == "authenticated" && len(c.API.Keys) == 0 {
+ return fmt.Errorf("api.access is 'authenticated' but no api.keys provided")
+ }
+
return nil
}
diff --git a/internal/server/server.go b/internal/server/server.go
index 92b0f09..b0192a4 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -65,9 +65,11 @@ func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, l
// Pages
mux.HandleFunc("GET /", s.handleIndex)
- mux.HandleFunc("GET /api/status", s.handleAPIStatus)
- mux.HandleFunc("GET /api/monitor/{name}", s.handleAPIMonitor)
- mux.HandleFunc("GET /api/history/{name}", s.handleAPIHistory)
+
+ // API endpoints (protected by API access control)
+ mux.HandleFunc("GET /api/status", s.withAPIAuth(s.handleAPIStatus))
+ mux.HandleFunc("GET /api/monitor/{name}", s.withAPIAuth(s.handleAPIMonitor))
+ mux.HandleFunc("GET /api/history/{name}", s.withAPIAuth(s.handleAPIHistory))
// Create HTTP server
s.server = &http.Server{
@@ -115,6 +117,49 @@ func (s *Server) withMiddleware(next http.Handler) http.Handler {
})
}
+// withAPIAuth wraps an API handler with access control based on config.API.Access
+func (s *Server) withAPIAuth(handler http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ switch s.config.API.Access {
+ case "private":
+ // API is disabled
+ s.jsonError(w, "API access is disabled", http.StatusForbidden)
+ return
+
+ case "authenticated":
+ // Check for API key in header or query param
+ apiKey := r.Header.Get("X-API-Key")
+ if apiKey == "" {
+ apiKey = r.URL.Query().Get("api_key")
+ }
+
+ if apiKey == "" {
+ w.Header().Set("WWW-Authenticate", "API-Key")
+ s.jsonError(w, "API key required", http.StatusUnauthorized)
+ return
+ }
+
+ // Check if key is valid
+ valid := false
+ for _, key := range s.config.API.Keys {
+ if key == apiKey {
+ valid = true
+ break
+ }
+ }
+
+ if !valid {
+ s.jsonError(w, "Invalid API key", http.StatusUnauthorized)
+ return
+ }
+
+ // case "public" or default: allow access
+ }
+
+ handler(w, r)
+ }
+}
+
// PageData contains data for rendering the status page
type PageData struct {
Site config.SiteConfig