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:
commit
56de2bcd43
30691 changed files with 3076956 additions and 0 deletions
921
nixos/modules/security/acme/default.nix
Normal file
921
nixos/modules/security/acme/default.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
413
nixos/modules/security/acme/doc.xml
Normal file
413
nixos/modules/security/acme/doc.xml
Normal 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 & 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 > /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 > /var/lib/secrets/certs.secret << 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 &
|
||||
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>
|
||||
|
|
@ -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}";
|
||||
}
|
||||
216
nixos/modules/security/apparmor.nix
Normal file
216
nixos/modules/security/apparmor.nix
Normal 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 ];
|
||||
}
|
||||
317
nixos/modules/security/apparmor/includes.nix
Normal file
317
nixos/modules/security/apparmor/includes.nix
Normal 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 = "/**"; }}
|
||||
'';
|
||||
};
|
||||
}
|
||||
11
nixos/modules/security/apparmor/profiles.nix
Normal file
11
nixos/modules/security/apparmor/profiles.nix
Normal 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>.
|
||||
'';
|
||||
}
|
||||
123
nixos/modules/security/audit.nix
Normal file
123
nixos/modules/security/audit.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
31
nixos/modules/security/auditd.nix
Normal file
31
nixos/modules/security/auditd.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
89
nixos/modules/security/ca.nix
Normal file
89
nixos/modules/security/ca.nix
Normal 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";
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
38
nixos/modules/security/chromium-suid-sandbox.nix
Normal file
38
nixos/modules/security/chromium-suid-sandbox.nix
Normal 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}";
|
||||
};
|
||||
};
|
||||
}
|
||||
177
nixos/modules/security/dhparams.nix
Normal file
177
nixos/modules/security/dhparams.nix
Normal 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 ];
|
||||
}
|
||||
288
nixos/modules/security/doas.nix
Normal file
288
nixos/modules/security/doas.nix
Normal 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 ];
|
||||
}
|
||||
240
nixos/modules/security/duosec.nix
Normal file
240
nixos/modules/security/duosec.nix
Normal 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 user’s 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
|
||||
''}
|
||||
'');
|
||||
};
|
||||
}
|
||||
71
nixos/modules/security/google_oslogin.nix
Normal file
71
nixos/modules/security/google_oslogin.nix
Normal 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";
|
||||
};
|
||||
|
||||
}
|
||||
58
nixos/modules/security/lock-kernel-modules.nix
Normal file
58
nixos/modules/security/lock-kernel-modules.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
155
nixos/modules/security/misc.nix
Normal file
155
nixos/modules/security/misc.nix
Normal 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}" ];
|
||||
})
|
||||
];
|
||||
}
|
||||
50
nixos/modules/security/oath.nix
Normal file
50
nixos/modules/security/oath.nix
Normal 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!
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
1259
nixos/modules/security/pam.nix
Normal file
1259
nixos/modules/security/pam.nix
Normal file
File diff suppressed because it is too large
Load diff
174
nixos/modules/security/pam_mount.nix
Normal file
174
nixos/modules/security/pam_mount.nix
Normal 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>
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
52
nixos/modules/security/pam_usb.nix
Normal file
52
nixos/modules/security/pam_usb.nix
Normal 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 ];
|
||||
|
||||
};
|
||||
}
|
||||
112
nixos/modules/security/polkit.nix
Normal file
112
nixos/modules/security/polkit.nix
Normal 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 = {};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
16
nixos/modules/security/rngd.nix
Normal file
16
nixos/modules/security/rngd.nix
Normal 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.")
|
||||
];
|
||||
}
|
||||
47
nixos/modules/security/rtkit.nix
Normal file
47
nixos/modules/security/rtkit.nix
Normal 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 = {};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
265
nixos/modules/security/sudo.nix
Normal file
265
nixos/modules/security/sudo.nix
Normal 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";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
202
nixos/modules/security/systemd-confinement.nix
Normal file
202
nixos/modules/security/systemd-confinement.nix
Normal 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);
|
||||
}
|
||||
184
nixos/modules/security/tpm2.nix
Normal file
184
nixos/modules/security/tpm2.nix
Normal 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 ];
|
||||
}
|
||||
305
nixos/modules/security/wrappers/default.nix
Normal file
305
nixos/modules/security/wrappers/default.nix
Normal 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"
|
||||
'');
|
||||
};
|
||||
}
|
||||
237
nixos/modules/security/wrappers/wrapper.c
Normal file
237
nixos/modules/security/wrappers/wrapper.c
Normal 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;
|
||||
}
|
||||
21
nixos/modules/security/wrappers/wrapper.nix
Normal file
21
nixos/modules/security/wrappers/wrapper.nix
Normal 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
|
||||
'';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue