uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead

https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948
this can do it nicely.

Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
Anton Arapov 2021-04-03 12:58:10 +02:00 committed by Alan Daniels
commit 56de2bcd43
30691 changed files with 3076956 additions and 0 deletions

View file

@ -0,0 +1,921 @@
{ config, lib, pkgs, options, ... }:
with lib;
let
cfg = config.security.acme;
opt = options.security.acme;
user = if cfg.useRoot then "root" else "acme";
# Used to calculate timer accuracy for coalescing
numCerts = length (builtins.attrNames cfg.certs);
_24hSecs = 60 * 60 * 24;
# Used to make unique paths for each cert/account config set
mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
accountDirRoot = "/var/lib/acme/.lego/accounts/";
# There are many services required to make cert renewals work.
# They all follow a common structure:
# - They inherit this commonServiceConfig
# - They all run as the acme user
# - They all use BindPath and StateDirectory where possible
# to set up a sort of build environment in /tmp
# The Group can vary depending on what the user has specified in
# security.acme.certs.<cert>.group on some of the services.
commonServiceConfig = {
Type = "oneshot";
User = user;
Group = mkDefault "acme";
UMask = 0022;
StateDirectoryMode = 750;
ProtectSystem = "strict";
ReadWritePaths = [
"/var/lib/acme"
];
PrivateTmp = true;
WorkingDirectory = "/tmp";
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
# 1. allow a reasonable set of syscalls
"@system-service"
# 2. and deny unreasonable ones
"~@privileged @resources"
# 3. then allow the required subset within denied groups
"@chown"
];
};
# In order to avoid race conditions creating the CA for selfsigned certs,
# we have a separate service which will create the necessary files.
selfsignCAService = {
description = "Generate self-signed certificate authority";
path = with pkgs; [ minica ];
unitConfig = {
ConditionPathExists = "!/var/lib/acme/.minica/key.pem";
StartLimitIntervalSec = 0;
};
serviceConfig = commonServiceConfig // {
StateDirectory = "acme/.minica";
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
UMask = 0077;
};
# Working directory will be /tmp
script = ''
minica \
--ca-key ca/key.pem \
--ca-cert ca/cert.pem \
--domains selfsigned.local
'';
};
# Ensures that directories which are shared across all certs
# exist and have the correct user and group, since group
# is configurable on a per-cert basis.
userMigrationService = let
script = with builtins; ''
chown -R ${user} .lego/accounts
'' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
if [ -d "$fixpath" ]; then
chmod -R u=rwX,g=rX,o= "$fixpath"
chown -R ${user}:${data.group} "$fixpath"
fi
done
'') certConfigs));
in {
description = "Fix owner and group of all ACME certificates";
serviceConfig = commonServiceConfig // {
# We don't want this to run every time a renewal happens
RemainAfterExit = true;
# These StateDirectory entries negate the need for tmpfiles
StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
StateDirectoryMode = 755;
WorkingDirectory = "/var/lib/acme";
# Run the start script as root
ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script);
};
};
certToConfig = cert: data: let
acmeServer = data.server;
useDns = data.dnsProvider != null;
destPath = "/var/lib/acme/${cert}";
selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
# Minica and lego have a "feature" which replaces * with _. We need
# to make this substitution to reference the output files from both programs.
# End users never see this since we rename the certs.
keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
# FIXME when mkChangedOptionModule supports submodules, change to that.
# This is a workaround
extraDomains = data.extraDomainNames ++ (
optionals
(data.extraDomains != "_mkMergedOptionModule")
(builtins.attrNames data.extraDomains)
);
# Create hashes for cert data directories based on configuration
# Flags are separated to avoid collisions
hashData = with builtins; ''
${concatStringsSep " " data.extraLegoFlags} -
${concatStringsSep " " data.extraLegoRunFlags} -
${concatStringsSep " " data.extraLegoRenewFlags} -
${toString acmeServer} ${toString data.dnsProvider}
${toString data.ocspMustStaple} ${data.keyType}
'';
certDir = mkHash hashData;
# TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532
domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
accountHash = (mkAccountHash acmeServer data);
accountDir = accountDirRoot + accountHash;
protocolOpts = if useDns then (
[ "--dns" data.dnsProvider ]
++ optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ]
++ optionals (data.dnsResolver != null) [ "--dns.resolvers" data.dnsResolver ]
) else if data.listenHTTP != null then [ "--http" "--http.port" data.listenHTTP ]
else [ "--http" "--http.webroot" data.webroot ];
commonOpts = [
"--accept-tos" # Checking the option is covered by the assertions
"--path" "."
"-d" data.domain
"--email" data.email
"--key-type" data.keyType
] ++ protocolOpts
++ optionals (acmeServer != null) [ "--server" acmeServer ]
++ concatMap (name: [ "-d" name ]) extraDomains
++ data.extraLegoFlags;
# Although --must-staple is common to both modes, it is not declared as a
# mode-agnostic argument in lego and thus must come after the mode.
runOpts = escapeShellArgs (
commonOpts
++ [ "run" ]
++ optionals data.ocspMustStaple [ "--must-staple" ]
++ data.extraLegoRunFlags
);
renewOpts = escapeShellArgs (
commonOpts
++ [ "renew" ]
++ optionals data.ocspMustStaple [ "--must-staple" ]
++ data.extraLegoRenewFlags
);
# We need to collect all the ACME webroots to grant them write
# access in the systemd service.
webroots =
lib.remove null
(lib.unique
(builtins.map
(certAttrs: certAttrs.webroot)
(lib.attrValues config.security.acme.certs)));
in {
inherit accountHash cert selfsignedDeps;
group = data.group;
renewTimer = {
description = "Renew ACME Certificate for ${cert}";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = data.renewInterval;
Unit = "acme-${cert}.service";
Persistent = "yes";
# Allow systemd to pick a convenient time within the day
# to run the check.
# This allows the coalescing of multiple timer jobs.
# We divide by the number of certificates so that if you
# have many certificates, the renewals are distributed over
# the course of the day to avoid rate limits.
AccuracySec = "${toString (_24hSecs / numCerts)}s";
# Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
RandomizedDelaySec = "24h";
};
};
selfsignService = {
description = "Generate self-signed certificate for ${cert}";
after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
path = with pkgs; [ minica ];
unitConfig = {
ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
StartLimitIntervalSec = 0;
};
serviceConfig = commonServiceConfig // {
Group = data.group;
UMask = 0027;
StateDirectory = "acme/${cert}";
BindPaths = [
"/var/lib/acme/.minica:/tmp/ca"
"/var/lib/acme/${cert}:/tmp/${keyName}"
];
};
# Working directory will be /tmp
# minica will output to a folder sharing the name of the first domain
# in the list, which will be ${data.domain}
script = ''
minica \
--ca-key ca/key.pem \
--ca-cert ca/cert.pem \
--domains ${escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
# Create files to match directory layout for real certificates
cd '${keyName}'
cp ../ca/cert.pem chain.pem
cat cert.pem chain.pem > fullchain.pem
cat key.pem fullchain.pem > full.pem
# Group might change between runs, re-apply it
chown '${user}:${data.group}' *
# Default permissions make the files unreadable by group + anon
# Need to be readable by group
chmod 640 *
'';
};
renewService = {
description = "Renew ACME certificate for ${cert}";
after = [ "network.target" "network-online.target" "acme-fixperms.service" "nss-lookup.target" ] ++ selfsignedDeps;
wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps;
# https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ];
path = with pkgs; [ lego coreutils diffutils openssl ];
serviceConfig = commonServiceConfig // {
Group = data.group;
# Keep in mind that these directories will be deleted if the user runs
# systemctl clean --what=state
# acme/.lego/${cert} is listed for this reason.
StateDirectory = [
"acme/${cert}"
"acme/.lego/${cert}"
"acme/.lego/${cert}/${certDir}"
"acme/.lego/accounts/${accountHash}"
];
ReadWritePaths = commonServiceConfig.ReadWritePaths ++ webroots;
# Needs to be space separated, but can't use a multiline string because that'll include newlines
BindPaths = [
"${accountDir}:/tmp/accounts"
"/var/lib/acme/${cert}:/tmp/out"
"/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates"
];
# Only try loading the credentialsFile if the dns challenge is enabled
EnvironmentFile = mkIf useDns data.credentialsFile;
# Run as root (Prefixed with +)
ExecStartPost = "+" + (pkgs.writeShellScript "acme-postrun" ''
cd /var/lib/acme/${escapeShellArg cert}
if [ -e renewed ]; then
rm renewed
${data.postRun}
${optionalString (data.reloadServices != [])
"systemctl --no-block try-reload-or-restart ${escapeShellArgs data.reloadServices}"
}
fi
'');
} // optionalAttrs (data.listenHTTP != null && toInt (elemAt (splitString ":" data.listenHTTP) 1) < 1024) {
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
};
# Working directory will be /tmp
script = ''
${optionalString data.enableDebugLogs "set -x"}
set -euo pipefail
# This reimplements the expiration date check, but without querying
# the acme server first. By doing this offline, we avoid errors
# when the network or DNS are unavailable, which can happen during
# nixos-rebuild switch.
is_expiration_skippable() {
pem=$1
# This function relies on set -e to exit early if any of the
# conditions or programs fail.
[[ -e $pem ]]
expiration_line="$(
set -euxo pipefail
openssl x509 -noout -enddate <$pem \
| grep notAfter \
| sed -e 's/^notAfter=//'
)"
[[ -n "$expiration_line" ]]
expiration_date="$(date -d "$expiration_line" +%s)"
now="$(date +%s)"
expiration_s=$[expiration_date - now]
expiration_days=$[expiration_s / (3600 * 24)] # rounds down
[[ $expiration_days -gt ${toString data.validMinDays} ]]
}
${optionalString (data.webroot != null) ''
# Ensure the webroot exists. Fixing group is required in case configuration was changed between runs.
# Lego will fail if the webroot does not exist at all.
(
mkdir -p '${data.webroot}/.well-known/acme-challenge' \
&& chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
) || (
echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
&& exit 1
)
''}
echo '${domainHash}' > domainhash.txt
# Check if we can renew.
# We can only renew if the list of domains has not changed.
if cmp -s domainhash.txt certificates/domainhash.txt && [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
# Even if a cert is not expired, it may be revoked by the CA.
# Try to renew, and silently fail if the cert is not expired.
# Avoids #85794 and resolves #129838
if ! lego ${renewOpts} --days ${toString data.validMinDays}; then
if is_expiration_skippable out/full.pem; then
echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days"
else
# High number to avoid Systemd reserved codes.
exit 11
fi
fi
# Otherwise do a full run
elif ! lego ${runOpts}; then
# Produce a nice error for those doing their first nixos-rebuild with these certs
echo Failed to fetch certificates. \
This may mean your DNS records are set up incorrectly. \
${optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."}
# Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
# High number to avoid Systemd reserved codes.
exit 10
fi
mv domainhash.txt certificates/
# Group might change between runs, re-apply it
chown '${user}:${data.group}' certificates/*
# Copy all certs to the "real" certs directory
if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then
touch out/renewed
echo Installing new certificate
cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
cp -vp 'certificates/${keyName}.key' out/key.pem
cp -vp 'certificates/${keyName}.issuer.crt' out/chain.pem
ln -sf fullchain.pem out/cert.pem
cat out/key.pem out/fullchain.pem > out/full.pem
fi
# By default group will have no access to the cert files.
# This chmod will fix that.
chmod 640 out/*
'';
};
};
certConfigs = mapAttrs certToConfig cfg.certs;
# These options can be specified within
# security.acme.defaults or security.acme.certs.<name>
inheritableModule = isDefaults: { config, ... }: let
defaultAndText = name: default: {
# When ! isDefaults then this is the option declaration for the
# security.acme.certs.<name> path, which has the extra inheritDefaults
# option, which if disabled means that we can't inherit it
default = if isDefaults || ! config.inheritDefaults then default else cfg.defaults.${name};
# The docs however don't need to depend on inheritDefaults, they should
# stay constant. Though notably it wouldn't matter much, because to get
# the option information, a submodule with name `<name>` is evaluated
# without any definitions.
defaultText = if isDefaults then default else literalExpression "config.security.acme.defaults.${name}";
};
in {
options = {
validMinDays = mkOption {
type = types.int;
inherit (defaultAndText "validMinDays" 30) default defaultText;
description = "Minimum remaining validity before renewal in days.";
};
renewInterval = mkOption {
type = types.str;
inherit (defaultAndText "renewInterval" "daily") default defaultText;
description = ''
Systemd calendar expression when to check for renewal. See
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
enableDebugLogs = mkEnableOption "debug logging for this certificate" // {
inherit (defaultAndText "enableDebugLogs" true) default defaultText;
};
webroot = mkOption {
type = types.nullOr types.str;
inherit (defaultAndText "webroot" null) default defaultText;
example = "/var/lib/acme/acme-challenge";
description = ''
Where the webroot of the HTTP vhost is located.
<filename>.well-known/acme-challenge/</filename> directory
will be created below the webroot if it doesn't exist.
<literal>http://example.org/.well-known/acme-challenge/</literal> must also
be available (notice unencrypted HTTP).
'';
};
server = mkOption {
type = types.nullOr types.str;
inherit (defaultAndText "server" null) default defaultText;
description = ''
ACME Directory Resource URI. Defaults to Let's Encrypt's
production endpoint,
<link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
'';
};
email = mkOption {
type = types.str;
inherit (defaultAndText "email" null) default defaultText;
description = ''
Email address for account creation and correspondence from the CA.
It is recommended to use the same email for all certs to avoid account
creation limits.
'';
};
group = mkOption {
type = types.str;
inherit (defaultAndText "group" "acme") default defaultText;
description = "Group running the ACME client.";
};
reloadServices = mkOption {
type = types.listOf types.str;
inherit (defaultAndText "reloadServices" []) default defaultText;
description = ''
The list of systemd services to call <code>systemctl try-reload-or-restart</code>
on.
'';
};
postRun = mkOption {
type = types.lines;
inherit (defaultAndText "postRun" "") default defaultText;
example = "cp full.pem backup.pem";
description = ''
Commands to run after new certificates go live. Note that
these commands run as the root user.
Executed in the same directory with the new certificate.
'';
};
keyType = mkOption {
type = types.str;
inherit (defaultAndText "keyType" "ec256") default defaultText;
description = ''
Key type to use for private keys.
For an up to date list of supported values check the --key-type option
at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>.
'';
};
dnsProvider = mkOption {
type = types.nullOr types.str;
inherit (defaultAndText "dnsProvider" null) default defaultText;
example = "route53";
description = ''
DNS Challenge provider. For a list of supported providers, see the "code"
field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>.
'';
};
dnsResolver = mkOption {
type = types.nullOr types.str;
inherit (defaultAndText "dnsResolver" null) default defaultText;
example = "1.1.1.1:53";
description = ''
Set the resolver to use for performing recursive DNS queries. Supported:
host:port. The default is to use the system resolvers, or Google's DNS
resolvers if the system's cannot be determined.
'';
};
credentialsFile = mkOption {
type = types.path;
inherit (defaultAndText "credentialsFile" null) default defaultText;
description = ''
Path to an EnvironmentFile for the cert's service containing any required and
optional environment variables for your selected dnsProvider.
To find out what values you need to set, consult the documentation at
<link xlink:href="https://go-acme.github.io/lego/dns/"/> for the corresponding dnsProvider.
'';
example = "/var/src/secrets/example.org-route53-api-token";
};
dnsPropagationCheck = mkOption {
type = types.bool;
inherit (defaultAndText "dnsPropagationCheck" true) default defaultText;
description = ''
Toggles lego DNS propagation check, which is used alongside DNS-01
challenge to ensure the DNS entries required are available.
'';
};
ocspMustStaple = mkOption {
type = types.bool;
inherit (defaultAndText "ocspMustStaple" false) default defaultText;
description = ''
Turns on the OCSP Must-Staple TLS extension.
Make sure you know what you're doing! See:
<itemizedlist>
<listitem><para><link xlink:href="https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/" /></para></listitem>
<listitem><para><link xlink:href="https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html" /></para></listitem>
</itemizedlist>
'';
};
extraLegoFlags = mkOption {
type = types.listOf types.str;
inherit (defaultAndText "extraLegoFlags" []) default defaultText;
description = ''
Additional global flags to pass to all lego commands.
'';
};
extraLegoRenewFlags = mkOption {
type = types.listOf types.str;
inherit (defaultAndText "extraLegoRenewFlags" []) default defaultText;
description = ''
Additional flags to pass to lego renew.
'';
};
extraLegoRunFlags = mkOption {
type = types.listOf types.str;
inherit (defaultAndText "extraLegoRunFlags" []) default defaultText;
description = ''
Additional flags to pass to lego run.
'';
};
};
};
certOpts = { name, config, ... }: {
options = {
# user option has been removed
user = mkOption {
visible = false;
default = "_mkRemovedOptionModule";
};
# allowKeysForGroup option has been removed
allowKeysForGroup = mkOption {
visible = false;
default = "_mkRemovedOptionModule";
};
# extraDomains was replaced with extraDomainNames
extraDomains = mkOption {
visible = false;
default = "_mkMergedOptionModule";
};
directory = mkOption {
type = types.str;
readOnly = true;
default = "/var/lib/acme/${name}";
description = "Directory where certificate and other state is stored.";
};
domain = mkOption {
type = types.str;
default = name;
description = "Domain to fetch certificate for (defaults to the entry name).";
};
extraDomainNames = mkOption {
type = types.listOf types.str;
default = [];
example = literalExpression ''
[
"example.org"
"mydomain.org"
]
'';
description = ''
A list of extra domain names, which are included in the one certificate to be issued.
'';
};
# This setting must be different for each configured certificate, otherwise
# two or more renewals may fail to bind to the address. Hence, it is not in
# the inheritableOpts.
listenHTTP = mkOption {
type = types.nullOr types.str;
default = null;
example = ":1360";
description = ''
Interface and port to listen on to solve HTTP challenges
in the form [INTERFACE]:PORT.
If you use a port other than 80, you must proxy port 80 to this port.
'';
};
inheritDefaults = mkOption {
default = true;
example = true;
description = "Whether to inherit values set in `security.acme.defaults` or not.";
type = lib.types.bool;
};
};
};
in {
options = {
security.acme = {
preliminarySelfsigned = mkOption {
type = types.bool;
default = true;
description = ''
Whether a preliminary self-signed certificate should be generated before
doing ACME requests. This can be useful when certificates are required in
a webserver, but ACME needs the webserver to make its requests.
With preliminary self-signed certificate the webserver can be started and
can later reload the correct ACME certificates.
'';
};
acceptTerms = mkOption {
type = types.bool;
default = false;
description = ''
Accept the CA's terms of service. The default provider is Let's Encrypt,
you can find their ToS at <link xlink:href="https://letsencrypt.org/repository/"/>.
'';
};
useRoot = mkOption {
type = types.bool;
default = false;
description = ''
Whether to use the root user when generating certs. This is not recommended
for security + compatiblity reasons. If a service requires root owned certificates
consider following the guide on "Using ACME with services demanding root
owned certificates" in the NixOS manual, and only using this as a fallback
or for testing.
'';
};
defaults = mkOption {
type = types.submodule (inheritableModule true);
description = ''
Default values inheritable by all configured certs. You can
use this to define options shared by all your certs. These defaults
can also be ignored on a per-cert basis using the
`security.acme.certs.''${cert}.inheritDefaults' option.
'';
};
certs = mkOption {
default = { };
type = with types; attrsOf (submodule [ (inheritableModule false) certOpts ]);
description = ''
Attribute set of certificates to get signed and renewed. Creates
<literal>acme-''${cert}.{service,timer}</literal> systemd units for
each certificate defined here. Other services can add dependencies
to those units if they rely on the certificates being present,
or trigger restarts of the service if certificates get renewed.
'';
example = literalExpression ''
{
"example.com" = {
webroot = "/var/lib/acme/acme-challenge/";
email = "foo@example.com";
extraDomainNames = [ "www.example.com" "foo.example.com" ];
};
"bar.example.com" = {
webroot = "/var/lib/acme/acme-challenge/";
email = "bar@example.com";
};
}
'';
};
};
};
imports = [
(mkRemovedOptionModule [ "security" "acme" "production" ] ''
Use security.acme.server to define your staging ACME server URL instead.
To use the let's encrypt staging server, use security.acme.server =
"https://acme-staging-v02.api.letsencrypt.org/directory".
'')
(mkRemovedOptionModule [ "security" "acme" "directory" ] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
(mkRemovedOptionModule [ "security" "acme" "preDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
(mkRemovedOptionModule [ "security" "acme" "activationDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
(mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
(mkChangedOptionModule [ "security" "acme" "validMinDays" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMinDays))
(mkChangedOptionModule [ "security" "acme" "renewInterval" ] [ "security" "acme" "defaults" "renewInterval" ] (config: config.security.acme.renewInterval))
(mkChangedOptionModule [ "security" "acme" "email" ] [ "security" "acme" "defaults" "email" ] (config: config.security.acme.email))
(mkChangedOptionModule [ "security" "acme" "server" ] [ "security" "acme" "defaults" "server" ] (config: config.security.acme.server))
(mkChangedOptionModule [ "security" "acme" "enableDebugLogs" ] [ "security" "acme" "defaults" "enableDebugLogs" ] (config: config.security.acme.enableDebugLogs))
];
config = mkMerge [
(mkIf (cfg.certs != { }) {
# FIXME Most of these custom warnings and filters for security.acme.certs.* are required
# because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then ''
The option definition `security.acme.certs.${cert}.extraDomains` has changed
to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
Setting a custom webroot for extra domains is not possible, instead use separate certs.
'' else "") cfg.certs);
assertions = let
certs = attrValues cfg.certs;
in [
{
assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
message = ''
You must define `security.acme.certs.<name>.email` or
`security.acme.email` to register with the CA. Note that using
many different addresses for certs may trigger account rate limits.
'';
}
{
assertion = cfg.acceptTerms;
message = ''
You must accept the CA's terms of service before using
the ACME module by setting `security.acme.acceptTerms`
to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
'';
}
] ++ (builtins.concatLists (mapAttrsToList (cert: data: [
{
assertion = data.user == "_mkRemovedOptionModule";
message = ''
The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it.
Certificate user is now hard coded to the "acme" user. If you would
like another user to have access, consider adding them to the
"acme" group or changing security.acme.certs.${cert}.group.
'';
}
{
assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
message = ''
The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it.
All certs are readable by the configured group. If this is undesired,
consider changing security.acme.certs.${cert}.group to an unused group.
'';
}
# * in the cert value breaks building of systemd services, and makes
# referencing them as a user quite weird too. Best practice is to use
# the domain option.
{
assertion = ! hasInfix "*" cert;
message = ''
The cert option path `security.acme.certs.${cert}.dnsProvider`
cannot contain a * character.
Instead, set `security.acme.certs.${cert}.domain = "${cert}";`
and remove the wildcard from the path.
'';
}
{
assertion = data.dnsProvider == null || data.webroot == null;
message = ''
Options `security.acme.certs.${cert}.dnsProvider` and
`security.acme.certs.${cert}.webroot` are mutually exclusive.
'';
}
{
assertion = data.webroot == null || data.listenHTTP == null;
message = ''
Options `security.acme.certs.${cert}.webroot` and
`security.acme.certs.${cert}.listenHTTP` are mutually exclusive.
'';
}
{
assertion = data.listenHTTP == null || data.dnsProvider == null;
message = ''
Options `security.acme.certs.${cert}.listenHTTP` and
`security.acme.certs.${cert}.dnsProvider` are mutually exclusive.
'';
}
{
assertion = data.dnsProvider != null || data.webroot != null || data.listenHTTP != null;
message = ''
One of `security.acme.certs.${cert}.dnsProvider`,
`security.acme.certs.${cert}.webroot`, or
`security.acme.certs.${cert}.listenHTTP` must be provided.
'';
}
]) cfg.certs));
users.users.acme = {
home = "/var/lib/acme";
group = "acme";
isSystemUser = true;
};
users.groups.acme = {};
systemd.services = {
"acme-fixperms" = userMigrationService;
} // (mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs)
// (optionalAttrs (cfg.preliminarySelfsigned) ({
"acme-selfsigned-ca" = selfsignCAService;
} // (mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs)));
systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
systemd.targets = let
# Create some targets which can be depended on to be "active" after cert renewals
finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
wantedBy = [ "default.target" ];
requires = [ "acme-${cert}.service" ];
after = [ "acme-${cert}.service" ];
}) certConfigs;
# Create targets to limit the number of simultaneous account creations
# How it works:
# - Pick a "leader" cert service, which will be in charge of creating the account,
# and run first (requires + after)
# - Make all other cert services sharing the same account wait for the leader to
# finish before starting (requiredBy + before).
# Using a target here is fine - account creation is a one time event. Even if
# systemd clean --what=state is used to delete the account, so long as the user
# then runs one of the cert services, there won't be any issues.
accountTargets = mapAttrs' (hash: confs: let
leader = "acme-${(builtins.head confs).cert}.service";
dependantServices = map (conf: "acme-${conf.cert}.service") (builtins.tail confs);
in nameValuePair "acme-account-${hash}" {
requiredBy = dependantServices;
before = dependantServices;
requires = [ leader ];
after = [ leader ];
}) (groupBy (conf: conf.accountHash) (attrValues certConfigs));
in finishedTargets // accountTargets;
})
];
meta = {
maintainers = lib.teams.acme.members;
doc = ./doc.xml;
};
}

View file

@ -0,0 +1,413 @@
<chapter xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xi="http://www.w3.org/2001/XInclude"
version="5.0"
xml:id="module-security-acme">
<title>SSL/TLS Certificates with ACME</title>
<para>
NixOS supports automatic domain validation &amp; certificate retrieval and
renewal using the ACME protocol. Any provider can be used, but by default
NixOS uses Let's Encrypt. The alternative ACME client
<link xlink:href="https://go-acme.github.io/lego/">lego</link> is used under
the hood.
</para>
<para>
Automatic cert validation and configuration for Apache and Nginx virtual
hosts is included in NixOS, however if you would like to generate a wildcard
cert or you are not using a web server you will have to configure DNS
based validation.
</para>
<section xml:id="module-security-acme-prerequisites">
<title>Prerequisites</title>
<para>
To use the ACME module, you must accept the provider's terms of service
by setting <literal><xref linkend="opt-security.acme.acceptTerms" /></literal>
to <literal>true</literal>. The Let's Encrypt ToS can be found
<link xlink:href="https://letsencrypt.org/repository/">here</link>.
</para>
<para>
You must also set an email address to be used when creating accounts with
Let's Encrypt. You can set this for all certs with
<literal><xref linkend="opt-security.acme.defaults.email" /></literal>
and/or on a per-cert basis with
<literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>.
This address is only used for registration and renewal reminders,
and cannot be used to administer the certificates in any way.
</para>
<para>
Alternatively, you can use a different ACME server by changing the
<literal><xref linkend="opt-security.acme.defaults.server" /></literal> option
to a provider of your choosing, or just change the server for one cert with
<literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>.
</para>
<para>
You will need an HTTP server or DNS server for verification. For HTTP,
the server must have a webroot defined that can serve
<filename>.well-known/acme-challenge</filename>. This directory must be
writeable by the user that will run the ACME client. For DNS, you must
set up credentials with your provider/server for use with lego.
</para>
</section>
<section xml:id="module-security-acme-nginx">
<title>Using ACME certificates in Nginx</title>
<para>
NixOS supports fetching ACME certificates for you by setting
<literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link>
= true;</literal> in a virtualHost config. We first create self-signed
placeholder certificates in place of the real ACME certs. The placeholder
certs are overwritten when the ACME certs arrive. For
<literal>foo.example.com</literal> the config would look like this:
</para>
<programlisting>
<xref linkend="opt-security.acme.acceptTerms" /> = true;
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
services.nginx = {
<link linkend="opt-services.nginx.enable">enable</link> = true;
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
"foo.example.com" = {
<link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
<link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
# All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomainNames">extra domain names</link> on the certificate.
<link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "bar.example.com" ];
locations."/" = {
<link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
};
};
# We can also add a different vhost and reuse the same certificate
# but we have to append extraDomainNames manually.
<link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."foo.example.com".extraDomainNames</link> = [ "baz.example.com" ];
"baz.example.com" = {
<link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
<link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">useACMEHost</link> = "foo.example.com";
locations."/" = {
<link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
};
};
};
}
</programlisting>
</section>
<section xml:id="module-security-acme-httpd">
<title>Using ACME certificates in Apache/httpd</title>
<para>
Using ACME certificates with Apache virtual hosts is identical
to using them with Nginx. The attribute names are all the same, just replace
"nginx" with "httpd" where appropriate.
</para>
</section>
<section xml:id="module-security-acme-configuring">
<title>Manual configuration of HTTP-01 validation</title>
<para>
First off you will need to set up a virtual host to serve the challenges.
This example uses a vhost called <literal>certs.example.com</literal>, with
the intent that you will generate certs for all your vhosts and redirect
everyone to HTTPS.
</para>
<programlisting>
<xref linkend="opt-security.acme.acceptTerms" /> = true;
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
# /var/lib/acme/.challenges must be writable by the ACME user
# and readable by the Nginx user. The easiest way to achieve
# this is to add the Nginx user to the ACME group.
<link linkend="opt-users.users._name_.extraGroups">users.users.nginx.extraGroups</link> = [ "acme" ];
services.nginx = {
<link linkend="opt-services.nginx.enable">enable</link> = true;
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
"acmechallenge.example.com" = {
# Catchall vhost, will redirect users to HTTPS for all vhosts
<link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
locations."/.well-known/acme-challenge" = {
<link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/lib/acme/.challenges";
};
locations."/" = {
<link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.return">return</link> = "301 https://$host$request_uri";
};
};
};
}
# Alternative config for Apache
<link linkend="opt-users.users._name_.extraGroups">users.users.wwwrun.extraGroups</link> = [ "acme" ];
services.httpd = {
<link linkend="opt-services.httpd.enable">enable = true;</link>
<link linkend="opt-services.httpd.virtualHosts">virtualHosts</link> = {
"acmechallenge.example.com" = {
# Catchall vhost, will redirect users to HTTPS for all vhosts
<link linkend="opt-services.httpd.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
# /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user.
# By default, this is the case.
<link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = "/var/lib/acme/.challenges";
<link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC]
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301]
'';
};
};
}
</programlisting>
<para>
Now you need to configure ACME to generate a certificate.
</para>
<programlisting>
<xref linkend="opt-security.acme.certs"/>."foo.example.com" = {
<link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges";
<link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com";
# Ensure that the web server you use can read the generated certs
# Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose.
<link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx";
# Since we have a wildcard vhost to handle port 80,
# we can generate certs for anything!
# Just make sure your DNS resolves them.
<link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "mail.example.com" ];
};
</programlisting>
<para>
The private key <filename>key.pem</filename> and certificate
<filename>fullchain.pem</filename> will be put into
<filename>/var/lib/acme/foo.example.com</filename>.
</para>
<para>
Refer to <xref linkend="ch-options" /> for all available configuration
options for the <link linkend="opt-security.acme.certs">security.acme</link>
module.
</para>
</section>
<section xml:id="module-security-acme-config-dns">
<title>Configuring ACME for DNS validation</title>
<para>
This is useful if you want to generate a wildcard certificate, since
ACME servers will only hand out wildcard certs over DNS validation.
There are a number of supported DNS providers and servers you can utilise,
see the <link xlink:href="https://go-acme.github.io/lego/dns/">lego docs</link>
for provider/server specific configuration values. For the sake of these
docs, we will provide a fully self-hosted example using bind.
</para>
<programlisting>
services.bind = {
<link linkend="opt-services.bind.enable">enable</link> = true;
<link linkend="opt-services.bind.extraConfig">extraConfig</link> = ''
include "/var/lib/secrets/dnskeys.conf";
'';
<link linkend="opt-services.bind.zones">zones</link> = [
rec {
name = "example.com";
file = "/var/db/bind/${name}";
master = true;
extraConfig = "allow-update { key rfc2136key.example.com.; };";
}
];
}
# Now we can configure ACME
<xref linkend="opt-security.acme.acceptTerms" /> = true;
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
<xref linkend="opt-security.acme.certs" />."example.com" = {
<link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com";
<link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136";
<link linkend="opt-security.acme.certs._name_.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
# We don't need to wait for propagation since this is a local DNS server
<link linkend="opt-security.acme.certs._name_.dnsPropagationCheck">dnsPropagationCheck</link> = false;
};
</programlisting>
<para>
The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename>
must be kept secure and thus you should not keep their contents in your
Nix config. Instead, generate them one time with a systemd service:
</para>
<programlisting>
systemd.services.dns-rfc2136-conf = {
requiredBy = ["acme-example.com.service", "bind.service"];
before = ["acme-example.com.service", "bind.service"];
unitConfig = {
ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
};
serviceConfig = {
Type = "oneshot";
UMask = 0077;
};
path = [ pkgs.bind ];
script = ''
mkdir -p /var/lib/secrets
tsig-keygen rfc2136key.example.com &gt; /var/lib/secrets/dnskeys.conf
chown named:root /var/lib/secrets/dnskeys.conf
chmod 400 /var/lib/secrets/dnskeys.conf
# Copy the secret value from the dnskeys.conf, and put it in
# RFC2136_TSIG_SECRET below
cat &gt; /var/lib/secrets/certs.secret &lt;&lt; EOF
RFC2136_NAMESERVER='127.0.0.1:53'
RFC2136_TSIG_ALGORITHM='hmac-sha256.'
RFC2136_TSIG_KEY='rfc2136key.example.com'
RFC2136_TSIG_SECRET='your secret key'
EOF
chmod 400 /var/lib/secrets/certs.secret
'';
};
</programlisting>
<para>
Now you're all set to generate certs! You should monitor the first invocation
by running <literal>systemctl start acme-example.com.service &amp;
journalctl -fu acme-example.com.service</literal> and watching its log output.
</para>
</section>
<section xml:id="module-security-acme-config-dns-with-vhosts">
<title>Using DNS validation with web server virtual hosts</title>
<para>
It is possible to use DNS-01 validation with all certificates,
including those automatically configured via the Nginx/Apache
<literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link></literal>
option. This configuration pattern is fully
supported and part of the module's test suite for Nginx + Apache.
</para>
<para>
You must follow the guide above on configuring DNS-01 validation
first, however instead of setting the options for one certificate
(e.g. <xref linkend="opt-security.acme.certs._name_.dnsProvider" />)
you will set them as defaults
(e.g. <xref linkend="opt-security.acme.defaults.dnsProvider" />).
</para>
<programlisting>
# Configure ACME appropriately
<xref linkend="opt-security.acme.acceptTerms" /> = true;
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
<xref linkend="opt-security.acme.defaults" /> = {
<link linkend="opt-security.acme.defaults.dnsProvider">dnsProvider</link> = "rfc2136";
<link linkend="opt-security.acme.defaults.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
# We don't need to wait for propagation since this is a local DNS server
<link linkend="opt-security.acme.defaults.dnsPropagationCheck">dnsPropagationCheck</link> = false;
};
# For each virtual host you would like to use DNS-01 validation with,
# set acmeRoot = null
services.nginx = {
<link linkend="opt-services.nginx.enable">enable</link> = true;
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
"foo.example.com" = {
<link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
<link linkend="opt-services.nginx.virtualHosts._name_.acmeRoot">acmeRoot</link> = null;
};
};
}
</programlisting>
<para>
And that's it! Next time your configuration is rebuilt, or when
you add a new virtualHost, it will be DNS-01 validated.
</para>
</section>
<section xml:id="module-security-acme-root-owned">
<title>Using ACME with services demanding root owned certificates</title>
<para>
Some services refuse to start if the configured certificate files
are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
There is no way to change the user the ACME module uses (it will always be
<literal>acme</literal>), however you can use systemd's
<literal>LoadCredential</literal> feature to resolve this elegantly.
Below is an example configuration for OpenSMTPD, but this pattern
can be applied to any service.
</para>
<programlisting>
# Configure ACME however you like (DNS or HTTP validation), adding
# the following configuration for the relevant certificate.
# Note: You cannot use `systemctl reload` here as that would mean
# the LoadCredential configuration below would be skipped and
# the service would continue to use old certificates.
security.acme.certs."mail.example.com".postRun = ''
systemctl restart opensmtpd
'';
# Now you must augment OpenSMTPD's systemd service to load
# the certificate files.
<link linkend="opt-systemd.services._name_.requires">systemd.services.opensmtpd.requires</link> = ["acme-finished-mail.example.com.target"];
<link linkend="opt-systemd.services._name_.serviceConfig">systemd.services.opensmtpd.serviceConfig.LoadCredential</link> = let
certDir = config.security.acme.certs."mail.example.com".directory;
in [
"cert.pem:${certDir}/cert.pem"
"key.pem:${certDir}/key.pem"
];
# Finally, configure OpenSMTPD to use these certs.
services.opensmtpd = let
credsDir = "/run/credentials/opensmtpd.service";
in {
enable = true;
setSendmail = false;
serverConfiguration = ''
pki mail.example.com cert "${credsDir}/cert.pem"
pki mail.example.com key "${credsDir}/key.pem"
listen on localhost tls pki mail.example.com
action act1 relay host smtp://127.0.0.1:10027
match for local action act1
'';
};
</programlisting>
</section>
<section xml:id="module-security-acme-regenerate">
<title>Regenerating certificates</title>
<para>
Should you need to regenerate a particular certificate in a hurry, such
as when a vulnerability is found in Let's Encrypt, there is now a convenient
mechanism for doing so. Running
<literal>systemctl clean --what=state acme-example.com.service</literal>
will remove all certificate files and the account data for the given domain,
allowing you to then <literal>systemctl start acme-example.com.service</literal>
to generate fresh ones.
</para>
</section>
<section xml:id="module-security-acme-fix-jws">
<title>Fixing JWS Verification error</title>
<para>
It is possible that your account credentials file may become corrupt and need
to be regenerated. In this scenario lego will produce the error <literal>JWS verification error</literal>.
The solution is to simply delete the associated accounts file and
re-run the affected service(s).
</para>
<programlisting>
# Find the accounts folder for the certificate
systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*'
export accountdir="$(!!)"
# Move this folder to some place else
mv /var/lib/acme/.lego/$accountdir{,.bak}
# Recreate the folder using systemd-tmpfiles
systemd-tmpfiles --create
# Get a new account and reissue certificates
# Note: Do this for all certs that share the same account email address
systemctl start acme-example.com.service
</programlisting>
</section>
</chapter>

View file

@ -0,0 +1,4 @@
{ cert, group, groups, user }: {
assertion = cert.group == group || builtins.any (u: u == user) groups.${cert.group}.members;
message = "Group for certificate ${cert.domain} must be ${group}, or user ${user} must be a member of group ${cert.group}";
}

View file

@ -0,0 +1,216 @@
{ config, lib, pkgs, ... }:
with lib;
let
inherit (builtins) attrNames head map match readFile;
inherit (lib) types;
inherit (config.environment) etc;
cfg = config.security.apparmor;
mkDisableOption = name: mkEnableOption name // {
default = true;
example = false;
};
enabledPolicies = filterAttrs (n: p: p.enable) cfg.policies;
in
{
imports = [
(mkRemovedOptionModule [ "security" "apparmor" "confineSUIDApplications" ] "Please use the new options: `security.apparmor.policies.<policy>.enable'.")
(mkRemovedOptionModule [ "security" "apparmor" "profiles" ] "Please use the new option: `security.apparmor.policies'.")
apparmor/includes.nix
apparmor/profiles.nix
];
options = {
security.apparmor = {
enable = mkEnableOption ''
the AppArmor Mandatory Access Control system.
If you're enabling this module on a running system,
note that a reboot will be required to activate AppArmor in the kernel.
Also, beware that enabling this module privileges stability over security
by not trying to kill unconfined but newly confinable running processes by default,
though it would be needed because AppArmor can only confine new
or already confined processes of an executable.
This killing would for instance be necessary when upgrading to a NixOS revision
introducing for the first time an AppArmor profile for the executable
of a running process.
Enable <xref linkend="opt-security.apparmor.killUnconfinedConfinables"/>
if you want this service to do such killing
by sending a <literal>SIGTERM</literal> to those running processes'';
policies = mkOption {
description = ''
AppArmor policies.
'';
type = types.attrsOf (types.submodule ({ name, config, ... }: {
options = {
enable = mkDisableOption "loading of the profile into the kernel";
enforce = mkDisableOption "enforcing of the policy or only complain in the logs";
profile = mkOption {
description = "The policy of the profile.";
type = types.lines;
apply = pkgs.writeText name;
};
};
}));
default = {};
};
includes = mkOption {
type = types.attrsOf types.lines;
default = {};
description = ''
List of paths to be added to AppArmor's searched paths
when resolving <literal>include</literal> directives.
'';
apply = mapAttrs pkgs.writeText;
};
packages = mkOption {
type = types.listOf types.package;
default = [];
description = "List of packages to be added to AppArmor's include path";
};
enableCache = mkEnableOption ''
caching of AppArmor policies
in <literal>/var/cache/apparmor/</literal>.
Beware that AppArmor policies almost always contain Nix store paths,
and thus produce at each change of these paths
a new cached version accumulating in the cache'';
killUnconfinedConfinables = mkEnableOption ''
killing of processes which have an AppArmor profile enabled
(in <xref linkend="opt-security.apparmor.policies"/>)
but are not confined (because AppArmor can only confine new processes).
This is only sending a gracious <literal>SIGTERM</literal> signal to the processes,
not a <literal>SIGKILL</literal>.
Beware that due to a current limitation of AppArmor,
only profiles with exact paths (and no name) can enable such kills'';
};
};
config = mkIf cfg.enable {
assertions = map (policy:
{ assertion = match ".*/.*" policy == null;
message = "`security.apparmor.policies.\"${policy}\"' must not contain a slash.";
# Because, for instance, aa-remove-unknown uses profiles_names_list() in rc.apparmor.functions
# which does not recurse into sub-directories.
}
) (attrNames cfg.policies);
environment.systemPackages = [
pkgs.apparmor-utils
pkgs.apparmor-bin-utils
];
environment.etc."apparmor.d".source = pkgs.linkFarm "apparmor.d" (
# It's important to put only enabledPolicies here and not all cfg.policies
# because aa-remove-unknown reads profiles from all /etc/apparmor.d/*
mapAttrsToList (name: p: { inherit name; path = p.profile; }) enabledPolicies ++
mapAttrsToList (name: path: { inherit name path; }) cfg.includes
);
environment.etc."apparmor/parser.conf".text = ''
${if cfg.enableCache then "write-cache" else "skip-cache"}
cache-loc /var/cache/apparmor
Include /etc/apparmor.d
'' +
concatMapStrings (p: "Include ${p}/etc/apparmor.d\n") cfg.packages;
# For aa-logprof
environment.etc."apparmor/apparmor.conf".text = ''
'';
# For aa-logprof
environment.etc."apparmor/severity.db".source = pkgs.apparmor-utils + "/etc/apparmor/severity.db";
environment.etc."apparmor/logprof.conf".source = pkgs.runCommand "logprof.conf" {
header = ''
[settings]
# /etc/apparmor.d/ is read-only on NixOS
profiledir = /var/cache/apparmor/logprof
inactive_profiledir = /etc/apparmor.d/disable
# Use: journalctl -b --since today --grep audit: | aa-logprof
logfiles = /dev/stdin
parser = ${pkgs.apparmor-parser}/bin/apparmor_parser
ldd = ${pkgs.glibc.bin}/bin/ldd
logger = ${pkgs.util-linux}/bin/logger
# customize how file ownership permissions are presented
# 0 - off
# 1 - default of what ever mode the log reported
# 2 - force the new permissions to be user
# 3 - force all perms on the rule to be user
default_owner_prompt = 1
custom_includes = /etc/apparmor.d ${concatMapStringsSep " " (p: "${p}/etc/apparmor.d") cfg.packages}
[qualifiers]
${pkgs.runtimeShell} = icnu
${pkgs.bashInteractive}/bin/sh = icnu
${pkgs.bashInteractive}/bin/bash = icnu
${config.users.defaultUserShell} = icnu
'';
footer = "${pkgs.apparmor-utils}/etc/apparmor/logprof.conf";
passAsFile = [ "header" ];
} ''
cp $headerPath $out
sed '1,/\[qualifiers\]/d' $footer >> $out
'';
boot.kernelParams = [ "apparmor=1" "security=apparmor" ];
systemd.services.apparmor = {
after = [
"local-fs.target"
"systemd-journald-audit.socket"
];
before = [ "sysinit.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig = {
Description="Load AppArmor policies";
DefaultDependencies = "no";
ConditionSecurity = "apparmor";
};
# Reloading instead of restarting enables to load new AppArmor profiles
# without necessarily restarting all services which have Requires=apparmor.service
reloadIfChanged = true;
restartTriggers = [
etc."apparmor/parser.conf".source
etc."apparmor.d".source
];
serviceConfig = let
killUnconfinedConfinables = pkgs.writeShellScript "apparmor-kill" ''
set -eu
${pkgs.apparmor-bin-utils}/bin/aa-status --json |
${pkgs.jq}/bin/jq --raw-output '.processes | .[] | .[] | select (.status == "unconfined") | .pid' |
xargs --verbose --no-run-if-empty --delimiter='\n' \
kill
'';
commonOpts = p: "--verbose --show-cache ${optionalString (!p.enforce) "--complain "}${p.profile}";
in {
Type = "oneshot";
RemainAfterExit = "yes";
ExecStartPre = "${pkgs.apparmor-utils}/bin/aa-teardown";
ExecStart = mapAttrsToList (n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --add ${commonOpts p}") enabledPolicies;
ExecStartPost = optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
ExecReload =
# Add or replace into the kernel profiles in enabledPolicies
# (because AppArmor can do that without stopping the processes already confined).
mapAttrsToList (n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --replace ${commonOpts p}") enabledPolicies ++
# Remove from the kernel any profile whose name is not
# one of the names within the content of the profiles in enabledPolicies
# (indirectly read from /etc/apparmor.d/*, without recursing into sub-directory).
# Note that this does not remove profiles dynamically generated by libvirt.
[ "${pkgs.apparmor-utils}/bin/aa-remove-unknown" ] ++
# Optionaly kill the processes which are unconfined but now have a profile loaded
# (because AppArmor can only start to confine new processes).
optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
ExecStop = "${pkgs.apparmor-utils}/bin/aa-teardown";
CacheDirectory = [ "apparmor" "apparmor/logprof" ];
CacheDirectoryMode = "0700";
};
};
};
meta.maintainers = with maintainers; [ julm ];
}

