nixpkgs/nixos/modules/security/wrappers/default.nix
Silvan Mosberger 374e6bcc40 treewide: Format all Nix files
Format all Nix files using the officially approved formatter,
making the CI check introduced in the previous commit succeed:

  nix-build ci -A fmt.check

This is the next step of the of the [implementation](https://github.com/NixOS/nixfmt/issues/153)
of the accepted [RFC 166](https://github.com/NixOS/rfcs/pull/166).

This commit will lead to merge conflicts for a number of PRs,
up to an estimated ~1100 (~33%) among the PRs with activity in the past 2
months, but that should be lower than what it would be without the previous
[partial treewide format](https://github.com/NixOS/nixpkgs/pull/322537).

Merge conflicts caused by this commit can now automatically be resolved while rebasing using the
[auto-rebase script](8616af08d9/maintainers/scripts/auto-rebase).

If you run into any problems regarding any of this, please reach out to the
[formatting team](https://nixos.org/community/teams/formatting/) by
pinging @NixOS/nix-formatting.
2025-04-01 20:10:43 +02:00

380 lines
12 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (config.security) wrapperDir;
wrappers = lib.filterAttrs (name: value: value.enable) config.security.wrappers;
parentWrapperDir = dirOf wrapperDir;
# This is security-sensitive code, and glibc vulns happen from time to time.
# musl is security-focused and generally more minimal, so it's a better choice here.
# The dynamic linker is still a fairly complex piece of code, and the wrappers are
# quite small, so linking it statically is more appropriate.
securityWrapper =
sourceProg:
pkgs.pkgsStatic.callPackage ./wrapper.nix {
inherit sourceProg;
# glibc definitions of insecure environment variables
#
# We extract the single header file we need into its own derivation,
# so that we don't have to pull full glibc sources to build wrappers.
#
# They're taken from pkgs.glibc so that we don't have to keep as close
# an eye on glibc changes. Not every relevant variable is in this header,
# so we maintain a slightly stricter list in wrapper.c itself as well.
unsecvars = lib.overrideDerivation (pkgs.srcOnly pkgs.glibc) (
{ name, ... }:
{
name = "${name}-unsecvars";
installPhase = ''
mkdir $out
cp sysdeps/generic/unsecvars.h $out
'';
}
);
};
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.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable the wrapper.";
};
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;
description = "The owner of the wrapper program.";
};
options.group = lib.mkOption {
type = lib.types.str;
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`.
'';
};
options.capabilities = lib.mkOption {
type = lib.types.commas;
default = "";
description = ''
A comma-separated list of capability clauses to be given to the
wrapper program. The format for capability clauses is described in the
TEXTUAL REPRESENTATION section of the {manpage}`cap_from_text(3)`
manual page. For a list of capabilities supported by the system, check
the {manpage}`capabilities(7)` manual page.
::: {.note}
`cap_setpcap`, 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.
:::
'';
};
options.setuid = lib.mkOption {
type = lib.types.bool;
default = false;
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,
...
}:
''
cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
# 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 source}/bin/security-wrapper "$wrapperDir/${program}"
# 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.enableWrappers = lib.mkEnableOption "SUID/SGID wrappers" // {
default = true;
};
security.wrappers = lib.mkOption {
type = lib.types.attrsOf wrapperType;
default = { };
example = lib.literalExpression ''
{
# a setuid root program
doas =
{ setuid = true;
owner = "root";
group = "root";
source = "''${pkgs.doas}/bin/doas";
};
# a setgid program
locate =
{ setgid = true;
owner = "root";
group = "mlocate";
source = "''${pkgs.locate}/bin/locate";
};
# a program with the CAP_NET_RAW capability
ping =
{ owner = "root";
group = "root";
capabilities = "cap_net_raw+ep";
source = "''${pkgs.iputils.out}/bin/ping";
};
}
'';
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 in a directory
(not configurable), which is then added to the shell `PATH`.
'';
};
security.wrapperDirSize = lib.mkOption {
default = "50%";
example = "10G";
type = lib.types.str;
description = ''
Size limit for the /run/wrappers tmpfs. Look at {manpage}`mount(8)`, tmpfs size option,
for the accepted syntax. WARNING: don't set to less than 64MB.
'';
};
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 overridden.
'';
};
};
###### implementation
config = lib.mkIf config.security.enableWrappers {
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 =
let
mkSetuidRoot = source: {
setuid = true;
owner = "root";
group = "root";
inherit source;
};
in
{
# These are mount related wrappers that require the +s permission.
fusermount = mkSetuidRoot "${lib.getBin pkgs.fuse}/bin/fusermount";
fusermount3 = mkSetuidRoot "${lib.getBin pkgs.fuse3}/bin/fusermount3";
mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
};
# 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 = lib.mapAttrs' (
wrapName: wrap:
lib.nameValuePair "nixos/security.wrappers/${wrapName}" ''
include "${
pkgs.apparmorRulesFromClosure { name = "security.wrappers.${wrapName}"; } [
(securityWrapper wrap.source)
]
}"
mrpx ${wrap.source},
''
) wrappers;
systemd.mounts = [
{
where = parentWrapperDir;
what = "tmpfs";
type = "tmpfs";
options = lib.concatStringsSep "," ([
"nodev"
"mode=755"
"size=${config.security.wrapperDirSize}"
]);
}
];
systemd.services.suid-sgid-wrappers = {
description = "Create SUID/SGID Wrappers";
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
after = [ "systemd-sysusers.service" ];
unitConfig.DefaultDependencies = false;
unitConfig.RequiresMountsFor = [
"/nix/store"
"/run/wrappers"
];
serviceConfig.Type = "oneshot";
script = ''
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.checks = lib.singleton (
pkgs.runCommand "ensure-all-wrappers-paths-exist"
{
preferLocalBuild = true;
}
''
# 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"
''
);
};
}