Skip to content

Commit

Permalink
Allow 'runsc do' to run without root
Browse files Browse the repository at this point in the history
'--rootless' flag lets a non-root user execute 'runsc do'.
The drawback is that the sandbox and gofer processes will
run as root inside a user namespace that is mapped to the
caller's user, intead of nobody. And network is defaulted
to '--network=host' inside the root network namespace. On
the bright side, it's very convenient for testing:

runsc --rootless do ls
runsc --rootless do curl www.google.com

PiperOrigin-RevId: 252840970
  • Loading branch information
fvoznika authored and shentubot committed Jun 12, 2019
1 parent df110ad commit 356d1be
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 154 deletions.
7 changes: 7 additions & 0 deletions runsc/boot/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ type Config struct {
// to the same underlying network device. This allows netstack to better
// scale for high throughput use cases.
NumNetworkChannels int

// Rootless allows the sandbox to be started with a user that is not root.
// Defense is depth measures are weaker with rootless. Specifically, the
// sandbox and Gofer process run as root inside a user namespace with root
// mapped to the caller's user.
Rootless bool
}

// ToFlags returns a slice of flags that correspond to the given Config.
Expand All @@ -250,6 +256,7 @@ func (c *Config) ToFlags() []string {
"--profile=" + strconv.FormatBool(c.ProfileEnable),
"--net-raw=" + strconv.FormatBool(c.EnableRaw),
"--num-network-channels=" + strconv.Itoa(c.NumNetworkChannels),
"--rootless=" + strconv.FormatBool(c.Rootless),
}
if c.TestOnlyAllowRunAsCurrentUserWithoutChroot {
// Only include if set since it is never to be used by users.
Expand Down
22 changes: 12 additions & 10 deletions runsc/cmd/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
// Ensure that if there is a panic, all goroutine stacks are printed.
debug.SetTraceback("all")

conf := args[0].(*boot.Config)

if b.setUpRoot {
if err := setUpChroot(b.pidns); err != nil {
Fatalf("error setting up chroot: %v", err)
Expand All @@ -143,14 +145,16 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
args = append(args, arg)
}
}
// Note that we've already read the spec from the spec FD, and
// we will read it again after the exec call. This works
// because the ReadSpecFromFile function seeks to the beginning
// of the file before reading.
if err := callSelfAsNobody(args); err != nil {
Fatalf("%v", err)
if !conf.Rootless {
// Note that we've already read the spec from the spec FD, and
// we will read it again after the exec call. This works
// because the ReadSpecFromFile function seeks to the beginning
// of the file before reading.
if err := callSelfAsNobody(args); err != nil {
Fatalf("%v", err)
}
panic("callSelfAsNobody must never return success")
}
panic("callSelfAsNobody must never return success")
}
}

Expand All @@ -163,9 +167,6 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
}
specutils.LogSpec(spec)

conf := args[0].(*boot.Config)
waitStatus := args[1].(*syscall.WaitStatus)

if b.applyCaps {
caps := spec.Process.Capabilities
if caps == nil {
Expand Down Expand Up @@ -251,6 +252,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})

ws := l.WaitExit()
log.Infof("application exiting with %+v", ws)
waitStatus := args[1].(*syscall.WaitStatus)
*waitStatus = syscall.WaitStatus(ws.Status())
l.Destroy()
return subcommands.ExitSuccess
Expand Down
2 changes: 1 addition & 1 deletion runsc/cmd/capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,6 @@ func TestCapabilities(t *testing.T) {
}

func TestMain(m *testing.M) {
testutil.RunAsRoot()
specutils.MaybeRunAsRoot()
os.Exit(m.Run())
}
8 changes: 6 additions & 2 deletions runsc/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,25 @@ func (c *Create) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}
id := f.Arg(0)
conf := args[0].(*boot.Config)

if conf.Rootless {
return Errorf("Rootless mode not supported with %q", c.Name())
}

bundleDir := c.bundleDir
if bundleDir == "" {
bundleDir = getwdOrDie()
}
spec, err := specutils.ReadSpec(bundleDir)
if err != nil {
Fatalf("reading spec: %v", err)
return Errorf("reading spec: %v", err)
}
specutils.LogSpec(spec)

// Create the container. A new sandbox will be created for the
// container unless the metadata specifies that it should be run in an
// existing container.
if _, err := container.Create(id, spec, conf, bundleDir, c.consoleSocket, c.pidFile, c.userLog); err != nil {
Fatalf("creating container: %v", err)
return Errorf("creating container: %v", err)
}
return subcommands.ExitSuccess
}
39 changes: 27 additions & 12 deletions runsc/cmd/do.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ import (
// Do implements subcommands.Command for the "do" command. It sets up a simple
// sandbox and executes the command inside it. See Usage() for more details.
type Do struct {
root string
cwd string
ip string
networkNamespace bool
root string
cwd string
ip string
}

// Name implements subcommands.Command.Name.
Expand Down Expand Up @@ -72,7 +71,6 @@ func (c *Do) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.root, "root", "/", `path to the root directory, defaults to "/"`)
f.StringVar(&c.cwd, "cwd", ".", "path to the current directory, defaults to the current directory")
f.StringVar(&c.ip, "ip", "192.168.10.2", "IPv4 address for the sandbox")
f.BoolVar(&c.networkNamespace, "netns", true, "run in a new network namespace")
}

