Skip to content

Commit fd179b6

Browse files
committed
feat(security): Add token-authenticated internal endpoint for nginx
Add secure internal endpoint for nginx to fetch blocked IPs without requiring user authentication credentials. - Add InternalAPIToken to SecurityConfig, auto-generated if empty - Create /_internal/blocked-ips endpoint with X-Internal-Token validation - Update Lua templates to include token in requests - Inject token into security.lua during template generation Signed-off-by: nfebe <fenn25.fn@gmail.com>
1 parent 6648a99 commit fd179b6

File tree

7 files changed

+45
-10
lines changed

7 files changed

+45
-10
lines changed

internal/api/security_handlers.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,19 @@ func (s *Server) listBlockedIPs(c *gin.Context) {
194194
c.JSON(http.StatusOK, gin.H{"blocked_ips": ips})
195195
}
196196

197+
// listBlockedIPsInternal returns blocked IPs for internal nginx communication
198+
func (s *Server) listBlockedIPsInternal(c *gin.Context) {
199+
token := c.GetHeader("X-Internal-Token")
200+
expectedToken := s.config.Security.InternalAPIToken
201+
202+
if token == "" || token != expectedToken {
203+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid internal token"})
204+
return
205+
}
206+
207+
s.listBlockedIPs(c)
208+
}
209+
197210
// blockIP blocks an IP address
198211
func (s *Server) blockIP(c *gin.Context) {
199212
if s.securityManager == nil {

internal/api/server.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ func (s *Server) setupRoutes() {
276276
protected.GET("/security/events", s.listSecurityEvents)
277277
protected.GET("/security/events/:id", s.getSecurityEvent)
278278
protected.POST("/security/cleanup", s.cleanupSecurityEvents)
279+
protected.GET("/security/blocked-ips", s.listBlockedIPs)
279280
protected.POST("/security/blocked-ips", s.blockIP)
280281
protected.DELETE("/security/blocked-ips/:ip", s.unblockIP)
281282
protected.GET("/security/ips/:ip/events", s.getEventsByIP)
@@ -301,7 +302,9 @@ func (s *Server) setupRoutes() {
301302
// Ingest endpoints (no auth - called by nginx Lua)
302303
api.POST("/security/events/ingest", s.ingestSecurityEvent)
303304
api.POST("/traffic/ingest", s.ingestTrafficLog)
304-
api.GET("/security/blocked-ips", s.listBlockedIPs)
305+
306+
// Internal nginx endpoint - token-authenticated for blocked IPs
307+
api.GET("/_internal/blocked-ips", s.listBlockedIPsInternal)
305308
}
306309
}
307310

internal/infra/manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ func (m *Manager) SetNginxRealtimeCaptureWithStatus(enabled bool) (map[string]in
377377
result["agent_ip"] = agentIP
378378
result["agent_port"] = agentPort
379379

380-
securityLua, err := templates.GetNginxSecurityLuaWithConfig(agentIP, agentPort)
380+
securityLua, err := templates.GetNginxSecurityLuaWithConfig(agentIP, agentPort, m.config.Security.InternalAPIToken)
381381
if err != nil {
382382
errors = append(errors, fmt.Sprintf("failed to get security.lua template: %v", err))
383383
} else {
@@ -1228,7 +1228,7 @@ func (m *Manager) RefreshSecurityScripts() (*RefreshSecurityScriptsResult, error
12281228
}
12291229

12301230
// Generate and write security.lua with injected IP
1231-
securityLua, err := templates.GetNginxSecurityLuaWithConfig(agentIP, agentPort)
1231+
securityLua, err := templates.GetNginxSecurityLuaWithConfig(agentIP, agentPort, m.config.Security.InternalAPIToken)
12321232
if err != nil {
12331233
result.Errors = append(result.Errors, fmt.Sprintf("failed to generate security.lua: %v", err))
12341234
result.Success = false

pkg/config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package config
22

33
import (
4+
"crypto/rand"
5+
"encoding/hex"
46
"os"
57
"time"
68

@@ -126,6 +128,9 @@ type SecurityConfig struct {
126128
AuthFailureThreshold int `yaml:"auth_failure_threshold" json:"auth_failure_threshold"`
127129
UniquePathsThreshold int `yaml:"unique_paths_threshold" json:"unique_paths_threshold"`
128130
RepeatedHitsThreshold int `yaml:"repeated_hits_threshold" json:"repeated_hits_threshold"`
131+
132+
// Internal API token for nginx-to-agent communication (auto-generated if empty)
133+
InternalAPIToken string `yaml:"internal_api_token" json:"-"`
129134
}
130135

131136
func FindConfigPath(providedPath string) string {
@@ -268,6 +273,12 @@ func setDefaults(cfg *Config) {
268273
if cfg.Security.RepeatedHitsThreshold == 0 {
269274
cfg.Security.RepeatedHitsThreshold = 30
270275
}
276+
if cfg.Security.InternalAPIToken == "" {
277+
bytes := make([]byte, 32)
278+
if _, err := rand.Read(bytes); err == nil {
279+
cfg.Security.InternalAPIToken = hex.EncodeToString(bytes)
280+
}
281+
}
271282
}
272283

273284
func Save(cfg *Config, path string) error {

templates/infra/nginx/lua/security.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local _M = {}
99
-- Configuration (injected by agent during deployment)
1010
local AGENT_IP = "{{.AgentIP}}"
1111
local AGENT_PORT = {{.AgentPort}}
12+
local INTERNAL_TOKEN = "{{.InternalAPIToken}}"
1213

1314
-- Blocked IPs cache settings
1415
local BLOCKED_IPS_CACHE_TTL = 30 -- seconds
@@ -110,9 +111,10 @@ function _M.refresh_blocked_ips()
110111

111112
local res, req_err = httpc:request({
112113
method = "GET",
113-
path = "/api/security/blocked-ips",
114+
path = "/api/_internal/blocked-ips",
114115
headers = {
115116
["Host"] = AGENT_IP .. ":" .. AGENT_PORT,
117+
["X-Internal-Token"] = INTERNAL_TOKEN,
116118
}
117119
})
118120

templates/templates.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,13 @@ func GetNginxSecurityLua() ([]byte, error) {
9292

9393
// LuaTemplateData contains the data for Lua template processing
9494
type LuaTemplateData struct {
95-
AgentIP string
96-
AgentPort int
95+
AgentIP string
96+
AgentPort int
97+
InternalAPIToken string
9798
}
9899

99100
// GetNginxSecurityLuaWithConfig returns the security.lua template processed with agent config
100-
func GetNginxSecurityLuaWithConfig(agentIP string, agentPort int) ([]byte, error) {
101+
func GetNginxSecurityLuaWithConfig(agentIP string, agentPort int, internalAPIToken string) ([]byte, error) {
101102
content, err := FS.ReadFile("infra/nginx/lua/security.lua")
102103
if err != nil {
103104
return nil, err
@@ -110,8 +111,9 @@ func GetNginxSecurityLuaWithConfig(agentIP string, agentPort int) ([]byte, error
110111

111112
var buf bytes.Buffer
112113
data := LuaTemplateData{
113-
AgentIP: agentIP,
114-
AgentPort: agentPort,
114+
AgentIP: agentIP,
115+
AgentPort: agentPort,
116+
InternalAPIToken: internalAPIToken,
115117
}
116118

117119
if err := tmpl.Execute(&buf, data); err != nil {

test/e2e/nginx/lua/security.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local _M = {}
1010

1111
-- Configuration via environment variable (test-specific)
1212
local AGENT_URL = os.getenv("FLATRUN_AGENT_URL") or "http://host.docker.internal:8080"
13+
local INTERNAL_TOKEN = os.getenv("FLATRUN_INTERNAL_TOKEN") or ""
1314

1415
-- Blocked IPs cache settings
1516
local BLOCKED_IPS_CACHE_TTL = 30 -- seconds
@@ -47,8 +48,11 @@ function _M.refresh_blocked_ips()
4748
local httpc = http.new()
4849
httpc:set_timeout(3000)
4950

50-
local res, err = httpc:request_uri(AGENT_URL .. "/api/security/blocked-ips", {
51+
local res, err = httpc:request_uri(AGENT_URL .. "/api/_internal/blocked-ips", {
5152
method = "GET",
53+
headers = {
54+
["X-Internal-Token"] = INTERNAL_TOKEN,
55+
},
5256
})
5357

5458
if not res then

0 commit comments

Comments
 (0)