dockerguard

package
v0.8.8 Latest Latest
Warning

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

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

Documentation

Overview

Package dockerguard is an in-process Unix socket proxy in front of the Docker Engine API. It forwards only an allowlisted set of endpoints and rejects container configurations that would hand a container the host. See docs/docker-access.md for the surface and the rules.

Index

Constants

View Source
const (
	EnvMode             = "APP_DOCKER_GUARD"
	EnvSocket           = "APP_DOCKER_GUARD_SOCKET"
	EnvMaxBodyBytes     = "APP_DOCKER_GUARD_MAX_BODY_BYTES"
	EnvAuditDedup       = "APP_DOCKER_GUARD_AUDIT_DEDUP"
	EnvSummaryWindow    = "APP_DOCKER_GUARD_SUMMARY_WINDOW"
	EnvAllowHostNetwork = "APP_DOCKER_GUARD_ALLOW_HOST_NETWORK"
	EnvHeaderTimeout    = "APP_DOCKER_GUARD_HEADER_TIMEOUT"
	EnvRecordQueue      = "APP_DOCKER_GUARD_RECORD_QUEUE"
)

Environment variables the guard reads.

View Source
const (
	DefaultMaxBodyBytes  int64 = 4 << 20
	DefaultAuditDedup          = time.Hour
	DefaultSummaryWindow       = 7 * 24 * time.Hour
	DefaultHeaderTimeout       = 30 * time.Second
	DefaultRecordQueue         = 256
	// SocketName is the guard socket's file name under the data dir.
	SocketName = "docker-guard.sock"
)

Defaults for the tunables above.

View Source
const (
	ActionDenied    = "docker_guard.denied"
	ActionWouldDeny = "docker_guard.would_deny"
	// ActionFamily matches both in an audit search.
	ActionFamily = "docker_guard"
)

Audit actions a Sink writes.

View Source
const (
	RuleEndpointNotAllowed  = "endpoint_not_allowed"
	RulePathNoncanonical    = "path_noncanonical"
	RuleBodyTooLarge        = "body_too_large"
	RuleBodyInvalid         = "body_invalid"
	RulePrivileged          = "privileged"
	RuleHostPID             = "host_pid"
	RuleHostIPC             = "host_ipc"
	RuleHostUTS             = "host_uts"
	RuleHostUserns          = "host_userns"
	RuleHostCgroupns        = "host_cgroupns"
	RuleContainerNS         = "container_namespace_share"
	RuleNetworkHost         = "network_host"
	RuleCapAdd              = "cap_add_disallowed"
	RuleDevices             = "devices_not_granted"
	RuleDeviceCgroupRules   = "device_cgroup_rules"
	RuleSecurityOpt         = "security_opt_weakened"
	RuleMaskedPaths         = "masked_paths_override"
	RuleBindSensitive       = "bind_sensitive_path"
	RuleBindUndeclared      = "bind_undeclared"
	RuleMountType           = "mount_type_disallowed"
	RuleVolumesFrom         = "volumes_from"
	RuleVolumeBindDriverOpt = "volume_bind_driver_opts"
	RuleExecPrivileged      = "exec_privileged"
	RuleImageImport         = "image_import"
)

Rule ids. They appear in audit rows, logs and the 403 body, so they are part of the public contract: never rename one, add a new id instead.

View Source
const (
	SourceEnv      = "env"
	SourceSettings = "settings"
	SourceDefault  = "default"
)

Mode sources, reported so an operator knows where to change it.

View Source
const DefaultMode = ModeAudit

DefaultMode is audit so an upgrade cannot break a running deploy; the attention feed offers enforce after a clean week. APP_DOCKER_GUARD sets it explicitly.

View Source
const FileSinkName = "docker-guard-audit.jsonl"

FileSinkName is the node-local audit file a node agent appends to.

View Source
const SettingsFile = "docker-guard.json"

SettingsFile is the operator's persisted mode choice under the data dir.

Variables

AllRules lists every rule id, in the order docs/docker-access.md documents them.

View Source
var ErrPinnedByEnv = errors.New("dockerguard: mode is pinned by " + EnvMode)

ErrPinnedByEnv means APP_DOCKER_GUARD is set, so the API cannot change it.

View Source
var GrantableCaps = []string{"NET_ADMIN", "NET_RAW"}

GrantableCaps are capabilities a container may add only when its own create was declared with them: the egress sidecar's firewall needs these.

Functions

func BootTimeout

func BootTimeout() time.Duration

BootTimeout bounds every Docker call made while the control plane boots.

func LogPrivilege

func LogPrivilege(ctx context.Context, logger *slog.Logger, client PrivilegeSource) bool

LogPrivilege records how much a compromise of this process would hold. It returns false when the daemon did not answer within BootTimeout, so boot can continue degraded instead of hanging.

func RunningInContainer

func RunningInContainer() bool

RunningInContainer reports whether this process sees a container's filesystem rather than the host's, where host path checks cannot resolve.

func SaveSettings

func SaveSettings(path string, s Settings) error

SaveSettings writes path atomically with mode 0600.

