Files
nomad/client/testutil/driver_compatible.go
Tim Gross df86503349 template: sandbox template rendering
The Nomad client renders templates in the same privileged process used for most
other client operations. During internal testing, we discovered that a malicious
task can create a symlink that can cause template rendering to read and write to
arbitrary files outside the allocation sandbox. Because the Nomad agent can be
restarted without restarting tasks, we can't simply check that the path is safe
at the time we write without encountering a time-of-check/time-of-use race.

To protect Nomad client hosts from this attack, we'll now read and write
templates in a subprocess:

* On Linux/Unix, this subprocess is sandboxed via chroot to the allocation
  directory. This requires that Nomad is running as a privileged process. A
  non-root Nomad agent will warn that it cannot sandbox the template renderer.

* On Windows, this process is sandboxed via a Windows AppContainer which has
  been granted access to only to the allocation directory. This does not require
  special privileges on Windows. (Creating symlinks in the first place can be
  prevented by running workloads as non-Administrator or
  non-ContainerAdministrator users.)

Both sandboxes cause encountered symlinks to be evaluated in the context of the
sandbox, which will result in a "file not found" or "access denied" error,
depending on the platform. This change will also require an update to
Consul-Template to allow callers to inject a custom `ReaderFunc` and
`RenderFunc`.

This design is intended as a workaround to allow us to fix this bug without
creating backwards compatibility issues for running tasks. A future version of
Nomad may introduce a read-only mount specifically for templates and artifacts
so that tasks cannot write into the same location that the Nomad agent is.

Fixes: https://github.com/hashicorp/nomad/issues/19888
Fixes: CVE-2024-1329
2024-02-08 10:40:24 -05:00

133 lines
3.1 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package testutil
import (
"os/exec"
"os/user"
"runtime"
"syscall"
"testing"
)
// RequireRoot skips tests unless:
// - running as root
func RequireRoot(t *testing.T) {
if syscall.Geteuid() != 0 {
t.Skip("Test requires root")
}
}
// RequireNonRoot skips tests unless:
// - running as non-root
func RequireNonRoot(t *testing.T) {
if syscall.Geteuid() == 0 {
t.Skip("Test requires non-root")
}
}
// RequireAdministrator skips tests unless:
// - running as Windows Administrator
func RequireAdministrator(t *testing.T) {
user, _ := user.Current()
if user.Name != "Administrator" {
t.Skip("Test requires Administrator")
}
}
// RequireConsul skips tests unless:
// - "consul" executable is detected on $PATH
func RequireConsul(t *testing.T) {
_, err := exec.Command("consul", "version").CombinedOutput()
if err != nil {
t.Skipf("Test requires Consul: %v", err)
}
}
// RequireVault skips tests unless:
// - "vault" executable is detected on $PATH
func RequireVault(t *testing.T) {
_, err := exec.Command("vault", "version").CombinedOutput()
if err != nil {
t.Skipf("Test requires Vault: %v", err)
}
}
// RequireLinux skips tests unless:
// - running on Linux
func RequireLinux(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("Test requires Linux")
}
}
// RequireNotWindows skips tests whenever:
// - running on Window
func RequireNotWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Test requires non-Windows")
}
}
// ExecCompatible skips tests unless:
// - running as root
// - running on Linux
func ExecCompatible(t *testing.T) {
if runtime.GOOS != "linux" || syscall.Geteuid() != 0 {
t.Skip("Test requires root on Linux")
}
}
// JavaCompatible skips tests unless:
// - "java" executable is detected on $PATH
// - running as root
// - running on Linux
func JavaCompatible(t *testing.T) {
_, err := exec.Command("java", "-version").CombinedOutput()
if err != nil {
t.Skipf("Test requires Java: %v", err)
}
if runtime.GOOS != "linux" || syscall.Geteuid() != 0 {
t.Skip("Test requires root on Linux")
}
}
// QemuCompatible skips tests unless:
// - "qemu-system-x86_64" executable is detected on $PATH (!windows)
// - "qemu-img" executable is detected on on $PATH (windows)
func QemuCompatible(t *testing.T) {
// Check if qemu exists
bin := "qemu-system-x86_64"
if runtime.GOOS == "windows" {
bin = "qemu-img"
}
_, err := exec.Command(bin, "--version").CombinedOutput()
if err != nil {
t.Skipf("Test requires QEMU (%s)", bin)
}
}
// MountCompatible skips tests unless:
// - not running as windows
// - running as root
func MountCompatible(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Test requires not using Windows")
}
if syscall.Geteuid() != 0 {
t.Skip("Test requires root")
}
}
// MinimumCores skips tests unless:
// - system has at least cores available CPU cores
func MinimumCores(t *testing.T, cores int) {
available := runtime.NumCPU()
if available < cores {
t.Skipf("Test requires at least %d cores, only %d available", cores, available)
}
}