Merge branch 'development' into renovate/docker-base-updates
This commit is contained in:
@@ -420,10 +420,10 @@ func generateSelfSignedCertPEM() (string, string, error) {
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Test Org"},
|
||||
},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
|
||||
@@ -296,12 +296,12 @@ func TestProxyHostCreate_AdvancedConfig_Normalization(t *testing.T) {
|
||||
// Provide an advanced_config value that will be normalized by caddy.NormalizeAdvancedConfig
|
||||
adv := `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`
|
||||
payload := map[string]interface{}{
|
||||
"name": "AdvHost",
|
||||
"domain_names": "adv.example.com",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"enabled": true,
|
||||
"name": "AdvHost",
|
||||
"domain_names": "adv.example.com",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"enabled": true,
|
||||
"advanced_config": adv,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
@@ -657,14 +657,14 @@ func TestProxyHostUpdate_AdvancedConfig_ClearAndBackup(t *testing.T) {
|
||||
|
||||
// Create host with advanced config
|
||||
host := &models.ProxyHost{
|
||||
UUID: "adv-clear-uuid",
|
||||
Name: "Advanced Host",
|
||||
DomainNames: "adv-clear.example.com",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 8080,
|
||||
AdvancedConfig: `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`,
|
||||
UUID: "adv-clear-uuid",
|
||||
Name: "Advanced Host",
|
||||
DomainNames: "adv-clear.example.com",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 8080,
|
||||
AdvancedConfig: `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`,
|
||||
AdvancedConfigBackup: "",
|
||||
Enabled: true,
|
||||
Enabled: true,
|
||||
}
|
||||
require.NoError(t, db.Create(host).Error)
|
||||
|
||||
@@ -854,7 +854,7 @@ func TestProxyHostUpdate_Locations_Replace(t *testing.T) {
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
Locations: []models.Location{{UUID: uuid.NewString(), Path: "/old", ForwardHost: "localhost", ForwardPort: 8080, ForwardScheme: "http"}},
|
||||
Locations: []models.Location{{UUID: uuid.NewString(), Path: "/old", ForwardHost: "localhost", ForwardPort: 8080, ForwardScheme: "http"}},
|
||||
}
|
||||
require.NoError(t, db.Create(host).Error)
|
||||
|
||||
@@ -884,14 +884,14 @@ func TestProxyHostCreate_WithCertificateAndLocations(t *testing.T) {
|
||||
|
||||
adv := `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`
|
||||
payload := map[string]interface{}{
|
||||
"name": "Create With Cert",
|
||||
"domain_names": "cert.example.com",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"enabled": true,
|
||||
"certificate_id": cert.ID,
|
||||
"locations": []map[string]interface{}{{"path": "/app", "forward_scheme": "http", "forward_host": "localhost", "forward_port": 8080}},
|
||||
"name": "Create With Cert",
|
||||
"domain_names": "cert.example.com",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"enabled": true,
|
||||
"certificate_id": cert.ID,
|
||||
"locations": []map[string]interface{}{{"path": "/app", "forward_scheme": "http", "forward_host": "localhost", "forward_port": 8080}},
|
||||
"advanced_config": adv,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
@@ -328,13 +328,13 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
|
||||
// Ensure it has a "handler" key
|
||||
if _, ok := v["handler"]; ok {
|
||||
// Capture ruleset_name if present, remove it from advanced_config,
|
||||
// and set up 'include' array for coraza-caddy plugin.
|
||||
// and set up 'directives' with Include statement for coraza-caddy plugin.
|
||||
if rn, has := v["ruleset_name"]; has {
|
||||
if rnStr, ok := rn.(string); ok && rnStr != "" {
|
||||
// Set 'include' array with the ruleset file path for coraza-caddy
|
||||
// Set 'directives' with Include statement for coraza-caddy
|
||||
if rulesetPaths != nil {
|
||||
if p, ok := rulesetPaths[rnStr]; ok && p != "" {
|
||||
v["include"] = []string{p}
|
||||
v["directives"] = fmt.Sprintf("Include %s", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
|
||||
if rnStr, ok := rn.(string); ok && rnStr != "" {
|
||||
if rulesetPaths != nil {
|
||||
if p, ok := rulesetPaths[rnStr]; ok && p != "" {
|
||||
m["include"] = []string{p}
|
||||
m["directives"] = fmt.Sprintf("Include %s", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,17 +168,17 @@ func TestGenerateConfig_WAFModeAndRulesetReference(t *testing.T) {
|
||||
sec := &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "nonexistent-rs"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, nil, sec)
|
||||
require.NoError(t, err)
|
||||
// Since a ruleset name was requested but none exists, waf handler should include a reference but no include array
|
||||
// Since a ruleset name was requested but none exists, waf handler should include a reference but no directives
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
found := false
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok && hn == "waf" {
|
||||
if _, ok := h["include"]; !ok {
|
||||
if _, ok := h["directives"]; !ok {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected waf handler without include array when referenced ruleset does not exist")
|
||||
require.True(t, found, "expected waf handler without directives when referenced ruleset does not exist")
|
||||
|
||||
// Now test learning/monitor mode mapping
|
||||
sec2 := &models.SecurityConfig{WAFMode: "block", WAFLearning: true}
|
||||
@@ -218,13 +218,13 @@ func TestGenerateConfig_WAFSelectedSetsContentAndMode(t *testing.T) {
|
||||
found := false
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok && hn == "waf" {
|
||||
if incl, ok := h["include"].([]string); ok && len(incl) > 0 {
|
||||
if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected waf handler with include array to be present")
|
||||
require.True(t, found, "expected waf handler with directives containing Include to be present")
|
||||
}
|
||||
|
||||
func TestGenerateConfig_DecisionAdminPartsEmpty(t *testing.T) {
|
||||
@@ -271,11 +271,11 @@ func TestGenerateConfig_WAFUsesRuleSet(t *testing.T) {
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, rulesetPaths, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// check waf handler present with include array
|
||||
// check waf handler present with directives containing Include
|
||||
found := false
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok && hn == "waf" {
|
||||
if incl, ok := h["include"].([]string); ok && len(incl) > 0 {
|
||||
if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func TestGenerateConfig_WAFUsesRuleSet(t *testing.T) {
|
||||
}
|
||||
if !found {
|
||||
b2, _ := json.MarshalIndent(route.Handle, "", " ")
|
||||
t.Fatalf("waf handler with include array should be present; handlers: %s", string(b2))
|
||||
t.Fatalf("waf handler with directives should be present; handlers: %s", string(b2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,17 +295,17 @@ func TestGenerateConfig_WAFUsesRuleSetFromAdvancedConfig(t *testing.T) {
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, rulesetPaths, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// check waf handler present with include array coming from host AdvancedConfig
|
||||
// check waf handler present with directives containing Include from host AdvancedConfig
|
||||
found := false
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok && hn == "waf" {
|
||||
if incl, ok := h["include"].([]string); ok && len(incl) > 0 && incl[0] == "/tmp/host-rs.conf" {
|
||||
if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include /tmp/host-rs.conf") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, found, "waf handler with include array should include host advanced_config ruleset path")
|
||||
require.True(t, found, "waf handler with directives should include host advanced_config ruleset path")
|
||||
}
|
||||
|
||||
func TestGenerateConfig_WAFUsesRuleSetFromAdvancedConfig_Array(t *testing.T) {
|
||||
@@ -316,17 +316,20 @@ func TestGenerateConfig_WAFUsesRuleSetFromAdvancedConfig_Array(t *testing.T) {
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, rulesetPaths, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// check waf handler present with include array coming from host AdvancedConfig array
|
||||
// check waf handler present with directives containing Include from host AdvancedConfig array
|
||||
found := false
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok && hn == "waf" {
|
||||
if incl, ok := h["include"].([]string); ok && len(incl) > 0 && incl[0] == "/tmp/host-rs-array.conf" {
|
||||
if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include /tmp/host-rs-array.conf") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, found, "waf handler with include array should include host advanced_config array ruleset path")
|
||||
if !found {
|
||||
b, _ := json.MarshalIndent(route.Handle, "", " ")
|
||||
t.Fatalf("waf handler with directives should include host advanced_config array ruleset path; handlers: %s", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateConfig_WAFUsesRulesetFromSecCfgFallback(t *testing.T) {
|
||||
@@ -336,18 +339,18 @@ func TestGenerateConfig_WAFUsesRulesetFromSecCfgFallback(t *testing.T) {
|
||||
rulesetPaths := map[string]string{"owasp-crs": "/tmp/owasp-fallback.conf"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, rulesetPaths, nil, sec)
|
||||
require.NoError(t, err)
|
||||
// since secCfg requested owasp-crs and we have a path, the waf handler should include the path in include array
|
||||
// since secCfg requested owasp-crs and we have a path, the waf handler should include the path in directives
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
found := false
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok && hn == "waf" {
|
||||
if incl, ok := h["include"].([]string); ok && len(incl) > 0 && incl[0] == "/tmp/owasp-fallback.conf" {
|
||||
if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include /tmp/owasp-fallback.conf") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, found, "waf handler with include array should include fallback secCfg ruleset path")
|
||||
require.True(t, found, "waf handler with directives should include fallback secCfg ruleset path")
|
||||
}
|
||||
|
||||
func TestGenerateConfig_RateLimitFromSecCfg(t *testing.T) {
|
||||
|
||||
@@ -119,11 +119,6 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
|
||||
safeName := strings.ReplaceAll(strings.ToLower(rs.Name), " ", "-")
|
||||
safeName = strings.ReplaceAll(safeName, "/", "-")
|
||||
|
||||
// Calculate hash of the content to ensure filename changes when content changes
|
||||
// This forces Caddy to reload the file instead of using a cached version
|
||||
hash := sha256.Sum256([]byte(rs.Content))
|
||||
shortHash := fmt.Sprintf("%x", hash)[:8]
|
||||
filePath := filepath.Join(corazaDir, fmt.Sprintf("%s-%s.conf", safeName, shortHash))
|
||||
// Prepend required Coraza directives if not already present.
|
||||
// These are essential for the WAF to actually enforce rules:
|
||||
// - SecRuleEngine On: enables blocking mode (blocks malicious requests)
|
||||
@@ -142,6 +137,13 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
|
||||
}
|
||||
content = fmt.Sprintf("SecRuleEngine %s\nSecRequestBodyAccess On\n\n", engineMode) + content
|
||||
}
|
||||
|
||||
// Calculate hash of the FINAL content (after prepending mode directives)
|
||||
// to ensure filename changes when mode changes, forcing Caddy to reload
|
||||
hash := sha256.Sum256([]byte(content))
|
||||
shortHash := fmt.Sprintf("%x", hash)[:8]
|
||||
filePath := filepath.Join(corazaDir, fmt.Sprintf("%s-%s.conf", safeName, shortHash))
|
||||
|
||||
// Write ruleset file with world-readable permissions so the Caddy
|
||||
// process (which may run as an unprivileged user) can read it.
|
||||
if err := writeFileFunc(filePath, []byte(content), 0644); err != nil {
|
||||
|
||||
@@ -718,9 +718,12 @@ func TestManager_ApplyConfig_IncludesWAFHandlerWithRuleset(t *testing.T) {
|
||||
if h == "ruleset.example.com" {
|
||||
for _, handle := range r.Handle {
|
||||
if handlerName, ok := handle["handler"].(string); ok && handlerName == "waf" {
|
||||
// Validate include array (coraza-caddy schema) or inline ruleset_content presence
|
||||
if incl, ok := handle["include"].([]interface{}); ok && len(incl) > 0 {
|
||||
if rf, ok := incl[0].(string); ok && rf != "" {
|
||||
// Validate directives field contains Include statement (coraza-caddy schema)
|
||||
if dir, ok := handle["directives"].(string); ok && strings.Contains(dir, "Include") {
|
||||
// Extract the file path from the Include directive
|
||||
parts := strings.Split(dir, " ")
|
||||
if len(parts) >= 2 {
|
||||
rf := parts[len(parts)-1]
|
||||
// Ensure file exists and contains our content
|
||||
// Note: manager prepends SecRuleEngine On directives, so we check Contains
|
||||
b, err := os.ReadFile(rf)
|
||||
@@ -739,7 +742,7 @@ func TestManager_ApplyConfig_IncludesWAFHandlerWithRuleset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "waf handler with inlined ruleset should be present in generated config")
|
||||
assert.True(t, found, "waf handler with directives should be present in generated config")
|
||||
}
|
||||
|
||||
func TestManager_ApplyConfig_RulesetWriteFileFailure(t *testing.T) {
|
||||
@@ -1310,10 +1313,17 @@ func TestManager_ApplyConfig_RulesetFileCleanup(t *testing.T) {
|
||||
assert.NoError(t, err, "Subdirectory should not be deleted")
|
||||
assert.True(t, info.IsDir(), "Subdirectory should still be a directory")
|
||||
|
||||
// Verify active ruleset file exists
|
||||
activeFile := filepath.Join(corazaDir, "active-ruleset.conf")
|
||||
_, err = os.Stat(activeFile)
|
||||
assert.NoError(t, err, "Active ruleset file should exist")
|
||||
// Verify active ruleset file exists (with hash suffix in filename)
|
||||
entries, err := os.ReadDir(corazaDir)
|
||||
assert.NoError(t, err, "Should be able to read corazaDir")
|
||||
foundActive := false
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasPrefix(entry.Name(), "active-ruleset-") && strings.HasSuffix(entry.Name(), ".conf") {
|
||||
foundActive = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, foundActive, "Active ruleset file with hash suffix should exist")
|
||||
}
|
||||
|
||||
func TestManager_ApplyConfig_RulesetCleanupReadDirError(t *testing.T) {
|
||||
|
||||
@@ -462,20 +462,20 @@ func TestComputeEffectiveFlags_DB_ACLTrueAndFalse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestComputeEffectiveFlags_DB_WAFMonitor(t *testing.T) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{}))
|
||||
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"}
|
||||
manager := NewManager(nil, db, "", "", false, secCfg)
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"}
|
||||
manager := NewManager(nil, db, "", "", false, secCfg)
|
||||
|
||||
// Set WAF mode to monitor
|
||||
// Set WAF mode to monitor
|
||||
res := db.Create(&models.SecurityConfig{Name: "default", Enabled: true, WAFMode: "monitor"})
|
||||
require.NoError(t, res.Error)
|
||||
|
||||
_, _, waf, _, _ := manager.computeEffectiveFlags(context.Background())
|
||||
require.True(t, waf) // Should still be true (enabled)
|
||||
_, _, waf, _, _ := manager.computeEffectiveFlags(context.Background())
|
||||
require.True(t, waf) // Should still be true (enabled)
|
||||
}
|
||||
|
||||
func TestManager_ApplyConfig_WAFMonitor(t *testing.T) {
|
||||
@@ -511,7 +511,7 @@ func TestManager_ApplyConfig_WAFMonitor(t *testing.T) {
|
||||
originalWriteFile := writeFileFunc
|
||||
defer func() { writeFileFunc = originalWriteFile }()
|
||||
writeFileFunc = func(filename string, data []byte, perm os.FileMode) error {
|
||||
if strings.Contains(filename, "owasp-crs.conf") {
|
||||
if strings.Contains(filename, "owasp-crs") && strings.HasSuffix(filename, ".conf") {
|
||||
writtenContent = string(data)
|
||||
}
|
||||
return originalWriteFile(filename, data, perm)
|
||||
|
||||
Reference in New Issue
Block a user