2023-09-12 04:44:07 +00:00
{ config , lib , options , pkgs , sane-lib , utils , . . . }:
2023-02-02 12:31:13 +00:00
let
2023-11-18 22:06:08 +00:00
saneCfg = config . sane ;
2023-02-02 12:31:13 +00:00
cfg = config . sane . programs ;
2024-02-23 05:24:13 +00:00
fs-lib = sane-lib . fs ;
2024-02-12 12:48:02 +00:00
path-lib = sane-lib . path ;
2023-07-03 07:55:05 +00:00
2023-07-03 07:04:57 +00:00
# create a map:
# {
# "${pkgName}" = {
# system = true|false;
2023-07-03 07:16:24 +00:00
# user = {
2023-07-03 07:04:57 +00:00
# "${name}" = true|false;
# };
# };
# }
# for every ${pkgName} in pkgSpecs.
# `system = true|false` is a computed expression over all the other programs, as evaluated.
solveDefaultEnableFor = pkgSpecs : lib . foldlAttrs (
acc : pname : pval : (
# add "${enableName}".system |= areSuggestionsEnabled pval
2023-07-03 07:16:24 +00:00
# for each `enableName` in pval.suggestedPrograms.
# do the same for `user` field.
2023-07-03 07:04:57 +00:00
lib . foldl ( acc' : enableName : acc' // {
2023-07-03 07:16:24 +00:00
" ${ enableName } " = let
super = acc' . " ${ enableName } " ;
in {
system = super . system || ( pval . enableFor . system && pval . enableSuggested ) ;
user = super . user // lib . filterAttrs ( _u : en : en && pval . enableSuggested ) pval . enableFor . user ;
} ;
2023-07-03 07:04:57 +00:00
} ) acc pval . suggestedPrograms
)
2023-07-03 07:16:24 +00:00
) ( mkDefaultEnables pkgSpecs ) pkgSpecs ;
mkDefaultEnables = lib . mapAttrs ( _pname : _pval : { system = false ; user = { } ; } ) ;
2023-07-03 07:04:57 +00:00
defaultEnables = solveDefaultEnableFor cfg ;
2024-01-20 11:11:12 +00:00
# wrap a package so that its binaries (maybe) run in a sandbox
2024-02-08 21:51:32 +00:00
wrapPkg = pkgName : { fs , persist , sandbox , . . . }: package : (
2024-01-27 17:08:27 +00:00
if ! sandbox . enable || sandbox . method == null then
2024-01-20 11:11:12 +00:00
package
2024-01-23 10:44:13 +00:00
else
2024-01-21 01:04:31 +00:00
let
2024-02-12 11:52:33 +00:00
makeProfile = pkgs . callPackage ./make-sandbox-profile.nix { } ;
2024-04-15 18:57:22 +00:00
makeSandboxed = pkgs . callPackage ./make-sandboxed.nix { sane-sandboxed = config . sane . programs . sane-sandboxed . package ; } ;
2024-02-12 12:48:02 +00:00
# removeStorePaths: [ str ] -> [ str ], but remove store paths, because nix evals aren't allowed to contain any (for purity reasons?)
2024-02-23 05:24:13 +00:00
removeStorePaths = paths : lib . filter ( p : ! ( lib . hasPrefix " / n i x / s t o r e " p ) ) paths ;
2024-02-24 11:47:39 +00:00
makeCanonical = paths : builtins . map path-lib . realpath paths ;
2024-02-23 05:24:13 +00:00
# derefSymlinks: [ str ] -> [ str ]: for each path which is a symlink (or a child of a symlink'd dir), dereference one layer of symlink. else, drop it from the list.
2024-02-24 05:36:44 +00:00
derefSymlinks' = paths : builtins . map ( fs-lib . derefSymlinkOrNull config . sane . fs ) paths ;
2024-02-23 05:24:13 +00:00
derefSymlinks = paths : lib . filter ( p : p != null ) ( derefSymlinks' paths ) ;
2024-02-12 12:48:02 +00:00
# expandSymlinksOnce: [ str ] -> [ str ], returning all the original paths plus dereferencing any symlinks and adding their targets to this list.
2024-02-24 11:47:39 +00:00
expandSymlinksOnce = paths : lib . unique ( paths ++ removeStorePaths ( makeCanonical ( derefSymlinks paths ) ) ) ;
2024-02-23 05:24:13 +00:00
expandSymlinks = paths : lib . converge expandSymlinksOnce paths ;
2024-02-12 12:48:02 +00:00
2024-01-22 03:50:28 +00:00
vpn = lib . findSingle ( v : v . default ) null null ( builtins . attrValues config . sane . vpn ) ;
2024-02-12 12:48:02 +00:00
2024-02-12 12:05:37 +00:00
sandboxProfilesFor = userName : let
homeDir = config . sane . users . " ${ userName } " . home ;
2024-02-12 12:17:37 +00:00
uid = config . users . users . " ${ userName } " . uid ;
xdgRuntimeDir = " / r u n / u s e r / ${ builtins . toString uid } " ;
2024-02-12 12:05:37 +00:00
fullHomePaths = lib . optionals ( userName != null ) (
builtins . map
2024-02-12 12:48:02 +00:00
( p : path-lib . concat [ homeDir p ] )
2024-02-12 12:05:37 +00:00
( builtins . attrNames fs ++ builtins . attrNames persist . byPath ++ sandbox . extraHomePaths )
) ;
2024-02-12 12:17:37 +00:00
fullRuntimePaths = lib . optionals ( userName != null ) (
builtins . map
2024-02-12 12:48:02 +00:00
( p : path-lib . concat [ xdgRuntimeDir p ] )
2024-03-24 11:58:17 +00:00
sandbox . extraRuntimePaths
2024-02-12 12:17:37 +00:00
) ;
2024-02-12 13:00:10 +00:00
allowedPaths = [
2024-02-12 12:48:02 +00:00
" / n i x / s t o r e "
" / b i n / s h "
" / e t c " #< especially for /etc/profiles/per-user/$USER/bin
" / r u n / c u r r e n t - s y s t e m " #< for basics like `ls`, and all this program's `suggestedPrograms` (/run/current-system/sw/bin)
2024-03-02 18:51:39 +00:00
" / r u n / w r a p p e r s " #< SUID wrappers, in this case so that firejail can be re-entrant. TODO: remove!
2024-02-12 12:48:02 +00:00
" / r u n / s y s t e m d / r e s o l v e " #< to allow reading /etc/resolv.conf, which ultimately symlinks here
# /run/opengl-driver is a symlink into /nix/store; needed by e.g. mpv
" / r u n / o p e n g l - d r i v e r "
" / r u n / o p e n g l - d r i v e r - 3 2 " #< XXX: doesn't exist on aarch64?
" / u s r / b i n / e n v "
2024-02-13 10:55:10 +00:00
] ++ lib . optionals ( builtins . elem " s y s t e m " sandbox . whitelistDbus ) [ " / r u n / d b u s / s y s t e m _ b u s _ s o c k e t " ]
++ sandbox . extraPaths ++ fullHomePaths ++ fullRuntimePaths ;
2024-02-12 12:05:37 +00:00
in makeProfile {
2024-02-12 11:52:33 +00:00
inherit pkgName ;
2024-02-03 00:17:24 +00:00
inherit ( sandbox )
autodetectCliPaths
capabilities
extraConfig
method
whitelistPwd
;
2024-02-08 21:51:32 +00:00
netDev = if sandbox . net == " v p n " then
2024-02-08 21:07:34 +00:00
vpn . bridgeDevice
else
2024-02-08 21:51:32 +00:00
sandbox . net ;
dns = if sandbox . net == " v p n " then
2024-02-08 21:07:34 +00:00
vpn . dns
else
null ;
2024-02-12 13:00:10 +00:00
allowedPaths = expandSymlinks allowedPaths ;
2024-02-12 11:52:33 +00:00
} ;
2024-02-12 13:57:59 +00:00
defaultProfile = sandboxProfilesFor config . sane . defaultUser ;
makeSandboxedArgs = {
2024-02-12 11:52:33 +00:00
inherit pkgName package ;
inherit ( sandbox )
embedSandboxer
wrapperType
;
2024-02-12 13:57:59 +00:00
} ;
in
makeSandboxed ( makeSandboxedArgs // {
2024-02-12 12:05:37 +00:00
passthru = {
inherit sandboxProfilesFor ;
2024-02-12 13:57:59 +00:00
withEmbeddedSandboxer = makeSandboxed ( makeSandboxedArgs // {
# embed the sandboxer AND a profile, whichever profile the package would have if installed by the default user.
# useful to iterate a package's sandbox config without redeploying.
embedSandboxer = true ;
extraSandboxerArgs = [
" - - s a n e - s a n d b o x - p r o f i l e - d i r " " ${ defaultProfile } / s h a r e / s a n e - s a n d b o x e d / p r o f i l e s "
] ;
} ) ;
withEmbeddedSandboxerOnly = makeSandboxed ( makeSandboxedArgs // {
# embed the sandboxer but no profile. useful pretty much only for testing changes within the actual sandboxer.
embedSandboxer = true ;
} ) ;
2024-02-12 12:05:37 +00:00
} ;
2024-02-12 13:57:59 +00:00
} )
2024-01-20 11:11:12 +00:00
) ;
2023-07-03 07:55:05 +00:00
pkgSpec = with lib ; types . submodule ( { config , name , . . . }: {
2023-02-02 12:31:13 +00:00
options = {
2024-01-20 11:11:12 +00:00
packageUnwrapped = mkOption {
2023-02-03 03:58:23 +00:00
type = types . nullOr types . package ;
description = ''
package , or ` null ` if the program is some sort of meta set ( in which case it much EXPLICITLY be set null ) .
'' ;
2023-02-03 04:23:26 +00:00
default =
let
2023-07-03 07:55:05 +00:00
pkgPath = lib . splitString " . " name ;
2023-02-03 04:23:26 +00:00
in
# package can be inferred by the attr name, allowing shorthand like
2023-02-03 05:26:57 +00:00
# `sane.programs.nano.enable = true;`
2023-02-03 04:23:26 +00:00
# this indexing will throw if the package doesn't exist and the user forgets to specify
# a valid source explicitly.
2023-07-03 07:55:05 +00:00
lib . getAttrFromPath pkgPath pkgs ;
2023-02-02 12:31:13 +00:00
} ;
2024-01-20 11:11:12 +00:00
package = mkOption {
type = types . nullOr types . package ;
description = ''
assigned internally .
this is ` packageUnwrapped ` , but with the binaries possibly wrapped in sandboxing measures .
'' ;
} ;
2023-02-02 12:31:13 +00:00
enableFor . system = mkOption {
type = types . bool ;
2023-07-03 07:16:24 +00:00
default = defaultEnables . " ${ name } " . system ;
2023-02-02 12:31:13 +00:00
description = ''
place this program on the system PATH
'' ;
} ;
enableFor . user = mkOption {
type = types . attrsOf types . bool ;
2023-07-03 07:16:24 +00:00
default = defaultEnables . " ${ name } " . user ;
2023-02-02 12:31:13 +00:00
description = ''
place this program on the PATH for some specified user ( s ) .
'' ;
} ;
2023-04-23 23:21:08 +00:00
enabled = mkOption {
type = types . bool ;
description = ''
generated ( i . e . read-only ) value indicating if the program is enabled either for any user or for the system .
'' ;
} ;
2023-02-02 12:31:13 +00:00
suggestedPrograms = mkOption {
type = types . listOf types . str ;
default = [ ] ;
description = ''
list of other programs a user may want to enable alongside this one .
for example , the gnome desktop environment would suggest things like its settings app .
'' ;
} ;
enableSuggested = mkOption {
type = types . bool ;
default = true ;
} ;
2023-07-15 10:04:22 +00:00
mime . priority = mkOption {
type = types . int ;
default = 100 ;
description = ''
program with the numerically lower priority takes precedence whenever two mime associations overlap .
'' ;
} ;
mime . associations = mkOption {
2023-07-15 08:44:18 +00:00
type = types . attrsOf types . str ;
default = { } ;
description = ''
mime associations . each entry takes the form of :
" < m i m e t y p e > " = " < l a u n c h e r > . d e s k t o p "
e . g .
{
" a u d i o / f l a c " = " v l c . d e s k t o p " ;
" a p p l i c a t i o n / p d f " = " o r g . g n o m e . E v i n c e . d e s k t o p " ;
}
'' ;
} ;
2023-12-11 03:03:22 +00:00
mime . urlAssociations = mkOption {
# TODO: it'd be cool to have the value part of this be `.desktop` files.
# mimeo doesn't quite do that well. would need a wrapper script which does `mimeo --desk2field Exec mpv.desktop` to get the command
# and then interpolate the paths into it (%U)
type = types . attrsOf types . str ;
default = { } ;
description = ''
map of regex -> command .
e . g . " ^ h t t p s ? : / / ( w w w . ) ? y o u t u b e . c o m / w a t c h \? . * v = " = " m p v % U "
'' ;
} ;
2023-07-13 07:17:09 +00:00
persist = mkOption {
type = options . sane . persist . sys . type ;
default = { } ;
description = ''
entries to pass onto ` sane . persist . sys ` or ` sane . user . persist `
when this program is enabled .
'' ;
2023-02-02 12:31:13 +00:00
} ;
2023-04-24 06:49:56 +00:00
fs = mkOption {
2023-09-12 05:44:53 +00:00
# funny type to allow deferring the option merging down to the layer below
2023-07-18 11:25:27 +00:00
type = types . attrsOf ( types . coercedTo types . attrs ( a : [ a ] ) ( types . listOf types . attrs ) ) ;
2023-04-24 06:49:56 +00:00
default = { } ;
description = " f i l e s t o p o p u l a t e w h e n t h i s p r o g r a m i s e n a b l e d " ;
} ;
2023-04-26 00:17:04 +00:00
secrets = mkOption {
type = types . attrsOf types . path ;
default = { } ;
description = ''
fs paths to link to some decrypted secret .
the secret will have same owner as the user under which the program is enabled .
'' ;
} ;
2023-06-27 10:24:48 +00:00
env = mkOption {
type = types . attrsOf types . str ;
default = { } ;
2024-02-14 11:08:43 +00:00
description = ''
environment variables to set when this program is enabled .
env vars set here are intended to propagate everywhere into the user's ( or system's ) session/services ;
they aren't visible just to the program which specified them .
'' ;
2023-06-27 10:24:48 +00:00
} ;
2023-09-12 04:44:07 +00:00
services = mkOption {
2024-03-16 07:35:54 +00:00
type = types . attrsOf types . anything ; # options.sane.users.value.type;
2023-09-12 04:44:07 +00:00
default = { } ;
description = ''
2024-03-16 07:35:54 +00:00
user services to define if this package is enabled .
acts as noop for root-enabled packages .
see ` sane . users . <user> . services ` for options ;
2023-09-12 04:44:07 +00:00
'' ;
} ;
2024-04-09 20:20:36 +00:00
buildCost = mkOption {
type = types . enum [ 0 1 2 ] ;
default = 0 ;
2023-11-18 22:06:08 +00:00
description = ''
whether this package is very slow , or has unique dependencies which are very slow to build .
marking packages like this can be used to achieve faster , but limited , rebuilds/deploys ( by omitting the package ) .
'' ;
} ;
2024-02-08 21:51:32 +00:00
sandbox . net = mkOption {
type = types . coercedTo
types . str
2024-02-15 00:09:16 +00:00
( s : if s == " c l e a r n e t " || s == " l o c a l h o s t " then " a l l " else s )
2024-02-08 21:51:32 +00:00
( types . enum [ null " a l l " " v p n " ] ) ;
default = null ;
2024-01-20 11:11:12 +00:00
description = ''
how this app should have its network traffic routed .
2024-02-08 21:51:32 +00:00
- " a l l " : unsandboxed network .
- " c l e a r n e t " : traffic is routed only over clearnet .
2024-02-15 00:09:16 +00:00
currently , just an alias for " a l l " .
- " l o c a l h o s t " : only needs access to other services running on this host .
currently , just an alias for " a l l " .
2024-02-08 21:07:34 +00:00
- " v p n " : to route all traffic over the default VPN .
- null : to maximally isolate from the network .
2024-01-20 11:11:12 +00:00
'' ;
} ;
2024-01-21 23:59:15 +00:00
sandbox . method = mkOption {
2024-01-28 05:57:11 +00:00
type = types . nullOr ( types . enum [ " b w r a p " " c a p s h o n l y " " f i r e j a i l " " l a n d l o c k " ] ) ;
2024-01-27 03:39:26 +00:00
default = null ; #< TODO: default to something non-null
2024-01-21 23:59:15 +00:00
description = ''
how/whether to sandbox all binaries in the package .
'' ;
} ;
2024-01-27 17:08:27 +00:00
sandbox . enable = mkOption {
type = types . bool ;
default = true ;
} ;
2024-01-29 09:13:49 +00:00
sandbox . embedSandboxer = mkOption {
type = types . bool ;
default = false ;
description = ''
whether the sandboxed application should reference its sandboxer by path or by name .
'' ;
} ;
2024-01-27 14:26:41 +00:00
sandbox . wrapperType = mkOption {
type = types . enum [ " i n p l a c e " " w r a p p e d D e r i v a t i o n " ] ;
2024-02-28 17:39:00 +00:00
default = " w r a p p e d D e r i v a t i o n " ;
2024-01-27 14:26:41 +00:00
description = ''
how to manipulate the ` packageUnwrapped ` derivation in order to achieve sandboxing .
- inplace : applies an override to ` packageUnwrapped ` , so that all ` bin / ` files are sandboxed ,
and call into un-sandboxed dot-files within ` bin / ` ( like makeWrapper does ) .
- wrappedDerivation : leaves the input derivation unchanged , and creates a _new_ derivation whose
binaries wrap the binaries in the original derivation with a sandbox .
" i n p l a c e " is more reliable , but " w r a p p e d D e r i v a t i o n " is more lightweight ( doesn't force any rebuilds ) .
2024-01-29 11:42:47 +00:00
the biggest gap in " w r a p p e d D e r i v a t i o n " is that it doesn't link anything outside ` bin / ` , except for
some limited ( verified safe ) support for ` share/applications /* . d e s k t o p `
2024-01-27 14:26:41 +00:00
'' ;
} ;
2024-01-27 17:19:48 +00:00
sandbox . autodetectCliPaths = mkOption {
2024-02-03 00:17:24 +00:00
type = types . coercedTo types . bool
( b : if b then " e x i s t i n g " else null )
2024-02-25 01:55:46 +00:00
( types . nullOr ( types . enum [ " e x i s t i n g " " e x i s t i n g F i l e " " e x i s t i n g F i l e O r P a r e n t " " e x i s t i n g O r P a r e n t " " p a r e n t " ] ) ) ;
2024-02-03 00:17:24 +00:00
default = null ;
2024-01-27 17:19:48 +00:00
description = ''
if a CLI argument looks like a PATH , should we add it to the sandbox ?
2024-02-03 00:17:24 +00:00
- null = > never
2024-02-25 01:55:46 +00:00
- " e x i s t i n g " = > only if the path exists .
- " e x i s t i n g F i l e " = > only if the path exists * and is file-like * ( i . e . a file or a symlink to a file , but not a directory )
2024-02-14 04:31:59 +00:00
- " p a r e n t " = > allow access to the directory containing any file ( whether that file exists or not ) . useful for certain media viewers/library managers .
2024-02-25 01:55:46 +00:00
- " e x i s t i n g O r P a r e n t " = > add the path if it exists ; if not , add its parent if that exists . useful for programs which create files or directories .
- " e x i s t i n g F i l e O r P a r e n t " = > add the path if it exists and is file-like ; if not , add its parent if that exists . useful for programs which create files .
2024-01-27 17:19:48 +00:00
'' ;
} ;
2024-01-27 17:08:27 +00:00
sandbox . capabilities = mkOption {
type = types . listOf types . str ;
default = [ ] ;
description = ''
list of Linux capabilities the program needs . lowercase , and without the cap_ prefix .
e . g . sandbox . capabilities = [ " n e t _ a d m i n " " n e t _ r a w " ] ;
'' ;
} ;
2024-02-12 15:22:49 +00:00
sandbox . whitelistAudio = mkOption {
type = types . bool ;
2024-02-13 11:14:38 +00:00
default = false ;
2024-02-12 15:22:49 +00:00
description = ''
allow sandbox to freely interact with pulse/pipewire.
'' ;
} ;
sandbox . whitelistDbus = mkOption {
2024-02-13 10:55:10 +00:00
type = types . listOf ( types . enum [ " u s e r " " s y s t e m " ] ) ;
2024-02-13 11:58:12 +00:00
default = [ ] ;
2024-02-12 15:22:49 +00:00
description = ''
allow sandbox to freely interact with dbus services .
'' ;
} ;
2024-02-02 17:18:51 +00:00
sandbox . whitelistDri = mkOption {
type = types . bool ;
default = false ;
description = ''
allow sandbox to access the kernel's /dev/dri interface ( s ) .
this enables GPU acceleration , particularly for mesa applications ,
however , this basically amounts to letting the sandbox send GPU-specific
commands directly to the GPU ( or , its kernel module ) , which is a rather
broad and unaudited attack surface .
'' ;
} ;
2024-02-12 15:22:49 +00:00
sandbox . whitelistPwd = mkOption {
type = types . bool ;
default = false ;
description = ''
allow the program full access to whichever directory it was launched from .
'' ;
} ;
2024-03-24 11:54:12 +00:00
sandbox . whitelistS6 = mkOption {
type = types . bool ;
default = false ;
description = ''
allow the program to start/stop s6 services .
'' ;
} ;
2024-02-13 10:24:35 +00:00
sandbox . whitelistWayland = mkOption {
type = types . bool ;
2024-02-14 01:49:49 +00:00
default = false ;
2024-02-13 10:24:35 +00:00
description = ''
allow sandbox to communicate with the wayland server .
note that this does NOT permit access to compositor admin tooling like ` swaymsg ` .
'' ;
} ;
2024-02-15 00:09:16 +00:00
sandbox . whitelistX = mkOption {
type = types . bool ;
default = false ;
description = ''
allow the sandbox to communicate with the X server .
typically , this is actually the Xwayland server and you should also enable ` whitelistWayland ` .
'' ;
} ;
2024-02-12 15:22:49 +00:00
2024-01-23 14:30:42 +00:00
sandbox . extraPaths = mkOption {
type = types . listOf types . str ;
default = [ ] ;
2024-01-22 11:11:08 +00:00
description = ''
2024-01-23 14:30:42 +00:00
additional absolute paths to bind into the sandbox .
'' ;
} ;
2024-01-27 09:11:32 +00:00
sandbox . extraHomePaths = mkOption {
type = types . listOf types . str ;
default = [ ] ;
description = ''
additional home-relative paths to bind into the sandbox .
'' ;
} ;
2024-02-12 12:17:37 +00:00
sandbox . extraRuntimePaths = mkOption {
type = types . listOf types . str ;
2024-02-13 10:28:30 +00:00
default = [ ] ;
2024-02-12 12:17:37 +00:00
description = ''
additional $ XDG_RUNTIME_DIR-relative paths to bind into the sandbox .
e . g . ` [ " b u s " " w a y l a n d - 1 " ] ` to bind the dbus and wayland sockets .
or ` [ " / " ] ` to bind all of XDG_RUNTIME_DIR .
'' ;
} ;
2024-01-23 14:30:42 +00:00
sandbox . extraConfig = mkOption {
type = types . listOf types . str ;
default = [ ] ;
description = ''
extra arguments to pass to the sandbox wrapper .
example : [
" - - s a n e - s a n d b o x - f i r e j a i l - a r g "
" - - w h i t e l i s t = ' ' ${ HOME } / . s s h "
" - - s a n e - s a n d b o x - f i r e j a i l - a r g "
" - - k e e p - d e v - s h m "
]
2024-01-22 11:11:08 +00:00
'' ;
} ;
2024-02-11 23:32:24 +00:00
sandbox . usePortal = mkOption {
type = types . bool ;
default = true ;
description = ''
instruct the sandboxed program to open external applications
via calls to xdg-desktop-portal .
'' ;
} ;
2023-05-08 09:49:58 +00:00
configOption = mkOption {
type = types . raw ;
default = mkOption {
type = types . submodule { } ;
default = { } ;
} ;
description = ''
declare any other options the program may be configured with .
you probably want this to be a submodule .
the option * definitions * can be set with ` sane . programs . " f o o " . config = . . . ` .
'' ;
} ;
config = config . configOption ;
2023-02-02 12:31:13 +00:00
} ;
2023-11-18 22:06:08 +00:00
config = let
enabledForUser = builtins . any ( en : en ) ( lib . attrValues config . enableFor . user ) ;
2024-04-09 20:20:36 +00:00
passesSlowTest = config . buildCost <= saneCfg . maxBuildCost ;
2023-11-18 22:06:08 +00:00
in {
enabled = ( config . enableFor . system || enabledForUser ) && passesSlowTest ;
2024-01-20 11:11:12 +00:00
package = if config . packageUnwrapped == null then
null
else
2024-01-22 11:11:08 +00:00
wrapPkg name config config . packageUnwrapped
2024-01-20 11:11:12 +00:00
;
2024-01-23 14:30:42 +00:00
suggestedPrograms = lib . optionals ( config . sandbox . method == " b w r a p " ) [ " b u b b l e w r a p " ]
++ lib . optionals ( config . sandbox . method == " f i r e j a i l " ) [ " f i r e j a i l " ] ;
2024-01-23 14:45:34 +00:00
# declare a fs dependency for each secret, but don't specify how to populate it yet.
# can't populate it here because it varies per-user.
# this gets the symlink into the sandbox, but not the actual secret.
fs = lib . mapAttrs ( _homePath : _secretSrc : { } ) config . secrets ;
2024-02-02 17:18:51 +00:00
2024-03-24 11:58:17 +00:00
sandbox . extraPaths =
lib . optionals config . sandbox . whitelistDri [
2024-03-23 15:33:23 +00:00
# /dev/dri/renderD128: requested by wayland-egl (e.g. KOreader, animatch, geary)
# - but everything seems to gracefully fallback to *something* (MESA software rendering?)
# - CPU usage difference between playing videos in Gtk apps (e.g. fractal) with v.s. without DRI is 10% v.s. 90%.
# - GPU attack surface is *large*: <https://security.stackexchange.com/questions/182501/modern-linux-gpu-driver-security>
" / d e v / d r i " " / s y s / d e v / c h a r " " / s y s / d e v i c e s " # (lappy: "/sys/devices/pci0000:00", moby needs something different)
2024-03-24 11:58:17 +00:00
]
++ lib . optionals config . sandbox . whitelistX [ " / t m p / . X 1 1 - u n i x " ]
;
sandbox . extraRuntimePaths =
lib . optionals config . sandbox . whitelistAudio [ " p i p e w i r e " " p u l s e " ] # this includes pipewire/pipewire-0-manager: is that ok?
++ lib . optionals ( builtins . elem " u s e r " config . sandbox . whitelistDbus ) [ " b u s " ]
++ lib . optionals config . sandbox . whitelistWayland [ " w a y l a n d " ] # app can still communicate with wayland server w/o this, if it has net access
++ lib . optionals config . sandbox . whitelistS6 [ " s 6 " ] # TODO: this allows re-writing the services themselves: don't allow that!
;
2024-02-11 23:32:24 +00:00
sandbox . extraConfig = lib . mkIf config . sandbox . usePortal [
" - - s a n e - s a n d b o x - p o r t a l "
] ;
2023-04-23 23:21:08 +00:00
} ;
2023-02-02 12:31:13 +00:00
} ) ;
2023-07-03 07:55:05 +00:00
toPkgSpec = with lib ; types . coercedTo types . package ( p : { package = p ; } ) pkgSpec ;
2023-02-02 12:31:13 +00:00
2023-07-03 07:55:05 +00:00
configs = lib . mapAttrsToList ( name : p : {
2024-01-21 23:59:15 +00:00
assertions = [
2024-01-27 17:11:07 +00:00
{
2024-02-05 05:28:02 +00:00
assertion = ! ( p . sandbox . enable && p . sandbox . method == null ) || ! p . enabled || p . package == null || config . sane . strictSandboxing != " a s s e r t " ;
2024-01-27 17:11:07 +00:00
message = " p r o g r a m ${ name } s p e c i f i e d n o ` s a n d b o x . m e t h o d ` ; p l e a s e c o n f i g u r e a m e t h o d , o r s e t s a n d b o x . e n a b l e = f a l s e . " ;
}
2024-01-21 23:59:15 +00:00
{
2024-02-08 21:51:32 +00:00
assertion = p . sandbox . net == " a l l " || p . sandbox . method != null || ! p . enabled || p . package == null || config . sane . strictSandboxing != " a s s e r t " ;
message = '' p r o g r a m " ${ name } " r e q u e s t s n e t " ${ builtins . toString p . sandbox . net } " , w h i c h r e q u i r e s s a n d b o x i n g , b u t s a n d b o x i n g w a s n ' t c o n f i g u r e d '' ;
2024-01-21 23:59:15 +00:00
}
] ++ builtins . map ( sug : {
2023-02-04 00:43:00 +00:00
assertion = cfg ? " ${ sug } " ;
message = '' p r o g r a m " ${ sug } " r e f e r e n c e d b y " ${ name } " , b u t n o t d e f i n e d '' ;
} ) p . suggestedPrograms ;
2024-02-05 05:28:02 +00:00
warnings = lib . mkIf ( config . sane . strictSandboxing == " w a r n " && p . sandbox . enable && p . sandbox . method == null && p . enabled && p . package != null ) [
" p r o g r a m ${ name } s p e c i f i e d n o ` s a n d b o x . m e t h o d ` ; p l e a s e c o n f i g u r e a m e t h o d , o r s e t s a n d b o x . e n a b l e = f a l s e . "
] ;
2024-02-06 20:10:29 +00:00
system . checks = lib . optionals ( p . enabled && p . sandbox . enable && p . sandbox . method != null && p . package != null ) [
2024-01-21 23:59:15 +00:00
p . package . passthru . checkSandboxed
] ;
2024-01-22 11:11:08 +00:00
2023-06-27 10:24:48 +00:00
# conditionally add to system PATH and env
2023-11-18 22:06:08 +00:00
environment = lib . optionalAttrs ( p . enabled && p . enableFor . system ) {
2024-02-12 10:52:44 +00:00
systemPackages = lib . optionals ( p . package != null ) (
2024-02-12 12:05:37 +00:00
[ p . package ] ++ lib . optional ( p . sandbox . enable && p . sandbox . method != null ) ( p . package . passthru . sandboxProfilesFor null )
2024-02-12 10:52:44 +00:00
) ;
2024-01-27 09:02:55 +00:00
# sessionVariables are set by PAM, as opposed to environment.variables which goes in /etc/profile
sessionVariables = p . env ;
2023-06-27 10:24:48 +00:00
} ;
2023-04-24 06:49:56 +00:00
2023-02-02 12:31:13 +00:00
# conditionally add to user(s) PATH
2024-02-12 12:05:37 +00:00
users . users = lib . mapAttrs ( userName : en : {
2024-02-12 10:52:44 +00:00
packages = lib . optionals ( p . package != null && en && p . enabled ) (
2024-02-12 12:05:37 +00:00
[ p . package ] ++ lib . optional ( p . sandbox . enable && p . sandbox . method != null ) ( p . package . passthru . sandboxProfilesFor userName )
2024-02-12 10:52:44 +00:00
) ;
2023-02-02 12:31:13 +00:00
} ) p . enableFor . user ;
2023-04-24 06:49:56 +00:00
# conditionally persist relevant user dirs and create files
2023-11-18 22:06:08 +00:00
sane . users = lib . mapAttrs ( user : en : lib . optionalAttrs ( en && p . enabled ) {
2024-03-16 07:35:54 +00:00
inherit ( p ) persist services ;
2023-06-30 08:50:58 +00:00
environment = p . env ;
2023-07-03 07:55:05 +00:00
fs = lib . mkMerge [
2023-07-08 02:06:44 +00:00
p . fs
2023-05-08 21:41:02 +00:00
# link every secret into the fs:
2024-01-27 11:26:10 +00:00
2023-07-03 07:55:05 +00:00
( lib . mapAttrs
2023-04-26 00:17:04 +00:00
# TODO: user the user's *actual* home directory, don't guess.
( homePath : _src : sane-lib . fs . wantedSymlinkTo " / r u n / s e c r e t s / h o m e / ${ user } / ${ homePath } " )
p . secrets
)
2024-01-27 11:26:10 +00:00
# alternative double indirection which may be slightly friendlier to sandboxing:
# ~/.config/FOO.secret => ~/.config/secrets/.config/FOO.secret => /run/secrets/home/${user}/.config/FOO.secret
# whereas /run/secrets/* is unreadable *except* for the leafs, ~/.config/secrets is readable and traversable by $USER.
# (lib.mapAttrs
# # TODO: user the user's *actual* home directory, don't guess.
# (homePath: _src: sane-lib.fs.wantedSymlinkTo "/home/${user}/.config/secrets/${homePath}")
# p.secrets
# )
# (lib.mapAttrs'
# (homePath: _src: {
# name = ".config/secrets/${homePath}";
# value = sane-lib.fs.wantedSymlinkTo "/run/secrets/home/${user}/${homePath}";
# })
# p.secrets
# )
2023-04-26 00:17:04 +00:00
] ;
2023-02-02 12:31:13 +00:00
} ) p . enableFor . user ;
2023-04-26 00:17:04 +00:00
# make secrets available for each user
2023-07-03 07:55:05 +00:00
sops . secrets = lib . concatMapAttrs
2023-11-18 22:06:08 +00:00
( user : en : lib . optionalAttrs ( en && p . enabled ) (
2023-07-03 07:55:05 +00:00
lib . mapAttrs'
2023-04-26 00:17:04 +00:00
( homePath : src : {
2024-01-27 11:26:10 +00:00
# TODO: use the user's *actual* home directory, don't guess.
2023-04-26 03:46:18 +00:00
# XXX: name CAN'T START WITH '/', else sops creates the directories funny.
# TODO: report this upstream.
name = " h o m e / ${ user } / ${ homePath } " ;
2023-04-26 00:17:04 +00:00
value = {
owner = user ;
sopsFile = src ;
format = " b i n a r y " ;
} ;
} )
p . secrets
) )
p . enableFor . user ;
2023-02-02 12:31:13 +00:00
} ) cfg ;
in
{
2023-07-03 07:55:05 +00:00
options = with lib ; {
2023-11-18 22:06:08 +00:00
# TODO: consolidate these options under one umbrella attrset
2023-02-02 12:31:13 +00:00
sane . programs = mkOption {
type = types . attrsOf toPkgSpec ;
default = { } ;
} ;
2024-04-09 20:20:36 +00:00
sane . maxBuildCost = mkOption {
type = types . enum [ 0 1 2 ] ;
default = 2 ;
2023-11-18 22:06:08 +00:00
description = ''
2024-04-09 20:20:36 +00:00
max build cost of programs to ship .
set to 0 to get the fastest , but most restrictive build .
2023-11-18 22:06:08 +00:00
'' ;
} ;
2024-01-27 17:11:07 +00:00
sane . strictSandboxing = mkOption {
2024-02-05 05:28:02 +00:00
type = types . enum [ false " w a r n " " a s s e r t " ] ;
default = " w a r n " ;
2024-01-27 17:11:07 +00:00
description = ''
2024-01-28 05:58:08 +00:00
whether to require that every ` sane . program ` explicitly specify its sandbox settings .
2024-01-27 17:11:07 +00:00
'' ;
} ;
2023-02-02 12:31:13 +00:00
} ;
config =
let
take = f : {
2023-02-04 00:43:00 +00:00
assertions = f . assertions ;
2023-02-02 12:31:13 +00:00
environment . systemPackages = f . environment . systemPackages ;
2024-01-27 09:02:55 +00:00
environment . sessionVariables = f . environment . sessionVariables ;
2023-02-02 12:31:13 +00:00
users . users = f . users . users ;
sane . users = f . sane . users ;
2023-04-26 00:17:04 +00:00
sops . secrets = f . sops . secrets ;
2024-01-21 23:59:15 +00:00
system . checks = f . system . checks ;
2024-02-05 05:28:02 +00:00
warnings = f . warnings ;
2023-02-02 12:31:13 +00:00
} ;
2023-07-03 07:55:05 +00:00
in lib . mkMerge [
2023-02-05 19:34:32 +00:00
( take ( sane-lib . mkTypedMerge take configs ) )
2024-01-23 02:29:33 +00:00
{
2024-01-23 08:01:23 +00:00
environment . pathsToLink = [ " / s h a r e / s a n e - s a n d b o x e d " ] ;
2024-04-15 18:57:22 +00:00
sane . programs . sane-sandboxed . enableFor . system = true ;
2023-02-05 19:34:32 +00:00
# expose the pkgs -- as available to the system -- as a build target.
system . build . pkgs = pkgs ;
}
] ;
2023-02-02 12:31:13 +00:00
}