Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,37 @@ Bind mount restrictions are applied to relevant Docker API endpoints and work wi

**Note**: This feature only restricts bind mounts. Other mount types (volumes, tmpfs, etc.) are not affected by this restriction.

#### Restricting dangerous host config fields

In addition to bind mount source allowlisting, socket-proxy can reject container creation requests that set `HostConfig` fields that break out of container isolation. Each check is opt-in via its own flag and defaults to off, so existing deployments are unaffected on upgrade.

| Flag | Env variable | Rejects when |
| --- | --- | --- |
| `-denyprivileged` | `SP_DENYPRIVILEGED` | `HostConfig.Privileged == true` |
| `-denycapadd` | `SP_DENYCAPADD` | `HostConfig.CapAdd` is non-empty (any added kernel capability) |
| `-denyhostnamespaces` | `SP_DENYHOSTNAMESPACES` | `HostConfig.NetworkMode` / `PidMode` / `IpcMode` / `UTSMode` / `UsernsMode` equals `host` (or a `host:*` form for modes that accept it) |

These checks apply to `POST /containers/create` and `POST /containers/{id}/update`. Swarm services do not surface these `HostConfig` fields in their API, so these flags have no effect there; only the bind mount filter applies to service create/update.

Example combining bind mount restrictions and host config deny flags:

```text
socket-proxy \
-allowbindmountfrom=/srv/projects \
-denyprivileged \
-denycapadd \
-denyhostnamespaces \
-allowGET='/v[0-9.]+/(_ping|version|containers/.*|images/.*)' \
-allowPOST='/v[0-9.]+/(containers/create|containers/.*/(start|stop|restart|kill))'
```

#### Setting up per-container allowlists

Allowlists for both requests and bind mount restrictions can be specified for particular containers. To do this:

1. Set `-proxycontainername` or the environment variable `SP_PROXYCONTAINERNAME` to the name of the socket proxy container.
2. Make sure that each container that will use the socket proxy is in a Docker network that the socket proxy container is also in.
3. Use the same regex syntax for request allowlists and for bind mount restrictions that were discussed earlier, but for labels on each container that will use the socket proxy. Each label name will have the prefix of `socket-proxy.allow.`, with `socket-proxy.allow.bindmountfrom` for bind mount restrictions. For example:
3. Use the same regex syntax for request allowlists and for bind mount restrictions that were discussed earlier, but for labels on each container that will use the socket proxy. Each label name will have the prefix of `socket-proxy.allow.`, with `socket-proxy.allow.bindmountfrom` for bind mount restrictions. Host config deny flags use the `socket-proxy.deny.` prefix and take a boolean string (`true`, `1`, `false`, `0`). For example:

```yaml
services:
Expand All @@ -142,6 +166,9 @@ services:
- 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy
- 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy
- 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy
- 'socket-proxy.deny.privileged=true' # reject container creation with HostConfig.Privileged=true
- 'socket-proxy.deny.capadd=true' # reject container creation that adds kernel capabilities
- 'socket-proxy.deny.hostnamespaces=true' # reject host NetworkMode/PidMode/IpcMode/UTSMode/UsernsMode
```

When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access.
Expand Down Expand Up @@ -249,6 +276,9 @@ socket-proxy can be configured via command-line parameters or via environment va
| `-proxysocketendpoint` | `SP_PROXYSOCKETENDPOINT` | (not set) | Proxy to the given unix socket instead of a TCP port |
| `-proxysocketendpointfilemode` | `SP_PROXYSOCKETENDPOINTFILEMODE` | `0600` | Explicitly set the file mode for the filtered unix socket endpoint (only useful with `-proxysocketendpoint`) |
| `-proxycontainername` | `SP_PROXYCONTAINERNAME` | (not set) | Provides the name of the socket proxy container to enable per-container allowlists specified by Docker container labels (not available with `-proxysocketendpoint`) |
| `-denyprivileged` | `SP_DENYPRIVILEGED` | (not set/false) | If set, reject container creation/update requests that set `HostConfig.Privileged=true`. Defaults to off. |
| `-denycapadd` | `SP_DENYCAPADD` | (not set/false) | If set, reject container creation/update requests that add kernel capabilities via `HostConfig.CapAdd`. Defaults to off. |
| `-denyhostnamespaces` | `SP_DENYHOSTNAMESPACES` | (not set/false) | If set, reject container creation/update requests that request host namespaces via `HostConfig.NetworkMode` / `PidMode` / `IpcMode` / `UTSMode` / `UsernsMode` equal to `host`. Defaults to off. |

