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
- Variables
- func BootTimeout() time.Duration
- func LogPrivilege(ctx context.Context, logger *slog.Logger, client PrivilegeSource) bool
- func RuleIsHighRisk(rule string) bool
- func RunningInContainer() bool
- func SaveSettings(path string, s Settings) error
- func SocketPath(lookup func(string) (string, bool), dataDir string) string
- func UpstreamSocket(host string) (string, error)
- type BootConfig
- type Booted
- type Config
- type Controller
- type Decision
- type FileSink
- type Grants
- type Guard
- type Mode
- type Policy
- type PrivilegeSource
- type Risk
- type RuleCount
- type Server
- type Settings
- type Sink
- type Stats
- type Status
- type Tunables
- type Violation
Constants ¶
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.
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.
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.
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.
const ( SourceEnv = "env" SourceSettings = "settings" SourceDefault = "default" )
Mode sources, reported so an operator knows where to change it.
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.
const FileSinkName = "docker-guard-audit.jsonl"
FileSinkName is the node-local audit file a node agent appends to.
const SettingsFile = "docker-guard.json"
SettingsFile is the operator's persisted mode choice under the data dir.
Variables ¶
var AllRules = []string{ RuleEndpointNotAllowed, RulePathNoncanonical, RuleBodyTooLarge, RuleBodyInvalid, RulePrivileged, RuleHostPID, RuleHostIPC, RuleHostUTS, RuleHostUserns, RuleHostCgroupns, RuleContainerNS, RuleNetworkHost, RuleCapAdd, RuleDevices, RuleDeviceCgroupRules, RuleSecurityOpt, RuleMaskedPaths, RuleBindSensitive, RuleBindUndeclared, RuleMountType, RuleVolumesFrom, RuleVolumeBindDriverOpt, RuleExecPrivileged, RuleImageImport, }
AllRules lists every rule id, in the order docs/docker-access.md documents them.
var ErrConfigInvalid = errors.New("dockerguard: settings file or mode is invalid")
ErrConfigInvalid means the settings file or mode failed to load, so the mode cannot be changed until the operator fixes it and restarts.
var ErrPinnedByEnv = errors.New("dockerguard: mode is pinned by " + EnvMode)
ErrPinnedByEnv means APP_DOCKER_GUARD is set, so the API cannot change it.
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 ¶
BootTimeout bounds every Docker call made while the control plane boots.
func LogPrivilege ¶
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 RuleIsHighRisk ¶
RuleIsHighRisk is true for every rule not explicitly classified low.
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 ¶
SaveSettings writes path atomically with mode 0600.
func SocketPath ¶
SocketPath is where the guard listens: APP_DOCKER_GUARD_SOCKET, or a private directory under dataDir.
func UpstreamSocket ¶
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).
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) NoteBoot ¶
func (c *Controller) NoteBoot(now time.Time) error
NoteBoot records what the guard actually ran as at boot: an audit window survives only a restart that stays in audit, anything else clears it.
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.
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 ¶
NewFileSink returns a sink appending to path.
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 (*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 (*Guard) Close ¶
func (g *Guard) Close()
Close stops the async recorder after draining what it already queued.
type Mode ¶
type Mode string
Mode selects what the guard does with a request a rule would deny.
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 ¶
Server is a running guard socket.
func Listen ¶
Listen creates the guard socket at socket (mode 0600, parent 0700) and serves g on it until ctx ends or Close is called.
type Settings ¶
type Settings struct {
Mode Mode `json:"mode,omitempty"`
// AuditSince is when the guard started observing in audit mode without a
// break, so the feed can tell a week of clean observation from a fresh start.
AuditSince time.Time `json:"audit_since,omitempty"`
// LastRunMode is the mode the guard actually ran in at the last boot or live switch.
LastRunMode Mode `json:"last_run_mode,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 ¶
LoadSettings reads path; a missing file is the zero Settings.
type Stats ¶
type Stats struct {
// contains filtered or unexported fields
}
Stats counts flagged requests per rule since the guard started.
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.