Merge branch 'development' into renovate/docker-base-updates

This commit is contained in:
Jeremy
2025-12-03 18:08:46 -05:00
committed by GitHub
7 changed files with 84 additions and 69 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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)