### Changelog

Expand Down
115 changes: 96 additions & 19 deletions cmd/socket-proxy/bindmount.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,15 @@ type (
}
// containerHostConfig is the subset of github.com/docker/docker/api/types/container.HostConfig.
containerHostConfig struct {
Binds []string // List of volume bindings for this container.
Mounts []mountMount `json:",omitempty"` // Mounts specs used by the container.
Binds []string // List of volume bindings for this container.
Mounts []mountMount `json:",omitempty"` // Mounts specs used by the container.
Privileged bool `json:",omitempty"` // Is the container in privileged mode.
CapAdd []string `json:",omitempty"` // List of kernel capabilities to add to the container.
NetworkMode string `json:",omitempty"` // Network namespace ("host" gives host networking).
PidMode string `json:",omitempty"` // PID namespace ("host" gives host PID).
IpcMode string `json:",omitempty"` // IPC namespace ("host" gives host IPC).
UTSMode string `json:",omitempty"` // UTS namespace ("host" gives host UTS).
UsernsMode string `json:",omitempty"` // User namespace mode ("host" disables user namespace remapping).
Comment on lines +55 to +63
}
// swarmServiceSpec is the subset of github.com/docker/docker/api/types/swarm.ServiceSpec.
swarmServiceSpec struct {
Expand All @@ -78,39 +85,54 @@ type (
}
)

