Files
nomad/client/allocrunner/taskrunner/getter/params.go
Seth Hoenig cfc67c3422 client: sandbox go-getter subprocess with landlock (#15328)
* client: sandbox go-getter subprocess with landlock

This PR re-implements the getter package for artifact downloads as a subprocess.

Key changes include

On all platforms, run getter as a child process of the Nomad agent.
On Linux platforms running as root, run the child process as the nobody user.
On supporting Linux kernels, uses landlock for filesystem isolation (via go-landlock).
On all platforms, restrict environment variables of the child process to a static set.
notably TMP/TEMP now points within the allocation's task directory
kernel.landlock attribute is fingerprinted (version number or unavailable)
These changes make Nomad client more resilient against a faulty go-getter implementation that may panic, and more secure against bad actors attempting to use artifact downloads as a privilege escalation vector.

Adds new e2e/artifact suite for ensuring artifact downloading works.

TODO: Windows git test (need to modify the image, etc... followup PR)

* landlock: fixup items from cr

* cr: fixup tests and go.mod file
2022-12-07 16:02:25 -06:00

158 lines
4.1 KiB
Go

package getter
import (
"context"
"encoding/json"
"io"
"io/fs"
"strings"
"time"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/nomad/helper"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
// parameters is encoded by the Nomad client and decoded by the getter sub-process
// so it can know what to do. We use standard IO to pass configuration to achieve
// better control over input sanitization risks.
// e.g. https://www.opencve.io/cve/CVE-2022-41716
type parameters struct {
// Config
HTTPReadTimeout time.Duration `json:"http_read_timeout"`
HTTPMaxBytes int64 `json:"http_max_bytes"`
GCSTimeout time.Duration `json:"gcs_timeout"`
GitTimeout time.Duration `json:"git_timeout"`
HgTimeout time.Duration `json:"hg_timeout"`
S3Timeout time.Duration `json:"s3_timeout"`
// Artifact
Mode getter.ClientMode `json:"artifact_mode"`
Source string `json:"artifact_source"`
Destination string `json:"artifact_destination"`
Headers map[string][]string `json:"artifact_headers"`
// Task Environment
TaskDir string `json:"task_dir"`
}
func (p *parameters) reader() io.Reader {
b, err := json.Marshal(p)
if err != nil {
b = nil
}
return strings.NewReader(string(b))
}
func (p *parameters) read(r io.Reader) error {
return json.NewDecoder(r).Decode(p)
}
// deadline returns an absolute deadline before the artifact download
// sub-process forcefully terminates. The default is 1/2 hour, unless one or
// more getter configurations is set higher. A 1 minute grace period is added
// so that an internal timeout has a moment to complete before the process is
// terminated via signal.
func (p *parameters) deadline() time.Duration {
const minimum = 30 * time.Minute
max := minimum
max = helper.Max(max, p.HTTPReadTimeout)
max = helper.Max(max, p.GCSTimeout)
max = helper.Max(max, p.GitTimeout)
max = helper.Max(max, p.HgTimeout)
max = helper.Max(max, p.S3Timeout)
return max + 1*time.Minute
}
// Equal returns whether p and o are the same.
func (p *parameters) Equal(o *parameters) bool {
if p == nil || o == nil {
return p == o
}
switch {
case p.HTTPReadTimeout != o.HTTPReadTimeout:
return false
case p.HTTPMaxBytes != o.HTTPMaxBytes:
return false
case p.GCSTimeout != o.GCSTimeout:
return false
case p.GitTimeout != o.GitTimeout:
return false
case p.HgTimeout != o.HgTimeout:
return false
case p.S3Timeout != o.S3Timeout:
return false
case p.Mode != o.Mode:
return false
case p.Source != o.Source:
return false
case p.Destination != o.Destination:
return false
case p.TaskDir != o.TaskDir:
return false
case !maps.EqualFunc(p.Headers, o.Headers, slices.Equal[string]):
return false
}
return true
}
const (
// stop privilege escalation via setuid/setgid
// https://github.com/hashicorp/nomad/issues/6176
umask = fs.ModeSetuid | fs.ModeSetgid
)
func (p *parameters) client(ctx context.Context) *getter.Client {
httpGetter := &getter.HttpGetter{
Netrc: true,
Client: cleanhttp.DefaultClient(),
Header: p.Headers,
// Do not support the custom X-Terraform-Get header and
// associated logic.
XTerraformGetDisabled: true,
// Disable HEAD requests as they can produce corrupt files when
// retrying a download of a resource that has changed.
// hashicorp/go-getter#219
DoNotCheckHeadFirst: true,
// Read timeout for HTTP operations. Must be long enough to
// accommodate large/slow downloads.
ReadTimeout: p.HTTPReadTimeout,
// Maximum download size. Must be large enough to accommodate
// large downloads.
MaxBytes: p.HTTPMaxBytes,
}
return &getter.Client{
Ctx: ctx,
Src: p.Source,
Dst: p.Destination,
Mode: p.Mode,
Umask: umask,
Insecure: false,
DisableSymlinks: true,
Getters: map[string]getter.Getter{
"git": &getter.GitGetter{
Timeout: p.GitTimeout,
},
"hg": &getter.HgGetter{
Timeout: p.HgTimeout,
},
"gcs": &getter.GCSGetter{
Timeout: p.GCSTimeout,
},
"s3": &getter.S3Getter{
Timeout: p.S3Timeout,
},
"http": httpGetter,
"https": httpGetter,
},
}
}