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
2 changes: 1 addition & 1 deletion config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ domain:
auto_ssl: false
subdomain_style: words
nginx:
enabled: false
enabled: true
image: nginx:alpine
container_name: nginx
config_path: ""
Expand Down
4 changes: 2 additions & 2 deletions internal/api/compose_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ services:
expectExpose: []string{"80"},
},
{
name: "invalid yaml returns unchanged",
inputCompose: `not valid yaml: [`,
name: "invalid yaml returns unchanged",
inputCompose: `not valid yaml: [`,
ports: []PortConfig{{ContainerPort: 80, HostPort: ""}},
expectNoChange: true,
},
Expand Down
6 changes: 3 additions & 3 deletions internal/api/container_exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ func TestWebSocketUpgrader(t *testing.T) {

func TestAuthMessageParsing(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
name string
input string
wantErr bool
wantType string
}{
{
Expand Down
228 changes: 222 additions & 6 deletions internal/api/security_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,12 @@ func (s *Server) getDeploymentSecurity(c *gin.Context) {
return
}

securityConfig := deployment.Metadata.Security
if securityConfig == nil {
var securityConfig *models.DeploymentSecurityConfig
if deployment.Metadata != nil && deployment.Metadata.Security != nil {
securityConfig = deployment.Metadata.Security
} else {
securityConfig = &models.DeploymentSecurityConfig{
Enabled: false,
ProtectedPaths: []models.ProtectedPath{},
RateLimits: []models.DeploymentRateLimit{},
}
Expand Down Expand Up @@ -391,7 +394,41 @@ func (s *Server) updateDeploymentSecurity(c *gin.Context) {
return
}

c.JSON(http.StatusOK, gin.H{"security": securityConfig})
vhostUpdated := false
if s.proxyOrchestrator != nil && s.proxyOrchestrator.NginxManager().VirtualHostExists(name) {
if err := s.proxyOrchestrator.NginxManager().UpdateVirtualHost(deployment); err != nil {
c.JSON(http.StatusOK, gin.H{
"security": securityConfig,
"vhost_updated": false,
"warning": "Security config saved but vhost update failed: " + err.Error(),
})
return
}

if err := s.proxyOrchestrator.NginxManager().TestConfig(); err != nil {
c.JSON(http.StatusOK, gin.H{
"security": securityConfig,
"vhost_updated": false,
"warning": "Security config saved but nginx config test failed: " + err.Error(),
})
return
}

if err := s.proxyOrchestrator.NginxManager().Reload(); err != nil {
c.JSON(http.StatusOK, gin.H{
"security": securityConfig,
"vhost_updated": true,
"warning": "Nginx reload failed (may need manual reload): " + err.Error(),
})
return
}
vhostUpdated = true
}

c.JSON(http.StatusOK, gin.H{
"security": securityConfig,
"vhost_updated": vhostUpdated,
})
}

// getDeploymentSecurityEvents returns security events for a deployment
Expand Down Expand Up @@ -440,22 +477,201 @@ func (s *Server) setRealtimeCaptureStatus(c *gin.Context) {
return
}

if err := s.infraManager.SetNginxRealtimeCapture(req.Enabled); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
// Check prerequisites before attempting to enable
if req.Enabled {
if s.config.Nginx.ContainerName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Nginx container name not configured",
"realtime_capture": s.config.Security.RealtimeCapture,
"details": "Set nginx.container_name in config to enable realtime capture",
})
return
}

// Check if nginx container is running
if !s.infraManager.IsNginxRunning() {
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Nginx container is not running",
"realtime_capture": s.config.Security.RealtimeCapture,
"details": "Start the nginx/proxy infrastructure before enabling realtime capture",
})
return
}
}

result, err := s.infraManager.SetNginxRealtimeCaptureWithStatus(req.Enabled)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
"realtime_capture": s.config.Security.RealtimeCapture,
"details": result,
})
return
}

s.config.Security.RealtimeCapture = req.Enabled

if s.configPath != "" {
if err := config.Save(s.config, s.configPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Config updated but failed to save: " + err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Lua configs updated but failed to save agent config: " + err.Error(),
"realtime_capture": req.Enabled,
})
return
}
}