func SocketPath

func SocketPath(lookup func(string) (string, bool), dataDir string) string

SocketPath is where the guard listens: APP_DOCKER_GUARD_SOCKET, or a private directory under dataDir.

func UpstreamSocket

func UpstreamSocket(host string) (string, error)

UpstreamSocket turns a docker host URL into the Unix socket path the guard forwards to. Only unix:// hosts can be guarded.

Types

type BootConfig

type BootConfig struct {
	// DataDir holds the socket and settings file and is itself protected.
	DataDir string
	// ExtraProtected are further host paths no bind may cover.
	ExtraProtected []string
	// ResolveSymlinks is false when this process does not share the
	// daemon's view of the host filesystem.
	ResolveSymlinks bool
	Sink            Sink
	Logger          *slog.Logger
	Lookup          func(string) (string, bool)
}

BootConfig is what a binary passes to Boot.

type Booted

type Booted struct {
	Server     *Server
	Controller *Controller
	Grants     *Grants
	Host       string
}

Booted is Boot's result. Host is empty when clients should dial Docker directly (mode off, or audit mode whose socket failed to start).

func Boot

func Boot(ctx context.Context, cfg BootConfig) (Booted, error)

Boot resolves the mode and, unless off, starts the guard in front of the detected daemon socket. In enforce mode any failure is returned so the caller refuses to start rather than run unguarded.

type Config

type Config struct {
	Mode     Mode
	Upstream string // Unix socket path of the real daemon
	Policy   Policy
	Grants   *Grants
	Tunables Tunables
	// Sink receives every decision worth an audit row; may be nil.
	Sink   Sink
	Logger *slog.Logger
}

Config is what New needs.

type Controller

type Controller struct {
	// contains filtered or unexported fields
}

Controller owns the guard's configuration for the API: read status, switch mode, persist the choice.

func NewController

func NewController(settingsPath string, lookup func(string) (string, bool), server *Server, upstream string, startErr error) *Controller

NewController wraps server (nil when the guard is off or failed to start).

func (*Controller) NoteAuditStart

func (c *Controller) NoteAuditStart(now time.Time) error

NoteAuditStart stamps AuditSince the first time audit mode runs.

func (*Controller) SetMode

func (c *Controller) SetMode(mode Mode, actor string, now time.Time) (Status, error)

SetMode persists mode and applies it live when the guard is running.

func (*Controller) Status

func (c *Controller) Status() Status

Status reports the current configuration and counters.

type Decision

type Decision struct {
	Mode       Mode
	Denied     bool
	Method     string
	Path       string
	Pattern    string
	Container  string
	Violations []Violation
	At         time.Time
}

Decision is one request a rule flagged. It never carries a body.

func (Decision) Action

func (d Decision) Action() string

Action is the audit action name for d.

func (Decision) Rule

func (d Decision) Rule() string

Rule is the primary (first) rule broken.

func (Decision) Rules

func (d Decision) Rules() []string

Rules lists every distinct rule broken, primary first.

type FileSink

type FileSink struct {
	// contains filtered or unexported fields
}

FileSink appends one JSON line per decision, for a process with no database (the node agent). The file is created 0600.

func NewFileSink

func NewFileSink(path string) *FileSink

NewFileSink returns a sink appending to path.

func (*FileSink) RecordDecision

func (f *FileSink) RecordDecision(_ context.Context, d Decision) error

RecordDecision implements Sink.

type Grants

type Grants struct {
	// contains filtered or unexported fields
}

Grants tracks what each in-flight container create was declared with by internal/docker's Create. A declaration lives only for the duration of that call, so a request that did not come from Create carries no grant.

func NewGrants

func NewGrants() *Grants

NewGrants returns an empty registry.

func (*Grants) DeclareCreate

func (g *Grants) DeclareCreate(d docker.CreateDeclaration) func()

DeclareCreate implements docker.CreateDeclarer.

type Guard

type Guard struct {
	// contains filtered or unexported fields
}

Guard is the policy-enforcing reverse proxy. It is an http.Handler; Serve puts it on a Unix socket.

func New

func New(cfg Config) *Guard

New builds a Guard forwarding to cfg.Upstream.

func (*Guard) Close

func (g *Guard) Close()

Close stops the async recorder after draining what it already queued.

func (*Guard) Mode

func (g *Guard) Mode() Mode

Mode returns the mode in force right now.

func (*Guard) ServeHTTP

func (g *Guard) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Guard) SetMode

func (g *Guard) SetMode(m Mode)

SetMode switches between audit and enforce without a restart. Off is treated as audit: the socket stays up until the next boot.

func (*Guard) Stats

func (g *Guard) Stats() *Stats

Stats exposes the in-memory counters since boot.

type Mode

type Mode string

Mode selects what the guard does with a request a rule would deny.

const (
	// ModeOff starts no proxy: Docker clients dial the daemon directly.
	ModeOff Mode = "off"
	// ModeAudit forwards everything and records what enforce would deny.
	ModeAudit Mode = "audit"
	// ModeEnforce rejects denied requests with 403 before they reach Docker.
	ModeEnforce Mode = "enforce"
)

