1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
|
package landlock
import (
"errors"
"fmt"
ll "github.com/landlock-lsm/go-landlock/landlock/syscall"
)
// Access permission sets for filesystem access.
const (
// The set of access rights that only apply to files.
accessFile AccessFSSet = ll.AccessFSExecute | ll.AccessFSWriteFile | ll.AccessFSTruncate | ll.AccessFSReadFile
// The set of access rights associated with read access to files and directories.
accessFSRead AccessFSSet = ll.AccessFSExecute | ll.AccessFSReadFile | ll.AccessFSReadDir
// The set of access rights associated with write access to files and directories.
accessFSWrite AccessFSSet = ll.AccessFSWriteFile | ll.AccessFSRemoveDir | ll.AccessFSRemoveFile | ll.AccessFSMakeChar | ll.AccessFSMakeDir | ll.AccessFSMakeReg | ll.AccessFSMakeSock | ll.AccessFSMakeFifo | ll.AccessFSMakeBlock | ll.AccessFSMakeSym | ll.AccessFSTruncate
// The set of access rights associated with read and write access to files and directories.
accessFSReadWrite AccessFSSet = accessFSRead | accessFSWrite
)
// These are Landlock configurations for the currently supported
// Landlock ABI versions, configured to restrict the highest possible
// set of operations possible for each version.
//
// The higher the ABI version, the more operations Landlock will be
// able to restrict.
var (
// Landlock V1 support (basic file operations).
V1 = abiInfos[1].asConfig()
// Landlock V2 support (V1 + file reparenting between different directories)
V2 = abiInfos[2].asConfig()
// Landlock V3 support (V2 + file truncation)
V3 = abiInfos[3].asConfig()
// Landlock V4 support (V3 + networking)
V4 = abiInfos[4].asConfig()
// Landlock V5 support (V4 + ioctl on device files)
V5 = abiInfos[5].asConfig()
)
// v0 denotes "no Landlock support". Only used internally.
var v0 = Config{}
// The Landlock configuration describes the desired set of
// landlockable operations to be restricted and the constraints on it
// (e.g. best effort mode).
type Config struct {
handledAccessFS AccessFSSet
handledAccessNet AccessNetSet
bestEffort bool
}
// NewConfig creates a new Landlock configuration with the given parameters.
//
// Passing an AccessFSSet will set that as the set of file system
// operations to restrict when enabling Landlock. The AccessFSSet
// needs to stay within the bounds of what go-landlock supports.
// (If you are getting an error, you might need to upgrade to a newer
// version of go-landlock.)
func NewConfig(args ...interface{}) (*Config, error) {
// Implementation note: This factory is written with future
// extensibility in mind. Only specific types are supported as
// input, but in the future more might be added.
//
// This constructor ensures that callers can't construct
// invalid Config values.
var c Config
for _, arg := range args {
switch arg := arg.(type) {
case AccessFSSet:
if !c.handledAccessFS.isEmpty() {
return nil, errors.New("only one AccessFSSet may be provided")
}
if !arg.valid() {
return nil, errors.New("unsupported AccessFSSet value; upgrade go-landlock?")
}
c.handledAccessFS = arg
case AccessNetSet:
if !c.handledAccessNet.isEmpty() {
return nil, errors.New("only one AccessNetSet may be provided")
}
if !arg.valid() {
return nil, errors.New("unsupported AccessNetSet value; upgrade go-landlock?")
}
c.handledAccessNet = arg
default:
return nil, fmt.Errorf("unknown argument %v; only AccessFSSet-type argument is supported", arg)
}
}
return &c, nil
}
// MustConfig is like NewConfig but panics on error.
func MustConfig(args ...interface{}) Config {
c, err := NewConfig(args...)
if err != nil {
panic(err)
}
return *c
}
// String builds a human-readable representation of the Config.
func (c Config) String() string {
abi := abiInfo{version: -1} // invalid
for i := len(abiInfos) - 1; i >= 0; i-- {
a := abiInfos[i]
if c.compatibleWithABI(a) {
abi = a
}
}
var fsDesc = c.handledAccessFS.String()
if abi.supportedAccessFS == c.handledAccessFS && c.handledAccessFS != 0 {
fsDesc = "all"
}
var netDesc = c.handledAccessNet.String()
if abi.supportedAccessNet == c.handledAccessNet && c.handledAccessNet != 0 {
fsDesc = "all"
}
var bestEffort = ""
if c.bestEffort {
bestEffort = " (best effort)"
}
var version string
if abi.version < 0 {
version = "V???"
} else {
version = fmt.Sprintf("V%v", abi.version)
}
return fmt.Sprintf("{Landlock %v; FS: %v; Net: %v%v}", version, fsDesc, netDesc, bestEffort)
}
// BestEffort returns a config that will opportunistically enforce
// the strongest rules it can, up to the given ABI version, working
// with the level of Landlock support available in the running kernel.
//
// Warning: A best-effort call to RestrictPaths() will succeed without
// error even when Landlock is not available at all on the current kernel.
func (c Config) BestEffort() Config {
cfg := c
cfg.bestEffort = true
return cfg
}
// RestrictPaths restricts all goroutines to only "see" the files
// provided as inputs. After this call successfully returns, the
// goroutines will only be able to use files in the ways as they were
// specified in advance in the call to RestrictPaths.
//
// Example: The following invocation will restrict all goroutines so
// that it can only read from /usr, /bin and /tmp, and only write to
// /tmp:
//
// err := landlock.V3.RestrictPaths(
// landlock.RODirs("/usr", "/bin"),
// landlock.RWDirs("/tmp"),
// )
// if err != nil {
// log.Fatalf("landlock.V3.RestrictPaths(): %v", err)
// }
//
// RestrictPaths returns an error if any of the given paths does not
// denote an actual directory or file, or if Landlock can't be enforced
// using the desired ABI version constraints.
//
// RestrictPaths also sets the "no new privileges" flag for all OS
// threads managed by the Go runtime.
//
// # Restrictable access rights
//
// The notions of what "reading" and "writing" mean are limited by what
// the selected Landlock version supports.
//
// Calling RestrictPaths with a given Landlock ABI version will
// inhibit all future calls to the access rights supported by this ABI
// version, unless the accessed path is in a file hierarchy that is
// specifically allow-listed for a specific set of access rights.
//
// The overall set of operations that RestrictPaths can restrict are:
//
// For reading:
//
// - Executing a file (V1+)
// - Opening a file with read access (V1+)
// - Opening a directory or listing its content (V1+)
//
// For writing:
//
// - Opening a file with write access (V1+)
// - Truncating file contents (V3+)
//
// For directory manipulation:
//
// - Removing an empty directory or renaming one (V1+)
// - Removing (or renaming) a file (V1+)
// - Creating (or renaming or linking) a character device (V1+)
// - Creating (or renaming) a directory (V1+)
// - Creating (or renaming or linking) a regular file (V1+)
// - Creating (or renaming or linking) a UNIX domain socket (V1+)
// - Creating (or renaming or linking) a named pipe (V1+)
// - Creating (or renaming or linking) a block device (V1+)
// - Creating (or renaming or linking) a symbolic link (V1+)
// - Renaming or linking a file between directories (V2+)
//
// Future versions of Landlock will be able to inhibit more operations.
// Quoting the Landlock documentation:
//
// It is currently not possible to restrict some file-related
// actions accessible through these syscall families: chdir(2),
// stat(2), flock(2), chmod(2), chown(2), setxattr(2), utime(2),
// ioctl(2), fcntl(2), access(2). Future Landlock evolutions will
// enable to restrict them.
//
// The access rights are documented in more depth in the
// [Kernel Documentation about Access Rights].
//
// # Helper functions for selecting access rights
//
// These helper functions help selecting common subsets of access rights:
//
// - [RODirs] selects access rights in the group "for reading".
// In V1, this means reading files, listing directories and executing files.
// - [RWDirs] selects access rights in the group "for reading", "for writing" and
// "for directory manipulation". This grants the full set of access rights which are
// available within the configuration.
// - [ROFiles] is like [RODirs], but does not select directory-specific access rights.
// In V1, this means reading and executing files.
// - [RWFiles] is like [RWDirs], but does not select directory-specific access rights.
// In V1, this means reading, writing and executing files.
//
// The [PathAccess] rule lets callers define custom subsets of these
// access rights. AccessFSSets permitted using [PathAccess] must be a
// subset of the [AccessFSSet] that the Config restricts.
//
// [Kernel Documentation about Access Rights]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights
func (c Config) RestrictPaths(rules ...Rule) error {
c.handledAccessNet = 0 // clear out everything but file system access
return restrict(c, rules...)
}
// RestrictNet restricts network access in goroutines.
//
// Using Landlock V4, this function will disallow the use of bind(2)
// and connect(2) for TCP ports, unless those TCP ports are
// specifically permitted using these rules:
//
// - [ConnectTCP] permits connect(2) operations to a given TCP port.
// - [BindTCP] permits bind(2) operations on a given TCP port.
//
// These network access rights are documented in more depth in the
// [Kernel Documentation about Network flags].
//
// [Kernel Documentation about Network flags]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#network-flags
func (c Config) RestrictNet(rules ...Rule) error {
c.handledAccessFS = 0 // clear out everything but network access
return restrict(c, rules...)
}
// Restrict restricts all types of access which is restrictable with the Config.
//
// Using Landlock V4, this is equivalent to calling both
// [RestrictPaths] and [RestrictNet] with the subset of arguments that
// apply to it.
//
// In future Landlock versions, this function might restrict
// additional kinds of operations outside of file system access and
// networking, provided that the [Config] specifies these.
func (c Config) Restrict(rules ...Rule) error {
return restrict(c, rules...)
}
// PathOpt is a deprecated alias for [Rule].
//
// Deprecated: This alias is only kept around for backwards
// compatibility and will disappear with the next major release.
type PathOpt = Rule
// compatibleWith is true if c is compatible to work at the given Landlock ABI level.
func (c Config) compatibleWithABI(abi abiInfo) bool {
return (c.handledAccessFS.isSubset(abi.supportedAccessFS) &&
c.handledAccessNet.isSubset(abi.supportedAccessNet))
}
// restrictTo returns a config that is a subset of c and which is compatible with the given ABI.
func (c Config) restrictTo(abi abiInfo) Config {
return Config{
handledAccessFS: c.handledAccessFS.intersect(abi.supportedAccessFS),
handledAccessNet: c.handledAccessNet.intersect(abi.supportedAccessNet),
bestEffort: true,
}
}
|