egress

package
v0.5.2 Latest Latest
Warning

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

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

Documentation

Overview

Package egress enforces the per-service network allowlist.

The rule is that a service reaches only what its manifest declares, and a service that declares nothing reaches nothing. That matters most for the step devbay runs on freshly cloned code it has never seen: installing dependencies. A malicious postinstall script is the delivery mechanism the self-replicating npm worms use, and its whole purpose is to reach the network with whatever credentials are lying around.

Why filtering, and not a network topology

Docker has an `internal` network that blocks outbound traffic entirely, and it would be a much simpler mechanism. It cannot be used here: a container on an internal network cannot publish a port, and devbay publishes ports so the daemon can health-probe a service and an agent can call it. That was measured rather than assumed -- an internal-network container with `-p` fails to serve at all.

So the filtering happens inside the container's own network namespace, applied by a short-lived privileged sidecar that joins that namespace and exits. This is deliberately scoped: the rules are written to one container's netns and never to the VM-wide chain, because a broad OUTPUT DROP applied at the VM level takes out the Docker VM's own DHCP and requires a factory reset to recover.

What this is and is not

It reduces blast radius. It is not a containment boundary for hostile code, and must not be described as one. Allowlisting resolves hostnames to addresses, so a name that resolves to a shared CDN address permits every other name on that address; DNS answers change; and a determined process inside the container can still talk to anything sharing an allowed address.

Index

Constants

View Source
const Image = "devbay-egress:1"

Image is the sidecar, built locally from baseImage.

It has to ship iptables rather than install it at run time. The sidecar joins the target's network namespace, so once a policy is in force the sidecar inherits it -- and an install step inside that namespace cannot reach a package repository. Re-applying a policy would fail for the very reason the policy exists, which is a bootstrap problem rather than a configuration one.

Building it locally also means the privileged container is the smallest thing that can do the job, rather than a general-purpose debugging image.

Variables

View Source
var DefaultAllowed = []string{
	"registry.npmjs.org",
	"registry.yarnpkg.com",
	"pypi.org",
	"files.pythonhosted.org",
	"rubygems.org",
	"index.rubygems.org",
	"proxy.golang.org",
	"sum.golang.org",
	"crates.io",
	"static.crates.io",
}

DefaultAllowed is the policy a service inherits when it declares egress but names only what it specifically needs.

Package registries are here because installing dependencies is the one thing almost every service must do and the one thing an allowlist most often forgets, producing a failure that looks like a broken lockfile rather than a blocked connection.

Functions

This section is empty.

Types

type Enforcer

type Enforcer struct {
	Log func(format string, args ...any)

	// Resolve maps a hostname to addresses. Replaced in tests so the rules can
	// be checked without depending on the internet.
	Resolve func(ctx context.Context, host string) ([]net.IP, error)
	// contains filtered or unexported fields
}

Enforcer applies policies to running containers.

func New

func New(cli *client.Client, logf func(string, ...any)) *Enforcer

New returns an Enforcer.

func (*Enforcer) Apply

func (e *Enforcer) Apply(ctx context.Context, containerID string, p Policy) error

Apply installs the policy in a container's network namespace.

It runs after the container has started, because a namespace has to exist before rules can be written to it. The window between start and enforcement is real and is the reason install steps run as their own container: an install is enforced before the long-running service is even created.

func (*Enforcer) Cleanup

func (e *Enforcer) Cleanup(ctx context.Context) error

Cleanup removes any sidecar left behind by an interrupted run.

type Policy

type Policy struct {
	// Service names the service, for messages.
	Service string
	// Allow is the hostnames the service declared. Empty means no outbound.
	Allow []string
	// AllowDefaults adds the package registries in DefaultAllowed.
	AllowDefaults bool
}

Policy is the allowlist for one container.

Jump to

Keyboard shortcuts

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