Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion internal/api/security_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,84 @@ func (s *Server) unblockIP(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "IP unblocked successfully"})
}

// getEventsByIP returns all security events for a specific IP
func (s *Server) listWhitelist(c *gin.Context) {
if s.securityManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Security module not enabled"})
return
}

entries, err := s.securityManager.GetWhitelist()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"whitelist": entries})
}

func (s *Server) addWhitelistEntry(c *gin.Context) {
if s.securityManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Security module not enabled"})
return
}

var req struct {
Value string `json:"value" binding:"required"`
Type string `json:"type" binding:"required"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if req.Type != "ip" && req.Type != "cidr" && req.Type != "path" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Type must be 'ip', 'cidr', or 'path'"})
return
}

id, err := s.securityManager.AddWhitelistEntry(req.Value, req.Type, req.Reason)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusCreated, gin.H{"id": id})
}

func (s *Server) removeWhitelistEntry(c *gin.Context) {
if s.securityManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Security module not enabled"})
return
}

idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}

if err := s.securityManager.RemoveWhitelistEntry(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Entry removed"})
}

func (s *Server) listWhitelistInternal(c *gin.Context) {
token := c.GetHeader("X-Internal-Token")
expectedToken := s.config.Security.InternalAPIToken

if token == "" || token != expectedToken {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid internal token"})
return
}

s.listWhitelist(c)
}

func (s *Server) getEventsByIP(c *gin.Context) {
if s.securityManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Security module not enabled"})
Expand Down
66 changes: 47 additions & 19 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ func New(cfg *config.Config, configPath string) *Server {
if err := securityManager.InitNginxConfigs(nginxConfigPath); err != nil {
log.Printf("Warning: Failed to initialize security nginx configs: %v", err)
}
// Add Docker gateway IP to whitelist
gatewayIP := infraManager.GetDockerHostIP()
if err := securityManager.AddDockerGatewayToWhitelist(gatewayIP); err != nil {
log.Printf("Warning: Failed to add Docker gateway to whitelist: %v", err)
}
}
}

Expand Down Expand Up @@ -284,6 +289,9 @@ func (s *Server) setupRoutes() {
protected.POST("/security/protected-routes", s.addProtectedRoute)
protected.PUT("/security/protected-routes/:id", s.updateProtectedRoute)
protected.DELETE("/security/protected-routes/:id", s.deleteProtectedRoute)
protected.GET("/security/whitelist", s.listWhitelist)
protected.POST("/security/whitelist", s.addWhitelistEntry)
protected.DELETE("/security/whitelist/:id", s.removeWhitelistEntry)
protected.GET("/security/realtime-capture", s.getRealtimeCaptureStatus)
protected.PUT("/security/realtime-capture", s.setRealtimeCaptureStatus)
protected.GET("/security/health", s.getSecurityHealth)
Expand All @@ -295,6 +303,7 @@ func (s *Server) setupRoutes() {
// Traffic endpoints
protected.GET("/traffic/logs", s.getTrafficLogs)
protected.GET("/traffic/stats", s.getTrafficStats)
protected.GET("/traffic/unknown-domains", s.getUnknownDomainStats)
protected.POST("/traffic/cleanup", s.cleanupTrafficLogs)
protected.GET("/deployments/:name/traffic", s.getDeploymentTrafficStats)
}
Expand All @@ -303,8 +312,9 @@ func (s *Server) setupRoutes() {
api.POST("/security/events/ingest", s.ingestSecurityEvent)
api.POST("/traffic/ingest", s.ingestTrafficLog)

// Internal nginx endpoint - token-authenticated for blocked IPs
// Internal nginx endpoints - token-authenticated
api.GET("/_internal/blocked-ips", s.listBlockedIPsInternal)
api.GET("/_internal/whitelist", s.listWhitelistInternal)
}
}

Expand Down Expand Up @@ -1185,12 +1195,13 @@ func (s *Server) getSettings(c *gin.Context) {
"subdomain_style": s.config.Domain.SubdomainStyle,
},
"nginx": gin.H{
"enabled": s.config.Nginx.Enabled,
"image": s.config.Nginx.Image,
"container_name": s.config.Nginx.ContainerName,
"config_path": s.config.Nginx.ConfigPath,
"reload_command": s.config.Nginx.ReloadCommand,
"external": s.config.Nginx.External,
"enabled": s.config.Nginx.Enabled,
"image": s.config.Nginx.Image,
"container_name": s.config.Nginx.ContainerName,
"config_path": s.config.Nginx.ConfigPath,
"reload_command": s.config.Nginx.ReloadCommand,
"external": s.config.Nginx.External,
"reject_unknown_domains": s.config.Nginx.RejectUnknownDomains,
},
"certbot": gin.H{
"enabled": s.config.Certbot.Enabled,
Expand Down Expand Up @@ -1241,12 +1252,13 @@ func (s *Server) updateSettings(c *gin.Context) {
SubdomainStyle string `json:"subdomain_style"`
} `json:"domain,omitempty"`
Nginx *struct {
Enabled bool `json:"enabled"`
Image string `json:"image"`
ContainerName string `json:"container_name"`
ConfigPath string `json:"config_path"`
ReloadCommand string `json:"reload_command"`
External bool `json:"external"`
Enabled bool `json:"enabled"`
Image string `json:"image"`
ContainerName string `json:"container_name"`
ConfigPath string `json:"config_path"`
ReloadCommand string `json:"reload_command"`
External bool `json:"external"`
RejectUnknownDomains *bool `json:"reject_unknown_domains"`
} `json:"nginx,omitempty"`
Certbot *struct {
Enabled bool `json:"enabled"`
Expand Down Expand Up @@ -1318,6 +1330,9 @@ func (s *Server) updateSettings(c *gin.Context) {
if req.Nginx.ReloadCommand != "" {
s.config.Nginx.ReloadCommand = req.Nginx.ReloadCommand
}
if req.Nginx.RejectUnknownDomains != nil {
s.config.Nginx.RejectUnknownDomains = *req.Nginx.RejectUnknownDomains
}
}

if req.Certbot != nil {
Expand Down Expand Up @@ -1426,12 +1441,13 @@ func (s *Server) updateSettings(c *gin.Context) {
"subdomain_style": s.config.Domain.SubdomainStyle,
},
"nginx": gin.H{
"enabled": s.config.Nginx.Enabled,
"image": s.config.Nginx.Image,
"container_name": s.config.Nginx.ContainerName,
"config_path": s.config.Nginx.ConfigPath,
"reload_command": s.config.Nginx.ReloadCommand,
"external": s.config.Nginx.External,
"enabled": s.config.Nginx.Enabled,
"image": s.config.Nginx.Image,
"container_name": s.config.Nginx.ContainerName,
"config_path": s.config.Nginx.ConfigPath,
"reload_command": s.config.Nginx.ReloadCommand,
"external": s.config.Nginx.External,
"reject_unknown_domains": s.config.Nginx.RejectUnknownDomains,
},
"certbot": gin.H{
"enabled": s.config.Certbot.Enabled,
Expand Down Expand Up @@ -2793,13 +2809,25 @@ func (s *Server) getSystemStats(c *gin.Context) {
imageStats, _ := s.networksManager.GetImageStats()
volumeStats, _ := s.networksManager.GetVolumeStats()

var networkCount, portCount int
if networks, err := s.networksManager.ListNetworks(); err == nil {
networkCount = len(networks)
}
if containers, err := s.networksManager.ListContainers(); err == nil {
for _, container := range containers {
portCount += len(container.Ports)
}
}

systemStats, _ := system.GetSystemStats()

c.JSON(http.StatusOK, gin.H{
"deployments": stats,
"containers": containerStats,
"images": imageStats,
"volumes": volumeStats,
"networks": gin.H{"total": networkCount},
"ports": gin.H{"total": portCount},
"system": systemStats,
})
}
Expand Down
33 changes: 33 additions & 0 deletions internal/api/traffic_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,39 @@ func (s *Server) getTrafficStats(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"stats": stats})
}

func (s *Server) getUnknownDomainStats(c *gin.Context) {
if s.trafficManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Traffic logging not enabled"})
return
}

since := 24 * time.Hour
if sinceStr := c.Query("since"); sinceStr != "" {
if d, err := time.ParseDuration(sinceStr); err == nil {
since = d
}
}

deployments, err := s.manager.ListDeployments()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

var knownDeployments []string
for _, d := range deployments {
knownDeployments = append(knownDeployments, d.Name)
}

stats, err := s.trafficManager.GetUnknownDomainStats(knownDeployments, since)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"stats": stats})
}

// cleanupTrafficLogs removes old traffic logs
func (s *Server) cleanupTrafficLogs(c *gin.Context) {
if s.trafficManager == nil {
Expand Down
21 changes: 19 additions & 2 deletions internal/infra/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,9 @@ func (m *Manager) SetNginxRealtimeCaptureWithStatus(enabled bool) (map[string]in

if enabled {
// Write nginx.conf with Lua support
nginxConf, err := templates.GetNginxConfig(true)
nginxConf, err := templates.GetNginxConfigWithData(true, templates.NginxConfigData{
RejectUnknownDomains: m.config.Nginx.RejectUnknownDomains,
})
if err != nil {
errors = append(errors, fmt.Sprintf("failed to get nginx lua config template: %v", err))
} else {
Expand Down Expand Up @@ -414,6 +416,13 @@ func (m *Manager) SetNginxRealtimeCaptureWithStatus(enabled bool) (map[string]in
}
result["conf_files_written"] = true
}

// Ensure ssl directory exists
sslDir := filepath.Join(nginxDir, "ssl")
if err := os.MkdirAll(sslDir, 0755); err != nil {
errors = append(errors, fmt.Sprintf("failed to create ssl directory: %v", err))
}

} else {
// Delete nginx.conf - container will use default from image
if _, err := os.Stat(confPath); err == nil {
Expand Down Expand Up @@ -1049,6 +1058,7 @@ func (m *Manager) checkNginxInternalAPIReachable() bool {
var securityVolumeMounts = []string{
"./nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro",
"./lua:/etc/nginx/lua:ro",
"./ssl:/etc/nginx/ssl:ro",
}

func (m *Manager) getNginxComposePath() string {
Expand Down Expand Up @@ -1215,8 +1225,15 @@ func (m *Manager) RefreshSecurityScripts() (*RefreshSecurityScriptsResult, error
result.Errors = append(result.Errors, fmt.Sprintf("failed to create conf.d directory: %v", err))
}

sslDir := filepath.Join(nginxDir, "ssl")
if err := os.MkdirAll(sslDir, 0755); err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("failed to create ssl directory: %v", err))
}

// Write nginx.conf with Lua support
nginxConf, err := templates.GetNginxConfig(true)
nginxConf, err := templates.GetNginxConfigWithData(true, templates.NginxConfigData{
RejectUnknownDomains: m.config.Nginx.RejectUnknownDomains,
})
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("failed to get nginx lua config template: %v", err))
} else {
Expand Down
Loading
Loading