netguard

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package netguard implements the design-13 security-group firewall plane. This G1 slice is read-only: it materializes the design-13 view (security group + node binding + resolved builtin zones) of a node's legacy NFTInputs baseline without mutating the store or any apply path. The G2 slice adds the compiler with a byte-parity gate against network.GenerateNFTPlan before the legacy path retires.

Index

Constants

View Source
const (
	// FindingLockoutRiskSSH fires when no compiled rule can accept traffic on
	// any of the node's management ports.
	FindingLockoutRiskSSH = "lockout_risk_ssh"
	// FindingUnverifiedApply fires when the server has no public URL, so the
	// node-side apply cannot run a control-plane selfcheck after committing.
	FindingUnverifiedApply = "unverified_apply"
	// FindingManagementPortAssumed fires when the lockout check had no reported
	// reality to learn the node's real shell port from and fell back to tcp/22.
	// It is a warning rather than a block because tcp/22 is usually right; it
	// exists so an operator can tell a checked plan from a guessed one.
	FindingManagementPortAssumed = "management_port_assumed"
	// FindingInterfaceMissing fires when a rule matches an inbound interface
	// the node does not report. The public zone defaults to eth0, and twelve
	// fleet nodes have no eth0 (ens17, ens5, enp2s0, wlo1): every accept on
	// that interface matches nothing, the default drop takes over, and the
	// lockout check above still counts those accepts as a way in.
	FindingInterfaceMissing = "interface_missing"
	// FindingInterfaceUnverified fires when a rule matches an inbound interface
	// but the node has never reported which interfaces it has, so the lint
	// cannot tell a right name from a wrong one. It blocks rather than warns
	// because the wrong name is the lockout above, and a node that has not
	// reported is exactly the node most likely to be running an agent old
	// enough to have been enrolled with the eth0 guess.
	FindingInterfaceUnverified = "interface_unverified"

	SeverityBlock = "block"
	SeverityWarn  = "warn"

	// ManagementPort is the port the lockout lint falls back to when the node
	// has never reported which port its shell daemon actually listens on.
	ManagementPort = 22
)
View Source
const (
	SuggestionListenerMissingAllow       = "listener_missing_allow"
	SuggestionAllowWithoutListener       = "allow_without_listener"
	SuggestionOverlayListenerPublicAllow = "overlay_listener_public_allow"
	SuggestionOverlayZoneUntrusted       = "overlay_zone_untrusted"
	SuggestionManagedTableDrift          = "managed_table_drift"
)
View Source
const (
	// LegacyGroupPrefix namespaces the node-private security groups derived
	// from legacy NFTInputs baselines (design-13 §7.1).
	LegacyGroupPrefix = "sg-legacy-"
)
View Source
const MaxExpandedPortsPerRule = 1024

MaxExpandedPortsPerRule bounds range expansion. The current renderer emits explicit port lists, so a very wide range would produce an unreadable, unreviewable ruleset. Native `from-to` nft range emission is a later renderer upgrade (design-13 L2); until then wide ranges fail closed with a named error rather than silently exploding the plan.

Variables

View Source
var ErrNodeUnmanaged = errors.New("node guard binding is observe-only; adopt the node before planning")

ErrNodeUnmanaged is returned when a plan is requested for an observe-only binding. Converted legacy baselines start unmanaged: an operator must adopt a node before its firewall can be planned from the new model.

Functions

func Blocking

func Blocking(findings []Finding) bool

Blocking reports whether any finding blocks the plan.

func Compile

func Compile(in CompileInput) (network.NFTPlan, error)

Compile lowers zones, trusted-zone accepts, per-node overrides, and attached security groups into a single network.NFTPlan.

func CompileRuleset

func CompileRuleset(in CompileInput) (string, error)

CompileRuleset renders the final lattice_guard ruleset for a node.

func ExpandPortRanges

func ExpandPortRanges(ranges []model.GuardPortRange) ([]int, error)

ExpandPortRanges flattens inclusive ranges into the explicit port list the current renderer emits, fail-closed on invalid or excessively wide ranges.

func PortRanges

func PortRanges(ports []int) []model.GuardPortRange