// checkBindMountRestrictions checks if bind mounts in the request are allowed.
func checkBindMountRestrictions(allowedBindMounts []string, r *http.Request) error {
// Only check if bind mount restrictions are configured
if len(allowedBindMounts) == 0 {
// hostConfigPolicy defines optional host config security restrictions applied
// alongside bind mount source allowlisting. The zero value means no extra
// restrictions are enforced beyond bind mount validation.
type hostConfigPolicy struct {
DenyPrivileged bool // reject HostConfig.Privileged == true
DenyCapAdd bool // reject non-empty HostConfig.CapAdd
DenyHostNamespaces bool // reject host value for NetworkMode/PidMode/IpcMode/UTSMode/UsernsMode
}

// isZero reports whether the policy enforces no restrictions.
func (p hostConfigPolicy) isZero() bool {
return !p.DenyPrivileged && !p.DenyCapAdd && !p.DenyHostNamespaces
}

// checkHostConfigRestrictions checks bind mount sources and host config
// security restrictions for relevant container/service API requests.
func checkHostConfigRestrictions(allowedBindMounts []string, policy hostConfigPolicy, r *http.Request) error {
// Only check if restrictions are configured
if len(allowedBindMounts) == 0 && policy.isZero() {
return nil
}

if r.Method != http.MethodPost {
return nil
}

// Check different API endpoints that can use bind mounts
// Check different API endpoints that can use bind mounts or set host config
pathParts := strings.Split(r.URL.Path, "/")
switch {
case len(pathParts) >= 4 && pathParts[2] == "containers" && pathParts[3] == "create":
// Container creation: /vX.xx/containers/create
return checkContainer(allowedBindMounts, r)
return checkContainer(allowedBindMounts, policy, r)
case len(pathParts) >= 5 && pathParts[2] == "containers" && pathParts[4] == "update":
// Container update: /vX.xx/containers/{id}/update
return checkContainer(allowedBindMounts, r)
return checkContainer(allowedBindMounts, policy, r)
case len(pathParts) >= 4 && pathParts[2] == "services" && pathParts[3] == "create":
// Service creation: /vX.xx/services/create
return checkService(allowedBindMounts, r)
return checkService(allowedBindMounts, policy, r)
case len(pathParts) >= 5 && pathParts[2] == "services" && pathParts[4] == "update":
// Service update: /vX.xx/services/{id}/update
return checkService(allowedBindMounts, r)
return checkService(allowedBindMounts, policy, r)
default:
return nil
}
}

// checkContainer checks bind mounts in container creation requests.
func checkContainer(allowedBindMounts []string, r *http.Request) error {
// checkContainer checks bind mounts and host config in container creation/update requests.
func checkContainer(allowedBindMounts []string, policy hostConfigPolicy, r *http.Request) error {
body, err := readAndRestoreBody(r)
if err != nil {
return err
Expand All @@ -122,11 +144,13 @@ func checkContainer(allowedBindMounts []string, r *http.Request) error {
return nil // Don't block if we can't parse.
}

return checkHostConfigBindMounts(allowedBindMounts, req.HostConfig)
return checkHostConfig(allowedBindMounts, policy, req.HostConfig)
}

// checkService checks bind mounts in service creation requests.
func checkService(allowedBindMounts []string, r *http.Request) error {
// checkService checks bind mounts and host config in service creation/update requests.
// Swarm services only allow specifying Mounts (not Binds) and do not expose the
// host-namespace/privileged fields, so only the Mounts list is forwarded for validation.
func checkService(allowedBindMounts []string, policy hostConfigPolicy, r *http.Request) error {
Comment on lines +150 to +153
Comment on lines +150 to +153
body, err := readAndRestoreBody(r)
if err != nil {
return err
Expand All @@ -141,20 +165,38 @@ func checkService(allowedBindMounts []string, r *http.Request) error {
if req.TaskTemplate.ContainerSpec == nil {
return nil // No container spec, nothing to check.
}
return checkHostConfigBindMounts(
return checkHostConfig(
allowedBindMounts,
policy,
&containerHostConfig{
Mounts: req.TaskTemplate.ContainerSpec.Mounts,
},
)
}

// checkHostConfigBindMounts checks bind mounts in HostConfig.
func checkHostConfigBindMounts(allowedBindMounts []string, hostConfig *containerHostConfig) error {
// checkHostConfig validates bind mount sources and host config security restrictions.
func checkHostConfig(allowedBindMounts []string, policy hostConfigPolicy, hostConfig *containerHostConfig) error {
if hostConfig == nil {
return nil // No HostConfig, nothing to check
}

if len(allowedBindMounts) > 0 {
if err := checkHostConfigBindMounts(allowedBindMounts, hostConfig); err != nil {
return err
}
}

if !policy.isZero() {
if err := checkHostConfigSecurity(policy, hostConfig); err != nil {
return err
}
}

return nil
}

// checkHostConfigBindMounts checks bind mounts in HostConfig.
func checkHostConfigBindMounts(allowedBindMounts []string, hostConfig *containerHostConfig) error {
// Check legacy Binds field
for _, bind := range hostConfig.Binds {
if err := validateBindMount(allowedBindMounts, bind); err != nil {
Expand All @@ -174,6 +216,41 @@ func checkHostConfigBindMounts(allowedBindMounts []string, hostConfig *container
return nil
}

// checkHostConfigSecurity rejects host config fields that punch through container isolation.
// Each check is gated by its corresponding policy flag; with the zero policy this function is a no-op.
func checkHostConfigSecurity(policy hostConfigPolicy, hostConfig *containerHostConfig) error {
if policy.DenyPrivileged && hostConfig.Privileged {
return fmt.Errorf("privileged containers not allowed")
}
if policy.DenyCapAdd && len(hostConfig.CapAdd) > 0 {
return fmt.Errorf("adding kernel capabilities not allowed: %v", hostConfig.CapAdd)
}
if policy.DenyHostNamespaces {
if mode := hostConfig.NetworkMode; isHostNamespace(mode) {
return fmt.Errorf("host network mode not allowed: %s", mode)
}
if mode := hostConfig.PidMode; isHostNamespace(mode) {
return fmt.Errorf("host PID namespace not allowed: %s", mode)
}
if mode := hostConfig.IpcMode; isHostNamespace(mode) {
return fmt.Errorf("host IPC namespace not allowed: %s", mode)
}
if mode := hostConfig.UTSMode; isHostNamespace(mode) {
return fmt.Errorf("host UTS namespace not allowed: %s", mode)
}
if mode := hostConfig.UsernsMode; isHostNamespace(mode) {
return fmt.Errorf("host user namespace mode not allowed: %s", mode)
}
}
return nil
}

// isHostNamespace reports whether a HostConfig namespace mode string requests the host namespace.
// Docker accepts both the bare "host" value and prefixed forms like "host:..." for some modes.
func isHostNamespace(mode string) bool {
return mode == "host" || strings.HasPrefix(mode, "host:")
}

// validateBindMount validates a bind mount string in the format "source:target:options".
func validateBindMount(allowedBindMounts []string, bind string) error {
parts := strings.Split(bind, ":")
Expand Down
Loading
Loading