c.JSON(http.StatusOK, gin.H{
"realtime_capture": req.Enabled,
"message": "Realtime capture " + map[bool]string{true: "enabled", false: "disabled"}[req.Enabled],
"details": result,
})
}

// getSecurityHealth returns the health status of the security setup
func (s *Server) getSecurityHealth(c *gin.Context) {
if s.securityManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "disabled",
"error": "Security module not enabled",
"checks": map[string]bool{},
"issues": []string{"Security is not enabled in agent configuration"},
"recommendations": []string{
"Set security.enabled: true in config.yml",
"Restart the agent after updating config",
},
})
return
}

health := s.infraManager.CheckSecurityHealth()
c.JSON(http.StatusOK, health)
}

// updateSecuritySettings handles dedicated security settings updates
func (s *Server) updateSecuritySettings(c *gin.Context) {
var req struct {
Enabled *bool `json:"enabled"`
RealtimeCapture *bool `json:"realtime_capture"`
ScanInterval string `json:"scan_interval"`
RetentionDays int `json:"retention_days"`
RateThreshold int `json:"rate_threshold"`
AutoBlockEnabled *bool `json:"auto_block_enabled"`
AutoBlockThreshold int `json:"auto_block_threshold"`
AutoBlockDuration string `json:"auto_block_duration"`
}

if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

result := gin.H{
"updated_fields": []string{},
}
var updatedFields []string

// Track previous enabled state for nginx action
prevEnabled := s.config.Security.Enabled

// Update enabled state - this controls nginx Lua setup
if req.Enabled != nil && *req.Enabled != s.config.Security.Enabled {
s.config.Security.Enabled = *req.Enabled
updatedFields = append(updatedFields, "enabled")
}

// Update realtime capture (kept for config compatibility)
if req.RealtimeCapture != nil && *req.RealtimeCapture != s.config.Security.RealtimeCapture {
s.config.Security.RealtimeCapture = *req.RealtimeCapture
updatedFields = append(updatedFields, "realtime_capture")
}

// Nginx action only when enabled state changes
needsNginxAction := s.config.Security.Enabled != prevEnabled

// Update other settings
if req.ScanInterval != "" {
if d, err := time.ParseDuration(req.ScanInterval); err == nil {
s.config.Security.ScanInterval = d
updatedFields = append(updatedFields, "scan_interval")
}
}
if req.RetentionDays > 0 {
s.config.Security.RetentionDays = req.RetentionDays
updatedFields = append(updatedFields, "retention_days")
}
if req.RateThreshold > 0 {
s.config.Security.RateThreshold = req.RateThreshold
updatedFields = append(updatedFields, "rate_threshold")
}
if req.AutoBlockEnabled != nil {
s.config.Security.AutoBlockEnabled = *req.AutoBlockEnabled
updatedFields = append(updatedFields, "auto_block_enabled")
}
if req.AutoBlockThreshold > 0 {
s.config.Security.AutoBlockThreshold = req.AutoBlockThreshold
updatedFields = append(updatedFields, "auto_block_threshold")
}
if req.AutoBlockDuration != "" {
if d, err := time.ParseDuration(req.AutoBlockDuration); err == nil {
s.config.Security.AutoBlockDuration = d
updatedFields = append(updatedFields, "auto_block_duration")
}
}

result["updated_fields"] = updatedFields

// Perform nginx action if needed
if needsNginxAction {
// Check prerequisites when enabling
if s.config.Security.Enabled {
if s.config.Nginx.ContainerName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Nginx container name not configured",
"details": "Set nginx.container_name in config to enable security",
})
return
}
if !s.infraManager.IsNginxRunning() {
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Nginx container is not running",
"details": "Start the nginx/proxy infrastructure before enabling security",
})
return
}
}

actionResult, err := s.infraManager.SetNginxRealtimeCaptureWithStatus(s.config.Security.Enabled)
result["nginx_action"] = actionResult

if err != nil {
result["nginx_error"] = err.Error()
}
}

// Save config
if s.configPath != "" {
if err := config.Save(s.config, s.configPath); err != nil {
result["config_save_error"] = err.Error()
} else {
result["config_saved"] = true
}
}

