Documentation
¶
Overview ¶
Package exposure answers "which published container ports can the internet reach" and manages comment-tagged DOCKER-USER rules that restrict them. Published ports skip the INPUT chain, so ufw alone says nothing about them.
Index ¶
- Constants
- Variables
- func BindsOf(ports []PortBinding) []string
- func CallerWarning(caller string) string
- func Explain(f Finding, outsideLabel string) (explanation, recommendation string)
- func ValidCIDR(s string) bool
- type Chain
- type ChainRule
- type Change
- type Class
- type Container
- type DriftFinding
- type DriftInput
- type DriftKind
- type Finding
- type ImageKind
- type Listener
- type Manager
- func (m *Manager) Apply(ctx context.Context, r Restriction) (changed bool, err error)
- func (m *Manager) ApplyWithRevert(ctx context.Context, r Restriction, previous *Restriction, ...) (Change, error)
- func (m *Manager) Confirm(proto string, port int) (Change, bool)
- func (m *Manager) DetectSSHPorts(ctx context.Context) []int
- func (m *Manager) Forget(proto string, port int)
- func (m *Manager) Guard(ctx context.Context, r Restriction, caller string) []string
- func (m *Manager) Normalize(r Restriction) (Restriction, error)
- func (m *Manager) OnRevert(f func(Change, error))
- func (m *Manager) Pending() []Change
- func (m *Manager) Plan(r Restriction) (Plan, error)
- func (m *Manager) Prefix() string
- func (m *Manager) Protected() []int
- func (m *Manager) ReadChain(ctx context.Context) Chain
- func (m *Manager) ReadListeners(ctx context.Context) []Listener
- func (m *Manager) Remove(ctx context.Context, port int, proto string) error
- func (m *Manager) SetClock(now func() time.Time, after func(time.Duration, func()) func() bool)
- func (m *Manager) SetSSHProbe(f func(context.Context) []int)
- func (m *Manager) Sync(ctx context.Context, want []Restriction) SyncResult
- type OwnListener
- type Owner
- type OwnerKind
- type Plan
- type PortBinding
- type ReachApp
- type ReachCounts
- type ReachDB
- type ReachEndpoint
- type ReachFinding
- type ReachFix
- type ReachInput
- type ReachKind
- type ReachNode
- type ReachRef
- type ReachReport
- type ReachSubject
- type Restriction
- type Runner
- type Severity
- type SyncResult
- type Verdict
Constants ¶
const ( SubjectApp = "app" SubjectDatabase = "database" SubjectHost = "host" )
Subject types a finding or endpoint list belongs to.
const ( ReachFixAuto = "auto" ReachFixGuided = "guided" )
Fix kinds: the control plane applies an auto fix, a guided fix links to the page that does it.
Variables ¶
var ErrLockout = errors.New("exposure: restriction would lock out a required port")
ErrLockout is wrapped when a restriction would cut off a port the platform or the operator's SSH session needs.
Functions ¶
func BindsOf ¶
func BindsOf(ports []PortBinding) []string
BindsOf collects the distinct bind addresses, wildcard spelled explicitly.
func CallerWarning ¶
CallerWarning explains why Guard cannot protect the caller's address: a loopback, IPv6 or unreadable address is never checked against the allow-list. It returns "" when the address was checked.
Types ¶
type Chain ¶
type Chain struct {
// Readable is false when the chain could not be read; Reason says why.
Readable bool
Reason string
// NoChain is true when the firewall backend answers but has no
// DOCKER-USER chain (nftables engines such as Podman with netavark).
// That is permanent, not a transient read failure.
NoChain bool
Rules []ChainRule
}
Chain is the DOCKER-USER chain as read from the host.
func ParseChain ¶
ParseChain parses `iptables -S DOCKER-USER` output.
type ChainRule ¶
type ChainRule struct {
Raw string
Proto string
DPort int
OrigDPort int
Source string
SourceNegated bool
Target string
Comment string
Complex bool
}
ChainRule is one parsed DOCKER-USER rule. Anything the parser does not fully understand sets Complex, so classification never trusts it.
type Change ¶
type Change struct {
Port int `json:"port"`
Protocol string `json:"protocol"`
Allow []string `json:"allow"`
ExpiresAt time.Time `json:"expires_at"`
}
Change is a restriction that is applied in the kernel but not yet confirmed.
type Container ¶
type Container struct {
ID string
Name string
Image string
Owner Owner
Ports []PortBinding
Running bool
}
Container is one running container's published ports, as the audit reads them.
type DriftFinding ¶
type DriftFinding struct {
Kind DriftKind `json:"kind"`
Severity Severity `json:"severity"`
Port int `json:"port"`
Protocol string `json:"protocol"`
Subject string `json:"subject,omitempty"`
Message string `json:"message"`
Fix string `json:"fix,omitempty"`
// Rules are the DOCKER-USER rules involved, verbatim.
Rules []string `json:"rules,omitempty"`
// Ports lists every port/protocol one grouped listener row covers, sorted.
Ports []string `json:"ports,omitempty"`
}
DriftFinding is one disagreement between declared and actual exposure.
func CheckDrift ¶
func CheckDrift(in DriftInput) []DriftFinding
CheckDrift compares declared exposure with the host. It only reads.
type DriftInput ¶
type DriftInput struct {
Findings []Finding
Declared []Restriction
Chain Chain
// Listeners is nil when sockets could not be read, which skips that check.
Listeners []Listener
// Known lists ports expected to listen publicly (control plane, ingress, SSH).
Known []int
// Own lists the platform's own listeners (mesh, mesh DNS) as configured now.
Own []OwnListener
Prefix string
}
DriftInput is everything a drift check compares. It is plain data so the check never touches the host.
type DriftKind ¶
type DriftKind string
DriftKind names one way declared exposure and the host disagree.
type Finding ¶
type Finding struct {
Container string `json:"container"`
Image string `json:"image"`
Owner Owner `json:"owner"`
ImageKind ImageKind `json:"image_kind"`
Protocol string `json:"protocol"`
HostPort int `json:"host_port"`
ContainerPort int `json:"container_port"`
Binds []string `json:"binds"`
Class Class `json:"class"`
Severity Severity `json:"severity"`
// AllowedSources lists the sources a matching rule lets through, when Class is restricted.
AllowedSources []string `json:"allowed_sources,omitempty"`
// Rules are the matching DOCKER-USER rules, verbatim.
Rules []string `json:"rules,omitempty"`
// Managed is true when the matching rule is one this platform created.
Managed bool `json:"managed"`
Explanation string `json:"explanation"`
Recommendation string `json:"recommendation,omitempty"`
// CanRestrict is false when this audit cannot offer the guided fix, with the reason.
CanRestrict bool `json:"can_restrict"`
CannotRestrictWhy string `json:"cannot_restrict_reason,omitempty"`
}
Finding is the audit result for one published port of one container.
func Audit ¶
Audit turns a container list into findings, one per published (port, protocol, container) and sorted worst first.
func (Finding) NeedsAttention ¶
NeedsAttention reports whether the finding is worth surfacing: published beyond the host's private interfaces and not provably restricted.
type ImageKind ¶
type ImageKind string
ImageKind is a coarse guess at what a container runs, from its image name and published port.
const ( KindDatastore ImageKind = "datastore" KindSearch ImageKind = "search" KindAdmin ImageKind = "admin" KindDockerAPI ImageKind = "docker_api" KindWeb ImageKind = "web" KindOther ImageKind = "other" )
Image kinds.
func ImageKindOf ¶
ImageKindOf guesses what a container runs from its image repository name, falling back to the container port.
type Listener ¶
type Listener struct {
Protocol string
Address string
Port int
// Process is the owning process name from ss -p, empty when ss could not say.
Process string
PID int
}
Listener is one listening socket as `ss` reports it.
func ParseListeners ¶
ParseListeners parses `ss -H -ltnup` output, with or without the process column. Lines it cannot read are skipped.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager reads and edits the DOCKER-USER chain, touching only rules that carry its own comment tag.
func NewFakeManager ¶
func NewFakeManager(prefix string, protected []int, run Runner, lookPath func(string) (string, error), goos string) *Manager
NewFakeManager builds a Manager over fake seams for tests.
func NewManager ¶
NewManager builds a Manager over the real iptables binary. protected lists ports that must never be restricted (control plane, ingress, SSH).
func (*Manager) Apply ¶
Apply installs the restriction idempotently and reports whether anything changed. The new rules go in above the old ones before the old ones are deleted, so the port always has a complete rule set in front of it.
func (*Manager) ApplyWithRevert ¶
func (m *Manager) ApplyWithRevert(ctx context.Context, r Restriction, previous *Restriction, window time.Duration) (Change, error)
ApplyWithRevert installs r like Apply, then rolls it back after window unless Confirm is called first. previous is what to restore on rollback (nil removes the rules). The control plane forgets pending changes on restart, and the next Sync then drops rules that are not stored, which is the same rollback.
func (*Manager) Confirm ¶
Confirm keeps a pending restriction and returns it so the caller can store it.
func (*Manager) DetectSSHPorts ¶
DetectSSHPorts returns the ports sshd listens on, so a restriction can never cut the operator's way in even when sshd is not on port 22.
func (*Manager) Forget ¶
Forget cancels a pending rollback without touching the rules. It waits for a rollback already running on the port, so the caller's next Apply comes last.
func (*Manager) Guard ¶
Guard lists the reasons a normalized restriction could lock the operator out: a detected sshd port, or an allow-list that excludes the caller's own address. It returns nothing when the restriction is safe.
func (*Manager) Normalize ¶
func (m *Manager) Normalize(r Restriction) (Restriction, error)
Normalize validates a restriction and returns it in canonical form.
func (*Manager) OnRevert ¶
OnRevert registers a callback run after an unconfirmed restriction was rolled back.
func (*Manager) Plan ¶
func (m *Manager) Plan(r Restriction) (Plan, error)
Plan renders the exact commands Apply would run, in execution order.
func (*Manager) ReadListeners ¶
ReadListeners lists listening sockets, or nil when they cannot be read.
func (*Manager) SetSSHProbe ¶
SetSSHProbe replaces the sshd port detection (tests).
func (*Manager) Sync ¶
func (m *Manager) Sync(ctx context.Context, want []Restriction) SyncResult
Sync converges the chain to want: it applies every restriction and removes tagged rules whose restriction no longer exists.
type OwnListener ¶
OwnListener is a port the platform itself opens, read from the same configuration that opens it. An empty Protocol covers tcp and udp.
type Owner ¶
type Owner struct {
Kind OwnerKind `json:"kind"`
Name string `json:"name,omitempty"`
// Intentional is true when the operator explicitly asked for public
// access (a database's PubliclyAccessible, an app's public bind).
Intentional bool `json:"intentional,omitempty"`
}
Owner is the platform resource behind a container, if any.
type Plan ¶
type Plan struct {
Commands []string `json:"commands"`
Drops string `json:"drops"`
Tag string `json:"tag"`
// Persistence says plainly how long the rules last.
Persistence string `json:"persistence"`
}
Plan is the exact rule set a restriction produces.
type PortBinding ¶
PortBinding is one published port as Docker reports it.
type ReachCounts ¶
type ReachCounts struct {
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
Info int `json:"info"`
}
ReachCounts is the number of findings per severity.
type ReachEndpoint ¶
type ReachEndpoint struct {
Kind string `json:"kind"`
Value string `json:"value"`
Class Class `json:"class,omitempty"`
}
ReachEndpoint is one way into a subject from outside.
type ReachFinding ¶
type ReachFinding struct {
ID string `json:"id"`
Kind ReachKind `json:"kind"`
Severity Severity `json:"severity"`
Subject ReachRef `json:"subject"`
Node string `json:"node,omitempty"`
Port int `json:"port,omitempty"`
Params map[string]string `json:"params,omitempty"`
Fix *ReachFix `json:"fix,omitempty"`
}
ReachFinding is one thing the internet can reach that deserves a look.
type ReachFix ¶
type ReachFix struct {
Kind string `json:"kind"`
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
Body map[string]string `json:"body,omitempty"`
Link string `json:"link,omitempty"`
// Restarts is true when applying the fix recreates a container.
Restarts bool `json:"restarts,omitempty"`
}
ReachFix is the typed remedy for one finding. An auto fix is a call the dashboard makes with the operator's own session; a guided fix is a link.
type ReachInput ¶
ReachInput is everything BuildReach reads.
type ReachReport ¶
type ReachReport struct {
GeneratedAt string `json:"generated_at"`
Counts ReachCounts `json:"counts"`
Subjects []ReachSubject `json:"subjects"`
Findings []ReachFinding `json:"findings"`
// RulesReadable is false when a node's firewall could not be read, so restricted ports may be missing.
RulesReadable bool `json:"rules_readable"`
// RestrictedPorts is how many published ports a firewall rule narrows.
RestrictedPorts int `json:"restricted_ports"`
}
ReachReport is the "what is reachable from the internet" answer.
func BuildReach ¶
func BuildReach(in ReachInput) ReachReport
BuildReach merges the port audit with each app's domains and shield state and each database's published port into one report, with a typed fix per finding.
type ReachSubject ¶
type ReachSubject struct {
Type string `json:"type"`
Name string `json:"name"`
Endpoints []ReachEndpoint `json:"endpoints"`
Findings int `json:"findings"`
}
ReachSubject lists everything reachable for one app or database.
type Restriction ¶
type Restriction struct {
Port int `json:"port"`
Protocol string `json:"protocol"`
Allow []string `json:"allow"`
}
Restriction is a desired allow-list for one published port.
type Severity ¶
type Severity string
Severity ranks a finding for the operator.
type SyncResult ¶
SyncResult counts what one Sync pass changed.