Skip to content

Commit

Permalink
Merge pull request #8261 from hashicorp/f-disable-host-volume-by-default
Browse files Browse the repository at this point in the history
Restrict Host filesystem access in Docker and Qemu
  • Loading branch information
Mahmood Ali committed Jun 25, 2020
2 parents 5b6d0eb + cd4315e commit 2ce820a
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 7 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ FEATURES:
* **Monitor UI**: Stream client and agent logs from the UI just like you would with the nomad monitor CLI command. [[GH-8177](https://github.com/hashicorp/nomad/issues/8177)]
* **Scaling UI**: Quickly adjust the count of a task group from the UI for task groups with a scaling declaration. [[GH-8207](https://github.com/hashicorp/nomad/issues/8207)]

__BACKWARDS INCOMPATIBILITIES:__
* driver/docker: The Docker driver no longer allows binding host volumes by default.
Operators can set `volume` `enabled` plugin configuration to restore previous permissive behavior. [[GH-8261](https://github.com/hashicorp/nomad/issues/8261)]
* driver/qemu: The Qemu driver requires images to reside in a operator-defined paths allowed for task access. [[GH-8261](https://github.com/hashicorp/nomad/issues/8261)]

IMPROVEMENTS:

* core: Support for persisting previous task group counts when updating a job [[GH-8168](https://github.com/hashicorp/nomad/issues/8168)]
Expand Down
5 changes: 1 addition & 4 deletions drivers/docker/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,7 @@ var (
// defaulted needed for both if the volumes {...} block is not set and
// if the default fields are missing
"volumes": hclspec.NewDefault(hclspec.NewBlock("volumes", false, hclspec.NewObject(map[string]*hclspec.Spec{
"enabled": hclspec.NewDefault(
hclspec.NewAttr("enabled", "bool", false),
hclspec.NewLiteral("true"),
),
"enabled": hclspec.NewAttr("enabled", "bool", false),
"selinuxlabel": hclspec.NewAttr("selinuxlabel", "string", false),
})), hclspec.NewLiteral("{ enabled = true }")),
"allow_privileged": hclspec.NewAttr("allow_privileged", "bool", false),
Expand Down
50 changes: 49 additions & 1 deletion drivers/qemu/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ var (
}

// configSpec is the hcl specification returned by the ConfigSchema RPC
configSpec = hclspec.NewObject(map[string]*hclspec.Spec{})
configSpec = hclspec.NewObject(map[string]*hclspec.Spec{
"image_paths": hclspec.NewAttr("image_paths", "list(string)", false),
})

// taskConfigSpec is the hcl specification for the driver config section of
// a taskConfig within a job. It is returned in the TaskConfigSchema RPC
Expand Down Expand Up @@ -126,12 +128,21 @@ type TaskState struct {
StartedAt time.Time
}

// Config is the driver configuration set by SetConfig RPC call
type Config struct {
// ImagePaths is an allow-list of paths qemu is allowed to load an image from
ImagePaths []string `codec:"image_paths"`
}

// Driver is a driver for running images via Qemu
type Driver struct {
// eventer is used to handle multiplexing of TaskEvents calls such that an
// event can be broadcast to all callers
eventer *eventer.Eventer

// config is the driver configuration set by the SetConfig RPC
config Config

// tasks is the in memory datastore mapping taskIDs to qemuTaskHandle
tasks *taskStore

Expand Down Expand Up @@ -165,6 +176,14 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) {
}

func (d *Driver) SetConfig(cfg *base.Config) error {
var config Config
if len(cfg.PluginConfig) != 0 {
if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil {
return err
}
}

d.config = config
if cfg.AgentConfig != nil {
d.nomadConfig = cfg.AgentConfig.Driver
}
Expand Down Expand Up @@ -290,6 +309,31 @@ func (d *Driver) RecoverTask(handle *drivers.TaskHandle) error {
return nil
}

func isAllowedImagePath(allowedPaths []string, allocDir, imagePath string) bool {
if !filepath.IsAbs(imagePath) {
imagePath = filepath.Join(allocDir, imagePath)
}

isParent := func(parent, path string) bool {
rel, err := filepath.Rel(parent, path)
return err == nil && !strings.HasPrefix(rel, "..")
}

// check if path is under alloc dir
if isParent(allocDir, imagePath) {
return true
}

// check allowed paths
for _, ap := range allowedPaths {
if isParent(ap, imagePath) {
return true
}
}

return false
}

func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error) {
if _, ok := d.tasks.Get(cfg.ID); ok {
return nil, nil, fmt.Errorf("taskConfig with ID '%s' already started", cfg.ID)
Expand All @@ -314,6 +358,10 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
}
vmID := filepath.Base(vmPath)

if !isAllowedImagePath(d.config.ImagePaths, cfg.AllocDir, vmPath) {
return nil, nil, fmt.Errorf("image_path is not in the allowed paths")
}

// Parse configuration arguments
// Create the base arguments
accelerator := "tcg"
Expand Down
29 changes: 29 additions & 0 deletions drivers/qemu/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,32 @@ config {

require.EqualValues(t, expected, tc)
}

func TestIsAllowedImagePath(t *testing.T) {
allowedPaths := []string{"/tmp", "/opt/qemu"}
allocDir := "/opt/nomad/some-alloc-dir"

validPaths := []string{
"local/path",
"/tmp/subdir/qemu-image",
"/opt/qemu/image",
"/opt/qemu/subdir/image",
"/opt/nomad/some-alloc-dir/local/image.img",
}

invalidPaths := []string{
"/image.img",
"../image.img",
"/tmpimage.img",
"/opt/other/image.img",
"/opt/nomad-submatch.img",
}

for _, p := range validPaths {
require.Truef(t, isAllowedImagePath(allowedPaths, allocDir, p), "path should be allowed: %v", p)
}

for _, p := range invalidPaths {
require.Falsef(t, isAllowedImagePath(allowedPaths, allocDir, p), "path should be not allowed: %v", p)
}
}
4 changes: 2 additions & 2 deletions website/pages/docs/drivers/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ plugin "docker" {

- `volumes` stanza:

- `enabled` - Defaults to `true`. Allows tasks to bind host paths
- `enabled` - Defaults to `false`. Allows tasks to bind host paths
(`volumes`) inside their container and use volume drivers
(`volume_driver`). Binding relative paths is always allowed and will be
resolved relative to the allocation's directory.
Expand Down Expand Up @@ -844,7 +844,7 @@ options](/docs/configuration/client#options):
deleting it. If a tasks is received that uses the same image within the delay,
the image will be reused.

- `docker.volumes.enabled`: Defaults to `true`. Allows tasks to bind host paths
- `docker.volumes.enabled`: Defaults to `false`. Allows tasks to bind host paths
(`volumes`) inside their container and use volume drivers (`volume_driver`).
Binding relative paths is always allowed and will be resolved relative to the
allocation's directory.
Expand Down
13 changes: 13 additions & 0 deletions website/pages/docs/drivers/qemu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ job "docs" {
}
```

## Plugin Options

```hcl
plugin "qemu" {
config {
image_paths = ["/mnt/image/paths"]
}
}
```

- `image_paths` (`[]string`: `[]`) - Specifies the host paths the Qemu driver is
allowed to load images from.

## Resource Isolation

Nomad uses Qemu to provide full software virtualization for virtual machine
Expand Down
44 changes: 44 additions & 0 deletions website/pages/docs/upgrade/upgrade-specific.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,50 @@ details provided for their upgrades as a result of new features or changed
behavior. This page is used to document those details separately from the
standard upgrade flow.

## Nomad 0.12.0

### Docker access host filesystem

Nomad 0.12.0 disables Docker tasks access to the host filesystem, by default.
Prior to Nomad 0.12, Docker tasks may mount and then manipulate any host file
and may pose a security risk.

Operators now must explicitly allow tasks to access host filesystem. [Host
Volumes](/docs/configuration/client#host_volume-stanza) provide a fine tune
access to individual paths.

To restore pre-0.12.0 behavior, you can enable [Docker
`volume`](/docs/drivers/docker#enabled-1) to allow binding host paths, by adding
the following to the nomad client config file:

```hcl
plugin "docker" {
config {
volume {
enabled = true
}
}
}
```

### Qemu images

Nomad 0.12.0 restricts the paths the Qemu tasks can load an image from. A Qemu
task may download an image to the allocation directory to load. But images
outside the allocation directories must be explicitly allowed by operators in
the client agent configuration file.

For example, you may allow loading Qemu images from `/mnt/qemu-images` by
adding the following to the agent configuration file:

```hcl
plugin "qemu" {
config {
image_paths = ["/mnt/qemu-images"]
}
}
```

## Nomad 0.11.3

Nomad 0.11.3 fixes a critical bug causing the nomad agent to become
Expand Down

0 comments on commit 2ce820a

Please sign in to comment.