Shadow mode: Housecarl beside the firewall in force, changing nothing
shadow.enabled in housecarl.yaml loads Housecarl's ruleset so that it blocks, rejects and translates nothing: drops and rejects count and accept (an accept only ends Housecarl's chain), policies accept and log what they would drop, NAT only counts, MSS clamping and the flowtable are left out, and raw rules that do more than accept, drop or reject are left out with a warning. Comments mark what would happen ([would drop]). Filter chains run after the other firewall's (+10) or before (-10); NAT chains always first. Every change is checked by nftables in both forms. No probation; takeovers and closing connections are refused. The boot fallback filters nothing. internal/tablewatch puts the table back after a flush ruleset. Mode in status, healthz, whoami, the web UI, the log and the tracer; hc ruleset --enforcing. docs/shadow.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e6c085ef9
commit
de075edd5d
48 files changed
+1345
-93
No files matched your search
@@ -41,8 +41,27 @@ type Config struct {
|
||||
Ruleset string `yaml:"ruleset"`
|
||||
// Stats keeps traffic, hits and drops over time (docs/stats.md).
|
||||
Stats Stats `yaml:"stats"`
|
||||
// Shadow loads the ruleset without letting it block, reject or
|
||||
// translate anything, beside the firewall in force (docs/shadow.md).
|
||||
Shadow Shadow `yaml:"shadow"`
|
||||
}
|
||||
|
||||
// Shadow is shadow mode: Housecarl's ruleset is loaded and counts, logs
|
||||
// and traces what it would do, while another firewall decides.
|
||||
type Shadow struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
// Position is where Housecarl's filter chains run: after the other
|
||||
// firewall's (it sees what that one lets through, so what it would
|
||||
// drop is what switching would break) or before (it sees everything).
|
||||
Position string `yaml:"position"`
|
||||
}
|
||||
|
||||
// Shadow positions.
|
||||
const (
|
||||
ShadowAfter = "after"
|
||||
ShadowBefore = "before"
|
||||
)
|
||||
|
||||
// Stats configures statistics: a database of their own, aggregates
|
||||
// only, pruned by age.
|
||||
type Stats struct {
|
||||
@@ -204,7 +223,8 @@ func Default() *Config {
|
||||
Backup: Backup{Dir: "/var/backups/housecarl", Retention: Duration(30 * 24 * time.Hour), KeepAtLeast: 7},
|
||||
Stats: Stats{Enabled: true, Database: "/var/lib/housecarl/stats.db", TopN: 100,
|
||||
Retention: StatsRetention{Minute: Duration(7 * 24 * time.Hour), Hour: Duration(90 * 24 * time.Hour), Day: Duration(730 * 24 * time.Hour)}},
|
||||
TLS: TLS{ACME: ACME{Directory: "letsencrypt-staging", Resolvers: []string{"1.1.1.1:53", "9.9.9.9:53", "8.8.8.8:53"}, CheckPropagation: true}},
|
||||
Shadow: Shadow{Position: ShadowAfter},
|
||||
TLS: TLS{ACME: ACME{Directory: "letsencrypt-staging", Resolvers: []string{"1.1.1.1:53", "9.9.9.9:53", "8.8.8.8:53"}, CheckPropagation: true}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +295,9 @@ func (c *Config) check() error {
|
||||
default:
|
||||
return fmt.Errorf("log.level must be debug, info, warn or error")
|
||||
}
|
||||
if c.Shadow.Position != ShadowAfter && c.Shadow.Position != ShadowBefore {
|
||||
return fmt.Errorf("shadow.position must be after or before")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -427,4 +450,16 @@ stats:
|
||||
hour: 90d # hourly figures and top lists
|
||||
day: 730d # daily figures and top lists
|
||||
top_n: 100 # entries kept per top list, per hour and day
|
||||
|
||||
# Shadow mode (docs/shadow.md): Housecarl's ruleset is loaded beside the
|
||||
# firewall you have now, but blocks, rejects and translates nothing. Its
|
||||
# counters, log and tracer show what it would do. No change is on
|
||||
# probation, and takeovers and closing connections are refused. housecarld
|
||||
# reads this when it starts: restart it to switch.
|
||||
shadow:
|
||||
enabled: false
|
||||
# after: Housecarl's filter chains run after the other firewall's, and
|
||||
# see only what it lets through: what Housecarl would drop is what
|
||||
# switching over would break. before: they see every packet.
|
||||
position: after
|
||||
`
|
||||
@@ -35,6 +35,7 @@ func TestErrors(t *testing.T) {
|
||||
"{tls: {acme: {domain: '*.example.com'}}, jarl: {server: https://j, token: x}}": "full name",
|
||||
"{tls: {acme: {domain: d.example.com, directory: http://x}}, jarl: {server: https://j, token: x}}": "https://",
|
||||
"ruleset: relative.nft": "ruleset",
|
||||
"shadow: {position: x}": "shadow.position",
|
||||
}
|
||||
for yml, want := range cases {
|
||||
path := filepath.Join(t.TempDir(), "s.yaml")
|
||||
|
||||
Reference in new issue
Block a user