diff --git a/examples/seccheck/pod_init.json b/examples/seccheck/pod_init.json new file mode 100644 index 000000000..a6ee6cca8 --- /dev/null +++ b/examples/seccheck/pod_init.json @@ -0,0 +1,19 @@ +{ + "trace_session": { + "name": "Default", + "points": [ + { + "name": "sentry/clone" + } + ], + "sinks": [ + { + "name": "remote", + "config": { + "endpoint": "/tmp/gvisor_events.sock" + }, + "ignore_setup_error": true + } + ] + } +} diff --git a/pkg/sentry/seccheck/BUILD b/pkg/sentry/seccheck/BUILD index 3ff67958f..49742f466 100644 --- a/pkg/sentry/seccheck/BUILD +++ b/pkg/sentry/seccheck/BUILD @@ -18,8 +18,10 @@ go_library( name = "seccheck", srcs = [ "clone.go", + "config.go", "execve.go", "exit.go", + "metadata.go", "seccheck.go", "seqatomic_checkerslice_unsafe.go", ], @@ -28,17 +30,24 @@ go_library( "//pkg/abi/linux", "//pkg/atomicbitops", "//pkg/context", + "//pkg/fd", "//pkg/gohacks", + "//pkg/log", + "//pkg/sentry/arch", "//pkg/sentry/kernel/time", "//pkg/sentry/seccheck/points:points_go_proto", "//pkg/sync", + "@org_golang_google_protobuf//proto:go_default_library", ], ) go_test( name = "seccheck_test", size = "small", - srcs = ["seccheck_test.go"], + srcs = [ + "metadata_test.go", + "seccheck_test.go", + ], library = ":seccheck", deps = [ "//pkg/context", diff --git a/pkg/sentry/seccheck/checkers/remote/remote.go b/pkg/sentry/seccheck/checkers/remote/remote.go index a01504c09..c7534fb17 100644 --- a/pkg/sentry/seccheck/checkers/remote/remote.go +++ b/pkg/sentry/seccheck/checkers/remote/remote.go @@ -33,6 +33,14 @@ import ( pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" ) +func init() { + seccheck.RegisterSink(seccheck.SinkDesc{ + Name: "remote", + Setup: Setup, + New: New, + }) +} + // Remote sends a serialized point to a remote process asynchronously over a // SOCK_SEQPACKET Unix-domain socket. Each message corresponds to a single // serialized point proto, preceded by a standard header. If the point cannot @@ -46,6 +54,21 @@ type Remote struct { var _ seccheck.Checker = (*Remote)(nil) +// Setup starts the connection to the remote process and returns a file that +// can be used to communicate with it. The caller is responsible to close to +// file. +func Setup(config map[string]interface{}) (*os.File, error) { + addrOpaque, ok := config["endpoint"] + if !ok { + return nil, fmt.Errorf("endpoint not present in configuration") + } + addr, ok := addrOpaque.(string) + if !ok { + return nil, fmt.Errorf("endpoint %q is not a string", addrOpaque) + } + return setup(addr) +} + func setup(path string) (*os.File, error) { log.Debugf("Remote sink connecting to %q", path) socket, err := unix.Socket(unix.AF_UNIX, unix.SOCK_SEQPACKET, 0) diff --git a/pkg/sentry/seccheck/config.go b/pkg/sentry/seccheck/config.go new file mode 100644 index 000000000..72541ef83 --- /dev/null +++ b/pkg/sentry/seccheck/config.go @@ -0,0 +1,158 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package seccheck + +import ( + "fmt" + "os" + + "gvisor.dev/gvisor/pkg/fd" + "gvisor.dev/gvisor/pkg/log" +) + +// SessionConfig describes a new session configuration. A session consists of a +// set of points to be enabled and sinks where the points are sent to. +type SessionConfig struct { + // Name is the unique session name. + Name string `json:"name,omitempty"` + // Points is the set of points to enable in this session. + Points []PointConfig `json:"points,omitempty"` + // Sinks are the sinks that will process the points enabled above. + Sinks []SinkConfig `json:"sinks,omitempty"` +} + +// PointConfig describes a point to be enabled in a given session. +type PointConfig struct { + // Name is the point to be enabled. The point must exist in the system. + Name string `json:"name,omitempty"` + // OptionalFields is the list of optional fields to collect from the point. + OptionalFields []string `json:"optional_fields,omitempty"` + // ContextFields is the list of context fields to collect. + ContextFields []string `json:"context_fields,omitempty"` +} + +// SinkConfig describes the sink that will process the points in a given +// session. +type SinkConfig struct { + // Name is the sink to be created. The sink must exist in the system. + Name string `json:"name,omitempty"` + // Config is a opaque json object that is passed to the sink. + Config map[string]interface{} `json:"config,omitempty"` + // IgnoreSetupError makes errors during sink setup to be ignored. Otherwise, + // failures will prevent the container from starting. + IgnoreSetupError bool `json:"ignore_setup_error,omitempty"` + // FD is the endpoint returned from Setup. It may be nil. + FD *fd.FD `json:"-"` +} + +// Configure reads the session configuration and applies it to the system. +func Configure(conf *SessionConfig) error { + log.Debugf("Configuring seccheck: %+v", conf) + state, err := findSession(conf.Name) + if err != nil { + return err + } + + var reqs []PointReq + for _, ptConfig := range conf.Points { + desc, err := findPointDesc(ptConfig.Name) + if err != nil { + return err + } + req := PointReq{Pt: desc.ID} + + mask, err := setFields(ptConfig.OptionalFields, desc.OptionalFields) + if err != nil { + return err + } + req.Fields.Local = mask + + mask, err = setFields(ptConfig.ContextFields, desc.ContextFields) + if err != nil { + return err + } + req.Fields.Context = mask + + reqs = append(reqs, req) + } + + for _, sinkConfig := range conf.Sinks { + sink, err := findSinkDesc(sinkConfig.Name) + if err != nil { + return err + } + checker, err := sink.New(sinkConfig.Config, sinkConfig.FD) + if err != nil { + return fmt.Errorf("creating event sink: %w", err) + } + state.AppendChecker(checker, reqs) + } + + return nil +} + +// SetupSink runs the setup step for a given sink. +func SetupSink(config SinkConfig) (*os.File, error) { + sink, err := findSinkDesc(config.Name) + if err != nil { + return nil, err + } + if sink.Setup == nil { + return nil, nil + } + return sink.Setup(config.Config) +} + +func findSession(name string) (*State, error) { + if name != "Default" { + return nil, fmt.Errorf(`only a single "Default" session is supported`) + } + return &Global, nil +} + +func findPointDesc(name string) (PointDesc, error) { + if desc, ok := points[name]; ok { + return desc, nil + } + return PointDesc{}, fmt.Errorf("point %q not found", name) +} + +func findField(name string, fields []FieldDesc) (FieldDesc, error) { + for _, f := range fields { + if f.Name == name { + return f, nil + } + } + return FieldDesc{}, fmt.Errorf("field %q not found", name) +} + +func setFields(names []string, fields []FieldDesc) (FieldMask, error) { + fm := FieldMask{} + for _, name := range names { + desc, err := findField(name, fields) + if err != nil { + return FieldMask{}, err + } + fm.Add(desc.ID) + } + return fm, nil +} + +func findSinkDesc(name string) (SinkDesc, error) { + if desc, ok := sinks[name]; ok { + return desc, nil + } + return SinkDesc{}, fmt.Errorf("sink %q not found", name) +} diff --git a/pkg/sentry/seccheck/metadata.go b/pkg/sentry/seccheck/metadata.go new file mode 100644 index 000000000..bf4a73eac --- /dev/null +++ b/pkg/sentry/seccheck/metadata.go @@ -0,0 +1,171 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package seccheck + +import ( + "fmt" + "os" + + "gvisor.dev/gvisor/pkg/fd" +) + +var points = map[string]PointDesc{} +var sinks = map[string]SinkDesc{} + +// defaultContextFields are the fields present in most points. +var defaultContextFields = []FieldDesc{ + { + ID: FieldCtxtTime, + Name: "time", + }, + { + ID: FieldCtxtThreadID, + Name: "thread_id", + }, + { + ID: FieldCtxtThreadStartTime, + Name: "task_start_time", + }, + { + ID: FieldCtxtThreadGroupID, + Name: "group_id", + }, + { + ID: FieldCtxtThreadGroupStartTime, + Name: "thread_group_start_time", + }, + { + ID: FieldCtxtContainerID, + Name: "container_id", + }, + { + ID: FieldCtxtCredentials, + Name: "credentials", + }, + { + ID: FieldCtxtCwd, + Name: "cwd", + }, + { + ID: FieldCtxtProcessName, + Name: "process_name", + }, +} + +// SinkDesc describes a sink that is available to be configured. +type SinkDesc struct { + // Name is a unique identifier for the sink. + Name string + // Setup is called outside the protection of the sandbox. This is done to + // allow the sink to do whatever is necessary to set it up. If it returns a + // file, this file is donated to the sandbox and passed to the sink when New + // is called. config is an opaque json object passed to the sink. + Setup func(config map[string]interface{}) (*os.File, error) + // New creates a new sink. config is an opaque json object passed to the sink. + // endpoing is a file descriptor to the file returned in Setup. It's set to -1 + // if Setup returned nil. + New func(config map[string]interface{}, endpoint *fd.FD) (Checker, error) +} + +// RegisterSink registers a new sink to make it discoverable. +func RegisterSink(sink SinkDesc) { + if _, ok := sinks[sink.Name]; ok { + panic(fmt.Sprintf("Sink %q already registered", sink.Name)) + } + sinks[sink.Name] = sink +} + +// PointDesc describes a Point that is available to be configured. +// Schema for these points are defined in pkg/sentry/seccheck/points/. +type PointDesc struct { + // ID is the point unique indentifier. + ID Point + // Name is the point unique name. Convention is to use the following format: + // namespace/name + // Examples: container/start, sentry/clone, etc. + Name string + // OptionalFields is a list of fields that are available in the point, but not + // collected unless specified when the Point is configured. + // Examples: fd_path, data for read/write Points, etc. + OptionalFields []FieldDesc + // ContextFields is a list of fields that can be collected from the context, + // but are not collected unless specified when the Point is configured. + // Examples: container_id, PID, etc. + ContextFields []FieldDesc +} + +// FieldDesc describes an optional/context field that is available to be +// configured. +type FieldDesc struct { + // ID is the numeric identifier of the field. + ID Field + // Name is the unique field name. + Name string +} + +func registerPoint(pt PointDesc) { + if _, ok := points[pt.Name]; ok { + panic(fmt.Sprintf("Point %q already registered", pt.Name)) + } + if err := validateFields(pt.OptionalFields); err != nil { + panic(err) + } + if err := validateFields(pt.ContextFields); err != nil { + panic(err) + } + points[pt.Name] = pt +} + +func validateFields(fields []FieldDesc) error { + ids := make(map[Field]FieldDesc) + names := make(map[string]FieldDesc) + for _, f := range fields { + if other, ok := names[f.Name]; ok { + return fmt.Errorf("field %q has repeated name with field %q", f.Name, other.Name) + } + if other, ok := ids[f.ID]; ok { + return fmt.Errorf("field %q has repeated ID (%d) with field %q", f.Name, f.ID, other.Name) + } + names[f.Name] = f + ids[f.ID] = f + } + return nil +} + +// These are all the points available in the system. +func init() { + // Points from the sentry namespace. + registerPoint(PointDesc{ + ID: PointClone, + Name: "sentry/clone", + ContextFields: defaultContextFields, + }) + registerPoint(PointDesc{ + ID: PointExecve, + Name: "sentry/execve", + OptionalFields: []FieldDesc{ + { + ID: ExecveFieldBinaryInfo, + Name: "binary_info", + }, + }, + ContextFields: defaultContextFields, + }) + registerPoint(PointDesc{ + ID: PointExitNotifyParent, + Name: "sentry/exit_notify_parent", + ContextFields: defaultContextFields, + }) +} diff --git a/pkg/sentry/seccheck/metadata_test.go b/pkg/sentry/seccheck/metadata_test.go new file mode 100644 index 000000000..5aa0e8d06 --- /dev/null +++ b/pkg/sentry/seccheck/metadata_test.go @@ -0,0 +1,104 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package seccheck + +import ( + "testing" +) + +func TestSinkRegistration(t *testing.T) { + sink := SinkDesc{Name: "test"} + RegisterSink(sink) + if _, ok := sinks["test"]; !ok { + t.Errorf("sink registration failed") + } + + defer func() { + recover() + }() + RegisterSink(sink) + t.Errorf("Registering the same sink twice should panic") +} + +func TestPointRegistration(t *testing.T) { + point := PointDesc{Name: "test"} + registerPoint(point) + if _, ok := points["test"]; !ok { + t.Errorf("point registration failed") + } + + defer func() { + recover() + }() + registerPoint(point) + t.Errorf("Registering the same point twice should panic") +} + +func TestPointRegistrationFields(t *testing.T) { + for _, tc := range []struct { + name string + point PointDesc + }{ + { + name: "optional_name", + point: PointDesc{ + Name: "test", + OptionalFields: []FieldDesc{ + {ID: 123, Name: "field1"}, + {ID: 456, Name: "field1"}, + }, + }, + }, + { + name: "optional_id", + point: PointDesc{ + Name: "test", + OptionalFields: []FieldDesc{ + {ID: 123, Name: "field1"}, + {ID: 123, Name: "field2"}, + }, + }, + }, + { + name: "context_name", + point: PointDesc{ + Name: "test", + ContextFields: []FieldDesc{ + {ID: 123, Name: "field1"}, + {ID: 456, Name: "field1"}, + }, + }, + }, + { + name: "context_id", + point: PointDesc{ + Name: "test", + ContextFields: []FieldDesc{ + {ID: 123, Name: "field1"}, + {ID: 123, Name: "field2"}, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + recover() + }() + registerPoint(tc.point) + t.Errorf("Registering the same point twice should panic") + + }) + } +} diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index e7ace61be..8aaee6612 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -16,6 +16,7 @@ go_library( "loader.go", "network.go", "profile.go", + "seccheck.go", "strace.go", "vfs.go", ], @@ -81,6 +82,8 @@ go_library( "//pkg/sentry/loader", "//pkg/sentry/pgalloc", "//pkg/sentry/platform", + "//pkg/sentry/seccheck", + "//pkg/sentry/seccheck/checkers/remote", "//pkg/sentry/socket/hostinet", "//pkg/sentry/socket/netfilter", "//pkg/sentry/socket/netlink", diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 1d5918a55..428ae77ec 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -225,6 +225,12 @@ type Args struct { // ProductName is the value to show in // /sys/devices/virtual/dmi/id/product_name. ProductName string + // PodInitConfigFD is the file descriptor to a file passed in the + // --pod-init-config flag + PodInitConfigFD int + // SinkFDs is an ordered array of file descriptors to be used by seccheck + // sinks configured from the --pod-init-config file. + SinkFDs []int } // make sure stdioFDs are always the same on initial start and on restore @@ -421,6 +427,12 @@ func New(args Args) (*Loader, error) { k.SetHostMount(k.VFS().NewDisconnectedMount(hostFilesystem, nil, &vfs.MountOptions{})) } + if args.PodInitConfigFD >= 0 { + if err := setupSeccheck(args.PodInitConfigFD, args.SinkFDs); err != nil { + log.Warningf("unable to configure event session: %v", err) + } + } + eid := execID{cid: args.ID} l := &Loader{ k: k, diff --git a/runsc/boot/loader_test.go b/runsc/boot/loader_test.go index 0f68f0795..ed796c75f 100644 --- a/runsc/boot/loader_test.go +++ b/runsc/boot/loader_test.go @@ -134,12 +134,13 @@ func createLoader(vfsEnabled bool, spec *specs.Spec) (*Loader, func(), error) { } args := Args{ - ID: "foo", - Spec: spec, - Conf: conf, - ControllerFD: fd, - GoferFDs: []int{sandEnd}, - StdioFDs: stdio, + ID: "foo", + Spec: spec, + Conf: conf, + ControllerFD: fd, + GoferFDs: []int{sandEnd}, + StdioFDs: stdio, + PodInitConfigFD: -1, } l, err := New(args) if err != nil { diff --git a/runsc/boot/seccheck.go b/runsc/boot/seccheck.go new file mode 100644 index 000000000..52a0d45e2 --- /dev/null +++ b/runsc/boot/seccheck.go @@ -0,0 +1,94 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boot + +import ( + "encoding/json" + "io" + "os" + + "gvisor.dev/gvisor/pkg/fd" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/seccheck" + + // Register supported of checkers. + _ "gvisor.dev/gvisor/pkg/sentry/seccheck/checkers/remote" +) + +// InitConfig represents the configuration to apply during pod creation. For +// now, it supports setting up an seccheck session. +type InitConfig struct { + TraceSession seccheck.SessionConfig `json:"trace_session"` +} + +func setupSeccheck(configFD int, sinkFDs []int) error { + config := fd.New(configFD) + defer config.Close() + + initConf, err := loadInitConfig(config) + if err != nil { + return err + } + return initConf.configure(sinkFDs) +} + +// LoadInitConfig loads an InitConfig struct from a json formatted file. +func LoadInitConfig(path string) (*InitConfig, error) { + config, err := os.Open(path) + if err != nil { + return nil, err + } + defer config.Close() + return loadInitConfig(config) +} + +func loadInitConfig(reader io.Reader) (*InitConfig, error) { + decoder := json.NewDecoder(reader) + init := &InitConfig{} + if err := decoder.Decode(init); err != nil { + return nil, err + } + return init, nil +} + +// Setup performs the actions defined in the InitConfig, e.g. setup seccheck +// session. +func (c *InitConfig) Setup() ([]*os.File, error) { + var files []*os.File + for _, sink := range c.TraceSession.Sinks { + sinkFile, err := seccheck.SetupSink(sink) + if err != nil { + if !sink.IgnoreSetupError { + return nil, err + } + log.Warningf("Ignoring sink setup failure: %v", err) + // Ensure sinkFile is nil and append it to the list to ensure the file + // order is preserved. + sinkFile = nil + } + files = append(files, sinkFile) + } + return files, nil +} + +func (c *InitConfig) configure(sinkFDs []int) error { + for i, sinkFD := range sinkFDs { + if sinkFD >= 0 { + c.TraceSession.Sinks[i].FD = fd.New(sinkFD) + } + } + return seccheck.Configure(&c.TraceSession) + +} diff --git a/runsc/cmd/boot.go b/runsc/cmd/boot.go index 99513103d..676b0189e 100644 --- a/runsc/cmd/boot.go +++ b/runsc/cmd/boot.go @@ -101,6 +101,10 @@ type Boot struct { // Valid if >= 0. traceFD int + podInitConfigFD int + + sinkFDs intFlags + // pidns is set if the sandbox is in its own pid namespace. pidns bool @@ -154,6 +158,8 @@ func (b *Boot) SetFlags(f *flag.FlagSet) { f.IntVar(&b.profileHeapFD, "profile-heap-fd", -1, "file descriptor to write heap profile to. -1 disables profiling.") f.IntVar(&b.profileMutexFD, "profile-mutex-fd", -1, "file descriptor to write mutex profile to. -1 disables profiling.") f.IntVar(&b.traceFD, "trace-fd", -1, "file descriptor to write Go execution trace to. -1 disables tracing.") + f.IntVar(&b.podInitConfigFD, "pod-init-config-fd", -1, "file descriptor to the pod init configuration file.") + f.Var(&b.sinkFDs, "sink-fds", "ordered list of file descriptors to be used by the sinks defined in --pod-init-config.") } // Execute implements subcommands.Command.Execute. It starts a sandbox in a @@ -274,22 +280,24 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) // Create the loader. bootArgs := boot.Args{ - ID: f.Arg(0), - Spec: spec, - Conf: conf, - ControllerFD: b.controllerFD, - Device: os.NewFile(uintptr(b.deviceFD), "platform device"), - GoferFDs: b.ioFDs.GetArray(), - StdioFDs: b.stdioFDs.GetArray(), - NumCPU: b.cpuNum, - TotalMem: b.totalMem, - UserLogFD: b.userLogFD, - ProfileBlockFD: b.profileBlockFD, - ProfileCPUFD: b.profileCPUFD, - ProfileHeapFD: b.profileHeapFD, - ProfileMutexFD: b.profileMutexFD, - TraceFD: b.traceFD, - ProductName: b.productName, + ID: f.Arg(0), + Spec: spec, + Conf: conf, + ControllerFD: b.controllerFD, + Device: os.NewFile(uintptr(b.deviceFD), "platform device"), + GoferFDs: b.ioFDs.GetArray(), + StdioFDs: b.stdioFDs.GetArray(), + NumCPU: b.cpuNum, + TotalMem: b.totalMem, + UserLogFD: b.userLogFD, + ProfileBlockFD: b.profileBlockFD, + ProfileCPUFD: b.profileCPUFD, + ProfileHeapFD: b.profileHeapFD, + ProfileMutexFD: b.profileMutexFD, + TraceFD: b.traceFD, + ProductName: b.productName, + PodInitConfigFD: b.podInitConfigFD, + SinkFDs: b.sinkFDs.GetArray(), } l, err := boot.New(bootArgs) if err != nil { diff --git a/runsc/config/config.go b/runsc/config/config.go index 2b32134b1..76eed6f9a 100644 --- a/runsc/config/config.go +++ b/runsc/config/config.go @@ -229,6 +229,10 @@ type Config struct { // Use systemd to configure cgroups. SystemdCgroup bool `flag:"systemd-cgroup"` + // PodInitConfig is the path to configuration file with additional steps to + // take during pod creation. + PodInitConfig string `flag:"pod-init-config"` + // TestOnlyAllowRunAsCurrentUserWithoutChroot should only be used in // tests. It allows runsc to start the sandbox process as the current // user, and without chrooting the sandbox process. This can be diff --git a/runsc/config/flags.go b/runsc/config/flags.go index d72d6597f..ce047b835 100644 --- a/runsc/config/flags.go +++ b/runsc/config/flags.go @@ -72,6 +72,7 @@ func RegisterFlags(flagSet *flag.FlagSet) { flagSet.Bool("oci-seccomp", false, "Enables loading OCI seccomp filters inside the sandbox.") flagSet.Var(defaultControlConfig(), "controls", "Sentry control endpoints.") flagSet.Bool("enable-core-tags", false, "enables core tagging. Requires host linux kernel >= 5.14.") + flagSet.String("pod-init-config", "", "path to configuration file with additional steps to take during pod creation.") // Flags that control sandbox runtime behavior: FS related. flagSet.Var(fileAccessTypePtr(FileAccessExclusive), "file-access", "specifies which filesystem validation to use for the root mount: exclusive (default), shared.") diff --git a/runsc/donation/donation.go b/runsc/donation/donation.go index c6b80601e..cb5632282 100644 --- a/runsc/donation/donation.go +++ b/runsc/donation/donation.go @@ -45,7 +45,8 @@ type donation struct { // Donate sets up the given files to be donated to another process. The FD // in which the new file will appear in the child process is added as a flag to -// the child process, e.g. --flag=3. +// the child process, e.g. --flag=3. In case the file is nil, -1 is used for the +// flag value and no file is donated to the next process. func (f *Agency) Donate(flag string, files ...*os.File) { f.donations = append(f.donations, donation{flag: flag, files: files}) } @@ -91,9 +92,13 @@ func (f *Agency) DonateDebugLogFile(flag, logPattern, command, test string) erro func (f *Agency) Transfer(cmd *exec.Cmd, nextFD int) int { for _, d := range f.donations { for _, file := range d.files { - cmd.ExtraFiles = append(cmd.ExtraFiles, file) - cmd.Args = append(cmd.Args, fmt.Sprintf("--%s=%d", d.flag, nextFD)) - nextFD++ + fd := -1 + if file != nil { + cmd.ExtraFiles = append(cmd.ExtraFiles, file) + fd = nextFD + nextFD++ + } + cmd.Args = append(cmd.Args, fmt.Sprintf("--%s=%d", d.flag, fd)) } } // Reset donations made so far in case more transfers are needed. @@ -104,7 +109,9 @@ func (f *Agency) Transfer(cmd *exec.Cmd, nextFD int) int { // Close closes any files the agency has taken ownership over. func (f *Agency) Close() { for _, file := range f.closePending { - _ = file.Close() + if file != nil { + _ = file.Close() + } } f.closePending = nil } diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index 6aab7c815..648e5a689 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -166,6 +166,10 @@ type Args struct { // Attached indicates that the sandbox lifecycle is attached with the caller. // If the caller exits, the sandbox should exit too. Attached bool + + // SinkFiles is the an ordered array of files to be used by seccheck sinks + // configured from the --pod-init-config file. + SinkFiles []*os.File } // New creates the sandbox process. The caller must call Destroy() on the @@ -189,6 +193,17 @@ func New(conf *config.Config, args *Args) (*Sandbox, error) { }) defer c.Clean() + if len(conf.PodInitConfig) > 0 { + initConf, err := boot.LoadInitConfig(conf.PodInitConfig) + if err != nil { + return nil, err + } + args.SinkFiles, err = initConf.Setup() + if err != nil { + return nil, err + } + } + // Create pipe to synchronize when sandbox process has been booted. clientSyncFile, sandboxSyncFile, err := os.Pipe() if err != nil { @@ -530,6 +545,11 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn } donations.DonateAndClose("spec-fd", specFile) + if err := donations.OpenAndDonate("pod-init-config-fd", conf.PodInitConfig, os.O_RDONLY); err != nil { + return err + } + donations.DonateAndClose("sink-fds", args.SinkFiles...) + gPlatform, err := platform.Lookup(conf.Platform) if err != nil { return err