Documentation
¶
Overview ¶
Package fwpolicy decides whether one IP-layer egress firewall is contained in another, and narrows one until it is (Task 20363, re-landing Task 20319 on top of the virtual executors of Tasks 20345 and 20356).
The levels ¶
A sandbox's reach is decided at up to four levels, outermost first:
- the executor's own configuration — executors.*.egress_filter for the hub's container and Kubernetes executors, written by whoever deploys the hub;
- the device's rule set, set by an admin in the dashboard and stored in the control plane (executor_firewall_rules);
- a virtual executor's firewall, part of its definition (executor.VirtualSpec.Firewall), or the network a device's own sandboxes are given;
- the project's rule set, set by the project's maintainers (project_firewall_rules).
Each level may only narrow the one above it. Without that, the inner levels would be privilege escalations with a text box: a maintainer able to name their own CIDRs on a device whose admin deliberately kept it off 10.0.0.0/8 would have given themselves the operator's network.
The model ¶
Every level is an executor.FirewallRules, the form the virtual-executor dialog already edits, so all four share one vocabulary and one parser. What a rule set reaches is what pkg/netfilter compiles it to:
- TCP to an address in AllowCIDRs, or outside the hard-block set when AllowPublicInternet is on, on a port in AllowPorts (any when empty);
- UDP and TCP to each resolver's own address and port;
- nothing in DenyCIDRs, whatever the allows say; and nothing else.
One rule is inherited rather than restated: a level's effective deny list is its own plus every deny list above it. A deny only ever removes reach, so inheriting one cannot widen anything, and requiring every project to copy its device's denylist would make the first forgotten copy a hole. Nothing else is inherited — an inner level states the ports and resolvers it means, because "empty" already means something in this form (every port; no resolver) and a second meaning would leave a narrowing unable to say "no DNS at all".
Nil and empty ¶
A nil *Rules is a level that does not filter: the absence of a bound, which permits everything. A zero Rules is the opposite: a level that permits nothing at all. The two look alike in a debugger and are opposites in a firewall, and every function here keeps them apart.
Index ¶
- func CheckAtDriver(spec executor.Spec, own *Rules, ownName, deviceID string) error
- func Describe(r *Rules) string
- func Equal(a, b Rules) bool
- func Fingerprint(r Rules) string
- func Input(r Rules) (netfilter.Input, error)
- func IsExceeds(err error) bool
- func Permits(parent *Rules, child Rules) []string
- func SetSource(s Source) (restore func())
- type ExceedsError
- type Level
- type Resolution
- type Rules
- type Source
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CheckAtDriver ¶
CheckAtDriver is the proof a driver makes immediately before it installs a workload's network, against everything it knows bounds that workload: its own configured level (own), the device's rule set the dispatch carried, and — in a process that holds the control plane — the device's rule set as it is stored right now.
The last is what makes this authoritative rather than a repeat of the dispatch step. A dispatch path that never ran Resolve, or ran it before an admin tightened the device, hands the driver a Spec this check reads fresh against the store; and an agent on another machine, which has no store, still proves the rules against the bound in the Spec and its own firewall before it installs a single rule.
func Describe ¶
Describe renders a level for a sentence: "no network" for an empty rule set, "unfiltered" for a nil one, the rules otherwise.
func Fingerprint ¶
Fingerprint is a short stable digest of the canonical rule set.
The container driver names a sandbox bridge and its nftables table after it when a workload carries rules of its own. Two workloads with different rules must never share a bridge — the second Apply would replace the first's ruleset, and one of them would run under the other's firewall — and deriving the name from the rules makes that collision impossible rather than unlikely. Two spellings of one policy normalize to one fingerprint and share.
func Input ¶
Input projects a rule set onto the authorisation pkg/netfilter compiles, exactly as the container driver installs it: an empty port list next to a destination means every port.
func Permits ¶
Permits reports whether child, enforced under parent, reaches only what parent reaches. It returns one sentence per way in which it does not, so an empty result means contained.
A nil parent permits everything; an unreadable parent permits nothing, since a bound this binary cannot read is not one it can prove anything against.
The reasons are the body of a refused save. An admin told only "not permitted" widens the parent until the form saves; one told "port 22 is not allowed by the governing rule set (80, 443)" fixes the field they meant.
Types ¶
type ExceedsError ¶
ExceedsError reports one level reaching further than the level it must fit inside. It unwraps to executor.ErrUnsupported, the refusal a dispatch that cannot be honoured as configured has always returned.
func (*ExceedsError) Error ¶
func (e *ExceedsError) Error() string
func (*ExceedsError) Unwrap ¶
func (e *ExceedsError) Unwrap() error
type Level ¶
type Level struct {
// Kind is "config", "device", "own", "scope" or "project".
Kind string `json:"kind"`
// Name says where the rules came from, in a sentence.
Name string `json:"name"`
// Rules is nil when this level adds no narrowing.
Rules *Rules `json:"rules,omitempty"`
}
Level is one link of the chain, outermost first.
type Resolution ¶
Resolution is what Resolve decided, for the run's event log.
func Resolve ¶
Resolve composes the firewall levels for one workload on ex and records the result on spec: the dispatch-time check.
A workload under no stored rule set — no device rules, no project rules — is left exactly as it was, so every executor keeps the behaviour it had before these levels existed, down to the bridge its sandboxes join. Otherwise the chain is proven level by level; a level that reaches further than the one above it, a store that cannot be read, or an executor that cannot enforce the result each refuse the dispatch, because starting a sandbox whose firewall could not be established is the failure this exists to prevent.
func (Resolution) Applied ¶
func (r Resolution) Applied() bool
Applied reports whether a stored level shaped the workload.
func (Resolution) Describe ¶
func (r Resolution) Describe() string
Describe names the levels and what came out of them, in one line.
type Rules ¶
type Rules = executor.FirewallRules
Rules is the editable firewall shared by every level.
func Chain ¶
Chain composes levels outermost first, proving each fits inside the effective rule set of the levels above it. It returns the innermost effective rule set, or nil when no level sets one.
func CloseMetadata ¶
CloseMetadata returns r with each cloud metadata service its allowlist contains without naming written into its denylist, and the services it wrote (Task 20397).
It changes nothing a filter built by this binary does — Compile drops those services ahead of the allows that contain them — and that is the point: the dispatch step ships rule sets through it, so a device whose agent predates the containment rule, and would compile the containing range as an open door, closes them too, because a deny list is something every agent honours.
A service that is one of r's own resolvers is left out. A deny closes every port, the resolver's included, and a Google Cloud VM's resolver is its metadata server; Compile carves it from the containing range and keeps its port 53, and an old agent is the one place it stays open on that range's ports.
func Constrain ¶
Constrain returns the widest rule set that is contained in both child and parent, and one sentence for each thing it had to take away.
It is what keeps rules saved under a level honest after that level is tightened. An admin narrowing a device does not have to find every virtual executor and project that was relying on the old reach: each is rewritten to the part of its old rule set the device still allows, and the sentences go to the audit trail and back to the admin's form, so nothing narrows silently.
A child already contained is returned unchanged with no sentences. The result is never wider than the child — Constrain only removes — and when it cannot be expressed within the rule set's bounds the destinations are dropped rather than kept: the failure direction is "reaches less", never "reaches more".
func Effective ¶
Effective returns child as it is enforced under parent: child's own allows, ports and resolvers, with parent's deny list added to child's.
A nil parent bounds nothing and adds nothing. The result is normalized.
func NetworkLevel ¶
NetworkLevel is the level a sandbox network amounts to: an empty rule set for no network, nil for an unfiltered one — the engine's bridge or a named network — which narrows nothing.
func VirtualLevel ¶
func VirtualLevel(vs executor.VirtualSpec) *Rules
VirtualLevel is a virtual executor's own level: its firewall, or the network its sandboxes get when it has none.
type Source ¶
type Source interface {
DeviceFirewall(executorID string) (*Rules, error)
ProjectFirewall(projectPath string) (*Rules, error)
}
Source reads the two stored levels: a device's rule set and a project's.
nil from either means "no rule set stored", and an error means the store could not be read — which every caller here treats as a refusal, never as "no rules": a decode fault that read as absence would widen a firewall.