diff --git a/.changelog/11542.txt b/.changelog/11542.txt new file mode 100644 index 000000000..8e3f7d0a2 --- /dev/null +++ b/.changelog/11542.txt @@ -0,0 +1,3 @@ +```release-note:security +Allow limiting QEMU arguments to reduce access to host resources. [CVE-2021-43415](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-43415) +``` diff --git a/drivers/qemu/driver.go b/drivers/qemu/driver.go index 5ec2a5ee2..4a3db8082 100644 --- a/drivers/qemu/driver.go +++ b/drivers/qemu/driver.go @@ -84,7 +84,8 @@ var ( // configSpec is the hcl specification returned by the ConfigSchema RPC configSpec = hclspec.NewObject(map[string]*hclspec.Spec{ - "image_paths": hclspec.NewAttr("image_paths", "list(string)", false), + "image_paths": hclspec.NewAttr("image_paths", "list(string)", false), + "args_allowlist": hclspec.NewAttr("args_allowlist", "list(string)", false), }) // taskConfigSpec is the hcl specification for the driver config section of @@ -136,6 +137,11 @@ type TaskState struct { type Config struct { // ImagePaths is an allow-list of paths qemu is allowed to load an image from ImagePaths []string `codec:"image_paths"` + + // ArgsAllowList is an allow-list of arguments the jobspec can + // include in arguments to qemu, so that cluster operators can can + // prevent access to devices + ArgsAllowList []string `codec:"args_allowlist"` } // Driver is a driver for running images via Qemu @@ -338,6 +344,26 @@ func isAllowedImagePath(allowedPaths []string, allocDir, imagePath string) bool return false } +// validateArgs ensures that all QEMU command line params are in the +// allowlist. This function must be called after all interpolation has +// taken place. +func validateArgs(pluginConfigAllowList, args []string) error { + if len(pluginConfigAllowList) > 0 { + allowed := map[string]struct{}{} + for _, arg := range pluginConfigAllowList { + allowed[arg] = struct{}{} + } + for _, arg := range args { + if strings.HasPrefix(strings.TrimSpace(arg), "-") { + if _, ok := allowed[arg]; !ok { + return fmt.Errorf("%q is not in args_allowlist", arg) + } + } + } + } + return nil +} + 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) @@ -355,6 +381,10 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive handle := drivers.NewTaskHandle(taskHandleVersion) handle.Config = cfg + if err := validateArgs(d.config.ArgsAllowList, driverConfig.Args); err != nil { + return nil, nil, err + } + // Get the image source vmPath := driverConfig.ImagePath if vmPath == "" { diff --git a/drivers/qemu/driver_test.go b/drivers/qemu/driver_test.go index b35f20a45..8777c7de5 100644 --- a/drivers/qemu/driver_test.go +++ b/drivers/qemu/driver_test.go @@ -453,3 +453,32 @@ func TestIsAllowedImagePath(t *testing.T) { require.Falsef(t, isAllowedImagePath(allowedPaths, allocDir, p), "path should be not allowed: %v", p) } } + +func TestArgsAllowList(t *testing.T) { + + pluginConfigAllowList := []string{"-drive", "-net", "-snapshot"} + + validArgs := [][]string{ + {"-drive", "/path/to/wherever", "-snapshot"}, + {"-net", "tap,vlan=0,ifname=tap0"}, + } + + invalidArgs := [][]string{ + {"-usbdevice", "mouse"}, + {"-singlestep"}, + {"--singlestep"}, + {" -singlestep"}, + {"\t-singlestep"}, + } + + for _, args := range validArgs { + require.NoError(t, validateArgs(pluginConfigAllowList, args)) + require.NoError(t, validateArgs([]string{}, args)) + + } + for _, args := range invalidArgs { + require.Error(t, validateArgs(pluginConfigAllowList, args)) + require.NoError(t, validateArgs([]string{}, args)) + } + +} diff --git a/website/content/docs/drivers/qemu.mdx b/website/content/docs/drivers/qemu.mdx index d4d1b8f76..7ba7630c3 100644 --- a/website/content/docs/drivers/qemu.mdx +++ b/website/content/docs/drivers/qemu.mdx @@ -54,7 +54,7 @@ The `qemu` driver supports the following configuration in the job spec: forcefully terminated. (Note that [prior to qemu 2.10.1](https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6), the monitor socket path is limited to 108 characters. Graceful shutdown will - be disabled if qemu is < 2.10.1 and the generated monitor path exceeds this + be disabled if QEMU is < 2.10.1 and the generated monitor path exceeds this length. You may encounter this issue if you set long [data_dir](/docs/configuration#data_dir) or @@ -72,7 +72,7 @@ The `qemu` driver supports the following configuration in the job spec: } ``` -- `args` - (Optional) A list of strings that is passed to qemu as command line +- `args` - (Optional) A list of strings that is passed to QEMU as command line options. ## Examples @@ -145,12 +145,19 @@ job "docs" { plugin "qemu" { config { image_paths = ["/mnt/image/paths"] + args_allowlist = ["-drive", "-usbdevice"] } } ``` -- `image_paths` (`[]string`: `[]`) - Specifies the host paths the QEMU driver is - allowed to load images from. +- `image_paths` (`[]string`: `[]`) - Specifies the host paths the QEMU + driver is allowed to load images from. +- `args_allowlist` (`[]string`: `[]`) - Specifies the command line + flags that the [`args`] option is permitted to pass to QEMU. If + unset, a job submitter can pass any command line flag into QEMU, + including flags that provide the VM with access to host devices such + as USB drives. Refer to the [QEMU documentation] for the available + flags. ## Resource Isolation @@ -162,3 +169,11 @@ Virtualization provides the highest level of isolation for workloads that require additional security, and resource use is constrained by the QEMU hypervisor rather than the host kernel. VM network traffic still flows through the host's interface(s). + +Note that the strong isolation provided by virtualization only applies +to the workload once the VM is started. Operators should use the +`args_allowlist` option to prevent job submitters from accessing +devices and resources they are not allowed to access. + +[`args`]: /docs/drivers/qemu#args +[QEMU documentation]: https://www.qemu.org/docs/master/system/invocation.html