Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include <stdio.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <misc.h>
#include <xf86.h>
Expand Down Expand Up @@ -422,6 +423,13 @@ PreInit(__attribute__ ((unused)) InputDriverPtr drv,
return BadValue;
}

/* Allow Neko in Xorg's group to connect without running as root. */
if (chmod(priv->socket_name, 0660) == -1)
{
xf86IDrvMsg(pInfo, X_ERROR, "unable to set socket permissions\n");
return BadValue;
}

/*
* Prepare for accepting connections. The backlog size is set
* to 5. So while one request is being processed other requests
Expand Down
4 changes: 4 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ curl http://localhost:10001/recording/download --output recording.mp4

Configure the server using environment variables:

`RUN_AS_ROOT=false` runs customer-facing image services as the `kernel` user;
`true` preserves root access. See [non-root mode](docs/non-root-mode.md) for the
privilege boundary, compatibility changes and validation steps.

| Variable | Default | Description |
| -------------- | --------- | ------------------------------------------- |
| `PORT` | `10001` | HTTP server port |
Expand Down
4 changes: 4 additions & 0 deletions server/cmd/api/api/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/kernel/kernel-images/server/lib/logger"
oapi "github.com/kernel/kernel-images/server/lib/oapi"
"github.com/kernel/kernel-images/server/lib/ptyio"
"github.com/kernel/kernel-images/server/lib/runasroot"
"github.com/kernel/kernel-images/server/lib/wsdrain"
openapi_types "github.com/oapi-codegen/runtime/types"
)
Expand Down Expand Up @@ -79,6 +80,9 @@ func buildCmd(body *oapi.ProcessExecRequest) (*exec.Cmd, error) {
if body == nil || body.Command == "" {
return nil, errors.New("command required")
}
if !runasroot.Enabled() && ((body.AsRoot != nil && *body.AsRoot) || (body.AsUser != nil && *body.AsUser != "")) {
return nil, errors.New("as_root and as_user are unavailable when RUN_AS_ROOT is false")
}
var args []string
if body.Args != nil {
args = append(args, (*body.Args)...)
Expand Down
45 changes: 45 additions & 0 deletions server/cmd/api/api/process_privileges_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api

import (
"context"
"testing"

oapi "github.com/kernel/kernel-images/server/lib/oapi"
"github.com/stretchr/testify/require"
)

func TestNonRootProcessIdentityRequests(t *testing.T) {
for _, mode := range []string{"", "false"} {
t.Run(mode, func(t *testing.T) {
t.Setenv("RUN_AS_ROOT", mode)
for _, body := range []oapi.ProcessExecRequest{
{Command: "true", AsRoot: ptrOf(true)},
{Command: "true", AsUser: ptrOf("root")},
{Command: "true", AsUser: ptrOf("0")},
{Command: "true", AsUser: ptrOf("1000:0")},
{Command: "true", AsUser: ptrOf("kernel")},
{Command: "true", AsRoot: ptrOf(true), Env: &map[string]string{"RUN_AS_ROOT": "true"}},
} {
_, err := buildCmd(&body)
require.ErrorContains(t, err, "RUN_AS_ROOT is false")
svc := &ApiService{}
resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &body})
require.NoError(t, err)
require.IsType(t, oapi.ProcessExec400JSONResponse{}, resp)
spawn := oapi.ProcessSpawnRequest{Command: body.Command, AsRoot: body.AsRoot, AsUser: body.AsUser, Env: body.Env}
spawnResp, err := svc.ProcessSpawn(context.Background(), oapi.ProcessSpawnRequestObject{Body: &spawn})
require.NoError(t, err)
require.IsType(t, oapi.ProcessSpawn400JSONResponse{}, spawnResp)
}
for _, body := range []oapi.ProcessExecRequest{
{Command: "true"},
{Command: "true", AsRoot: ptrOf(false)},
{Command: "true", AsUser: ptrOf("")},
} {
cmd, err := buildCmd(&body)
require.NoError(t, err)
require.Nil(t, cmd.SysProcAttr)
}
})
}
}
6 changes: 3 additions & 3 deletions server/cmd/api/api/process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func TestProcessSpawn_CommandNotFound(t *testing.T) {
}

func TestBuildCmd_AsRootSetsCredential(t *testing.T) {
t.Parallel()
t.Setenv("RUN_AS_ROOT", "true")
asRoot := true
body := &oapi.ProcessExecRequest{Command: "true", AsRoot: &asRoot}
cmd, err := buildCmd(body)
Expand All @@ -260,7 +260,7 @@ func TestBuildCmd_AsRootSetsCredential(t *testing.T) {
}

func TestBuildCmd_AsUserUidAndGidOverride(t *testing.T) {
t.Parallel()
t.Setenv("RUN_AS_ROOT", "true")
cur, err := user.Current()
if err != nil {
t.Skipf("skipping: failed to determine current user: %v", err)
Expand All @@ -284,7 +284,7 @@ func TestBuildCmd_AsUserUidAndGidOverride(t *testing.T) {
}

func TestBuildCmd_AsRootAndAsUserConflict(t *testing.T) {
t.Parallel()
t.Setenv("RUN_AS_ROOT", "true")
asRoot := true
asUser := "0"
body := &oapi.ProcessExecRequest{Command: "true", AsRoot: &asRoot, AsUser: &asUser}
Expand Down
5 changes: 5 additions & 0 deletions server/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/kernel/kernel-images/server/lib/nekoclient"
oapi "github.com/kernel/kernel-images/server/lib/oapi"
"github.com/kernel/kernel-images/server/lib/recorder"
"github.com/kernel/kernel-images/server/lib/runasroot"
"github.com/kernel/kernel-images/server/lib/scaletozero"
"github.com/kernel/kernel-images/server/lib/sysmon"
"github.com/kernel/kernel-images/server/lib/telemetry"
Expand All @@ -47,6 +48,10 @@ import (

func main() {
slogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
if !runasroot.Enabled() && os.Geteuid() == 0 {
slogger.Error("RUN_AS_ROOT is false but the API was started as root")
os.Exit(1)
}

// Load configuration from environment variables
config, err := config.Load()
Expand Down
5 changes: 3 additions & 2 deletions server/cmd/chromium-launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/kernel/kernel-images/server/lib/browserlocation"
"github.com/kernel/kernel-images/server/lib/chromiumflags"
"github.com/kernel/kernel-images/server/lib/runasroot"
"github.com/kernel/kernel-images/server/lib/x11"
)

Expand Down Expand Up @@ -111,7 +112,7 @@ func main() {
}
chromiumArgs = append(chromiumArgs, final...)

runAsRoot := strings.EqualFold(strings.TrimSpace(os.Getenv("RUN_AS_ROOT")), "true")
runAsRoot := runasroot.Enabled()

// Prepare environment. PULSE_SERVER/PULSE_SINK route chromium's audio into the
// recorder's sink; the root path below relies on this inherited env, while the
Expand All @@ -124,7 +125,7 @@ func main() {
"PULSE_SINK="+pulseSink,
)

if runAsRoot {
if runAsRoot || os.Geteuid() != 0 {
// Replace current process with Chromium
if p, err := execLookPath(*chromiumPath); err == nil {
if err := syscall.Exec(p, append([]string{filepath.Base(p)}, chromiumArgs...), env); err != nil {
Expand Down
40 changes: 34 additions & 6 deletions server/cmd/wrapper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
// where the dependency graph allows, and waits for CDP to be reachable through
// kernel-images-api.
//
// Replaces the legacy /wrapper.sh shipped in both images. Behavior parity is
// intentional — we still rely on supervisord, sysctl, dbus, etc. The only goal
// beyond parity is minimizing time-to-CDP-ready by removing serial dead time.
// Replaces the legacy /wrapper.sh shipped in both images. RUN_AS_ROOT selects
// whether customer-facing services retain root access or run as kernel.
//
// The headful vs headless profile is detected at boot from supervisor's conf.d
// (xorg.conf → headful, xvfb.conf → headless), which keeps a single binary
Expand All @@ -23,6 +22,8 @@ import (
"strings"
"syscall"
"time"

"github.com/kernel/kernel-images/server/lib/runasroot"
)

const (
Expand Down Expand Up @@ -113,7 +114,12 @@ func main() {
_ = os.WriteFile("/proc/sys/net/ipv6/conf/default/disable_ipv6", []byte("1"), 0o644)

// Pre-create per-user dirs so chromium subsystems don't error.
prepareUserDirs(os.Getenv("RUN_AS_ROOT") == "true")
prepareUserDirs(runasroot.Enabled())
if !runasroot.Enabled() {
if err := prepareUnprivilegedServices(); err != nil {
fatalf("non-root service setup: %v", err)
}
}

// Tail aggregator for service logs.
startLogAggregator()
Expand All @@ -140,6 +146,19 @@ func main() {
// drop us out of supCmd.Wait() and the container would stop.
logf("starting supervisord")
supCmd := exec.Command("supervisord", "-n", "-c", supervisorConf)
if !runasroot.Enabled() {
rootSup := exec.Command("supervisord", "-n", "-c", rootSupervisorConf)
rootSup.Stdout, rootSup.Stderr = os.Stdout, os.Stderr
if err := rootSup.Start(); err != nil {
fatalf("root supervisord start: %v", err)
}
defer func() {
_ = rootSup.Process.Signal(syscall.SIGTERM)
_ = rootSup.Wait()
}()
waitForSocket(rootSupervisorSock, 10*time.Second)
supCmd = userSupervisorCommand()
}
supCmd.Stdout = os.Stdout
supCmd.Stderr = os.Stderr
if err := supCmd.Start(); err != nil {
Expand All @@ -155,7 +174,11 @@ func main() {
_ = exec.Command("supervisorctl", "-c", supervisorConf, "stop", "all").Run()
_ = supCmd.Process.Signal(syscall.SIGTERM)
}()
waitForSocket(supervisorSock, 10*time.Second)
if runasroot.Enabled() {
waitForSocket(supervisorSock, 10*time.Second)
} else {
waitForSocket(userSupervisorDir+"/supervisor.sock", 10*time.Second)
}

// Browser phase: identity-free services. Chromium itself doesn't read
// any per-instance identity envs — it just needs the envoy CA cert
Expand All @@ -181,7 +204,12 @@ func main() {
// Pre-touch chromium's supervisord log so kernel-images-api's `tail -f`
// doesn't bail out and enter its 250ms retry backoff when started in
// parallel with chromium.
_ = os.WriteFile(filepath.Join(supervisordLogD, "chromium"), nil, 0o644)
chromiumLog := filepath.Join(supervisordLogD, "chromium")
if runasroot.Enabled() {
_ = os.WriteFile(chromiumLog, nil, 0o644)
} else {
runStreamFatal("chromium-log", "runuser", "-u", "kernel", "--", "touch", chromiumLog)
}

browserStart := time.Now()
startAll(xServer, "dbus", "chromedriver", "pulseaudio")
Expand Down
Loading
Loading