Documentation
¶
Overview ¶
Package firewall renders cargoship's backend-neutral firewall configuration onto whichever host firewall a node runs.
A Plan describes what the cluster needs open on one node: the addresses of its peers, the pod and service CIDRs, the ports from the inventory's `.host.ports`, and the rules from the inventory's `.host.firewall.rules`. A Backend translates that Plan into one firewall's own dialect and applies it. Backends are matched to a node by the node's OS and by Detect, so a single inventory can target a mix of firewalld, ufw, and nftables hosts.
Index ¶
Constants ¶
const (
// FirewalldService is the name of the firewalld system service.
FirewalldService = "firewalld"
)
const (
// NftablesService is the name of the nftables system service.
NftablesService = "nftables"
)
const (
// UFWService is the name of the ufw system service.
UFWService = "ufw"
)
Variables ¶
This section is empty.
Functions ¶
func NftSplitFamilies ¶
NftSplitFamilies sorts addresses and CIDRs into their families, dropping anything that parses as neither. Order is preserved so the rendered ruleset is stable across runs.
Types ¶
type Backend ¶
type Backend interface {
// Name is the backend identifier, e.g. "firewalld" or "ufw".
Name() string
// Detect is true when this backend manages the firewall on h, meaning the firewall is
// installed and running. It runs commands on the host, so callers should call it once per
// host and reuse the result.
Detect(ctx context.Context, h *cluster.ZarfHost) bool
// Installed is true when this firewall is present on h, whether or not it is running. It
// separates a host that has the firewall stopped from one that never had it at all.
Installed(ctx context.Context, h *cluster.ZarfHost) bool
// Apply makes the node's firewall match p, then reloads the firewall. It is
// idempotent: applying the same plan twice leaves the node in the same state, and
// rules cargoship applied on an earlier run that p no longer contains are removed.
Apply(ctx context.Context, h *cluster.ZarfHost, p Plan) error
}
Backend applies a Plan to a node using one host firewall implementation.
type Firewalld ¶
type Firewalld struct{}
Firewalld applies a Plan to a node running firewalld.
func (*Firewalld) Apply ¶
Apply writes the ipsets, service, and policy files for p, enables them, and restarts firewalld.
type Nftables ¶
type Nftables struct{}
Nftables applies a Plan to a node that manages its firewall with nftables directly, rather than through firewalld or ufw. It is the backend for hosts that never had either -- Debian and Arch nodes configured by hand, and the minimal or immutable images (CoreOS, Flatcar) that ship no firewall front end at all.
Everything cargoship writes lives in one table, `inet cargoship`, which is replaced whole in a single nft transaction on every apply. Cargoship never flushes the ruleset and never touches another table, because kube-proxy and every CNI keep their own service and policy rules in this same subsystem; a global flush would break cluster networking until they resynced.
Two things differ from the other backends. Every base chain cargoship writes has an accept policy: a default-drop policy applied from a remote phase would cut the connection cargoship is running over, the same reason cargoship never runs `ufw enable`. And an accept here ends only cargoship's chain, not the hook, so a rule in an operator's own table can still drop traffic cargoship allowed -- unlike firewalld's trusted zone, cargoship's trust is not the last word on a packet.
func (*Nftables) Apply ¶
Apply renders p as a complete ruleset, checks it, and loads it in one transaction. The table is replaced rather than edited, so rules cargoship applied on an earlier run that p no longer contains are gone once the transaction commits.
func (*Nftables) Detect ¶
Detect is true when the node persists an nftables ruleset of its own, meaning the nftables service is running or one of the distro ruleset files exists.
A node whose only nftables content comes from kube-proxy and the CNI is deliberately not a match. Every node in a running cluster has a non-empty ruleset, so matching on that would claim hosts whose operator never configured a firewall at all.
type Plan ¶
type Plan struct {
// NodeAddresses lists the private addresses of every node in the cluster. A node trusts
// all traffic from these addresses.
NodeAddresses []string
// ClusterCIDRs lists the pod and service CIDR blocks the distro engine uses. A node
// trusts all traffic from these blocks.
ClusterCIDRs []string
// Ports lists the ports the node exposes publicly, from the inventory's `.host.ports`.
Ports []cluster.ZarfHostPort
// Rules lists the backend-neutral rules from the inventory's `.host.firewall.rules`.
Rules []cluster.ZarfFirewallRule
// Policies holds the legacy firewalld-only policies from the inventory's `.host.policy`.
// Backends other than firewalld ignore it.
Policies map[string]cluster.ZarfFirewallPolicyConfig
}
Plan is the desired firewall state for a single node, expressed without reference to any particular firewall implementation.
type Selection ¶
type Selection struct {
// Backend manages the host's firewall and is the one cargoship configures. It is nil when
// cargoship configures nothing on the host.
Backend Backend
// Skipped is the host's preferred firewall, installed but not running. Cargoship makes no
// change to a host in this state: the operator installed a front end and chose to leave it
// down, and starting it, or writing rules into the nftables underneath it, would take that
// decision away from them.
Skipped Backend
}
Selection is the outcome of matching a host against the registered backends. At most one of its fields is set.
func Select ¶
Select returns the backend cargoship configures on h.
The host's OS decides first. An OS module names the firewall front end its distribution ships, firewalld on Enterprise Linux and SUSE, ufw on Debian and Ubuntu, and that front end is used when it is running. When it is installed but stopped, cargoship leaves the host alone rather than reaching past the front end to the nftables underneath it. Only a host whose preferred front end is absent, or whose OS ships none, falls through to the ordered Detect match, which is how a host that manages nftables directly is picked up.
type UFW ¶
type UFW struct{}
UFW applies a Plan to a node running ufw.
Cargoship never enables or disables ufw. A node is only configured when ufw is already active, because enabling a default-deny firewall from a remote phase would cut the connection cargoship is running over.
Two parts of the neutral model mean something slightly different here than they do on firewalld: a forward rule's ingress and egress name interfaces rather than zones, since ufw has no zones, and a rule with no address match becomes a rule from any address.
func (*UFW) Apply ¶
Apply reconciles the node's ufw rules with p and reloads ufw. Rules cargoship applied on an earlier run that p no longer contains are deleted.