View file

@ -0,0 +1,317 @@
{ config, lib, pkgs, ... }:
let
inherit (builtins) attrNames hasAttr isAttrs;
inherit (lib) getLib;
inherit (config.environment) etc;
# Utility to generate an AppArmor rule
# only when the given path exists in config.environment.etc
etcRule = arg:
let go = { path ? null, mode ? "r", trail ? "" }:
lib.optionalString (hasAttr path etc)
"${mode} ${config.environment.etc.${path}.source}${trail},";
in if isAttrs arg
then go arg
else go { path = arg; };
in
{
# FIXME: most of the etcRule calls below have been
# written systematically by converting from apparmor-profiles's profiles
# without testing nor deep understanding of their uses,
# and thus may need more rules or can have less rules;
# this remains to be determined case by case,
# some may even be completely useless.
config.security.apparmor.includes = {
# This one is included by <tunables/global>
# which is usualy included before any profile.
"abstractions/tunables/alias" = ''
alias /bin -> /run/current-system/sw/bin,
alias /lib/modules -> /run/current-system/kernel/lib/modules,
alias /sbin -> /run/current-system/sw/sbin,
alias /usr -> /run/current-system/sw,
'';
"abstractions/audio" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/audio"
'' + lib.concatMapStringsSep "\n" etcRule [
"asound.conf"
"esound/esd.conf"
"libao.conf"
{ path = "pulse"; trail = "/"; }
{ path = "pulse"; trail = "/**"; }
{ path = "sound"; trail = "/"; }
{ path = "sound"; trail = "/**"; }
{ path = "alsa/conf.d"; trail = "/"; }
{ path = "alsa/conf.d"; trail = "/*"; }
"openal/alsoft.conf"
"wildmidi/wildmidi.conf"
];
"abstractions/authentication" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/authentication"
# Defined in security.pam
include <abstractions/pam>
'' + lib.concatMapStringsSep "\n" etcRule [
"nologin"
"securetty"
{ path = "security"; trail = "/*"; }
"shadow"
"gshadow"
"pwdb.conf"
"default/passwd"
"login.defs"
];
"abstractions/base" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/base"
r ${pkgs.stdenv.cc.libc}/share/locale/**,
r ${pkgs.stdenv.cc.libc}/share/locale.alias,
${lib.optionalString (pkgs.glibcLocales != null) "r ${pkgs.glibcLocales}/lib/locale/locale-archive,"}
${etcRule "localtime"}
r ${pkgs.tzdata}/share/zoneinfo/**,
r ${pkgs.stdenv.cc.libc}/share/i18n/**,
'';
"abstractions/bash" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/bash"
# bash inspects filesystems at startup
# and /etc/mtab is linked to /proc/mounts
@{PROC}/mounts
# system-wide bash configuration
'' + lib.concatMapStringsSep "\n" etcRule [
"profile.dos"
"profile"
"profile.d"
{ path = "profile.d"; trail = "/*"; }
"bashrc"
"bash.bashrc"
"bash.bashrc.local"
"bash_completion"
"bash_completion.d"
{ path = "bash_completion.d"; trail = "/*"; }
# bash relies on system-wide readline configuration
"inputrc"
# run out of /etc/bash.bashrc
"DIR_COLORS"
];
"abstractions/consoles" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/consoles"
'';
"abstractions/cups-client" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/cups-client"
${etcRule "cups/cups-client.conf"}
'';
"abstractions/dbus-session-strict" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/dbus-session-strict"
${etcRule "machine-id"}
'';
"abstractions/dconf" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/dconf"
${etcRule { path = "dconf"; trail = "/**"; }}
'';
"abstractions/dri-common" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/dri-common"
${etcRule "drirc"}
'';
# The config.fonts.fontconfig NixOS module adds many files to /etc/fonts/
# by symlinking them but without exporting them outside of its NixOS module,
# those are therefore added there to this "abstractions/fonts".
"abstractions/fonts" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/fonts"
${etcRule { path = "fonts"; trail = "/**"; }}
'';
"abstractions/gnome" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/gnome"
include <abstractions/fonts>
'' + lib.concatMapStringsSep "\n" etcRule [
{ path = "gnome"; trail = "/gtkrc*"; }
{ path = "gtk"; trail = "/*"; }
{ path = "gtk-2.0"; trail = "/*"; }
{ path = "gtk-3.0"; trail = "/*"; }
"orbitrc"
{ path = "pango"; trail = "/*"; }
{ path = "/etc/gnome-vfs-2.0"; trail = "/modules/"; }
{ path = "/etc/gnome-vfs-2.0"; trail = "/modules/*"; }
"papersize"
{ path = "cups"; trail = "/lpoptions"; }
{ path = "gnome"; trail = "/defaults.list"; }
{ path = "xdg"; trail = "/{,*-}mimeapps.list"; }
"xdg/mimeapps.list"
];
"abstractions/kde" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/kde"
'' + lib.concatMapStringsSep "\n" etcRule [
{ path = "qt3"; trail = "/kstylerc"; }
{ path = "qt3"; trail = "/qt_plugins_3.3rc"; }
{ path = "qt3"; trail = "/qtrc"; }
"kderc"
{ path = "kde3"; trail = "/*"; }
"kde4rc"
{ path = "xdg"; trail = "/kdeglobals"; }
{ path = "xdg"; trail = "/Trolltech.conf"; }
];
"abstractions/kerberosclient" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/kerberosclient"
'' + lib.concatMapStringsSep "\n" etcRule [
{ path = "krb5.keytab"; mode="rk"; }
"krb5.conf"
"krb5.conf.d"
{ path = "krb5.conf.d"; trail = "/*"; }
# config files found via strings on libs
"krb.conf"
"krb.realms"
"srvtab"
];
"abstractions/ldapclient" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/ldapclient"
'' + lib.concatMapStringsSep "\n" etcRule [
"ldap.conf"
"ldap.secret"
{ path = "openldap"; trail = "/*"; }
{ path = "openldap"; trail = "/cacerts/*"; }
{ path = "sasl2"; trail = "/*"; }
];
"abstractions/likewise" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/likewise"
'';
"abstractions/mdns" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/mdns"
${etcRule "nss_mdns.conf"}
'';
"abstractions/nameservice" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/nameservice"
# Many programs wish to perform nameservice-like operations, such as
# looking up users by name or id, groups by name or id, hosts by name
# or IP, etc. These operations may be performed through files, dns,
# NIS, NIS+, LDAP, hesiod, wins, etc. Allow them all here.
mr ${getLib pkgs.nss}/lib/libnss_*.so*,
mr ${getLib pkgs.nss}/lib64/libnss_*.so*,
'' + lib.concatMapStringsSep "\n" etcRule [
"group"
"host.conf"
"hosts"
"nsswitch.conf"
"gai.conf"
"passwd"
"protocols"
# libtirpc (used for NIS/YP login) needs this
"netconfig"
"resolv.conf"
{ path = "samba"; trail = "/lmhosts"; }
"services"
"default/nss"
# libnl-3-200 via libnss-gw-name
{ path = "libnl"; trail = "/classid"; }
{ path = "libnl-3"; trail = "/classid"; }
];
"abstractions/nis" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/nis"
'';
"abstractions/nvidia" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/nvidia"
${etcRule "vdpau_wrapper.cfg"}
'';
"abstractions/opencl-common" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/opencl-common"
${etcRule { path = "OpenCL"; trail = "/**"; }}
'';
"abstractions/opencl-mesa" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/opencl-mesa"
${etcRule "default/drirc"}
'';
"abstractions/openssl" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/openssl"
${etcRule { path = "ssl"; trail = "/openssl.cnf"; }}
'';
"abstractions/p11-kit" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/p11-kit"
'' + lib.concatMapStringsSep "\n" etcRule [
{ path = "pkcs11"; trail = "/"; }
{ path = "pkcs11"; trail = "/pkcs11.conf"; }
{ path = "pkcs11"; trail = "/modules/"; }
{ path = "pkcs11"; trail = "/modules/*"; }
];
"abstractions/perl" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/perl"
${etcRule { path = "perl"; trail = "/**"; }}
'';
"abstractions/php" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/php"
'' + lib.concatMapStringsSep "\n" etcRule [
{ path = "php"; trail = "/**/"; }
{ path = "php5"; trail = "/**/"; }
{ path = "php7"; trail = "/**/"; }
{ path = "php"; trail = "/**.ini"; }
{ path = "php5"; trail = "/**.ini"; }
{ path = "php7"; trail = "/**.ini"; }
];
"abstractions/postfix-common" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/postfix-common"
'' + lib.concatMapStringsSep "\n" etcRule [
"mailname"
{ path = "postfix"; trail = "/*.cf"; }
"postfix/main.cf"
"postfix/master.cf"
];
"abstractions/python" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/python"
'';
"abstractions/qt5" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/qt5"
'' + lib.concatMapStringsSep "\n" etcRule [
{ path = "xdg"; trail = "/QtProject/qtlogging.ini"; }
{ path = "xdg/QtProject"; trail = "/qtlogging.ini"; }
"xdg/QtProject/qtlogging.ini"
];
"abstractions/samba" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/samba"
${etcRule { path = "samba"; trail = "/*"; }}
'';
"abstractions/ssl_certs" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/ssl_certs"
# For the NixOS module: security.acme
r /var/lib/acme/*/cert.pem,
r /var/lib/acme/*/chain.pem,
r /var/lib/acme/*/fullchain.pem,
'' + lib.concatMapStringsSep "\n" etcRule [
"ssl/certs/ca-certificates.crt"
"ssl/certs/ca-bundle.crt"
"pki/tls/certs/ca-bundle.crt"
{ path = "ssl/trust"; trail = "/"; }
{ path = "ssl/trust"; trail = "/*"; }
{ path = "ssl/trust/anchors"; trail = "/"; }
{ path = "ssl/trust/anchors"; trail = "/**"; }
{ path = "pki/trust"; trail = "/"; }
{ path = "pki/trust"; trail = "/*"; }
{ path = "pki/trust/anchors"; trail = "/"; }
{ path = "pki/trust/anchors"; trail = "/**"; }
];
"abstractions/ssl_keys" = ''
# security.acme NixOS module
r /var/lib/acme/*/full.pem,
r /var/lib/acme/*/key.pem,
'';
"abstractions/vulkan" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/vulkan"
${etcRule { path = "vulkan/icd.d"; trail = "/"; }}
${etcRule { path = "vulkan/icd.d"; trail = "/*.json"; }}
'';
"abstractions/winbind" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/winbind"
${etcRule { path = "samba"; trail = "/smb.conf"; }}
${etcRule { path = "samba"; trail = "/dhcp.conf"; }}
'';
"abstractions/X" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/X"
${etcRule { path = "X11/cursors"; trail = "/"; }}
${etcRule { path = "X11/cursors"; trail = "/**"; }}
'';
};
}

View file

@ -0,0 +1,11 @@
{ config, lib, pkgs, ... }:
let apparmor = config.security.apparmor; in
{
config.security.apparmor.packages = [ pkgs.apparmor-profiles ];
config.security.apparmor.policies."bin.ping".profile = lib.mkIf apparmor.policies."bin.ping".enable ''
include "${pkgs.iputils.apparmor}/bin.ping"
include "${pkgs.inetutils.apparmor}/bin.ping"
# Note that including those two profiles in the same profile
# would not work if the second one were to re-include <tunables/global>.
'';
}

View file

@ -0,0 +1,123 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.audit;
enabled = cfg.enable == "lock" || cfg.enable;
failureModes = {
silent = 0;
printk = 1;
panic = 2;
};
disableScript = pkgs.writeScript "audit-disable" ''
#!${pkgs.runtimeShell} -eu
# Explicitly disable everything, as otherwise journald might start it.
auditctl -D
auditctl -e 0 -a task,never
'';
# TODO: it seems like people like their rules to be somewhat secret, yet they will not be if
# put in the store like this. At the same time, it doesn't feel like a huge deal and working
# around that is a pain so I'm leaving it like this for now.
startScript = pkgs.writeScript "audit-start" ''
#!${pkgs.runtimeShell} -eu
# Clear out any rules we may start with
auditctl -D
# Put the rules in a temporary file owned and only readable by root
rulesfile="$(mktemp)"
${concatMapStrings (x: "echo '${x}' >> $rulesfile\n") cfg.rules}
# Apply the requested rules
auditctl -R "$rulesfile"
# Enable and configure auditing
auditctl \
-e ${if cfg.enable == "lock" then "2" else "1"} \
-b ${toString cfg.backlogLimit} \
-f ${toString failureModes.${cfg.failureMode}} \
-r ${toString cfg.rateLimit}
'';
stopScript = pkgs.writeScript "audit-stop" ''
#!${pkgs.runtimeShell} -eu
# Clear the rules
auditctl -D
# Disable auditing
auditctl -e 0
'';
in {
options = {
security.audit = {
enable = mkOption {
type = types.enum [ false true "lock" ];
default = false;
description = ''
Whether to enable the Linux audit system. The special `lock' value can be used to
enable auditing and prevent disabling it until a restart. Be careful about locking
this, as it will prevent you from changing your audit configuration until you
restart. If possible, test your configuration using build-vm beforehand.
'';
};
failureMode = mkOption {
type = types.enum [ "silent" "printk" "panic" ];
default = "printk";
description = "How to handle critical errors in the auditing system";
};
backlogLimit = mkOption {
type = types.int;
default = 64; # Apparently the kernel default
description = ''
The maximum number of outstanding audit buffers allowed; exceeding this is
considered a failure and handled in a manner specified by failureMode.
'';
};
rateLimit = mkOption {
type = types.int;
default = 0;
description = ''
The maximum messages per second permitted before triggering a failure as
specified by failureMode. Setting it to zero disables the limit.
'';
};
rules = mkOption {
type = types.listOf types.str; # (types.either types.str (types.submodule rule));
default = [];
example = [ "-a exit,always -F arch=b64 -S execve" ];
description = ''
The ordered audit rules, with each string appearing as one line of the audit.rules file.
'';
};
};
};
config = {
systemd.services.audit = {
description = "Kernel Auditing";
wantedBy = [ "basic.target" ];
unitConfig = {
ConditionVirtualization = "!container";
ConditionSecurity = [ "audit" ];
};
path = [ pkgs.audit ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "@${if enabled then startScript else disableScript} audit-start";
ExecStop = "@${stopScript} audit-stop";
};
};
};
}

View file

@ -0,0 +1,31 @@
{ config, lib, pkgs, ... }:
with lib;
{
options.security.auditd.enable = mkEnableOption "the Linux Audit daemon";
config = mkIf config.security.auditd.enable {
boot.kernelParams = [ "audit=1" ];
environment.systemPackages = [ pkgs.audit ];
systemd.services.auditd = {
description = "Linux Audit daemon";
wantedBy = [ "basic.target" ];
unitConfig = {
ConditionVirtualization = "!container";
ConditionSecurity = [ "audit" ];
DefaultDependencies = false;
};
path = [ pkgs.audit ];
serviceConfig = {
ExecStartPre="${pkgs.coreutils}/bin/mkdir -p /var/log/audit";
ExecStart = "${pkgs.audit}/bin/auditd -l -n -s nochange";
};
};
};
}

View file

@ -0,0 +1,89 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.pki;
cacertPackage = pkgs.cacert.override {
blacklist = cfg.caCertificateBlacklist;
extraCertificateFiles = cfg.certificateFiles;
extraCertificateStrings = cfg.certificates;
};
caBundle = "${cacertPackage}/etc/ssl/certs/ca-bundle.crt";
in
{
options = {
security.pki.certificateFiles = mkOption {
type = types.listOf types.path;
default = [];
example = literalExpression ''[ "''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ]'';
description = ''
A list of files containing trusted root certificates in PEM
format. These are concatenated to form
<filename>/etc/ssl/certs/ca-certificates.crt</filename>, which is
used by many programs that use OpenSSL, such as
<command>curl</command> and <command>git</command>.
'';
};
security.pki.certificates = mkOption {
type = types.listOf types.str;
default = [];
example = literalExpression ''
[ '''
NixOS.org
=========
-----BEGIN CERTIFICATE-----
MIIGUDCCBTigAwIBAgIDD8KWMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ
TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0
...
-----END CERTIFICATE-----
'''
]
'';
description = ''
A list of trusted root certificates in PEM format.
'';
};
security.pki.caCertificateBlacklist = mkOption {
type = types.listOf types.str;
default = [];
example = [
"WoSign" "WoSign China"
"CA WoSign ECC Root"
"Certification Authority of WoSign G2"
];
description = ''
A list of blacklisted CA certificate names that won't be imported from
the Mozilla Trust Store into
<filename>/etc/ssl/certs/ca-certificates.crt</filename>. Use the
names from that file.
'';
};
};
config = {
# NixOS canonical location + Debian/Ubuntu/Arch/Gentoo compatibility.
environment.etc."ssl/certs/ca-certificates.crt".source = caBundle;
# Old NixOS compatibility.
environment.etc."ssl/certs/ca-bundle.crt".source = caBundle;
# CentOS/Fedora compatibility.
environment.etc."pki/tls/certs/ca-bundle.crt".source = caBundle;
# P11-Kit trust source.
environment.etc."ssl/trust-source".source = "${cacertPackage.p11kit}/etc/ssl/trust-source";
};
}

View file

@ -0,0 +1,38 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.chromiumSuidSandbox;
sandbox = pkgs.chromium.sandbox;
in
{
imports = [
(mkRenamedOptionModule [ "programs" "unity3d" "enable" ] [ "security" "chromiumSuidSandbox" "enable" ])
];
options.security.chromiumSuidSandbox.enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to install the Chromium SUID sandbox which is an executable that
Chromium may use in order to achieve sandboxing.
If you get the error "The SUID sandbox helper binary was found, but is not
configured correctly.", turning this on might help.
Also, if the URL chrome://sandbox tells you that "You are not adequately
sandboxed!", turning this on might resolve the issue.
'';
};
config = mkIf cfg.enable {
environment.systemPackages = [ sandbox ];
security.wrappers.${sandbox.passthru.sandboxExecutableName} =
{ setuid = true;
owner = "root";
group = "root";
source = "${sandbox}/bin/${sandbox.passthru.sandboxExecutableName}";
};
};
}

View file

@ -0,0 +1,177 @@
{ config, lib, options, pkgs, ... }:
let
inherit (lib) literalExpression mkOption types;
cfg = config.security.dhparams;
opt = options.security.dhparams;
bitType = types.addCheck types.int (b: b >= 16) // {
name = "bits";
description = "integer of at least 16 bits";
};
paramsSubmodule = { name, config, ... }: {
options.bits = mkOption {
type = bitType;
default = cfg.defaultBitSize;
defaultText = literalExpression "config.${opt.defaultBitSize}";
description = ''
The bit size for the prime that is used during a Diffie-Hellman
key exchange.
'';
};
options.path = mkOption {
type = types.path;
readOnly = true;
description = ''
The resulting path of the generated Diffie-Hellman parameters
file for other services to reference. This could be either a
store path or a file inside the directory specified by
<option>security.dhparams.path</option>.
'';
};
config.path = let
generated = pkgs.runCommand "dhparams-${name}.pem" {
nativeBuildInputs = [ pkgs.openssl ];
} "openssl dhparam -out \"$out\" ${toString config.bits}";
in if cfg.stateful then "${cfg.path}/${name}.pem" else generated;
};
in {
options = {
security.dhparams = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to generate new DH params and clean up old DH params.
'';
};
params = mkOption {
type = with types; let
coerce = bits: { inherit bits; };
in attrsOf (coercedTo int coerce (submodule paramsSubmodule));
default = {};
example = lib.literalExpression "{ nginx.bits = 3072; }";
description = ''
Diffie-Hellman parameters to generate.
The value is the size (in bits) of the DH params to generate. The
generated DH params path can be found in
<literal>config.security.dhparams.params.<replaceable>name</replaceable>.path</literal>.
<note><para>The name of the DH params is taken as being the name of
the service it serves and the params will be generated before the
said service is started.</para></note>
<warning><para>If you are removing all dhparams from this list, you
have to leave <option>security.dhparams.enable</option> for at
least one activation in order to have them be cleaned up. This also
means if you rollback to a version without any dhparams the
existing ones won't be cleaned up. Of course this only applies if
<option>security.dhparams.stateful</option> is
<literal>true</literal>.</para></warning>
<note><title>For module implementers:</title><para>It's recommended
to not set a specific bit size here, so that users can easily
override this by setting
<option>security.dhparams.defaultBitSize</option>.</para></note>
'';
};
stateful = mkOption {
type = types.bool;
default = true;
description = ''
Whether generation of Diffie-Hellman parameters should be stateful or
not. If this is enabled, PEM-encoded files for Diffie-Hellman
parameters are placed in the directory specified by
<option>security.dhparams.path</option>. Otherwise the files are
created within the Nix store.
<note><para>If this is <literal>false</literal> the resulting store
path will be non-deterministic and will be rebuilt every time the
<package>openssl</package> package changes.</para></note>
'';
};
defaultBitSize = mkOption {
type = bitType;
default = 2048;
description = ''
This allows to override the default bit size for all of the
Diffie-Hellman parameters set in
<option>security.dhparams.params</option>.
'';
};
path = mkOption {
type = types.str;
default = "/var/lib/dhparams";
description = ''
Path to the directory in which Diffie-Hellman parameters will be
stored. This only is relevant if
<option>security.dhparams.stateful</option> is
<literal>true</literal>.
'';
};
};
};
config = lib.mkIf (cfg.enable && cfg.stateful) {
systemd.services = {
dhparams-init = {
description = "Clean Up Old Diffie-Hellman Parameters";
# Clean up even when no DH params is set
wantedBy = [ "multi-user.target" ];
serviceConfig.RemainAfterExit = true;
serviceConfig.Type = "oneshot";
script = ''
if [ ! -d ${cfg.path} ]; then
mkdir -p ${cfg.path}
fi
# Remove old dhparams
for file in ${cfg.path}/*; do
if [ ! -f "$file" ]; then
continue
fi
${lib.concatStrings (lib.mapAttrsToList (name: { bits, path, ... }: ''
if [ "$file" = ${lib.escapeShellArg path} ] && \
${pkgs.openssl}/bin/openssl dhparam -in "$file" -text \
| head -n 1 | grep "(${toString bits} bit)" > /dev/null; then
continue
fi
'') cfg.params)}
rm $file
done
# TODO: Ideally this would be removing the *former* cfg.path, though
# this does not seem really important as changes to it are quite
# unlikely
rmdir --ignore-fail-on-non-empty ${cfg.path}
'';
};
} // lib.mapAttrs' (name: { bits, path, ... }: lib.nameValuePair "dhparams-gen-${name}" {
description = "Generate Diffie-Hellman Parameters for ${name}";
after = [ "dhparams-init.service" ];
before = [ "${name}.service" ];
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExists = "!${path}";
serviceConfig.Type = "oneshot";
script = ''
mkdir -p ${lib.escapeShellArg cfg.path}
${pkgs.openssl}/bin/openssl dhparam -out ${lib.escapeShellArg path} \
${toString bits}
'';
}) cfg.params;
};
meta.maintainers = with lib.maintainers; [ ekleog ];
}

View file

@ -0,0 +1,288 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.doas;
inherit (pkgs) doas;
mkUsrString = user: toString user;
mkGrpString = group: ":${toString group}";
mkOpts = rule: concatStringsSep " " [
(optionalString rule.noPass "nopass")
(optionalString rule.noLog "nolog")
(optionalString rule.persist "persist")
(optionalString rule.keepEnv "keepenv")
"setenv { SSH_AUTH_SOCK TERMINFO TERMINFO_DIRS ${concatStringsSep " " rule.setEnv} }"
];
mkArgs = rule:
if (isNull rule.args) then ""
else if (length rule.args == 0) then "args"
else "args ${concatStringsSep " " rule.args}";
mkRule = rule:
let
opts = mkOpts rule;
as = optionalString (!isNull rule.runAs) "as ${rule.runAs}";
cmd = optionalString (!isNull rule.cmd) "cmd ${rule.cmd}";
args = mkArgs rule;
in
optionals (length cfg.extraRules > 0) [
(
optionalString (length rule.users > 0)
(map (usr: "permit ${opts} ${mkUsrString usr} ${as} ${cmd} ${args}") rule.users)
)
(
optionalString (length rule.groups > 0)
(map (grp: "permit ${opts} ${mkGrpString grp} ${as} ${cmd} ${args}") rule.groups)
)
];
in
{
###### interface
options.security.doas = {
enable = mkOption {
type = with types; bool;
default = false;
description = ''
Whether to enable the <command>doas</command> command, which allows
non-root users to execute commands as root.
'';
};
wheelNeedsPassword = mkOption {
type = with types; bool;
default = true;
description = ''
Whether users of the <code>wheel</code> group must provide a password to
run commands as super user via <command>doas</command>.
'';
};
extraRules = mkOption {
default = [];
description = ''
Define specific rules to be set in the
<filename>/etc/doas.conf</filename> file. More specific rules should
come after more general ones in order to yield the expected behavior.
You can use <code>mkBefore</code> and/or <code>mkAfter</code> to ensure
this is the case when configuration options are merged.
'';
example = literalExpression ''
[
# Allow execution of any command by any user in group doas, requiring
# a password and keeping any previously-defined environment variables.
{ groups = [ "doas" ]; noPass = false; keepEnv = true; }
# Allow execution of "/home/root/secret.sh" by user `backup` OR user
# `database` OR any member of the group with GID `1006`, without a
# password.
{ users = [ "backup" "database" ]; groups = [ 1006 ];
cmd = "/home/root/secret.sh"; noPass = true; }
# Allow any member of group `bar` to run `/home/baz/cmd1.sh` as user
# `foo` with argument `hello-doas`.
{ groups = [ "bar" ]; runAs = "foo";
cmd = "/home/baz/cmd1.sh"; args = [ "hello-doas" ]; }
# Allow any member of group `bar` to run `/home/baz/cmd2.sh` as user
# `foo` with no arguments.
{ groups = [ "bar" ]; runAs = "foo";
cmd = "/home/baz/cmd2.sh"; args = [ ]; }
# Allow user `abusers` to execute "nano" and unset the value of
# SSH_AUTH_SOCK, override the value of ALPHA to 1, and inherit the
# value of BETA from the current environment.
{ users = [ "abusers" ]; cmd = "nano";
setEnv = [ "-SSH_AUTH_SOCK" "ALPHA=1" "BETA" ]; }
]
'';
type = with types; listOf (
submodule {
options = {
noPass = mkOption {
type = with types; bool;
default = false;
description = ''
If <code>true</code>, the user is not required to enter a
password.
'';
};
noLog = mkOption {
type = with types; bool;
default = false;
description = ''
If <code>true</code>, successful executions will not be logged
to
<citerefentry><refentrytitle>syslogd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
'';
};
persist = mkOption {
type = with types; bool;
default = false;
description = ''
If <code>true</code>, do not ask for a password again for some
time after the user successfully authenticates.
'';
};
keepEnv = mkOption {
type = with types; bool;
default = false;
description = ''
If <code>true</code>, environment variables other than those
listed in
<citerefentry><refentrytitle>doas</refentrytitle><manvolnum>1</manvolnum></citerefentry>
are kept when creating the environment for the new process.
'';
};
setEnv = mkOption {
type = with types; listOf str;
default = [];
description = ''
Keep or set the specified variables. Variables may also be
removed with a leading '-' or set using
<code>variable=value</code>. If the first character of
<code>value</code> is a '$', the value to be set is taken from
the existing environment variable of the indicated name. This
option is processed after the default environment has been
created.
NOTE: All rules have <code>setenv { SSH_AUTH_SOCK }</code> by
default. To prevent <code>SSH_AUTH_SOCK</code> from being
inherited, add <code>"-SSH_AUTH_SOCK"</code> anywhere in this
list.
'';
};
users = mkOption {
type = with types; listOf (either str int);
default = [];
description = "The usernames / UIDs this rule should apply for.";
};
groups = mkOption {
type = with types; listOf (either str int);
default = [];
description = "The groups / GIDs this rule should apply for.";
};
runAs = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Which user or group the specified command is allowed to run as.
When set to <code>null</code> (the default), all users are
allowed.
A user can be specified using just the username:
<code>"foo"</code>. It is also possible to only allow running as
a specific group with <code>":bar"</code>.
'';
};
cmd = mkOption {
type = with types; nullOr str;
default = null;
description = ''
The command the user is allowed to run. When set to
<code>null</code> (the default), all commands are allowed.
NOTE: It is best practice to specify absolute paths. If a
relative path is specified, only a restricted PATH will be
searched.
'';
};
args = mkOption {
type = with types; nullOr (listOf str);
default = null;
description = ''
Arguments that must be provided to the command. When set to
<code>[]</code>, the command must be run without any arguments.
'';
};
};
}
);
};
extraConfig = mkOption {
type = with types; lines;
default = "";
description = ''
Extra configuration text appended to <filename>doas.conf</filename>.
'';
};
};
###### implementation
config = mkIf cfg.enable {
security.doas.extraRules = mkOrder 600 [
{
groups = [ "wheel" ];
noPass = !cfg.wheelNeedsPassword;
}
];
security.wrappers.doas =
{ setuid = true;
owner = "root";
group = "root";
source = "${doas}/bin/doas";
};
environment.systemPackages = [
doas
];
security.pam.services.doas = {
allowNullPassword = true;
sshAgentAuth = true;
};
environment.etc."doas.conf" = {
source = pkgs.runCommand "doas-conf"
{
src = pkgs.writeText "doas-conf-in" ''
# To modify this file, set the NixOS options
# `security.doas.extraRules` or `security.doas.extraConfig`. To
# completely replace the contents of this file, use
# `environment.etc."doas.conf"`.
# "root" is allowed to do anything.
permit nopass keepenv root
# extraRules
${concatStringsSep "\n" (lists.flatten (map mkRule cfg.extraRules))}
# extraConfig
${cfg.extraConfig}
'';
preferLocalBuild = true;
}
# Make sure that the doas.conf file is syntactically valid.
"${pkgs.buildPackages.doas}/bin/doas -C $src && cp $src $out";
mode = "0440";
};
};
meta.maintainers = with maintainers; [ cole-h ];
}

View file

@ -0,0 +1,240 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.duosec;
boolToStr = b: if b then "yes" else "no";
configFilePam = ''
[duo]
ikey=${cfg.integrationKey}
host=${cfg.host}
${optionalString (cfg.groups != "") ("groups="+cfg.groups)}
failmode=${cfg.failmode}
pushinfo=${boolToStr cfg.pushinfo}
autopush=${boolToStr cfg.autopush}
prompts=${toString cfg.prompts}
fallback_local_ip=${boolToStr cfg.fallbackLocalIP}
'';
configFileLogin = configFilePam + ''
motd=${boolToStr cfg.motd}
accept_env_factor=${boolToStr cfg.acceptEnvFactor}
'';
in
{
imports = [
(mkRenamedOptionModule [ "security" "duosec" "group" ] [ "security" "duosec" "groups" ])
(mkRenamedOptionModule [ "security" "duosec" "ikey" ] [ "security" "duosec" "integrationKey" ])
(mkRemovedOptionModule [ "security" "duosec" "skey" ] "The insecure security.duosec.skey option has been replaced by a new security.duosec.secretKeyFile option. Use this new option to store a secure copy of your key instead.")
];
options = {
security.duosec = {
ssh.enable = mkOption {
type = types.bool;
default = false;
description = "If enabled, protect SSH logins with Duo Security.";
};
pam.enable = mkOption {
type = types.bool;
default = false;
description = "If enabled, protect logins with Duo Security using PAM support.";
};
integrationKey = mkOption {
type = types.str;
description = "Integration key.";
};
secretKeyFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
A file containing your secret key. The security of your Duo application is tied to the security of your secret key.
'';
example = "/run/keys/duo-skey";
};
host = mkOption {
type = types.str;
description = "Duo API hostname.";
};
groups = mkOption {
type = types.str;
default = "";
example = "users,!wheel,!*admin guests";
description = ''
If specified, Duo authentication is required only for users
whose primary group or supplementary group list matches one
of the space-separated pattern lists. Refer to
<link xlink:href="https://duo.com/docs/duounix"/> for details.
'';
};
failmode = mkOption {
type = types.enum [ "safe" "secure" ];
default = "safe";
description = ''
On service or configuration errors that prevent Duo
authentication, fail "safe" (allow access) or "secure" (deny
access). The default is "safe".
'';
};
pushinfo = mkOption {
type = types.bool;
default = false;
description = ''
Include information such as the command to be executed in
the Duo Push message.
'';
};
autopush = mkOption {
type = types.bool;
default = false;
description = ''
If <literal>true</literal>, Duo Unix will automatically send
a push login request to the users phone, falling back on a
phone call if push is unavailable. If
<literal>false</literal>, the user will be prompted to
choose an authentication method. When configured with
<literal>autopush = yes</literal>, we recommend setting
<literal>prompts = 1</literal>.
'';
};
motd = mkOption {
type = types.bool;
default = false;
description = ''
Print the contents of <literal>/etc/motd</literal> to screen
after a successful login.
'';
};
prompts = mkOption {
type = types.enum [ 1 2 3 ];
default = 3;
description = ''
If a user fails to authenticate with a second factor, Duo
Unix will prompt the user to authenticate again. This option
sets the maximum number of prompts that Duo Unix will
display before denying access. Must be 1, 2, or 3. Default
is 3.
For example, when <literal>prompts = 1</literal>, the user
will have to successfully authenticate on the first prompt,
whereas if <literal>prompts = 2</literal>, if the user
enters incorrect information at the initial prompt, he/she
will be prompted to authenticate again.
When configured with <literal>autopush = true</literal>, we
recommend setting <literal>prompts = 1</literal>.
'';
};
acceptEnvFactor = mkOption {
type = types.bool;
default = false;
description = ''
Look for factor selection or passcode in the
<literal>$DUO_PASSCODE</literal> environment variable before
prompting the user for input.
When $DUO_PASSCODE is non-empty, it will override
autopush. The SSH client will need SendEnv DUO_PASSCODE in
its configuration, and the SSH server will similarly need
AcceptEnv DUO_PASSCODE.
'';
};
fallbackLocalIP = mkOption {
type = types.bool;
default = false;
description = ''
Duo Unix reports the IP address of the authorizing user, for
the purposes of authorization and whitelisting. If Duo Unix
cannot detect the IP address of the client, setting
<literal>fallbackLocalIP = yes</literal> will cause Duo Unix
to send the IP address of the server it is running on.
If you are using IP whitelisting, enabling this option could
cause unauthorized logins if the local IP is listed in the
whitelist.
'';
};
allowTcpForwarding = mkOption {
type = types.bool;
default = false;
description = ''
By default, when SSH forwarding, enabling Duo Security will
disable TCP forwarding. By enabling this, you potentially
undermine some of the SSH based login security. Note this is
not needed if you use PAM.
'';
};
};
};
config = mkIf (cfg.ssh.enable || cfg.pam.enable) {
environment.systemPackages = [ pkgs.duo-unix ];
security.wrappers.login_duo =
{ setuid = true;
owner = "root";
group = "root";
source = "${pkgs.duo-unix.out}/bin/login_duo";
};
system.activationScripts = {
login_duo = mkIf cfg.ssh.enable ''
if test -f "${cfg.secretKeyFile}"; then
mkdir -m 0755 -p /etc/duo
umask 0077
conf="$(mktemp)"
{
cat ${pkgs.writeText "login_duo.conf" configFileLogin}
printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
} >"$conf"
chown sshd "$conf"
mv -fT "$conf" /etc/duo/login_duo.conf
fi
'';
pam_duo = mkIf cfg.pam.enable ''
if test -f "${cfg.secretKeyFile}"; then
mkdir -m 0755 -p /etc/duo
umask 0077
conf="$(mktemp)"
{
cat ${pkgs.writeText "login_duo.conf" configFilePam}
printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
} >"$conf"
mv -fT "$conf" /etc/duo/pam_duo.conf
fi
'';
};
/* If PAM *and* SSH are enabled, then don't do anything special.
If PAM isn't used, set the default SSH-only options. */
services.openssh.extraConfig = mkIf (cfg.ssh.enable || cfg.pam.enable) (
if cfg.pam.enable then "UseDNS no" else ''
# Duo Security configuration
ForceCommand ${config.security.wrapperDir}/login_duo
PermitTunnel no
${optionalString (!cfg.allowTcpForwarding) ''
AllowTcpForwarding no
''}
'');
};
}

View file

@ -0,0 +1,71 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.googleOsLogin;
package = pkgs.google-guest-oslogin;
in
{
options = {
security.googleOsLogin.enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Google OS Login.
The OS Login package enables the following components:
AuthorizedKeysCommand to query valid SSH keys from the user's OS Login
profile during ssh authentication phase.
NSS Module to provide user and group information
PAM Module for the sshd service, providing authorization and
authentication support, allowing the system to use data stored in
Google Cloud IAM permissions to control both, the ability to log into
an instance, and to perform operations as root (sudo).
'';
};
};
config = mkIf cfg.enable {
security.pam.services.sshd = {
makeHomeDir = true;
googleOsLoginAccountVerification = true;
googleOsLoginAuthentication = true;
};
security.sudo.extraConfig = ''
#includedir /run/google-sudoers.d
'';
systemd.tmpfiles.rules = [
"d /run/google-sudoers.d 750 root root -"
"d /var/google-users.d 750 root root -"
];
systemd.packages = [ package ];
systemd.timers.google-oslogin-cache.wantedBy = [ "timers.target" ];
# enable the nss module, so user lookups etc. work
system.nssModules = [ package ];
system.nssDatabases.passwd = [ "cache_oslogin" "oslogin" ];
system.nssDatabases.group = [ "cache_oslogin" "oslogin" ];
# Ugly: sshd refuses to start if a store path is given because /nix/store is group-writable.
# So indirect by a symlink.
environment.etc."ssh/authorized_keys_command_google_oslogin" = {
mode = "0755";
text = ''
#!/bin/sh
exec ${package}/bin/google_authorized_keys "$@"
'';
};
services.openssh.authorizedKeysCommand = "/etc/ssh/authorized_keys_command_google_oslogin %u";
services.openssh.authorizedKeysCommandUser = "nobody";
};
}

View file

@ -0,0 +1,58 @@
{ config, pkgs, lib, ... }:
with lib;
{
meta = {
maintainers = [ maintainers.joachifm ];
};
options = {
security.lockKernelModules = mkOption {
type = types.bool;
default = false;
description = ''
Disable kernel module loading once the system is fully initialised.
Module loading is disabled until the next reboot. Problems caused
by delayed module loading can be fixed by adding the module(s) in
question to <option>boot.kernelModules</option>.
'';
};
};
config = mkIf config.security.lockKernelModules {
boot.kernelModules = concatMap (x:
if x.device != null
then
if x.fsType == "vfat"
then [ "vfat" "nls-cp437" "nls-iso8859-1" ]
else [ x.fsType ]
else []) config.system.build.fileSystems;
systemd.services.disable-kernel-module-loading = {
description = "Disable kernel module loading";
wants = [ "systemd-udevd.service" ];
wantedBy = [ config.systemd.defaultUnit ];
after =
[ "firewall.service"
"systemd-modules-load.service"
config.systemd.defaultUnit
];
unitConfig.ConditionPathIsReadWrite = "/proc/sys/kernel";
serviceConfig =
{ Type = "oneshot";
RemainAfterExit = true;
TimeoutSec = 180;
};
script = ''
${pkgs.udev}/bin/udevadm settle
echo -n 1 >/proc/sys/kernel/modules_disabled
'';
};
};
}

View file

@ -0,0 +1,155 @@
{ config, lib, ... }:
with lib;
{
meta = {
maintainers = [ maintainers.joachifm ];
};
imports = [
(lib.mkRenamedOptionModule [ "security" "virtualization" "flushL1DataCache" ] [ "security" "virtualisation" "flushL1DataCache" ])
];
options = {
security.allowUserNamespaces = mkOption {
type = types.bool;
default = true;
description = ''
Whether to allow creation of user namespaces.
The motivation for disabling user namespaces is the potential
presence of code paths where the kernel's permission checking
logic fails to account for namespacing, instead permitting a
namespaced process to act outside the namespace with the same
privileges as it would have inside it. This is particularly
damaging in the common case of running as root within the namespace.
When user namespace creation is disallowed, attempting to create a
user namespace fails with "no space left on device" (ENOSPC).
root may re-enable user namespace creation at runtime.
'';
};
security.unprivilegedUsernsClone = mkOption {
type = types.bool;
default = false;
description = ''
When disabled, unprivileged users will not be able to create new namespaces.
By default unprivileged user namespaces are disabled.
This option only works in a hardened profile.
'';
};
security.protectKernelImage = mkOption {
type = types.bool;
default = false;
description = ''
Whether to prevent replacing the running kernel image.
'';
};
security.allowSimultaneousMultithreading = mkOption {
type = types.bool;
default = true;
description = ''
Whether to allow SMT/hyperthreading. Disabling SMT means that only
physical CPU cores will be usable at runtime, potentially at
significant performance cost.
The primary motivation for disabling SMT is to mitigate the risk of
leaking data between threads running on the same CPU core (due to
e.g., shared caches). This attack vector is unproven.
Disabling SMT is a supplement to the L1 data cache flushing mitigation
(see <xref linkend="opt-security.virtualisation.flushL1DataCache"/>)
versus malicious VM guests (SMT could "bring back" previously flushed
data).
'';
};
security.forcePageTableIsolation = mkOption {
type = types.bool;
default = false;
description = ''
Whether to force-enable the Page Table Isolation (PTI) Linux kernel
feature even on CPU models that claim to be safe from Meltdown.
This hardening feature is most beneficial to systems that run untrusted
workloads that rely on address space isolation for security.
'';
};
security.virtualisation.flushL1DataCache = mkOption {
type = types.nullOr (types.enum [ "never" "cond" "always" ]);
default = null;
description = ''
Whether the hypervisor should flush the L1 data cache before
entering guests.
See also <xref linkend="opt-security.allowSimultaneousMultithreading"/>.
<variablelist>
<varlistentry>
<term><literal>null</literal></term>
<listitem><para>uses the kernel default</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>"never"</literal></term>
<listitem><para>disables L1 data cache flushing entirely.
May be appropriate if all guests are trusted.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>"cond"</literal></term>
<listitem><para>flushes L1 data cache only for pre-determined
code paths. May leak information about the host address space
layout.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>"always"</literal></term>
<listitem><para>flushes L1 data cache every time the hypervisor
enters the guest. May incur significant performance cost.
</para></listitem>
</varlistentry>
</variablelist>
'';
};
};
config = mkMerge [
(mkIf (!config.security.allowUserNamespaces) {
# Setting the number of allowed user namespaces to 0 effectively disables
# the feature at runtime. Note that root may raise the limit again
# at any time.
boot.kernel.sysctl."user.max_user_namespaces" = 0;
assertions = [
{ assertion = config.nix.settings.sandbox -> config.security.allowUserNamespaces;
message = "`nix.settings.sandbox = true` conflicts with `!security.allowUserNamespaces`.";
}
];
})
(mkIf config.security.unprivilegedUsernsClone {
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = mkDefault true;
})
(mkIf config.security.protectKernelImage {
# Disable hibernation (allows replacing the running kernel)
boot.kernelParams = [ "nohibernate" ];
# Prevent replacing the running kernel image w/o reboot
boot.kernel.sysctl."kernel.kexec_load_disabled" = mkDefault true;
})
(mkIf (!config.security.allowSimultaneousMultithreading) {
boot.kernelParams = [ "nosmt" ];
})
(mkIf config.security.forcePageTableIsolation {
boot.kernelParams = [ "pti=on" ];
})
(mkIf (config.security.virtualisation.flushL1DataCache != null) {
boot.kernelParams = [ "kvm-intel.vmentry_l1d_flush=${config.security.virtualisation.flushL1DataCache}" ];
})
];
}

View file

@ -0,0 +1,50 @@
# This module provides configuration for the OATH PAM modules.
{ lib, ... }:
with lib;
{
options = {
security.pam.oath = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable the OATH (one-time password) PAM module.
'';
};
digits = mkOption {
type = types.enum [ 6 7 8 ];
default = 6;
description = ''
Specify the length of the one-time password in number of
digits.
'';
};
window = mkOption {
type = types.int;
default = 5;
description = ''
Specify the number of one-time passwords to check in order
to accommodate for situations where the system and the
client are slightly out of sync (iteration for HOTP or time
steps for TOTP).
'';
};
usersFile = mkOption {
type = types.path;
default = "/etc/users.oath";
description = ''
Set the path to file where the user's credentials are
stored. This file must not be world readable!
'';
};
};
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,174 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.pam.mount;
oflRequired = cfg.logoutHup || cfg.logoutTerm || cfg.logoutKill;
fake_ofl = pkgs.writeShellScriptBin "fake_ofl" ''
SIGNAL=$1
MNTPT=$2
${pkgs.lsof}/bin/lsof | ${pkgs.gnugrep}/bin/grep $MNTPT | ${pkgs.gawk}/bin/awk '{print $2}' | ${pkgs.findutils}/bin/xargs ${pkgs.util-linux}/bin/kill -$SIGNAL
'';
anyPamMount = any (attrByPath ["pamMount"] false) (attrValues config.security.pam.services);
in
{
options = {
security.pam.mount = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable PAM mount system to mount fileystems on user login.
'';
};
extraVolumes = mkOption {
type = types.listOf types.str;
default = [];
description = ''
List of volume definitions for pam_mount.
For more information, visit <link
xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />.
'';
};
additionalSearchPaths = mkOption {
type = types.listOf types.package;
default = [];
example = literalExpression "[ pkgs.bindfs ]";
description = ''
Additional programs to include in the search path of pam_mount.
Useful for example if you want to use some FUSE filesystems like bindfs.
'';
};
fuseMountOptions = mkOption {
type = types.listOf types.str;
default = [];
example = literalExpression ''
[ "nodev" "nosuid" "force-user=%(USER)" "gid=%(USERGID)" "perms=0700" "chmod-deny" "chown-deny" "chgrp-deny" ]
'';
description = ''
Global mount options that apply to every FUSE volume.
You can define volume-specific options in the volume definitions.
'';
};
debugLevel = mkOption {
type = types.int;
default = 0;
example = 1;
description = ''
Sets the Debug-Level. 0 disables debugging, 1 enables pam_mount tracing,
and 2 additionally enables tracing in mount.crypt. The default is 0.
For more information, visit <link
xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />.
'';
};
logoutWait = mkOption {
type = types.int;
default = 0;
description = ''
Amount of microseconds to wait until killing remaining processes after
final logout.
For more information, visit <link
xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />.
'';
};
logoutHup = mkOption {
type = types.bool;
default = false;
description = ''
Kill remaining processes after logout by sending a SIGHUP.
'';
};
logoutTerm = mkOption {
type = types.bool;
default = false;
description = ''
Kill remaining processes after logout by sending a SIGTERM.
'';
};
logoutKill = mkOption {
type = types.bool;
default = false;
description = ''
Kill remaining processes after logout by sending a SIGKILL.
'';
};
createMountPoints = mkOption {
type = types.bool;
default = true;
description = ''
Create mountpoints for volumes if they do not exist.
'';
};
removeCreatedMountPoints = mkOption {
type = types.bool;
default = true;
description = ''
Remove mountpoints created by pam_mount after logout. This
only affects mountpoints that have been created by pam_mount
in the same session.
'';
};
};
};
config = mkIf (cfg.enable || anyPamMount) {
environment.systemPackages = [ pkgs.pam_mount ];
environment.etc."security/pam_mount.conf.xml" = {
source =
let
extraUserVolumes = filterAttrs (n: u: u.cryptHomeLuks != null || u.pamMount != {}) config.users.users;
mkAttr = k: v: ''${k}="${v}"'';
userVolumeEntry = user: let
attrs = {
user = user.name;
path = user.cryptHomeLuks;
mountpoint = user.home;
} // user.pamMount;
in
"<volume ${concatStringsSep " " (mapAttrsToList mkAttr attrs)} />\n";
in
pkgs.writeText "pam_mount.conf.xml" ''
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE pam_mount SYSTEM "pam_mount.conf.xml.dtd">
<!-- auto generated from Nixos: modules/config/users-groups.nix -->
<pam_mount>
<debug enable="${toString cfg.debugLevel}" />
<!-- if activated, requires ofl from hxtools to be present -->
<logout wait="${toString cfg.logoutWait}" hup="${if cfg.logoutHup then "yes" else "no"}" term="${if cfg.logoutTerm then "yes" else "no"}" kill="${if cfg.logoutKill then "yes" else "no"}" />
<!-- set PATH variable for pam_mount module -->
<path>${makeBinPath ([ pkgs.util-linux ] ++ cfg.additionalSearchPaths)}</path>
<!-- create mount point if not present -->
<mkmountpoint enable="${if cfg.createMountPoints then "1" else "0"}" remove="${if cfg.removeCreatedMountPoints then "true" else "false"}" />
<!-- specify the binaries to be called -->
<fusemount>${pkgs.fuse}/bin/mount.fuse %(VOLUME) %(MNTPT) -o ${concatStringsSep "," (cfg.fuseMountOptions ++ [ "%(OPTIONS)" ])}</fusemount>
<fuseumount>${pkgs.fuse}/bin/fusermount -u %(MNTPT)</fuseumount>
<cryptmount>${pkgs.pam_mount}/bin/mount.crypt %(VOLUME) %(MNTPT)</cryptmount>
<cryptumount>${pkgs.pam_mount}/bin/umount.crypt %(MNTPT)</cryptumount>
<pmvarrun>${pkgs.pam_mount}/bin/pmvarrun -u %(USER) -o %(OPERATION)</pmvarrun>
${optionalString oflRequired "<ofl>${fake_ofl}/bin/fake_ofl %(SIGNAL) %(MNTPT)</ofl>"}
${concatStrings (map userVolumeEntry (attrValues extraUserVolumes))}
${concatStringsSep "\n" cfg.extraVolumes}
</pam_mount>
'';
};
};
}

View file

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.pam.usb;
anyUsbAuth = any (attrByPath ["usbAuth"] false) (attrValues config.security.pam.services);
in
{
options = {
security.pam.usb = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable USB login for all login systems that support it. For
more information, visit <link
xlink:href="https://github.com/aluzzardi/pam_usb/wiki/Getting-Started#setting-up-devices-and-users" />.
'';
};
};
};
config = mkIf (cfg.enable || anyUsbAuth) {
# Make sure pmount and pumount are setuid wrapped.
security.wrappers = {
pmount =
{ setuid = true;
owner = "root";
group = "root";
source = "${pkgs.pmount.out}/bin/pmount";
};
pumount =
{ setuid = true;
owner = "root";
group = "root";
source = "${pkgs.pmount.out}/bin/pumount";
};
};
environment.systemPackages = [ pkgs.pmount ];
};
}

View file

@ -0,0 +1,112 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.polkit;
in
{
options = {
security.polkit.enable = mkEnableOption "polkit";
security.polkit.extraConfig = mkOption {
type = types.lines;
default = "";
example =
''
/* Log authorization checks. */
polkit.addRule(function(action, subject) {
polkit.log("user " + subject.user + " is attempting action " + action.id + " from PID " + subject.pid);
});
/* Allow any local user to do anything (dangerous!). */
polkit.addRule(function(action, subject) {
if (subject.local) return "yes";
});
'';
description =
''
Any polkit rules to be added to config (in JavaScript ;-). See:
http://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html#polkit-rules
'';
};
security.polkit.adminIdentities = mkOption {
type = types.listOf types.str;
default = [ "unix-group:wheel" ];
example = [ "unix-user:alice" "unix-group:admin" ];
description =
''
Specifies which users are considered administrators, for those
actions that require the user to authenticate as an
administrator (i.e. have an <literal>auth_admin</literal>
value). By default, this is all users in the <literal>wheel</literal> group.
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.polkit.bin pkgs.polkit.out ];
systemd.packages = [ pkgs.polkit.out ];
systemd.services.polkit.restartTriggers = [ config.system.path ];
systemd.services.polkit.stopIfChanged = false;
# The polkit daemon reads action/rule files
environment.pathsToLink = [ "/share/polkit-1" ];
# PolKit rules for NixOS.
environment.etc."polkit-1/rules.d/10-nixos.rules".text =
''
polkit.addAdminRule(function(action, subject) {
return [${concatStringsSep ", " (map (i: "\"${i}\"") cfg.adminIdentities)}];
});
${cfg.extraConfig}
''; #TODO: validation on compilation (at least against typos)
services.dbus.packages = [ pkgs.polkit.out ];
security.pam.services.polkit-1 = {};
security.wrappers = {
pkexec =
{ setuid = true;
owner = "root";
group = "root";
source = "${pkgs.polkit.bin}/bin/pkexec";
};
polkit-agent-helper-1 =
{ setuid = true;
owner = "root";
group = "root";
source = "${pkgs.polkit.out}/lib/polkit-1/polkit-agent-helper-1";
};
};
systemd.tmpfiles.rules = [
# Probably no more needed, clean up
"R /var/lib/polkit-1"
"R /var/lib/PolicyKit"
];
users.users.polkituser = {
description = "PolKit daemon";
uid = config.ids.uids.polkituser;
group = "polkituser";
};
users.groups.polkituser = {};
};
}

View file

@ -0,0 +1,16 @@
{ lib, ... }:
let
removed = k: lib.mkRemovedOptionModule [ "security" "rngd" k ];
in
{
imports = [
(removed "enable" ''
rngd is not necessary for any device that the kernel recognises
as an hardware RNG, as it will automatically run the krngd task
to periodically collect random data from the device and mix it
into the kernel's RNG.
'')
(removed "debug"
"The rngd module was removed, so its debug option does nothing.")
];
}

View file

@ -0,0 +1,47 @@
# A module for rtkit, a DBus system service that hands out realtime
# scheduling priority to processes that ask for it.
{ config, lib, pkgs, ... }:
with lib;
{
options = {
security.rtkit.enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the RealtimeKit system service, which hands
out realtime scheduling priority to user processes on
demand. For example, the PulseAudio server uses this to
acquire realtime priority.
'';
};
};
config = mkIf config.security.rtkit.enable {
security.polkit.enable = true;
# To make polkit pickup rtkit policies
environment.systemPackages = [ pkgs.rtkit ];
systemd.packages = [ pkgs.rtkit ];
services.dbus.packages = [ pkgs.rtkit ];
users.users.rtkit =
{
isSystemUser = true;
group = "rtkit";
description = "RealtimeKit daemon";
};
users.groups.rtkit = {};
};
}

View file

@ -0,0 +1,265 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.security.sudo;
inherit (pkgs) sudo;
toUserString = user: if (isInt user) then "#${toString user}" else "${user}";
toGroupString = group: if (isInt group) then "%#${toString group}" else "%${group}";
toCommandOptionsString = options:
"${concatStringsSep ":" options}${optionalString (length options != 0) ":"} ";
toCommandsString = commands:
concatStringsSep ", " (
map (command:
if (isString command) then
command
else
"${toCommandOptionsString command.options}${command.command}"
) commands
);
in
{
###### interface
options = {
security.sudo.enable = mkOption {
type = types.bool;
default = true;
description =
''
Whether to enable the <command>sudo</command> command, which
allows non-root users to execute commands as root.
'';
};
security.sudo.package = mkOption {
type = types.package;
default = pkgs.sudo;
defaultText = literalExpression "pkgs.sudo";
description = ''
Which package to use for `sudo`.
'';
};
security.sudo.wheelNeedsPassword = mkOption {
type = types.bool;
default = true;
description =
''
Whether users of the <code>wheel</code> group must
provide a password to run commands as super user via <command>sudo</command>.
'';
};
security.sudo.execWheelOnly = mkOption {
type = types.bool;
default = false;
description = ''
Only allow members of the <code>wheel</code> group to execute sudo by
setting the executable's permissions accordingly.
This prevents users that are not members of <code>wheel</code> from
exploiting vulnerabilities in sudo such as CVE-2021-3156.
'';
};
security.sudo.configFile = mkOption {
type = types.lines;
# Note: if syntax errors are detected in this file, the NixOS
# configuration will fail to build.
description =
''
This string contains the contents of the
<filename>sudoers</filename> file.
'';
};
security.sudo.extraRules = mkOption {
description = ''
Define specific rules to be in the <filename>sudoers</filename> file.
More specific rules should come after more general ones in order to
yield the expected behavior. You can use mkBefore/mkAfter to ensure
this is the case when configuration options are merged.
'';
default = [];
example = literalExpression ''
[
# Allow execution of any command by all users in group sudo,
# requiring a password.
{ groups = [ "sudo" ]; commands = [ "ALL" ]; }
# Allow execution of "/home/root/secret.sh" by user `backup`, `database`
# and the group with GID `1006` without a password.
{ users = [ "backup" "database" ]; groups = [ 1006 ];
commands = [ { command = "/home/root/secret.sh"; options = [ "SETENV" "NOPASSWD" ]; } ]; }
# Allow all users of group `bar` to run two executables as user `foo`
# with arguments being pre-set.
{ groups = [ "bar" ]; runAs = "foo";
commands =
[ "/home/baz/cmd1.sh hello-sudo"
{ command = '''/home/baz/cmd2.sh ""'''; options = [ "SETENV" ]; } ]; }
]
'';
type = with types; listOf (submodule {
options = {
users = mkOption {
type = with types; listOf (either str int);
description = ''
The usernames / UIDs this rule should apply for.
'';
default = [];
};
groups = mkOption {
type = with types; listOf (either str int);
description = ''
The groups / GIDs this rule should apply for.
'';
default = [];
};
host = mkOption {
type = types.str;
default = "ALL";
description = ''
For what host this rule should apply.
'';
};
runAs = mkOption {
type = with types; str;
default = "ALL:ALL";
description = ''
Under which user/group the specified command is allowed to run.
A user can be specified using just the username: <code>"foo"</code>.
It is also possible to specify a user/group combination using <code>"foo:bar"</code>
or to only allow running as a specific group with <code>":bar"</code>.
'';
};
commands = mkOption {
description = ''
The commands for which the rule should apply.
'';
type = with types; listOf (either str (submodule {
options = {
command = mkOption {
type = with types; str;
description = ''
A command being either just a path to a binary to allow any arguments,
the full command with arguments pre-set or with <code>""</code> used as the argument,
not allowing arguments to the command at all.
'';
};
options = mkOption {
type = with types; listOf (enum [ "NOPASSWD" "PASSWD" "NOEXEC" "EXEC" "SETENV" "NOSETENV" "LOG_INPUT" "NOLOG_INPUT" "LOG_OUTPUT" "NOLOG_OUTPUT" ]);
description = ''
Options for running the command. Refer to the <a href="https://www.sudo.ws/man/1.7.10/sudoers.man.html">sudo manual</a>.
'';
default = [];
};
};
}));
};
};
});
};
security.sudo.extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration text appended to <filename>sudoers</filename>.
'';
};
};
###### implementation
config = mkIf cfg.enable {
# We `mkOrder 600` so that the default rule shows up first, but there is
# still enough room for a user to `mkBefore` it.
security.sudo.extraRules = mkOrder 600 [
{ groups = [ "wheel" ];
commands = [ { command = "ALL"; options = (if cfg.wheelNeedsPassword then [ "SETENV" ] else [ "NOPASSWD" "SETENV" ]); } ];
}
];
security.sudo.configFile =
''
# Don't edit this file. Set the NixOS options security.sudo.configFile
# or security.sudo.extraRules instead.
# Keep SSH_AUTH_SOCK so that pam_ssh_agent_auth.so can do its magic.
Defaults env_keep+=SSH_AUTH_SOCK
# "root" is allowed to do anything.
root ALL=(ALL:ALL) SETENV: ALL
# extraRules
${concatStringsSep "\n" (
lists.flatten (
map (
rule: if (length rule.commands != 0) then [
(map (user: "${toUserString user} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.users)
(map (group: "${toGroupString group} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.groups)
] else []
) cfg.extraRules
)
)}
${cfg.extraConfig}
'';
security.wrappers = let
owner = "root";
group = if cfg.execWheelOnly then "wheel" else "root";
setuid = true;
permissions = if cfg.execWheelOnly then "u+rx,g+x" else "u+rx,g+x,o+x";
in {
sudo = {
source = "${cfg.package.out}/bin/sudo";
inherit owner group setuid permissions;
};
sudoedit = {
source = "${cfg.package.out}/bin/sudoedit";
inherit owner group setuid permissions;
};
};
environment.systemPackages = [ sudo ];
security.pam.services.sudo = { sshAgentAuth = true; usshAuth = true; };
environment.etc.sudoers =
{ source =
pkgs.runCommand "sudoers"
{
src = pkgs.writeText "sudoers-in" cfg.configFile;
preferLocalBuild = true;
}
# Make sure that the sudoers file is syntactically valid.
# (currently disabled - NIXOS-66)
"${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out";
mode = "0440";
};
};
}

View file

@ -0,0 +1,202 @@
{ config, pkgs, lib, utils, ... }:
let
toplevelConfig = config;
inherit (lib) types;
inherit (utils.systemdUtils.lib) mkPathSafeName;
in {
options.systemd.services = lib.mkOption {
type = types.attrsOf (types.submodule ({ name, config, ... }: {
options.confinement.enable = lib.mkOption {
type = types.bool;
default = false;
description = ''
If set, all the required runtime store paths for this service are
bind-mounted into a <literal>tmpfs</literal>-based <citerefentry>
<refentrytitle>chroot</refentrytitle>
<manvolnum>2</manvolnum>
</citerefentry>.
'';
};
options.confinement.fullUnit = lib.mkOption {
type = types.bool;
default = false;
description = ''
Whether to include the full closure of the systemd unit file into the
chroot, instead of just the dependencies for the executables.
<warning><para>While it may be tempting to just enable this option to
make things work quickly, please be aware that this might add paths
to the closure of the chroot that you didn't anticipate. It's better
to use <option>confinement.packages</option> to <emphasis
role="strong">explicitly</emphasis> add additional store paths to the
chroot.</para></warning>
'';
};
options.confinement.packages = lib.mkOption {
type = types.listOf (types.either types.str types.package);
default = [];
description = let
mkScOption = optName: "<option>serviceConfig.${optName}</option>";
in ''
Additional packages or strings with context to add to the closure of
the chroot. By default, this includes all the packages from the
${lib.concatMapStringsSep ", " mkScOption [
"ExecReload" "ExecStartPost" "ExecStartPre" "ExecStop"
"ExecStopPost"
]} and ${mkScOption "ExecStart"} options. If you want to have all the
dependencies of this systemd unit, you can use
<option>confinement.fullUnit</option>.
<note><para>The store paths listed in <option>path</option> are
<emphasis role="strong">not</emphasis> included in the closure as
well as paths from other options except those listed
above.</para></note>
'';
};
options.confinement.binSh = lib.mkOption {
type = types.nullOr types.path;
default = toplevelConfig.environment.binsh;
defaultText = lib.literalExpression "config.environment.binsh";
example = lib.literalExpression ''"''${pkgs.dash}/bin/dash"'';
description = ''
The program to make available as <filename>/bin/sh</filename> inside
the chroot. If this is set to <literal>null</literal>, no
<filename>/bin/sh</filename> is provided at all.
This is useful for some applications, which for example use the
<citerefentry>
<refentrytitle>system</refentrytitle>
<manvolnum>3</manvolnum>
</citerefentry> library function to execute commands.
'';
};
options.confinement.mode = lib.mkOption {
type = types.enum [ "full-apivfs" "chroot-only" ];
default = "full-apivfs";
description = ''
The value <literal>full-apivfs</literal> (the default) sets up
private <filename class="directory">/dev</filename>, <filename
class="directory">/proc</filename>, <filename
class="directory">/sys</filename> and <filename
class="directory">/tmp</filename> file systems in a separate user
name space.
If this is set to <literal>chroot-only</literal>, only the file
system name space is set up along with the call to <citerefentry>
<refentrytitle>chroot</refentrytitle>
<manvolnum>2</manvolnum>
</citerefentry>.
<note><para>This doesn't cover network namespaces and is solely for
file system level isolation.</para></note>
'';
};
config = let
rootName = "${mkPathSafeName name}-chroot";
inherit (config.confinement) binSh fullUnit;
wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs");
in lib.mkIf config.confinement.enable {
serviceConfig = {
RootDirectory = "/var/empty";
TemporaryFileSystem = "/";
PrivateMounts = lib.mkDefault true;
# https://github.com/NixOS/nixpkgs/issues/14645 is a future attempt
# to change some of these to default to true.
#
# If we run in chroot-only mode, having something like PrivateDevices
# set to true by default will mount /dev within the chroot, whereas
# with "chroot-only" it's expected that there are no /dev, /proc and
# /sys file systems available.
#
# However, if this suddenly becomes true, the attack surface will
# increase, so let's explicitly set these options to true/false
# depending on the mode.
MountAPIVFS = wantsAPIVFS;
PrivateDevices = wantsAPIVFS;
PrivateTmp = wantsAPIVFS;
PrivateUsers = wantsAPIVFS;
ProtectControlGroups = wantsAPIVFS;
ProtectKernelModules = wantsAPIVFS;
ProtectKernelTunables = wantsAPIVFS;
};
confinement.packages = let
execOpts = [
"ExecReload" "ExecStart" "ExecStartPost" "ExecStartPre" "ExecStop"
"ExecStopPost"
];
execPkgs = lib.concatMap (opt: let
isSet = config.serviceConfig ? ${opt};
in lib.flatten (lib.optional isSet config.serviceConfig.${opt})) execOpts;
unitAttrs = toplevelConfig.systemd.units."${name}.service";
allPkgs = lib.singleton (builtins.toJSON unitAttrs);
unitPkgs = if fullUnit then allPkgs else execPkgs;
in unitPkgs ++ lib.optional (binSh != null) binSh;
};
}));
};
config.assertions = lib.concatLists (lib.mapAttrsToList (name: cfg: let
whatOpt = optName: "The 'serviceConfig' option '${optName}' for"
+ " service '${name}' is enabled in conjunction with"
+ " 'confinement.enable'";
in lib.optionals cfg.confinement.enable [
{ assertion = !cfg.serviceConfig.RootDirectoryStartOnly or false;
message = "${whatOpt "RootDirectoryStartOnly"}, but right now systemd"
+ " doesn't support restricting bind-mounts to 'ExecStart'."
+ " Please either define a separate service or find a way to run"
+ " commands other than ExecStart within the chroot.";
}
{ assertion = !cfg.serviceConfig.DynamicUser or false;
message = "${whatOpt "DynamicUser"}. Please create a dedicated user via"
+ " the 'users.users' option instead as this combination is"
+ " currently not supported.";
}
{ assertion = cfg.serviceConfig ? ProtectSystem -> cfg.serviceConfig.ProtectSystem == false;
message = "${whatOpt "ProtectSystem"}. ProtectSystem is not compatible"
+ " with service confinement as it fails to remount /usr within"
+ " our chroot. Please disable the option.";
}
]) config.systemd.services);
config.systemd.packages = lib.concatLists (lib.mapAttrsToList (name: cfg: let
rootPaths = let
contents = lib.concatStringsSep "\n" cfg.confinement.packages;
in pkgs.writeText "${mkPathSafeName name}-string-contexts.txt" contents;
chrootPaths = pkgs.runCommand "${mkPathSafeName name}-chroot-paths" {
closureInfo = pkgs.closureInfo { inherit rootPaths; };
serviceName = "${name}.service";
excludedPath = rootPaths;
} ''
mkdir -p "$out/lib/systemd/system/$serviceName.d"
serviceFile="$out/lib/systemd/system/$serviceName.d/confinement.conf"
echo '[Service]' > "$serviceFile"
# /bin/sh is special here, because the option value could contain a
# symlink and we need to properly resolve it.
${lib.optionalString (cfg.confinement.binSh != null) ''
binsh=${lib.escapeShellArg cfg.confinement.binSh}
realprog="$(readlink -e "$binsh")"
echo "BindReadOnlyPaths=$realprog:/bin/sh" >> "$serviceFile"
''}
while read storePath; do
if [ -L "$storePath" ]; then
# Currently, systemd can't cope with symlinks in Bind(ReadOnly)Paths,
# so let's just bind-mount the target to that location.
echo "BindReadOnlyPaths=$(readlink -e "$storePath"):$storePath"
elif [ "$storePath" != "$excludedPath" ]; then
echo "BindReadOnlyPaths=$storePath"
fi
done < "$closureInfo/store-paths" >> "$serviceFile"
'';
in lib.optional cfg.confinement.enable chrootPaths) config.systemd.services);
}

View file

@ -0,0 +1,184 @@
{ lib, pkgs, config, ... }:
let
cfg = config.security.tpm2;
# This snippet is taken from tpm2-tss/dist/tpm-udev.rules, but modified to allow custom user/groups
# The idea is that the tssUser is allowed to acess the TPM and kernel TPM resource manager, while
# the tssGroup is only allowed to access the kernel resource manager
# Therefore, if either of the two are null, the respective part isn't generated
udevRules = tssUser: tssGroup: ''
${lib.optionalString (tssUser != null) ''KERNEL=="tpm[0-9]*", MODE="0660", OWNER="${tssUser}"''}
${lib.optionalString (tssUser != null || tssGroup != null)
''KERNEL=="tpmrm[0-9]*", MODE="0660"''
+ lib.optionalString (tssUser != null) '', OWNER="${tssUser}"''
+ lib.optionalString (tssGroup != null) '', GROUP="${tssGroup}"''
}
'';
in {
options.security.tpm2 = {
enable = lib.mkEnableOption "Trusted Platform Module 2 support";
tssUser = lib.mkOption {
description = ''
Name of the tpm device-owner and service user, set if applyUdevRules is
set.
'';
type = lib.types.nullOr lib.types.str;
default = if cfg.abrmd.enable then "tss" else "root";
defaultText = lib.literalExpression ''if config.security.tpm2.abrmd.enable then "tss" else "root"'';
};
tssGroup = lib.mkOption {
description = ''
Group of the tpm kernel resource manager (tpmrm) device-group, set if
applyUdevRules is set.
'';
type = lib.types.nullOr lib.types.str;
default = "tss";
};
applyUdevRules = lib.mkOption {
description = ''
Whether to make the /dev/tpm[0-9] devices accessible by the tssUser, or
the /dev/tpmrm[0-9] by tssGroup respectively
'';
type = lib.types.bool;
default = true;
};
abrmd = {
enable = lib.mkEnableOption ''
Trusted Platform 2 userspace resource manager daemon
'';
package = lib.mkOption {
description = "tpm2-abrmd package to use";
type = lib.types.package;
default = pkgs.tpm2-abrmd;
defaultText = lib.literalExpression "pkgs.tpm2-abrmd";
};
};
pkcs11 = {
enable = lib.mkEnableOption ''
TPM2 PKCS#11 tool and shared library in system path
(<literal>/run/current-system/sw/lib/libtpm2_pkcs11.so</literal>)
'';
package = lib.mkOption {
description = "tpm2-pkcs11 package to use";
type = lib.types.package;
default = pkgs.tpm2-pkcs11;
defaultText = lib.literalExpression "pkgs.tpm2-pkcs11";
};
};
tctiEnvironment = {
enable = lib.mkOption {
description = ''
Set common TCTI environment variables to the specified value.
The variables are
<itemizedlist>
<listitem>
<para>
<literal>TPM2TOOLS_TCTI</literal>
</para>
</listitem>
<listitem>
<para>
<literal>TPM2_PKCS11_TCTI</literal>
</para>
</listitem>
</itemizedlist>
'';
type = lib.types.bool;
default = false;
};
interface = lib.mkOption {
description = ''
The name of the TPM command transmission interface (TCTI) library to
use.
'';
type = lib.types.enum [ "tabrmd" "device" ];
default = "device";
};
deviceConf = lib.mkOption {
description = ''
Configuration part of the device TCTI, e.g. the path to the TPM device.
Applies if interface is set to "device".
The format is specified in the
<link xlink:href="https://github.com/tpm2-software/tpm2-tools/blob/master/man/common/tcti.md#tcti-options">
tpm2-tools repository</link>.
'';
type = lib.types.str;
default = "/dev/tpmrm0";
};
tabrmdConf = lib.mkOption {
description = ''
Configuration part of the tabrmd TCTI, like the D-Bus bus name.
Applies if interface is set to "tabrmd".
The format is specified in the
<link xlink:href="https://github.com/tpm2-software/tpm2-tools/blob/master/man/common/tcti.md#tcti-options">
tpm2-tools repository</link>.
'';
type = lib.types.str;
default = "bus_name=com.intel.tss2.Tabrmd";
};
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
# PKCS11 tools and library
environment.systemPackages = lib.mkIf cfg.pkcs11.enable [
(lib.getBin cfg.pkcs11.package)
(lib.getLib cfg.pkcs11.package)
];
services.udev.extraRules = lib.mkIf cfg.applyUdevRules
(udevRules cfg.tssUser cfg.tssGroup);
# Create the tss user and group only if the default value is used
users.users.${cfg.tssUser} = lib.mkIf (cfg.tssUser == "tss") {
isSystemUser = true;
group = "tss";
};
users.groups.${cfg.tssGroup} = lib.mkIf (cfg.tssGroup == "tss") {};
environment.variables = lib.mkIf cfg.tctiEnvironment.enable (
lib.attrsets.genAttrs [
"TPM2TOOLS_TCTI"
"TPM2_PKCS11_TCTI"
] (_: ''${cfg.tctiEnvironment.interface}:${
if cfg.tctiEnvironment.interface == "tabrmd" then
cfg.tctiEnvironment.tabrmdConf
else
cfg.tctiEnvironment.deviceConf
}'')
);
}
(lib.mkIf cfg.abrmd.enable {
systemd.services."tpm2-abrmd" = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "dbus";
Restart = "always";
RestartSec = 30;
BusName = "com.intel.tss2.Tabrmd";
ExecStart = "${cfg.abrmd.package}/bin/tpm2-abrmd";
User = "tss";
Group = "tss";
};
};
services.dbus.packages = lib.singleton cfg.abrmd.package;
})
]);
meta.maintainers = with lib.maintainers; [ lschuermann ];
}

View file

@ -0,0 +1,305 @@
{ 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;
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</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 = 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}/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.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 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 =
let
mkSetuidRoot = source:
{ setuid = true;
owner = "root";
group = "root";
inherit source;
};
in
{ # These are mount related wrappers that require the +s permission.
fusermount = mkSetuidRoot "${pkgs.fuse}/bin/fusermount";
fusermount3 = mkSetuidRoot "${pkgs.fuse3}/bin/fusermount3";
mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
umount = mkSetuidRoot "${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
]}"
'';
###### wrappers activation script
system.activationScripts.wrappers =
lib.stringAfter [ "specialfs" "users" ]
''
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"
'');
};
}

View file

@ -0,0 +1,237 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdnoreturn.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <linux/capability.h>
#include <sys/prctl.h>
#include <limits.h>
#include <stdint.h>
#include <syscall.h>
#include <byteswap.h>
#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))
extern char **environ;
// The WRAPPER_DIR macro is supplied at compile time so that it cannot
// be changed at runtime
static char *wrapper_dir = WRAPPER_DIR;
// Wrapper debug variable name
static char *wrapper_debug = "WRAPPER_DEBUG";
#define CAP_SETPCAP 8
#if __BYTE_ORDER == __BIG_ENDIAN
#define LE32_TO_H(x) bswap_32(x)
#else
#define LE32_TO_H(x) (x)
#endif
static noreturn void assert_failure(const char *assertion) {
fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion);
fflush(stderr);
abort();
}
int get_last_cap(unsigned *last_cap) {
FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r");
if (file == NULL) {
int saved_errno = errno;
fprintf(stderr, "failed to open /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
return -saved_errno;
}
int res = fscanf(file, "%u", last_cap);
if (res == EOF) {
int saved_errno = errno;
fprintf(stderr, "could not read number from /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
return -saved_errno;
}
fclose(file);
return 0;
}
// Given the path to this program, fetch its configured capability set
// (as set by `setcap ... /path/to/file`) and raise those capabilities
// into the Ambient set.
static int make_caps_ambient(const char *self_path) {
struct vfs_ns_cap_data data = {};
int r = getxattr(self_path, "security.capability", &data, sizeof(data));
if (r < 0) {
if (errno == ENODATA) {
// no capabilities set
return 0;
}
fprintf(stderr, "cannot get capabilities for %s: %s", self_path, strerror(errno));
return 1;
}
size_t size;
uint32_t version = LE32_TO_H(data.magic_etc) & VFS_CAP_REVISION_MASK;
switch (version) {
case VFS_CAP_REVISION_1:
size = VFS_CAP_U32_1;
break;
case VFS_CAP_REVISION_2:
case VFS_CAP_REVISION_3:
size = VFS_CAP_U32_3;
break;
default:
fprintf(stderr, "BUG! Unsupported capability version 0x%x on %s. Report to NixOS bugtracker\n", version, self_path);
return 1;
}
const struct __user_cap_header_struct header = {
.version = _LINUX_CAPABILITY_VERSION_3,
.pid = getpid(),
};
struct __user_cap_data_struct user_data[2] = {};
for (size_t i = 0; i < size; i++) {
// merge inheritable & permitted into one
user_data[i].permitted = user_data[i].inheritable =
LE32_TO_H(data.data[i].inheritable) | LE32_TO_H(data.data[i].permitted);
}
if (syscall(SYS_capset, &header, &user_data) < 0) {
fprintf(stderr, "failed to inherit capabilities: %s", strerror(errno));
return 1;
}
unsigned last_cap;
r = get_last_cap(&last_cap);
if (r < 0) {
return 1;
}
uint64_t set = user_data[0].permitted | (uint64_t)user_data[1].permitted << 32;
for (unsigned cap = 0; cap < last_cap; cap++) {
if (!(set & (1ULL << cap))) {
continue;
}
// Check for the cap_setpcap capability, we set this on the
// wrapper so it can elevate the capabilities to the Ambient
// set but we do not want to propagate it down into the
// wrapped program.
//
// TODO: what happens if that's the behavior you want
// though???? I'm preferring a strict vs. loose policy here.
if (cap == CAP_SETPCAP) {
if(getenv(wrapper_debug)) {
fprintf(stderr, "cap_setpcap in set, skipping it\n");
}
continue;
}
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, (unsigned long) cap, 0, 0)) {
fprintf(stderr, "cannot raise the capability %d into the ambient set: %s\n", cap, strerror(errno));
return 1;
}
if (getenv(wrapper_debug)) {
fprintf(stderr, "raised %d into the ambient capability set\n", cap);
}
}
return 0;
}
int readlink_malloc(const char *p, char **ret) {
size_t l = FILENAME_MAX+1;
int r;
for (;;) {
char *c = calloc(l, sizeof(char));
if (!c) {
return -ENOMEM;
}
ssize_t n = readlink(p, c, l-1);
if (n < 0) {
r = -errno;
free(c);
return r;
}
if ((size_t) n < l-1) {
c[n] = 0;
*ret = c;
return 0;
}
free(c);
l *= 2;
}
}
int main(int argc, char **argv) {
ASSERT(argc >= 1);
char *self_path = NULL;
int self_path_size = readlink_malloc("/proc/self/exe", &self_path);
if (self_path_size < 0) {
fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size));
}
// Make sure that we are being executed from the right location,
// i.e., `safe_wrapper_dir'. This is to prevent someone from creating
// hard link `X' from some other location, along with a false
// `X.real' file, to allow arbitrary programs from being executed
// with elevated capabilities.
int len = strlen(wrapper_dir);
if (len > 0 && '/' == wrapper_dir[len - 1])
--len;
ASSERT(!strncmp(self_path, wrapper_dir, len));
ASSERT('/' == wrapper_dir[0]);
ASSERT('/' == self_path[len]);
// Make *really* *really* sure that we were executed as
// `self_path', and not, say, as some other setuid program. That
// is, our effective uid/gid should match the uid/gid of
// `self_path'.
struct stat st;
ASSERT(lstat(self_path, &st) != -1);
ASSERT(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid()));
ASSERT(!(st.st_mode & S_ISGID) || (st.st_gid == getegid()));
// And, of course, we shouldn't be writable.
ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH)));
// Read the path of the real (wrapped) program from <self>.real.
char real_fn[PATH_MAX + 10];
int real_fn_size = snprintf(real_fn, sizeof(real_fn), "%s.real", self_path);
ASSERT(real_fn_size < sizeof(real_fn));
int fd_self = open(real_fn, O_RDONLY);
ASSERT(fd_self != -1);
char source_prog[PATH_MAX];
len = read(fd_self, source_prog, PATH_MAX);
ASSERT(len != -1);
ASSERT(len < sizeof(source_prog));
ASSERT(len > 0);
source_prog[len] = 0;
close(fd_self);
// Read the capabilities set on the wrapper and raise them in to
// the ambient set so the program we're wrapping receives the
// capabilities too!
if (make_caps_ambient(self_path) != 0) {
free(self_path);
return 1;
}
free(self_path);
execve(source_prog, argv, environ);
fprintf(stderr, "%s: cannot run `%s': %s\n",
argv[0], source_prog, strerror(errno));
return 1;
}

View file

@ -0,0 +1,21 @@
{ stdenv, linuxHeaders, parentWrapperDir, debug ? false }:
# For testing:
# $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { parentWrapperDir = "/run/wrappers"; debug = true; }'
stdenv.mkDerivation {
name = "security-wrapper";
buildInputs = [ linuxHeaders ];
dontUnpack = true;
hardeningEnable = [ "pie" ];
CFLAGS = [
''-DWRAPPER_DIR="${parentWrapperDir}"''
] ++ (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
'';
}