Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
150 lines
5.1 KiB
Go
150 lines
5.1 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package landlockx
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
|
|
ll "github.com/landlock-lsm/go-landlock/landlock"
|
|
llsyscall "github.com/landlock-lsm/go-landlock/landlock/syscall"
|
|
|
|
"github.com/ory/x/logrusx"
|
|
)
|
|
|
|
// IsSupported returns true if the running kernel supports Landlock LSM.
|
|
// On non-Linux platforms or kernels older than 5.13, this returns false.
|
|
func IsSupported() bool {
|
|
v, err := llsyscall.LandlockGetABIVersion()
|
|
return err == nil && v > 0
|
|
}
|
|
|
|
// runningExe returns the kernel-canonical path of the running binary.
|
|
// jsonnetsecure re-execs the running binary as a sandboxed worker, so
|
|
// the rule we attach must use the inode the kernel resolves at execve
|
|
// time. os.Executable on Linux returns the readlink of /proc/self/exe;
|
|
// EvalSymlinks resolves any further symlinks so the rule attaches to
|
|
// the actual file inode.
|
|
func runningExe() string {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if real, err := filepath.EvalSymlinks(exe); err == nil {
|
|
return real
|
|
}
|
|
return exe
|
|
}
|
|
|
|
// Apply activates the Landlock LSM filesystem sandbox for the calling process.
|
|
//
|
|
// roPaths is a list of paths that the process may read. Each path is stat-ed
|
|
// to determine whether to apply directory or file access rights; non-existent
|
|
// paths are silently skipped.
|
|
//
|
|
// rwDirs is a list of directories that the process may read and write
|
|
// recursively. Missing paths are silently ignored.
|
|
//
|
|
// Apply is a no-op on non-Linux platforms and on Linux kernels older than
|
|
// 5.13 (BestEffort degrades gracefully).
|
|
func Apply(l *logrusx.Logger, roPaths, rwDirs []string) error {
|
|
rules := defaultRules()
|
|
|
|
for _, p := range roPaths {
|
|
info, err := os.Stat(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if info.IsDir() {
|
|
rules = append(rules, ll.RODirs(p))
|
|
} else {
|
|
rules = append(rules, ll.ROFiles(p))
|
|
}
|
|
}
|
|
|
|
for _, d := range rwDirs {
|
|
rules = append(rules, ll.RWDirs(d).IgnoreIfMissing())
|
|
}
|
|
|
|
if !IsSupported() {
|
|
l.Warn("Landlock LSM is not available on this kernel; filesystem sandbox not applied.")
|
|
return nil
|
|
}
|
|
|
|
if err := ll.V3.BestEffort().RestrictPaths(rules...); err != nil {
|
|
return err
|
|
}
|
|
|
|
l.Info("Landlock filesystem sandbox is active.")
|
|
return nil
|
|
}
|
|
|
|
// ApplyEmpty activates a Landlock filesystem sandbox with no allowed
|
|
// paths, denying every filesystem access the V3 ABI handles. Already-open
|
|
// file descriptors (stdin, stdout, stderr inherited from the parent)
|
|
// keep working, but no path-based open/read/write/exec is permitted.
|
|
//
|
|
// Use it inside helper subprocesses that only need stdio — for example,
|
|
// the hidden `jsonnet` worker that jsonnetsecure re-execs to evaluate
|
|
// mappers in isolation.
|
|
//
|
|
// Landlock layers stack: this call adds a strictly-more-restrictive
|
|
// layer on top of whatever the parent already enforced.
|
|
//
|
|
// ApplyEmpty is a no-op on non-Linux platforms and on Linux kernels
|
|
// older than 5.13.
|
|
func ApplyEmpty(l *logrusx.Logger) error {
|
|
if !IsSupported() {
|
|
if l != nil {
|
|
l.Warn("Landlock LSM is not available on this kernel; empty sandbox not applied.")
|
|
}
|
|
return nil
|
|
}
|
|
if err := ll.V3.BestEffort().RestrictPaths(); err != nil {
|
|
return err
|
|
}
|
|
if l != nil {
|
|
l.Info("Landlock empty filesystem sandbox is active.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// defaultRules returns the rules every kratos serve process needs regardless
|
|
// of operator config:
|
|
// - /dev/null is a kernel-managed sink used by subprocess plumbing
|
|
// (exec.Command stdin redirection, log discarding, etc.); it cannot
|
|
// leak data.
|
|
// - /etc/resolv.conf carries the DNS nameserver list and /etc/hosts
|
|
// the static host table; Go's pure-Go resolver reads both for every
|
|
// hostname lookup. /etc/nsswitch.conf is intentionally not granted:
|
|
// the binary is built with CGO_ENABLED=0, so when nsswitch.conf is
|
|
// unreadable the resolver falls back to "files then DNS", which is
|
|
// what we want anyway.
|
|
// - the running binary is re-exec'd by jsonnetsecure for sandboxed mapper
|
|
// evaluation, so it needs read+execute. The binary must be statically
|
|
// linked (CGO_ENABLED=0) — execve of a dynamically-linked binary also
|
|
// requires read+execute on the system dynamic linker (PT_INTERP) and
|
|
// its NEEDED libraries, which we deliberately do not grant.
|
|
//
|
|
// /etc/ssl (system trust roots for outbound HTTPS) is intentionally not
|
|
// included. Cloud binaries blank-import
|
|
// golang.org/x/crypto/x509roots/fallback (see cloudlib/x509_roots.go)
|
|
// and set `godebug x509usefallbackroots=1` in their go.mod, so
|
|
// crypto/x509 ships its own embedded Mozilla bundle and never reads
|
|
// /etc/ssl at runtime. Operators who need to trust an additional CA
|
|
// continue to set SSL_CERT_FILE / SSL_CERT_DIR and list the file under
|
|
// security.landlock.allowed_paths.
|
|
func defaultRules() []ll.Rule {
|
|
rules := []ll.Rule{
|
|
ll.RWFiles("/dev/null"),
|
|
ll.ROFiles("/etc/resolv.conf").IgnoreIfMissing(),
|
|
ll.ROFiles("/etc/hosts").IgnoreIfMissing(),
|
|
}
|
|
if exe := runningExe(); exe != "" {
|
|
access := ll.AccessFSSet(llsyscall.AccessFSReadFile | llsyscall.AccessFSExecute)
|
|
rules = append(rules, ll.PathAccess(access, exe).IgnoreIfMissing())
|
|
}
|
|
return rules
|
|
}
|