func ParseMode

func ParseMode(s string) (Mode, error)

ParseMode validates s. Empty means DefaultMode. An unknown value returns ModeEnforce with an error: a typo must never silently disable the guard.

func Resolve

func Resolve(lookup func(string) (string, bool), s Settings) (Mode, string, error)

Resolve picks the effective mode: the env var wins, then the settings file, then DefaultMode. An invalid value anywhere resolves to enforce.

type Policy

type Policy struct {
	// AllowedCaps may appear in CapAdd on any container: the hardening
	// profile's minimal set plus operator extras.
	AllowedCaps []string
	// SensitivePaths are host paths no bind may cover, on top of
	// internal/bindmount.ForbiddenPaths.
	SensitivePaths []string
	// AllowHostNetwork lets a create declared with host networking through.
	AllowHostNetwork bool
	// ResolveSymlinks checks a bind's resolved host path too. Off when this
	// process cannot see the host filesystem (the agent in a container).
	ResolveSymlinks bool
}

Policy is the fixed part of what the body validator allows.

func PolicyFromHardening

func PolicyFromHardening(h docker.HardeningConfig, t Tunables, dataDirs ...string) Policy

PolicyFromHardening builds the default policy from the hardening profile and the data directories this process owns.

type PrivilegeSource

type PrivilegeSource interface {
	Runtime() docker.RuntimeInfo
	DaemonSecurity(ctx context.Context) (docker.DaemonSecurity, error)
}

PrivilegeSource is the slice of the Docker client LogPrivilege reads.

type RuleCount

type RuleCount struct {
	Rule      string    `json:"rule"`
	Denied    int64     `json:"denied"`
	WouldDeny int64     `json:"would_deny"`
	LastSeen  time.Time `json:"last_seen"`
	// LastPath is the most recent canonical path, never a body.
	LastPath string `json:"last_path"`
}

RuleCount is how often one rule fired since boot.

type Server

type Server struct {
	Guard  *Guard
	Socket string
	// contains filtered or unexported fields
}

Server is a running guard socket.

func Listen

func Listen(ctx context.Context, g *Guard, socket string) (*Server, error)

Listen creates the guard socket at socket (mode 0600, parent 0700) and serves g on it until ctx ends or Close is called.

func (*Server) Close

func (s *Server) Close() error

Close stops accepting, waits briefly for in-flight requests, then drains the recorder. Hijacked streams are left to their own clients.

func (*Server) Host

func (s *Server) Host() string

Host is the docker host URL clients dial to reach the guard.

type Settings

type Settings struct {
	Mode Mode `json:"mode,omitempty"`
	// AuditSince is when audit mode was first observed, so the attention
	// feed can tell a week of clean observation from a fresh install.
	AuditSince time.Time `json:"audit_since,omitempty"`
	UpdatedAt  time.Time `json:"updated_at,omitempty"`
	UpdatedBy  string    `json:"updated_by,omitempty"`
}

Settings is the persisted part of the guard configuration.

func LoadSettings

func LoadSettings(path string) (Settings, error)

LoadSettings reads path; a missing file is the zero Settings.

type Sink

type Sink interface {
	RecordDecision(ctx context.Context, d Decision) error
}

Sink persists a decision, typically as an audit row.

type Stats

type Stats struct {
	// contains filtered or unexported fields
}

Stats counts flagged requests per rule since the guard started.

func (*Stats) Snapshot

func (s *Stats) Snapshot() []RuleCount

Snapshot returns the counters sorted by rule id.

type Status

type Status struct {
	// Mode is the configured mode, Running whether the socket is up, and
	// Effective what the running guard does now.
	Mode            Mode        `json:"mode"`
	Source          string      `json:"source"`
	Effective       Mode        `json:"effective"`
	Running         bool        `json:"running"`
	Socket          string      `json:"socket,omitempty"`
	Upstream        string      `json:"upstream,omitempty"`
	RestartRequired bool        `json:"restart_required"`
	ConfigError     string      `json:"config_error,omitempty"`
	StartError      string      `json:"start_error,omitempty"`
	AuditSince      time.Time   `json:"audit_since,omitempty"`
	SinceBoot       []RuleCount `json:"since_boot"`
	Rules           []string    `json:"rules"`
}

Status is what the API and doctor report.

type Tunables

type Tunables struct {
	MaxBodyBytes     int64
	AuditDedup       time.Duration
	SummaryWindow    time.Duration
	HeaderTimeout    time.Duration
	RecordQueue      int
	AllowHostNetwork bool
}

Tunables are the guard's operator-adjustable limits.

func TunablesFromEnv

func TunablesFromEnv(lookup func(string) (string, bool)) (Tunables, error)

TunablesFromEnv reads Tunables, keeping the default for any unset or invalid value and returning the first parse error for the caller to log.

type Violation

type Violation struct {
	Rule   string `json:"rule"`
	Reason string `json:"reason"`
}

Violation is one rule a request breaks. Reason names fields and host paths only, never values such as env vars or credentials.

Jump to

Keyboard shortcuts

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