PortRanges compresses a port list into sorted, deduplicated inclusive ranges: [9009,9010,9011,9013] becomes 9009-9011 and 9013. Out-of-range values are dropped rather than widened (fail-closed).

func ZoneMap

func ZoneMap(zones []model.GuardZone) map[string]model.GuardZone

ZoneMap indexes zones by id for CompileInput.

Types

type CompileInput

type CompileInput struct {
	Binding model.NodeGuardBinding
	// Groups in binding order. The caller resolves Binding.GroupIDs.
	Groups []model.SecurityGroup
	// Zones by id, including the builtin zones resolved for this node.
	Zones   map[string]model.GuardZone
	Resolve NodeResolver
}

CompileInput is the fully-resolved authoring state for one node.

type Finding

type Finding struct {
	Code     string `json:"code"`
	Severity string `json:"severity"`
	Message  string `json:"message"`
}

Finding is one lint result. Blocking findings refuse the plan unless the operator explicitly accepts the risk, which is audited.

func Lint

func Lint(plan network.NFTPlan, opts LintOptions) []Finding

Lint inspects a compiled plan for the failure modes that make a guard apply unsafe. It never mutates the plan.

type LegacyView

type LegacyView struct {
	Group   model.SecurityGroup
	Binding model.NodeGuardBinding
	Zones   []model.GuardZone
}

LegacyView is the read-only design-13 rendering of one node's legacy NFTInputs baseline.

func LegacyBaseline

func LegacyBaseline(inputs model.NFTInputs) LegacyView

LegacyBaseline converts a legacy NFTInputs record into the design-13 shape: one node-private security group whose rules reference the builtin public and wireguard zones, a binding attaching that group, and the node-resolved zone definitions. Semantics are preserved exactly: legacy "wireguard ports" were port-scoped source-CIDR allows, so the wireguard zone appears as a rule remote, never as a trusted zone in Binding.ZoneIDs. Managed is false: the node stays observe-only until an operator explicitly adopts it (G2).

type LintOptions

type LintOptions struct {
	// PublicURLConfigured reports whether the node-side apply will be able to
	// run `lattice-agent --selfcheck-controlplane` after committing.
	PublicURLConfigured bool
	// Reality is the node's last reported firewall reality, or nil when the
	// node has never reported one. Its listeners are what let the lockout check
	// protect the port the operator actually reaches this box on, instead of
	// assuming every fleet member runs sshd on 22. A node whose sshd moved to
	// 2222 used to pass this lint with tcp/22 open and tcp/2222 dropped, which
	// is the exact plan that locks the operator out for good.
	Reality *model.GuardNodeReality
}

LintOptions carries the plan-time context the compiled ruleset cannot know.

type NodeResolver

type NodeResolver func(nodeID string) (model.Node, bool)

NodeResolver mirrors netpolicy.NodeResolver so node remotes resolve against current fleet state at compile time.

type SuggestInput added in v0.2.3

type SuggestInput struct {
	Binding model.NodeGuardBinding
	Groups  []model.SecurityGroup
	Zones   map[string]model.GuardZone
	Reality model.GuardNodeReality
}

SuggestInput is the read-only state needed to compare operator intent with a low-trust node reality report. It intentionally contains no store, HTTP, or task-executor dependency so G3 can wire persistence and routes later without changing the core diff logic.

type Suggestion added in v0.2.3

type Suggestion struct {
	ID        string `json:"id"`
	Code      string `json:"code"`
	Severity  string `json:"severity"`
	Title     string `json:"title"`
	Detail    string `json:"detail"`
	ZoneID    string `json:"zone_id,omitempty"`
	Interface string `json:"interface,omitempty"`
	Protocol  string `json:"protocol,omitempty"`
	Port      int    `json:"port,omitempty"`
	Address   string `json:"address,omitempty"`
	Process   string `json:"process,omitempty"`
}

Suggestion is an operator-review prompt derived from reality. Suggestions are display/diff input only; accepting one is a later, audited mutation path.

func Suggest added in v0.2.3

func Suggest(in SuggestInput) ([]Suggestion, error)

Suggest compares the node's current guard intent with the latest reality snapshot and emits deterministic, de-duplicated suggestions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL