Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

security.wrappers: init #890

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
./security/pki
./security/sandbox
./security/sudo.nix
./security/wrappers
./system
./system/base.nix
./system/checks.nix
Expand Down
193 changes: 193 additions & 0 deletions modules/security/wrappers/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
{ config, lib, pkgs, ... }:

let
cfg = config.security;

parentWrapperDir = dirOf cfg.wrapperDir;

wrapperType = lib.types.submodule ({ name, config, ... }: {
options = with lib; {
source = mkOption {
type = types.path;
description = mdDoc "The absolute path to the program to be wrapped.";
};
program = mkOption {
type = with types; nullOr str;
default = name;
description = mdDoc "The name of the wrapper program. Defaults to the attribute name.";
};
owner = mkOption {
type = types.str;
description = mdDoc "The owner of the wrapper program.";
};
group = mkOption {
type = types.str;
description = mdDoc "The group of the wrapper program.";
};
permissions = mkOption {
type = types.str;
default = "u+rx,g+x,o+x";
description = mdDoc "The permissions to set on the wrapper.";
};
setuid = mkOption {
type = types.bool;
default = false;
description = mdDoc "Whether to add the setuid bit to the wrapper program.";
};
setgid = mkOption {
type = types.bool;
default = false;
description = mdDoc "Whether to add the setgid bit to the wrapper program.";
};
# codesign = mkOption {
# type = types.bool;
# default = false;
# description = mdDoc "Whether to codesign the wrapper program.";
# };
};
});

mkWrappedPrograms =
builtins.map
(opts: mkWrapper opts)
(builtins.attrValues cfg.wrappers);

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
# '';
# });
};

mkWrapper =
{ program
, source
, owner
, group
, permissions
, setuid
, setgid
, codesign ? false
, ...
}:
let
codesigned = if codesign
then ''
# codesign ${source} to "$wrapperDir/${program}" INSTEAD OF the next line
cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
''
else ''
cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
'';
in
''
${codesigned}

# 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}"
'';
in
{
# probably not necessary since these options never existed in nix-darwin?
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.literalExpression
''
{
# a setuid root program
doas =
{ setuid = true;
owner = "root";
group = "wheel";
source = "''${pkgs.doas}/bin/doas";
};


# a setgid program
locate =
{ setgid = true;
owner = "root";
group = "mlocate";
source = "''${pkgs.locate}/bin/locate";
};
}
'';
description = lib.mdDoc ''
This option effectively allows adding setuid/setgid bits and/or changing
file ownership and permissions without directly modifying it. This works
by creating a wrapper program under the {option}`security.wrapperDir`
directory, which is then added to the shell `PATH`.
'';
};
wrapperDir = lib.mkOption {
type = lib.types.path;
default = "/run/wrappers/bin";
internal = true;
description = lib.mdDoc ''
This option defines the path to the wrapper programs. It
should not be overridden.
'';
};
# codesignIdentity = lib.mkOption {
# type = lib.types.str;
# default = "-";
# description = lib.mdDoc "Identity to use for codesigning.";
# };
};

###### implementation
config = {
environment.extraInit = ''
# Wrappers override other bin directories.
export PATH="${cfg.wrapperDir}:$PATH"
'';

system.activationScripts.wrappers.text = ''
echo "setting up wrappers..." >&2
if ! test -e /run/wrappers; then mkdir /run/wrappers; fi

# We want to place the tmpdirs for the wrappers to the parent dir.
wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
chmod a+rx $wrapperDir

${builtins.concatStringsSep "\n" mkWrappedPrograms}

if test -L ${cfg.wrapperDir}; then
# Atomically replace the symlink
# See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
old=$(readlink -f ${cfg.wrapperDir})
ln --symbolic --force --no-dereference $wrapperDir ${cfg.wrapperDir}-tmp
mv --no-target-directory ${cfg.wrapperDir}-tmp ${cfg.wrapperDir}
rm --force --recursive $old
else
# For initial setup
ln --symbolic $wrapperDir ${cfg.wrapperDir}
fi
'';
};
}
128 changes: 128 additions & 0 deletions modules/security/wrappers/wrapper.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdnoreturn.h>
#include <sys/errno.h>
// #include <sys/types.h>
// #include <sys/stat.h>
// #include <sys/xattr.h>
// #include <fcntl.h>
// #include <dirent.h>
// #include <errno.h>
// #include <sys/prctl.h>
// #include <limits.h>
// #include <stdint.h>
// #include <syscall.h>
// #include <byteswap.h>

// imported from glibc
// #include "unsecvars.h"

#ifndef SOURCE_PROG
#error SOURCE_PROG should be defined via preprocessor commandline
#endif

// aborts when false, printing the failed expression
#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))

extern char **environ;

// Wrapper debug variable name
static char *wrapper_debug = "WRAPPER_DEBUG";

static noreturn void assert_failure(const char *assertion) {
fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion);
fflush(stderr);
abort();
}

// These are environment variable aliases for glibc tunables.
// This list shouldn't grow further, since this is a legacy mechanism.
// Any future tunables are expected to only be accessible through GLIBC_TUNABLES.
//
// They are not included in the glibc-provided UNSECURE_ENVVARS list,
// since any SUID executable ignores them. This wrapper also serves
// executables that are merely granted ambient capabilities, rather than
// being SUID, and hence don't run in secure mode. We'd like them to
// defend those in depth as well, so we clear these explicitly.
//
// Except for MALLOC_CHECK_ (which is marked SXID_ERASE), these are all
// marked SXID_IGNORE (ignored in secure mode), so even the glibc version
// of this wrapper would leave them intact.
#define UNSECURE_ENVVARS_TUNABLES \
"MALLOC_CHECK_\0" \
"MALLOC_TOP_PAD_\0" \
"MALLOC_PERTURB_\0" \
"MALLOC_MMAP_THRESHOLD_\0" \
"MALLOC_TRIM_THRESHOLD_\0" \
"MALLOC_MMAP_MAX_\0" \
"MALLOC_ARENA_MAX\0" \
"MALLOC_ARENA_TEST\0"

#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
// GLIBC_TUNABLES_ENVVAR \

int main(int argc, char **argv) {
ASSERT(argc >= 1);

// argv[0] goes into a lot of places, to a far greater degree than other elements
// of argv. glibc has had buffer overflows relating to argv[0], eg CVE-2023-6246.
// Since we expect the wrappers to be invoked from either $PATH or /run/wrappers/bin,
// there should be no reason to pass any particularly large values here, so we can
// be strict for strictness' sake.
ASSERT(strlen(argv[0]) < 512);

int debug = getenv(wrapper_debug) != NULL;

// Drop insecure environment variables explicitly
//
// glibc does this automatically in SUID binaries, but we'd like to cover this:
//
// a) before it gets to glibc
// b) in binaries that are only granted ambient capabilities by the wrapper,
// but don't run with an altered effective UID/GID, nor directly gain
// capabilities themselves, and thus don't run in secure mode.
//
// We're using musl, which doesn't drop environment variables in secure mode,
// and we'd also like glibc-specific variables to be covered.
//
// If we don't explicitly unset them, it's quite easy to just set LD_PRELOAD,
// have it passed through to the wrapped program, and gain privileges.
for (char *unsec = UNSECURE_ENVVARS_TUNABLES UNSECURE_ENVVARS; *unsec; unsec = strchr(unsec, 0) + 1) {
if (debug) {
fprintf(stderr, "unsetting %s\n", unsec);
}
unsetenv(unsec);
}

execve(SOURCE_PROG, argv, environ);

fprintf(stderr, "%s: cannot run `%s': %s\n",
argv[0], SOURCE_PROG, strerror(errno));

return 1;
}
19 changes: 19 additions & 0 deletions modules/security/wrappers/wrapper.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{ stdenv, sourceProg, debug ? false }:
# For testing:
# $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { sourceProg = "${pkgs.hello}/bin/hello"; debug = true; }'
stdenv.mkDerivation {
name = "security-wrapper-${baseNameOf sourceProg}";
dontUnpack = true;
CFLAGS = [
''-DSOURCE_PROG="${sourceProg}"''
] ++ (if debug then [
"-Werror" "-Og" "-g"
] else [
"-Wall" "-O2"
]);
dontStrip = debug;
installPhase = ''
mkdir -p $out/bin
$CC $CFLAGS ${./wrapper.c} -o $out/bin/security-wrapper
'';
}
1 change: 1 addition & 0 deletions modules/system/activation-scripts.nix
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ in
${cfg.activationScripts.keyboard.text}
${cfg.activationScripts.fonts.text}
${cfg.activationScripts.nvram.text}
${cfg.activationScripts.wrappers.text}

${cfg.activationScripts.postActivation.text}

Expand Down