// Execute implements subcommands.Command.Execute.
Expand All @@ -85,15 +83,21 @@ func (c *Do) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) su
conf := args[0].(*boot.Config)
waitStatus := args[1].(*syscall.WaitStatus)

// Map the entire host file system, but make it readonly with a writable
// overlay on top (ignore --overlay option).
conf.Overlay = true
if conf.Rootless {
if err := specutils.MaybeRunAsRoot(); err != nil {
return Errorf("Error executing inside namespace: %v", err)
}
// Execution will continue here if no more capabilities are needed...
}

hostname, err := os.Hostname()
if err != nil {
return Errorf("Error to retrieve hostname: %v", err)
}

// Map the entire host file system, but make it readonly with a writable
// overlay on top (ignore --overlay option).
conf.Overlay = true
absRoot, err := resolvePath(c.root)
if err != nil {
return Errorf("Error resolving root: %v", err)
Expand All @@ -119,11 +123,22 @@ func (c *Do) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) su
specutils.LogSpec(spec)

cid := fmt.Sprintf("runsc-%06d", rand.Int31n(1000000))
if !c.networkNamespace {
if conf.Network != boot.NetworkHost {
Fatalf("The current network namespace can be used only if --network=host is set", nil)
if conf.Network == boot.NetworkNone {
netns := specs.LinuxNamespace{
Type: specs.NetworkNamespace,
}
if spec.Linux != nil {
panic("spec.Linux is not nil")
}
} else if conf.Network != boot.NetworkNone {
spec.Linux = &specs.Linux{Namespaces: []specs.LinuxNamespace{netns}}

} else if conf.Rootless {
if conf.Network == boot.NetworkSandbox {
fmt.Println("*** Rootless requires changing network type to host ***")
conf.Network = boot.NetworkHost
}

} else {
clean, err := c.setupNet(cid, spec)
if err != nil {
return Errorf("Error setting up network: %v", err)
Expand Down
10 changes: 7 additions & 3 deletions runsc/cmd/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,29 @@ func (r *Restore) Execute(_ context.Context, f *flag.FlagSet, args ...interface{
conf := args[0].(*boot.Config)
waitStatus := args[1].(*syscall.WaitStatus)

if conf.Rootless {
return Errorf("Rootless mode not supported with %q", r.Name())
}

bundleDir := r.bundleDir
if bundleDir == "" {
bundleDir = getwdOrDie()
}
spec, err := specutils.ReadSpec(bundleDir)
if err != nil {
Fatalf("reading spec: %v", err)
return Errorf("reading spec: %v", err)
}
specutils.LogSpec(spec)

if r.imagePath == "" {
Fatalf("image-path flag must be provided")
return Errorf("image-path flag must be provided")
}

conf.RestoreFile = filepath.Join(r.imagePath, checkpointFileName)

ws, err := container.Run(id, spec, conf, bundleDir, r.consoleSocket, r.pidFile, r.userLog, r.detach)
if err != nil {
Fatalf("running container: %v", err)
return Errorf("running container: %v", err)
}
*waitStatus = ws

Expand Down
8 changes: 6 additions & 2 deletions runsc/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,23 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) s
conf := args[0].(*boot.Config)
waitStatus := args[1].(*syscall.WaitStatus)

if conf.Rootless {
return Errorf("Rootless mode not supported with %q", r.Name())
}

bundleDir := r.bundleDir
if bundleDir == "" {
bundleDir = getwdOrDie()
}
spec, err := specutils.ReadSpec(bundleDir)
if err != nil {
Fatalf("reading spec: %v", err)
return Errorf("reading spec: %v", err)
}
specutils.LogSpec(spec)

ws, err := container.Run(id, spec, conf, bundleDir, r.consoleSocket, r.pidFile, r.userLog, r.detach)
if err != nil {
Fatalf("running container: %v", err)
return Errorf("running container: %v", err)
}

*waitStatus = ws
Expand Down
3 changes: 2 additions & 1 deletion runsc/container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"gvisor.googlesource.com/gvisor/pkg/sentry/control"
"gvisor.googlesource.com/gvisor/pkg/sentry/kernel/auth"
"gvisor.googlesource.com/gvisor/runsc/boot"
"gvisor.googlesource.com/gvisor/runsc/specutils"
"gvisor.googlesource.com/gvisor/runsc/test/testutil"
)

Expand Down Expand Up @@ -1853,7 +1854,7 @@ func TestMain(m *testing.M) {
if err := testutil.ConfigureExePath(); err != nil {
panic(err.Error())
}
testutil.RunAsRoot()
specutils.MaybeRunAsRoot()

os.Exit(m.Run())
}
63 changes: 34 additions & 29 deletions runsc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,19 @@ var (
straceLogSize = flag.Uint("strace-log-size", 1024, "default size (in bytes) to log data argument blobs")

// Flags that control sandbox runtime behavior.
platform = flag.String("platform", "ptrace", "specifies which platform to use: ptrace (default), kvm")
network = flag.String("network", "sandbox", "specifies which network to use: sandbox (default), host, none. Using network inside the sandbox is more secure because it's isolated from the host network.")
gso = flag.Bool("gso", true, "enable generic segmenation offload")
fileAccess = flag.String("file-access", "exclusive", "specifies which filesystem to use for the root mount: exclusive (default), shared. Volume mounts are always shared.")
overlay = flag.Bool("overlay", false, "wrap filesystem mounts with writable overlay. All modifications are stored in memory inside the sandbox.")
watchdogAction = flag.String("watchdog-action", "log", "sets what action the watchdog takes when triggered: log (default), panic.")
panicSignal = flag.Int("panic-signal", -1, "register signal handling that panics. Usually set to SIGUSR2(12) to troubleshoot hangs. -1 disables it.")
profile = flag.Bool("profile", false, "prepares the sandbox to use Golang profiler. Note that enabling profiler loosens the seccomp protection added to the sandbox (DO NOT USE IN PRODUCTION).")
netRaw = flag.Bool("net-raw", false, "enable raw sockets. When false, raw sockets are disabled by removing CAP_NET_RAW from containers (`runsc exec` will still be able to utilize raw sockets). Raw sockets allow malicious containers to craft packets and potentially attack the network.")
numNetworkChannels = flag.Int("num-network-channels", 1, "number of underlying channels(FDs) to use for network link endpoints.")
platform = flag.String("platform", "ptrace", "specifies which platform to use: ptrace (default), kvm")
network = flag.String("network", "sandbox", "specifies which network to use: sandbox (default), host, none. Using network inside the sandbox is more secure because it's isolated from the host network.")
gso = flag.Bool("gso", true, "enable generic segmenation offload")
fileAccess = flag.String("file-access", "exclusive", "specifies which filesystem to use for the root mount: exclusive (default), shared. Volume mounts are always shared.")
overlay = flag.Bool("overlay", false, "wrap filesystem mounts with writable overlay. All modifications are stored in memory inside the sandbox.")
watchdogAction = flag.String("watchdog-action", "log", "sets what action the watchdog takes when triggered: log (default), panic.")
panicSignal = flag.Int("panic-signal", -1, "register signal handling that panics. Usually set to SIGUSR2(12) to troubleshoot hangs. -1 disables it.")
profile = flag.Bool("profile", false, "prepares the sandbox to use Golang profiler. Note that enabling profiler loosens the seccomp protection added to the sandbox (DO NOT USE IN PRODUCTION).")
netRaw = flag.Bool("net-raw", false, "enable raw sockets. When false, raw sockets are disabled by removing CAP_NET_RAW from containers (`runsc exec` will still be able to utilize raw sockets). Raw sockets allow malicious containers to craft packets and potentially attack the network.")
numNetworkChannels = flag.Int("num-network-channels", 1, "number of underlying channels(FDs) to use for network link endpoints.")
rootless = flag.Bool("rootless", false, "it allows the sandbox to be started with a user that is not root. Sandbox and Gofer processes may run with same privileges as current user.")

// Test flags, not to be used outside tests, ever.
testOnlyAllowRunAsCurrentUserWithoutChroot = flag.Bool("TESTONLY-unsafe-nonroot", false, "TEST ONLY; do not ever use! This skips many security measures that isolate the host from the sandbox.")
)

Expand Down Expand Up @@ -166,26 +169,28 @@ func main() {

// Create a new Config from the flags.
conf := &boot.Config{
RootDir: *rootDir,
Debug: *debug,
LogFilename: *logFilename,
LogFormat: *logFormat,
DebugLog: *debugLog,
DebugLogFormat: *debugLogFormat,
FileAccess: fsAccess,
Overlay: *overlay,
Network: netType,
GSO: *gso,
LogPackets: *logPackets,
Platform: platformType,
Strace: *strace,
StraceLogSize: *straceLogSize,
WatchdogAction: wa,
PanicSignal: *panicSignal,
ProfileEnable: *profile,
EnableRaw: *netRaw,
RootDir: *rootDir,
Debug: *debug,
LogFilename: *logFilename,
LogFormat: *logFormat,
DebugLog: *debugLog,
DebugLogFormat: *debugLogFormat,
FileAccess: fsAccess,
Overlay: *overlay,
Network: netType,
GSO: *gso,
LogPackets: *logPackets,
Platform: platformType,
Strace: *strace,
StraceLogSize: *straceLogSize,
WatchdogAction: wa,
PanicSignal: *panicSignal,
ProfileEnable: *profile,
EnableRaw: *netRaw,
NumNetworkChannels: *numNetworkChannels,
Rootless: *rootless,

TestOnlyAllowRunAsCurrentUserWithoutChroot: *testOnlyAllowRunAsCurrentUserWithoutChroot,
NumNetworkChannels: *numNetworkChannels,
}
if len(*straceSyscalls) != 0 {
conf.StraceSyscalls = strings.Split(*straceSyscalls, ",")
Expand Down
Loading

0 comments on commit 356d1be

Please sign in to comment.