diff --git a/dbus/dbus_test.go b/dbus/dbus_test.go index 36e9d65..77a3e4f 100644 --- a/dbus/dbus_test.go +++ b/dbus/dbus_test.go @@ -178,7 +178,7 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { t.Run("string", func(t *testing.T) { wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0]) if useSandbox { - wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], flags: 0x0, seccomp: 0x1, presets: 0xf`, os.Args[0]) + wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`, os.Args[0]) } if got := p.String(); !strings.Contains(got, wantSubstr) { t.Errorf("String: %q, want %q", diff --git a/dbus/proc.go b/dbus/proc.go index 1a51961..283f31c 100644 --- a/dbus/proc.go +++ b/dbus/proc.go @@ -67,6 +67,7 @@ func (p *Proxy) Start() error { p.final, true, argF, func(container *sandbox.Container) { container.SeccompFlags |= seccomp.AllowMultiarch + container.SeccompPresets |= seccomp.PresetStrict container.Hostname = "hakurei-dbus" container.CommandContext = p.CommandContext if p.output != nil { diff --git a/hst/container.go b/hst/container.go index 43d7577..bc36bef 100644 --- a/hst/container.go +++ b/hst/container.go @@ -14,6 +14,8 @@ type ( SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"` // extra seccomp presets SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"` + // disable project-specific filter extensions + SeccompCompat bool `json:"seccomp_compat,omitempty"` // allow ptrace and friends Devel bool `json:"devel,omitempty"` // allow userns creation in container diff --git a/internal/app/instance/common/container.go b/internal/app/instance/common/container.go index 6a32e9a..d634238 100644 --- a/internal/app/instance/common/container.go +++ b/internal/app/instance/common/container.go @@ -30,6 +30,8 @@ func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*sandbox Hostname: s.Hostname, SeccompFlags: s.SeccompFlags, SeccompPresets: s.SeccompPresets, + RetainSession: s.Tty, + HostNet: s.Net, } { @@ -41,17 +43,17 @@ func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*sandbox container.SeccompFlags |= seccomp.AllowMultiarch } - if s.Devel { - container.Flags |= sandbox.FAllowDevel + if !s.SeccompCompat { + container.SeccompPresets |= seccomp.PresetExt } - if s.Userns { - container.Flags |= sandbox.FAllowUserns + if !s.Devel { + container.SeccompPresets |= seccomp.PresetDenyDevel } - if s.Net { - container.Flags |= sandbox.FAllowNet + if !s.Userns { + container.SeccompPresets |= seccomp.PresetDenyNS } - if s.Tty { - container.Flags |= sandbox.FAllowTTY + if !s.Tty { + container.SeccompPresets |= seccomp.PresetDenyTTY } if s.MapRealUID { diff --git a/internal/app/internal/setuid/app_nixos_test.go b/internal/app/internal/setuid/app_nixos_test.go index 975d889..a516252 100644 --- a/internal/app/internal/setuid/app_nixos_test.go +++ b/internal/app/internal/setuid/app_nixos_test.go @@ -6,6 +6,7 @@ import ( "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal/app" "git.gensokyo.uk/security/hakurei/sandbox" + "git.gensokyo.uk/security/hakurei/sandbox/seccomp" "git.gensokyo.uk/security/hakurei/system" ) @@ -94,12 +95,11 @@ var testCasesNixos = []sealTestCase{ UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), &sandbox.Params{ - Uid: 1971, - Gid: 100, - Flags: sandbox.FAllowNet | sandbox.FAllowUserns, - Dir: "/var/lib/persist/module/hakurei/0/1", - Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", - Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, + Uid: 1971, + Gid: 100, + Dir: "/var/lib/persist/module/hakurei/0/1", + Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", + Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, Env: []string{ "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket", @@ -142,6 +142,8 @@ var testCasesNixos = []sealTestCase{ Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0). Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0). Tmpfs("/var/run/nscd", 8192, 0755), + SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, + HostNet: true, }, }, } diff --git a/internal/app/internal/setuid/app_pd_test.go b/internal/app/internal/setuid/app_pd_test.go index 73dbd7e..7513301 100644 --- a/internal/app/internal/setuid/app_pd_test.go +++ b/internal/app/internal/setuid/app_pd_test.go @@ -8,6 +8,7 @@ import ( "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal/app" "git.gensokyo.uk/security/hakurei/sandbox" + "git.gensokyo.uk/security/hakurei/sandbox/seccomp" "git.gensokyo.uk/security/hakurei/system" ) @@ -28,10 +29,9 @@ var testCasesPd = []sealTestCase{ Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute). Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute), &sandbox.Params{ - Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY, - Dir: "/home/chronos", - Path: "/run/current-system/sw/bin/zsh", - Args: []string{"/run/current-system/sw/bin/zsh"}, + Dir: "/home/chronos", + Path: "/run/current-system/sw/bin/zsh", + Args: []string{"/run/current-system/sw/bin/zsh"}, Env: []string{ "HOME=/home/chronos", "SHELL=/run/current-system/sw/bin/zsh", @@ -68,6 +68,9 @@ var testCasesPd = []sealTestCase{ Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place("/etc/group", []byte("hakurei:x:65534:\n")). Tmpfs("/var/run/nscd", 8192, 0755), + SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, + HostNet: true, + RetainSession: true, }, }, { @@ -164,10 +167,9 @@ var testCasesPd = []sealTestCase{ UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), &sandbox.Params{ - Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY, - Dir: "/home/chronos", - Path: "/run/current-system/sw/bin/zsh", - Args: []string{"zsh", "-c", "exec chromium "}, + Dir: "/home/chronos", + Path: "/run/current-system/sw/bin/zsh", + Args: []string{"zsh", "-c", "exec chromium "}, Env: []string{ "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket", @@ -215,6 +217,9 @@ var testCasesPd = []sealTestCase{ Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0). Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0). Tmpfs("/var/run/nscd", 8192, 0755), + SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, + HostNet: true, + RetainSession: true, }, }, } diff --git a/ldd/exec.go b/ldd/exec.go index f6f940e..dbbe205 100644 --- a/ldd/exec.go +++ b/ldd/exec.go @@ -9,6 +9,7 @@ import ( "time" "git.gensokyo.uk/security/hakurei/sandbox" + "git.gensokyo.uk/security/hakurei/sandbox/seccomp" ) const lddTimeout = 2 * time.Second @@ -29,6 +30,8 @@ func ExecFilter(ctx context.Context, container := sandbox.New(c, "ldd", p) container.CommandContext = commandContext container.Hostname = "hakurei-ldd" + container.SeccompFlags |= seccomp.AllowMultiarch + container.SeccompPresets |= seccomp.PresetStrict stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) container.Stdout = stdout container.Stderr = stderr diff --git a/sandbox/container.go b/sandbox/container.go index df15810..88b0358 100644 --- a/sandbox/container.go +++ b/sandbox/container.go @@ -17,32 +17,6 @@ import ( "git.gensokyo.uk/security/hakurei/sandbox/seccomp" ) -type HardeningFlags uintptr - -const ( - FSyscallCompat HardeningFlags = 1 << iota - FAllowDevel - FAllowUserns - FAllowTTY - FAllowNet -) - -func (flags HardeningFlags) seccomp(presets seccomp.FilterPreset) seccomp.FilterPreset { - if flags&FSyscallCompat == 0 { - presets |= seccomp.PresetExt - } - if flags&FAllowDevel == 0 { - presets |= seccomp.PresetDenyDevel - } - if flags&FAllowUserns == 0 { - presets |= seccomp.PresetDenyNS - } - if flags&FAllowTTY == 0 { - presets |= seccomp.PresetDenyTTY - } - return presets -} - type ( // Container represents a container environment being prepared or run. // None of [Container] methods are safe for concurrent use. @@ -94,17 +68,23 @@ type ( Hostname string // Sequential container setup ops. *Ops + // Seccomp system call filter rules. + SeccompRules []seccomp.NativeRule // Extra seccomp flags. SeccompFlags seccomp.ExportFlag - // Extra seccomp presets. + // Seccomp presets. Has no effect unless SeccompRules is zero-length. SeccompPresets seccomp.FilterPreset + // Do not load seccomp program. + SeccompDisable bool // Permission bits of newly created parent directories. // The zero value is interpreted as 0755. ParentPerm os.FileMode + // Do not syscall.Setsid. + RetainSession bool + // Do not [syscall.CLONE_NEWNET]. + HostNet bool // Retain CAP_SYS_ADMIN. Privileged bool - - Flags HardeningFlags } ) @@ -120,7 +100,7 @@ func (p *Container) Start() error { p.cancel = cancel var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP - if p.Flags&FAllowNet == 0 { + if !p.HostNet { cloneFlags |= CLONE_NEWNET } @@ -132,6 +112,10 @@ func (p *Container) Start() error { p.Gid = OverflowGid() } + if !p.RetainSession { + p.SeccompPresets |= seccomp.PresetDenyTTY + } + if p.CommandContext != nil { p.cmd = p.CommandContext(ctx) } else { @@ -148,7 +132,7 @@ func (p *Container) Start() error { } p.cmd.Dir = "/" p.cmd.SysProcAttr = &SysProcAttr{ - Setsid: p.Flags&FAllowTTY == 0, + Setsid: !p.RetainSession, Pdeathsig: SIGKILL, Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS, @@ -211,6 +195,11 @@ func (p *Container) Serve() error { } } + if p.SeccompRules == nil { + // do not transmit nil + p.SeccompRules = make([]seccomp.NativeRule, 0) + } + err := setup.Encode( &initParams{ p.Params, @@ -229,8 +218,8 @@ func (p *Container) Serve() error { func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } func (p *Container) String() string { - return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x, presets: %#x", - p.Args, p.Flags, int(p.SeccompFlags), int(p.Flags.seccomp(p.SeccompPresets))) + return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", + p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) } func New(ctx context.Context, name string, args ...string) *Container { diff --git a/sandbox/container_test.go b/sandbox/container_test.go index 85e40bf..d7b774f 100644 --- a/sandbox/container_test.go +++ b/sandbox/container_test.go @@ -36,22 +36,39 @@ func TestContainer(t *testing.T) { } testCases := []struct { - name string - flags sandbox.HardeningFlags - ops *sandbox.Ops - mnt []*vfs.MountInfoEntry - host string + name string + filter bool + session bool + net bool + ops *sandbox.Ops + mnt []*vfs.MountInfoEntry + host string + rules []seccomp.NativeRule + flags seccomp.ExportFlag + presets seccomp.FilterPreset }{ - {"minimal", 0, new(sandbox.Ops), nil, "test-minimal"}, - {"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY, - new(sandbox.Ops), nil, "test-minimal"}, - {"tmpfs", 0, + {"minimal", true, false, false, + new(sandbox.Ops), nil, "test-minimal", + nil, 0, seccomp.PresetStrict}, + {"allow", true, true, true, + new(sandbox.Ops), nil, "test-minimal", + nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel}, + {"no filter", false, true, true, + new(sandbox.Ops), nil, "test-no-filter", + nil, 0, seccomp.PresetExt}, + {"custom rules", true, true, true, + new(sandbox.Ops), nil, "test-no-filter", + []seccomp.NativeRule{ + {seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}, + }, 0, seccomp.PresetExt}, + {"tmpfs", true, false, false, new(sandbox.Ops). Tmpfs(hst.Tmp, 0, 0755), []*vfs.MountInfoEntry{ e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), - }, "test-tmpfs"}, - {"dev", sandbox.FAllowTTY, // go test output is not a tty + }, "test-tmpfs", + nil, 0, seccomp.PresetStrict}, + {"dev", true, true /* go test output is not a tty */, false, new(sandbox.Ops). Dev("/dev"). Mqueue("/dev/mqueue"), @@ -65,7 +82,8 @@ func TestContainer(t *testing.T) { e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), - }, ""}, + }, "", + nil, 0, seccomp.PresetStrict}, } for _, tc := range testCases { @@ -79,9 +97,14 @@ func TestContainer(t *testing.T) { container.Gid = 100 container.Hostname = tc.host container.CommandContext = commandContext - container.Flags |= tc.flags container.Stdout, container.Stderr = os.Stdout, os.Stderr container.Ops = tc.ops + container.SeccompRules = tc.rules + container.SeccompFlags = tc.flags | seccomp.AllowMultiarch + container.SeccompPresets = tc.presets + container.SeccompDisable = !tc.filter + container.RetainSession = tc.session + container.HostNet = tc.net if container.Args[5] == "" { if name, err := os.Hostname(); err != nil { t.Fatalf("cannot get hostname: %v", err) @@ -163,9 +186,12 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE func TestContainerString(t *testing.T) { container := sandbox.New(t.Context(), "ldd", "/usr/bin/env") - container.Flags |= sandbox.FAllowDevel container.SeccompFlags |= seccomp.AllowMultiarch - want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x1, presets: 0x7` + container.SeccompRules = seccomp.Preset( + seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, + container.SeccompFlags) + container.SeccompPresets = seccomp.PresetStrict + want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf` if got := container.String(); got != want { t.Errorf("String: %s, want %s", got, want) } diff --git a/sandbox/init.go b/sandbox/init.go index 2fa84e6..2cd7f6c 100644 --- a/sandbox/init.go +++ b/sandbox/init.go @@ -229,8 +229,18 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { log.Fatalf("cannot capset: %v", err) } - if err := seccomp.Load(seccomp.Preset(params.Flags.seccomp(params.SeccompPresets), params.SeccompFlags), params.SeccompFlags); err != nil { - log.Fatalf("cannot load syscall filter: %v", err) + if !params.SeccompDisable { + rules := params.SeccompRules + if len(rules) == 0 { // non-empty rules slice always overrides presets + msg.Verbosef("resolving presets %#x", params.SeccompPresets) + rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) + } + if err := seccomp.Load(rules, params.SeccompFlags); err != nil { + log.Fatalf("cannot load syscall filter: %v", err) + } + msg.Verbosef("%d filter rules loaded", len(rules)) + } else { + msg.Verbose("syscall filter not configured") } extraFiles := make([]*os.File, params.Count) diff --git a/sandbox/ops.go b/sandbox/ops.go index 5226c7a..016b265 100644 --- a/sandbox/ops.go +++ b/sandbox/ops.go @@ -205,7 +205,7 @@ func (d MountDevOp) apply(params *Params) error { fmt.Sprintf("cannot mount devpts on %q:", devPtsPath)) } - if params.Flags&FAllowTTY != 0 { + if params.RetainSession { var buf [8]byte if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 { consolePath := toSysroot(path.Join(v, "console")) diff --git a/sandbox/seccomp/libseccomp.go b/sandbox/seccomp/libseccomp.go index d28df63..a43b8f2 100644 --- a/sandbox/seccomp/libseccomp.go +++ b/sandbox/seccomp/libseccomp.go @@ -171,11 +171,11 @@ type ScmpDatum uint64 // Argument / Value comparison definition type ScmpArgCmp struct { // argument number, starting at 0 - arg C.uint + Arg C.uint // the comparison op, e.g. SCMP_CMP_* - op ScmpCompare + Op ScmpCompare - datum_a, datum_b ScmpDatum + DatumA, DatumB ScmpDatum } // only used for testing