Add a shell script that checks if the paths of all wrapped programs actually exist to catch mistakes. This only checks for Nix store paths, which are always expected to exist at build time.
290 lines
9.2 KiB
Nix
290 lines
9.2 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
let
|
|
|
|
inherit (config.security) wrapperDir wrappers;
|
|
|
|
parentWrapperDir = dirOf wrapperDir;
|
|
|
|
securityWrapper = pkgs.callPackage ./wrapper.nix {
|
|
inherit parentWrapperDir;
|
|
};
|
|
|
|
fileModeType =
|
|
let
|
|
# taken from the chmod(1) man page
|
|
symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+";
|
|
numeric = "[-+=]?[0-7]{0,4}";
|
|
mode = "((${symbolic})(,${symbolic})*)|(${numeric})";
|
|
in
|
|
lib.types.strMatching mode
|
|
// { description = "file mode string"; };
|
|
|
|
wrapperType = lib.types.submodule ({ name, config, ... }: {
|
|
options.source = lib.mkOption
|
|
{ type = lib.types.path;
|
|
description = "The absolute path to the program to be wrapped.";
|
|
};
|
|
options.program = lib.mkOption
|
|
{ type = with lib.types; nullOr str;
|
|
default = name;
|
|
description = ''
|
|
The name of the wrapper program. Defaults to the attribute name.
|
|
'';
|
|
};
|
|
options.owner = lib.mkOption
|
|
{ type = lib.types.str;
|
|
default = "root";
|
|
description = "The owner of the wrapper program.";
|
|
};
|
|
options.group = lib.mkOption
|
|
{ type = lib.types.str;
|
|
default = "root";
|
|
description = "The group of the wrapper program.";
|
|
};
|
|
options.permissions = lib.mkOption
|
|
{ type = fileModeType;
|
|
default = "u+rx,g+x,o+x";
|
|
example = "a+rx";
|
|
description = ''
|
|
The permissions of the wrapper program. The format is that of a
|
|
symbolic or numeric file mode understood by <command>chmod</command>.
|
|
'';
|
|
};
|
|
options.capabilities = lib.mkOption
|
|
{ type = lib.types.commas;
|
|
default = "";
|
|
description = ''
|
|
A comma-separated list of capabilities to be given to the wrapper
|
|
program. For capabilities supported by the system check the
|
|
<citerefentry>
|
|
<refentrytitle>capabilities</refentrytitle>
|
|
<manvolnum>7</manvolnum>
|
|
</citerefentry>
|
|
manual page.
|
|
|
|
<note><para>
|
|
<literal>cap_setpcap</literal>, which is required for the wrapper
|
|
program to be able to raise caps into the Ambient set is NOT raised
|
|
to the Ambient set so that the real program cannot modify its own
|
|
capabilities!! This may be too restrictive for cases in which the
|
|
real program needs cap_setpcap but it at least leans on the side
|
|
security paranoid vs. too relaxed.
|
|
</para></note>
|
|
'';
|
|
};
|
|
options.setuid = lib.mkOption
|
|
{ type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to add the setuid bit the wrapper program.";
|
|
};
|
|
options.setgid = lib.mkOption
|
|
{ type = lib.types.bool;
|
|
default = false;
|
|
description = "Whether to add the setgid bit the wrapper program.";
|
|
};
|
|
});
|
|
|
|
###### Activation script for the setcap wrappers
|
|
mkSetcapProgram =
|
|
{ program
|
|
, capabilities
|
|
, source
|
|
, owner
|
|
, group
|
|
, permissions
|
|
, ...
|
|
}:
|
|
assert (lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.3");
|
|
''
|
|
cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
|
|
echo -n "${source}" > $wrapperDir/${program}.real
|
|
|
|
# Prevent races
|
|
chmod 0000 $wrapperDir/${program}
|
|
chown ${owner}.${group} $wrapperDir/${program}
|
|
|
|
# Set desired capabilities on the file plus cap_setpcap so
|
|
# the wrapper program can elevate the capabilities set on
|
|
# its file into the Ambient set.
|
|
${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" $wrapperDir/${program}
|
|
|
|
# Set the executable bit
|
|
chmod ${permissions} $wrapperDir/${program}
|
|
'';
|
|
|
|
###### Activation script for the setuid wrappers
|
|
mkSetuidProgram =
|
|
{ program
|
|
, source
|
|
, owner
|
|
, group
|
|
, setuid
|
|
, setgid
|
|
, permissions
|
|
, ...
|
|
}:
|
|
''
|
|
cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
|
|
echo -n "${source}" > $wrapperDir/${program}.real
|
|
|
|
# Prevent races
|
|
chmod 0000 $wrapperDir/${program}
|
|
chown ${owner}.${group} $wrapperDir/${program}
|
|
|
|
chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" $wrapperDir/${program}
|
|
'';
|
|
|
|
mkWrappedPrograms =
|
|
builtins.map
|
|
(opts:
|
|
if opts.capabilities != ""
|
|
then mkSetcapProgram opts
|
|
else mkSetuidProgram opts
|
|
) (lib.attrValues wrappers);
|
|
in
|
|
{
|
|
imports = [
|
|
(lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
|
|
(lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
|
|
];
|
|
|
|
###### interface
|
|
|
|
options = {
|
|
security.wrappers = lib.mkOption {
|
|
type = lib.types.attrsOf wrapperType;
|
|
default = {};
|
|
example = lib.literalExample
|
|
''
|
|
{ sendmail.source = "/nix/store/.../bin/sendmail";
|
|
ping = {
|
|
source = "${pkgs.iputils.out}/bin/ping";
|
|
owner = "nobody";
|
|
group = "nogroup";
|
|
capabilities = "cap_net_raw+ep";
|
|
};
|
|
}
|
|
'';
|
|
description = ''
|
|
This option effectively allows adding setuid/setgid bits, capabilities,
|
|
changing file ownership and permissions of a program without directly
|
|
modifying it. This works by creating a wrapper program under the
|
|
<option>security.wrapperDir</option> directory, which is then added to
|
|
the shell <literal>PATH</literal>.
|
|
'';
|
|
};
|
|
|
|
security.wrapperDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/run/wrappers/bin";
|
|
internal = true;
|
|
description = ''
|
|
This option defines the path to the wrapper programs. It
|
|
should not be overriden.
|
|
'';
|
|
};
|
|
};
|
|
|
|
###### implementation
|
|
config = {
|
|
|
|
assertions = lib.mapAttrsToList
|
|
(name: opts:
|
|
{ assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
|
|
message = ''
|
|
The security.wrappers.${name} wrapper is not valid:
|
|
setuid/setgid and capabilities are mutually exclusive.
|
|
'';
|
|
}
|
|
) wrappers;
|
|
|
|
security.wrappers = {
|
|
# These are mount related wrappers that require the +s permission.
|
|
fusermount.source = "${pkgs.fuse}/bin/fusermount";
|
|
fusermount3.source = "${pkgs.fuse3}/bin/fusermount3";
|
|
mount.source = "${lib.getBin pkgs.util-linux}/bin/mount";
|
|
umount.source = "${lib.getBin pkgs.util-linux}/bin/umount";
|
|
};
|
|
|
|
boot.specialFileSystems.${parentWrapperDir} = {
|
|
fsType = "tmpfs";
|
|
options = [ "nodev" "mode=755" ];
|
|
};
|
|
|
|
# Make sure our wrapperDir exports to the PATH env variable when
|
|
# initializing the shell
|
|
environment.extraInit = ''
|
|
# Wrappers override other bin directories.
|
|
export PATH="${wrapperDir}:$PATH"
|
|
'';
|
|
|
|
security.apparmor.includes."nixos/security.wrappers" = ''
|
|
include "${pkgs.apparmorRulesFromClosure { name="security.wrappers"; } [
|
|
securityWrapper
|
|
pkgs.stdenv.cc.cc
|
|
pkgs.stdenv.cc.libc
|
|
]}"
|
|
'';
|
|
|
|
###### wrappers activation script
|
|
system.activationScripts.wrappers =
|
|
lib.stringAfter [ "specialfs" "users" ]
|
|
''
|
|
# Look in the system path and in the default profile for
|
|
# programs to be wrapped.
|
|
WRAPPER_PATH=${config.system.path}/bin:${config.system.path}/sbin
|
|
|
|
chmod 755 "${parentWrapperDir}"
|
|
|
|
# We want to place the tmpdirs for the wrappers to the parent dir.
|
|
wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
|
|
chmod a+rx $wrapperDir
|
|
|
|
${lib.concatStringsSep "\n" mkWrappedPrograms}
|
|
|
|
if [ -L ${wrapperDir} ]; then
|
|
# Atomically replace the symlink
|
|
# See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
|
|
old=$(readlink -f ${wrapperDir})
|
|
if [ -e ${wrapperDir}-tmp ]; then
|
|
rm --force --recursive ${wrapperDir}-tmp
|
|
fi
|
|
ln --symbolic --force --no-dereference $wrapperDir ${wrapperDir}-tmp
|
|
mv --no-target-directory ${wrapperDir}-tmp ${wrapperDir}
|
|
rm --force --recursive $old
|
|
else
|
|
# For initial setup
|
|
ln --symbolic $wrapperDir ${wrapperDir}
|
|
fi
|
|
'';
|
|
|
|
###### wrappers consistency checks
|
|
system.extraDependencies = lib.singleton (pkgs.runCommandLocal
|
|
"ensure-all-wrappers-paths-exist" { }
|
|
''
|
|
# make sure we produce output
|
|
mkdir -p $out
|
|
|
|
echo -n "Checking that Nix store paths of all wrapped programs exist... "
|
|
|
|
declare -A wrappers
|
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
|
|
"wrappers['${n}']='${v.source}'") wrappers)}
|
|
|
|
for name in "''${!wrappers[@]}"; do
|
|
path="''${wrappers[$name]}"
|
|
if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
|
|
test -t 1 && echo -ne '\033[1;31m'
|
|
echo "FAIL"
|
|
echo "The path $path does not exist!"
|
|
echo 'Please, check the value of `security.wrappers."'$name'".source`.'
|
|
test -t 1 && echo -ne '\033[0m'
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo "OK"
|
|
'');
|
|
};
|
|
}
|