exposure

package
v0.8.6 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

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

View Source
const (
	SubjectApp      = "app"
	SubjectDatabase = "database"
	SubjectHost     = "host"
)

Subject types a finding or endpoint list belongs to.

View Source
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

View Source
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 Explain

func Explain(f Finding, outsideLabel string) (explanation, recommendation string)

Explain returns the plain-language explanation and the suggested action.

func ValidCIDR

func ValidCIDR(s string) bool

ValidCIDR reports whether s parses as an IP or CIDR.

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

func ParseChain(out string) Chain

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 Class

type Class string

Class is how reachable one published port is.

const (
	ClassLoopback   Class = "loopback"
	ClassPrivate    Class = "private"
	ClassRestricted Class = "restricted"
	ClassExposed    Class = "exposed"
	ClassUnknown    Class = "unknown"
)

Reachability classes.

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 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

func Audit(containers []Container, chain Chain, prefix string) []Finding

Audit turns a container list into findings, one per published (port, protocol, container) and sorted worst first.

func (Finding) NeedsAttention

func (f Finding) NeedsAttention() bool

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

func ImageKindOf(image string, containerPort int) ImageKind

ImageKindOf guesses what a container runs from its image repository name, falling back to the container port.

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

func NewManager(prefix string, protected []int) *Manager

NewManager builds a Manager over the real iptables binary. protected lists ports that must never be restricted (control plane, ingress, SSH).

func (*Manager) Apply

func (m *Manager) Apply(ctx context.Context, r Restriction) (changed bool, err error)

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) Normalize

func (m *Manager) Normalize(r Restriction) (Restriction, error)

Normalize validates a restriction and returns it in canonical form.

func (*Manager) Plan

func (m *Manager) Plan(r Restriction) (Plan, error)

Plan renders the exact commands Apply would run, in execution order.

func (*Manager) Prefix

func (m *Manager) Prefix() string

Prefix returns the rule comment prefix this manager owns.

func (*Manager) ReadChain

func (m *Manager) ReadChain(ctx context.Context) Chain

ReadChain reads DOCKER-USER, reporting why it could not when it cannot.

func (*Manager) Remove

func (m *Manager) Remove(ctx context.Context, port int, proto string) error

Remove deletes the rules this manager tagged for a port, and nothing else.

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 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 OwnerKind

type OwnerKind string

OwnerKind says who owns the container publishing a port.

const (
	OwnerDatabase  OwnerKind = "database"
	OwnerApp       OwnerKind = "app"
	OwnerUnmanaged OwnerKind = "unmanaged"
)

Owner kinds.

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

type PortBinding struct {
	HostIP        string
	HostPort      int
	ContainerPort int
	Protocol      string
}

PortBinding is one published port as Docker reports it.

type ReachApp

type ReachApp struct {
	Name       string
	Domains    []string
	Unshielded []string
}

ReachApp is an app's public names and the domains that have no shield.

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 ReachDB

type ReachDB struct {
	Name       string
	PublicPort int
}

ReachDB is a database with a published port, if any.

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

type ReachInput struct {
	Now       string
	Apps      []ReachApp
	Databases []ReachDB
	Nodes     []ReachNode
}

ReachInput is everything BuildReach reads.

type ReachKind

type ReachKind string

ReachKind names one kind of internet-reachability finding.

const (
	ReachAppDirectPort  ReachKind = "app_direct_port"
	ReachAppNoShield    ReachKind = "app_no_shield"
	ReachDatabasePublic ReachKind = "database_public"
	ReachPortExposed    ReachKind = "port_exposed"
	ReachDockerAPI      ReachKind = "docker_api"
)

Reachability finding kinds.

type ReachNode

type ReachNode struct {
	Name          string
	Local         bool
	RulesReadable bool
	Findings      []Finding
}

ReachNode is one node's audit result.

type ReachRef

type ReachRef struct {
	Type string `json:"type"`
	Name string `json:"name,omitempty"`
}

ReachRef points at the app, database or host a finding is about.

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 Runner

type Runner func(ctx context.Context, name string, args ...string) ([]byte, error)

Runner executes a command, abstracted so tests never touch a real firewall.

type Severity

type Severity string

Severity ranks a finding for the operator.

const (
	SeverityHigh   Severity = "high"
	SeverityMedium Severity = "medium"
	SeverityLow    Severity = "low"
	SeverityInfo   Severity = "info"
)

Finding severities.

func SeverityFor

func SeverityFor(class Class, kind ImageKind, owner Owner) Severity

SeverityFor ranks a finding by what is exposed, not just that it is.

type SyncResult

type SyncResult struct {
	Applied int
	Removed int
	Errors  []string
}

SyncResult counts what one Sync pass changed.

type Verdict

type Verdict struct {
	Class   Class
	Allowed []string
	Rules   []string
	Managed bool
	Reason  string
}

Verdict is the reachability decision for one published port.

func Classify

func Classify(binds []string, hostPort, containerPort int, proto string, chain Chain, prefix string) Verdict

Classify decides how reachable a binding is given the DOCKER-USER chain. It never reports restricted unless a simple matching DROP or REJECT exists.

Jump to

Keyboard shortcuts

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