// Update dependent managers
s.infraManager.UpdateConfig(s.config)

// Return current security settings
result["security"] = gin.H{
"enabled": s.config.Security.Enabled,
"realtime_capture": s.config.Security.RealtimeCapture,
"scan_interval": s.config.Security.ScanInterval.String(),
"retention_days": s.config.Security.RetentionDays,
"rate_threshold": s.config.Security.RateThreshold,
"auto_block_enabled": s.config.Security.AutoBlockEnabled,
"auto_block_threshold": s.config.Security.AutoBlockThreshold,
"auto_block_duration": s.config.Security.AutoBlockDuration.String(),
}

c.JSON(http.StatusOK, result)
}
20 changes: 11 additions & 9 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ func (s *Server) setupRoutes() {

protected.GET("/settings", s.getSettings)
protected.PUT("/settings", s.updateSettings)
protected.PUT("/settings/security", s.updateSecuritySettings)
protected.GET("/subdomain/generate", s.generateSubdomain)
protected.GET("/plugins", s.listPlugins)
protected.GET("/plugins/:name", s.getPlugin)
Expand Down Expand Up @@ -265,6 +266,7 @@ func (s *Server) setupRoutes() {
protected.DELETE("/security/protected-routes/:id", s.deleteProtectedRoute)
protected.GET("/security/realtime-capture", s.getRealtimeCaptureStatus)
protected.PUT("/security/realtime-capture", s.setRealtimeCaptureStatus)
protected.GET("/security/health", s.getSecurityHealth)
protected.GET("/deployments/:name/security", s.getDeploymentSecurity)
protected.PUT("/deployments/:name/security", s.updateDeploymentSecurity)
protected.GET("/deployments/:name/security/events", s.getDeploymentSecurityEvents)
Expand Down Expand Up @@ -351,7 +353,7 @@ func (s *Server) createDeployment(c *gin.Context) {
AutoStart bool `json:"auto_start"`
UseSharedDatabase bool `json:"use_shared_database"`
ExistingDatabaseContainer string `json:"existing_database_container,omitempty"`
RegistryCredential *struct {
RegistryCredential *struct {
CredentialID string `json:"credential_id,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Expand Down Expand Up @@ -543,8 +545,8 @@ func (s *Server) createDeployment(c *gin.Context) {
}

c.JSON(http.StatusCreated, gin.H{
"message": "Deployment created",
"name": req.Name,
"message": "Deployment created",
"name": req.Name,
"proxy_result": proxyResult,
"auto_started": req.AutoStart,
"start_output": startOutput,
Expand Down Expand Up @@ -2722,12 +2724,12 @@ func (s *Server) syncAllProxies(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{
"message": "Proxy sync completed",
"synced": synced,
"skipped": skipped,
"failed": failed,
"total": len(results),
"results": results,
"message": "Proxy sync completed",
"synced": synced,
"skipped": skipped,
"failed": failed,
"total": len(results),
"results": results,
})
}

Expand Down
12 changes: 6 additions & 6 deletions internal/credentials/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import (
)

type Manager struct {
mu sync.RWMutex
registryTypes map[string]*models.RegistryType
credentials map[string]*models.RegistryCredential
storagePath string
typesFilePath string
credsFilePath string
mu sync.RWMutex
registryTypes map[string]*models.RegistryType
credentials map[string]*models.RegistryCredential
storagePath string
typesFilePath string
credsFilePath string
}

func NewManager(deploymentsPath string) *Manager {
Expand Down
8 changes: 4 additions & 4 deletions internal/docker/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ func (c *ComposeExecutor) PS(deploymentPath string) (string, error) {
}

type ImageInfo struct {
Service string `json:"service"`
Image string `json:"image"`
IsLatest bool `json:"is_latest"`
IsBuild bool `json:"is_build"`
Service string `json:"service"`
Image string `json:"image"`
IsLatest bool `json:"is_latest"`
IsBuild bool `json:"is_build"`
}

func (c *ComposeExecutor) Pull(deploymentPath string, onlyLatest bool) (string, error) {
Expand Down
Loading
Loading