push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
# SSL/TLS Certificates with ACME {#module-security-acme}
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
[lego](https://go-acme.github.io/lego/) is used under
the hood.
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.
## Prerequisites {#module-security-acme-prerequisites}
To use the ACME module, you must accept the provider's terms of service
by setting [](#opt-security.acme.acceptTerms)
to `true`. The Let's Encrypt ToS can be found
[here](https://letsencrypt.org/repository/).
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
[](#opt-security.acme.defaults.email)
and/or on a per-cert basis with
[](#opt-security.acme.certs._name_.email).
This address is only used for registration and renewal reminders,
and cannot be used to administer the certificates in any way.
Alternatively, you can use a different ACME server by changing the
[](#opt-security.acme.defaults.server) option
to a provider of your choosing, or just change the server for one cert with
[](#opt-security.acme.certs._name_.server).
You will need an HTTP server or DNS server for verification. For HTTP,
the server must have a webroot defined that can serve
{file}`.well-known/acme-challenge`. 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.
## Using ACME certificates in Nginx {#module-security-acme-nginx}
NixOS supports fetching ACME certificates for you by setting
`enableACME = true;` 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
`foo.example.com` the config would look like this:
```nix
{
security.acme.acceptTerms = true;
security.acme.defaults.email = "admin+acme@example.com";
services.nginx = {
enable = true;
virtualHosts = {
"foo.example.com" = {
forceSSL = true;
enableACME = true;
# All serverAliases will be added as extra domain names on the certificate.
serverAliases = [ "bar.example.com" ];
locations."/" = {
root = "/var/www";
};
};
# We can also add a different vhost and reuse the same certificate
# but we have to append extraDomainNames manually beforehand:
# security.acme.certs."foo.example.com".extraDomainNames = [ "baz.example.com" ];
"baz.example.com" = {
forceSSL = true;
useACMEHost = "foo.example.com";
locations."/" = {
root = "/var/www";
};
};
};
};
}
```
## Using ACME certificates in Apache/httpd {#module-security-acme-httpd}
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.
## Manual configuration of HTTP-01 validation {#module-security-acme-configuring}
First off you will need to set up a virtual host to serve the challenges.
This example uses a vhost called `certs.example.com`, with
the intent that you will generate certs for all your vhosts and redirect
everyone to HTTPS.
```nix
{
security.acme.acceptTerms = true;
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.
users.users.nginx.extraGroups = [ "acme" ];
services.nginx = {
enable = true;
virtualHosts = {
"acmechallenge.example.com" = {
# Catchall vhost, will redirect users to HTTPS for all vhosts
serverAliases = [ "*.example.com" ];
locations."/.well-known/acme-challenge" = {
root = "/var/lib/acme/.challenges";
};
locations."/" = {
return = "301 https://$host$request_uri";
};
};
};
};
# Alternative config for Apache
users.users.wwwrun.extraGroups = [ "acme" ];
services.httpd = {
enable = true;
virtualHosts = {
"acmechallenge.example.com" = {
# Catchall vhost, will redirect users to HTTPS for all vhosts
serverAliases = [ "*.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.
documentRoot = "/var/lib/acme/.challenges";
extraConfig = ''
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC]
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301]
'';
};
};
};
}
```
Now you need to configure ACME to generate a certificate.
```nix
{
security.acme.certs."foo.example.com" = {
webroot = "/var/lib/acme/.challenges";
email = "foo@example.com";
# Ensure that the web server you use can read the generated certs
# Take a look at the group option for the web server you choose.
group = "nginx";
# Since we have a wildcard vhost to handle port 80,
# we can generate certs for anything!
# Just make sure your DNS resolves them.
extraDomainNames = [ "mail.example.com" ];
};
}
```
The private key {file}`key.pem` and certificate
{file}`fullchain.pem` will be put into
{file}`/var/lib/acme/foo.example.com`.
Refer to [](#ch-options) for all available configuration
options for the [security.acme](#opt-security.acme.certs)
module.
## Configuring ACME for DNS validation {#module-security-acme-config-dns}
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 [lego docs](https://go-acme.github.io/lego/dns/)
for provider/server specific configuration values. For the sake of these
docs, we will provide a fully self-hosted example using bind.
```nix
{
services.bind = {
enable = true;
extraConfig = ''
include "/var/lib/secrets/dnskeys.conf";
'';
zones = [
rec {
name = "example.com";
file = "/var/db/bind/${name}";
master = true;
extraConfig = "allow-update { key rfc2136key.example.com.; };";
}
];
};
# Now we can configure ACME
security.acme.acceptTerms = true;
security.acme.defaults.email = "admin+acme@example.com";
security.acme.certs."example.com" = {
domain = "*.example.com";
dnsProvider = "rfc2136";
environmentFile = "/var/lib/secrets/certs.secret";
# We don't need to wait for propagation since this is a local DNS server
dnsPropagationCheck = false;
};
}
```
The {file}`dnskeys.conf` and {file}`certs.secret`
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:
```nix
{
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 = 77;
};
path = [ pkgs.bind ];
script = ''
mkdir -p /var/lib/secrets
chmod 755 /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
# extract secret value from the dnskeys.conf
while read x y; do if [ "$x" = "secret" ]; then secret="''${y:1:''${#y}-3}"; fi; done < /var/lib/secrets/dnskeys.conf
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='$secret'
EOF
chmod 400 /var/lib/secrets/certs.secret
'';
};
}
```
Now you're all set to generate certs! You should monitor the first invocation
by running `systemctl start acme-example.com.service &
journalctl -fu acme-example.com.service` and watching its log output.
## Using DNS validation with web server virtual hosts {#module-security-acme-config-dns-with-vhosts}
It is possible to use DNS-01 validation with all certificates,
including those automatically configured via the Nginx/Apache
[`enableACME`](#opt-services.nginx.virtualHosts._name_.enableACME)
option. This configuration pattern is fully
supported and part of the module's test suite for Nginx + Apache.
You must follow the guide above on configuring DNS-01 validation
first, however instead of setting the options for one certificate
(e.g. [](#opt-security.acme.certs._name_.dnsProvider))
you will set them as defaults
(e.g. [](#opt-security.acme.defaults.dnsProvider)).
```nix
{
# Configure ACME appropriately
security.acme.acceptTerms = true;
security.acme.defaults.email = "admin+acme@example.com";
security.acme.defaults = {
dnsProvider = "rfc2136";
environmentFile = "/var/lib/secrets/certs.secret";
# We don't need to wait for propagation since this is a local DNS server
dnsPropagationCheck = false;
};
# For each virtual host you would like to use DNS-01 validation with,
# set acmeRoot = null
services.nginx = {
enable = true;
virtualHosts = {
"foo.example.com" = {
enableACME = true;
acmeRoot = null;
};
};
};
}
```
And that's it! Next time your configuration is rebuilt, or when
you add a new virtualHost, it will be DNS-01 validated.
## Using ACME with services demanding root owned certificates {#module-security-acme-root-owned}
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
`acme`), however you can use systemd's
`LoadCredential` feature to resolve this elegantly.
Below is an example configuration for OpenSMTPD, but this pattern
can be applied to any service.
```nix
{
# 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.
systemd.services.opensmtpd.requires = [ "acme-mail.example.com.service" ];
systemd.services.opensmtpd.serviceConfig.LoadCredential =
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
'';
};
}
```
## Regenerating certificates {#module-security-acme-regenerate}
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
`systemctl clean --what=state acme-example.com.service`
will remove all certificate files and the account data for the given domain,
allowing you to then `systemctl start acme-example.com.service`
to generate fresh ones.
## Fixing JWS Verification error {#module-security-acme-fix-jws}
It is possible that your account credentials file may become corrupt and need
to be regenerated. In this scenario lego will produce the error `JWS verification error`.
The solution is to simply delete the associated accounts file and
re-run the affected service(s).
```shell
# 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
```
## Ensuring dependencies for services that need to be reloaded when a certificate challenges {#module-security-acme-reload-dependencies}
Services that depend on ACME certificates and need to be reloaded can use one of two approaches to reload upon successfull certificate acquisition or renewal:
1. **Using the `security.acme.certs.<name>.reloadServices` option**: This will cause `systemctl try-reload-or-restart` to be run for the listed services.
2. **Using a separate reload unit**: if you need perform more complex actions you can implement a separate reload unit but need to ensure that it lists the `acme-renew-<name>.service` unit both as `wantedBy` AND `after`. See the nginx module implementation with its `nginx-config-reload` service.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
lib:
{
cert,
groups,
services,
}:
let
catSep = builtins.concatStringsSep;
svcUser = svc: svc.serviceConfig.User or "root";
svcGroups =
svc:
(lib.optional (svc.serviceConfig ? Group) svc.serviceConfig.Group)
++ lib.toList (svc.serviceConfig.SupplementaryGroups or [ ]);
in
{
assertion = builtins.all (
svc:
svcUser svc == "root"
|| builtins.elem (svcUser svc) groups.${cert.group}.members
|| builtins.elem cert.group (svcGroups svc)
) services;
message = "Certificate ${cert.domain} (group=${cert.group}) must be readable by service(s) ${
catSep ", " (
map (svc: "${svc.name} (user=${svcUser svc} groups=${catSep "," (svcGroups svc)})") services
)
}";
}

View File

@@ -0,0 +1,314 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.agnos;
format = pkgs.formats.toml { };
name = "agnos";
stateDir = "/var/lib/${name}";
accountType =
let
inherit (lib) types mkOption;
in
types.submodule {
freeformType = types.attrsOf format.type;
options = {
email = mkOption {
type = types.str;
description = ''
Email associated with this account.
'';
};
private_key_path = mkOption {
type = types.str;
description = ''
Path of the PEM-encoded private key for this account.
Currently, only RSA keys are supported.
If this path does not exist, then the behavior depends on `generateKeys.enable`.
When this option is `true`,
the key will be automatically generated and saved to this path.
When it is `false`, agnos will fail.
If a relative path is specified,
the key will be looked up (or generated and saved to) under `${stateDir}`.
'';
};
certificates = mkOption {
type = types.listOf certificateType;
description = ''
Certificates for agnos to issue or renew.
'';
};
};
};
certificateType =
let
inherit (lib) types literalExpression mkOption;
in
types.submodule {
freeformType = types.attrsOf format.type;
options = {
domains = mkOption {
type = types.listOf types.str;
description = ''
Domains the certificate represents
'';
example = literalExpression ''["a.example.com", "b.example.com", "*b.example.com"]'';
};
fullchain_output_file = mkOption {
type = types.str;
description = ''
Output path for the full chain including the acquired certificate.
If a relative path is specified, the file will be created in `${stateDir}`.
'';
};
key_output_file = mkOption {
type = types.str;
description = ''
Output path for the certificate private key.
If a relative path is specified, the file will be created in `${stateDir}`.
'';
};
};
};
in
{
options.security.agnos =
let
inherit (lib) types mkEnableOption mkOption;
in
{
enable = mkEnableOption name;
settings = mkOption {
description = "Settings";
type = types.submodule {
freeformType = types.attrsOf format.type;
options = {
dns_listen_addr = mkOption {
type = types.str;
default = "0.0.0.0:53";
description = ''
Address for agnos to listen on.
Note that this needs to be reachable by the outside world,
and 53 is required in most situations
since `NS` records do not allow specifying the port.
'';
};
accounts = mkOption {
type = types.listOf accountType;
description = ''
A list of ACME accounts.
Each account is associated with an email address
and can be used to obtain an arbitrary amount of certificate
(subject to provider's rate limits,
see e.g. [Let's Encrypt Rate Limits](https://letsencrypt.org/docs/rate-limits/)).
'';
};
};
};
};
generateKeys = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable automatic generation of account keys.
When this is `true`, a key will be generated for each account where
the file referred to by the `private_key` path does not exist yet.
Currently, only RSA keys can be generated.
'';
};
keySize = mkOption {
type = types.int;
default = 4096;
description = ''
Key size in bits to use when generating new keys.
'';
};
};
server = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
ACME Directory Resource URI. Defaults to Let's Encrypt's production endpoint,
`https://acme-v02.api.letsencrypt.org/directory`, if unset.
'';
};
serverCa = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
The root certificate (in PEM format) of the ACME server's HTTPS interface.
'';
};
persistent = mkOption {
type = types.bool;
default = true;
description = ''
When `true`, use a persistent systemd timer.
'';
};
startAt = mkOption {
type = types.either types.str (types.listOf types.str);
default = "daily";
example = "02:00";
description = ''
How often or when to run agnos.
The format is described in
{manpage}`systemd.time(7)`.
'';
};
temporarilyOpenFirewall = mkOption {
type = types.bool;
default = false;
description = ''
When `true`, will open the port specified in `settings.dns_listen_addr`
before running the agnos service, and close it when agnos finishes running.
'';
};
group = mkOption {
type = types.str;
default = name;
description = ''
Group to run Agnos as. The acquired certificates will be owned by this group.
'';
};
user = mkOption {
type = types.str;
default = name;
description = ''
User to run Agnos as. The acquired certificates will be owned by this user.
'';
};
};
config =
let
configFile = format.generate "agnos.toml" cfg.settings;
port = lib.toInt (lib.last (builtins.split ":" cfg.settings.dns_listen_addr));
useNftables = config.networking.nftables.enable;
# nftables implementation for temporarilyOpenFirewall
nftablesSetup = pkgs.writeShellScript "agnos-fw-setup" ''
${lib.getExe pkgs.nftables} add element inet nixos-fw temp-ports "{ tcp . ${toString port} }"
${lib.getExe pkgs.nftables} add element inet nixos-fw temp-ports "{ udp . ${toString port} }"
'';
nftablesTeardown = pkgs.writeShellScript "agnos-fw-teardown" ''
${lib.getExe pkgs.nftables} delete element inet nixos-fw temp-ports "{ tcp . ${toString port} }"
${lib.getExe pkgs.nftables} delete element inet nixos-fw temp-ports "{ udp . ${toString port} }"
'';
# iptables implementation for temporarilyOpenFirewall
helpers = ''
function ip46tables() {
${lib.getExe' pkgs.iptables "iptables"} -w "$@"
${lib.getExe' pkgs.iptables "ip6tables"} -w "$@"
}
'';
fwFilter = ''--dport ${toString port} -j ACCEPT -m comment --comment "agnos"'';
iptablesSetup = pkgs.writeShellScript "agnos-fw-setup" ''
${helpers}
ip46tables -I INPUT 1 -p tcp ${fwFilter}
ip46tables -I INPUT 1 -p udp ${fwFilter}
'';
iptablesTeardown = pkgs.writeShellScript "agnos-fw-setup" ''
${helpers}
ip46tables -D INPUT -p tcp ${fwFilter}
ip46tables -D INPUT -p udp ${fwFilter}
'';
in
lib.mkIf cfg.enable {
assertions = [
{
assertion = !cfg.temporarilyOpenFirewall || config.networking.firewall.enable;
message = "temporarilyOpenFirewall is only useful when firewall is enabled";
}
];
systemd.services.agnos = {
serviceConfig = {
ExecStartPre =
lib.optional cfg.generateKeys.enable ''
${pkgs.agnos}/bin/agnos-generate-accounts-keys \
--no-confirm \
--key-size ${toString cfg.generateKeys.keySize} \
${configFile}
''
++ lib.optional cfg.temporarilyOpenFirewall (
"+" + (if useNftables then nftablesSetup else iptablesSetup)
);
ExecStopPost = lib.optional cfg.temporarilyOpenFirewall (
"+" + (if useNftables then nftablesTeardown else iptablesTeardown)
);
ExecStart = ''
${pkgs.agnos}/bin/agnos \
${if cfg.server != null then "--acme-url=${cfg.server}" else "--no-staging"} \
${lib.optionalString (cfg.serverCa != null) "--acme-serv-ca=${cfg.serverCa}"} \
${configFile}
'';
Type = "oneshot";
User = cfg.user;
Group = cfg.group;
StateDirectory = name;
StateDirectoryMode = "0750";
WorkingDirectory = "${stateDir}";
# Allow binding privileged ports if necessary
CapabilityBoundingSet = lib.mkIf (port < 1024) [ "CAP_NET_BIND_SERVICE" ];
AmbientCapabilities = lib.mkIf (port < 1024) [ "CAP_NET_BIND_SERVICE" ];
};
after = [
"firewall.target"
"network-online.target"
"nftables.service"
];
wants = [ "network-online.target" ];
};
systemd.timers.agnos = {
timerConfig = {
OnCalendar = cfg.startAt;
Persistent = cfg.persistent;
Unit = "agnos.service";
};
wantedBy = [ "timers.target" ];
};
users.groups = lib.mkIf (cfg.group == name) {
${cfg.group} = { };
};
users.users = lib.mkIf (cfg.user == name) {
${cfg.user} = {
isSystemUser = true;
description = "Agnos service user";
group = cfg.group;
};
};
};
}

View File

@@ -0,0 +1,278 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
inherit (config.environment) etc;
cfg = config.security.apparmor;
enabledPolicies = lib.filterAttrs (n: p: p.state != "disable") cfg.policies;
buildPolicyPath = n: p: lib.defaultTo (pkgs.writeText n p.profile) p.path;
# Accessing submodule options when not defined results in an error thunk rather than a regular option object
# We can emulate the behavior of `<option>.isDefined` by attempting to evaluate it instead
# This is required because getting isDefined on a submodule is not possible in global module asserts.
submoduleOptionIsDefined = value: (builtins.tryEval value).success;
in
{
imports = [
(lib.mkRemovedOptionModule [
"security"
"apparmor"
"confineSUIDApplications"
] "Please use the new options: `security.apparmor.policies.<policy>.state'.")
(lib.mkRemovedOptionModule [
"security"
"apparmor"
"profiles"
] "Please use the new option: `security.apparmor.policies'.")
apparmor/includes.nix
apparmor/profiles.nix
];
options = {
security.apparmor = {
enable = lib.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 [](#opt-security.apparmor.killUnconfinedConfinables)
if you want this service to do such killing
by sending a `SIGTERM` to those running processes'';
policies = lib.mkOption {
description = ''
AppArmor policies.
'';
type = types.attrsOf (
types.submodule {
options = {
state = lib.mkOption {
description = "How strictly this policy should be enforced";
type = types.enum [
"disable"
"complain"
"enforce"
];
# should enforce really be the default?
# the docs state that this should only be used once one is REALLY sure nothing's gonna break
default = "enforce";
};
profile = lib.mkOption {
description = "The profile file contents. Incompatible with path.";
type = types.lines;
};
path = lib.mkOption {
description = "A path of a profile file to include. Incompatible with profile.";
type = types.nullOr types.path;
default = null;
};
};
}
);
default = { };
};
includes = lib.mkOption {
type = types.attrsOf types.lines;
default = { };
description = ''
List of paths to be added to AppArmor's searched paths
when resolving `include` directives.
'';
apply = lib.mapAttrs pkgs.writeText;
};
packages = lib.mkOption {
type = types.listOf types.package;
default = [ ];
description = "List of packages to be added to AppArmor's include path";
};
enableCache = lib.mkEnableOption ''
caching of AppArmor policies
in `/var/cache/apparmor/`.
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 = lib.mkEnableOption ''
killing of processes which have an AppArmor profile enabled
(in [](#opt-security.apparmor.policies))
but are not confined (because AppArmor can only confine new processes).
This is only sending a gracious `SIGTERM` signal to the processes,
not a `SIGKILL`.
Beware that due to a current limitation of AppArmor,
only profiles with exact paths (and no name) can enable such kills'';
};
};
config = lib.mkIf cfg.enable {
assertions = lib.concatLists (
lib.mapAttrsToList (policyName: policyCfg: [
{
assertion = builtins.match ".*/.*" policyName == null;
message = "`security.apparmor.policies.\"${policyName}\"' 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.
}
{
assertion = lib.xor (policyCfg.path != null) (submoduleOptionIsDefined policyCfg.profile);
message = "`security.apparmor.policies.\"${policyName}\"` must define exactly one of either path or profile.";
}
]) 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/*
lib.mapAttrsToList (name: p: {
inherit name;
path = buildPolicyPath name p;
}) enabledPolicies
++ lib.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
''
+ lib.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 = ${lib.getExe' pkgs.stdenv.cc.libc "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 ${
lib.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.lsm = [ "apparmor" ];
systemd.services.apparmor = {
after = [
"local-fs.target"
"systemd-journald-audit.socket"
];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.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 =
n: p:
"--verbose --show-cache ${
lib.optionalString (p.state == "complain") "--complain "
}${buildPolicyPath n p}";
in
{
Type = "oneshot";
RemainAfterExit = "yes";
ExecStartPre = "${pkgs.apparmor-utils}/bin/aa-teardown";
ExecStart = lib.mapAttrsToList (
n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --add ${commonOpts n p}"
) enabledPolicies;
ExecStartPost = lib.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).
lib.mapAttrsToList (
n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --replace ${commonOpts n 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" ]
++
# Optionally kill the processes which are unconfined but now have a profile loaded
# (because AppArmor can only start to confine new processes).
lib.optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
ExecStop = "${pkgs.apparmor-utils}/bin/aa-teardown";
CacheDirectory = [
"apparmor"
"apparmor/logprof"
];
CacheDirectoryMode = "0700";
};
};
};
meta.maintainers = lib.teams.apparmor.members;
}

View File

@@ -0,0 +1,543 @@
{
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 usually 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,
r ${config.i18n.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
r @{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/nss-systemd" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/nss-systemd"
'';
"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,
r /etc/pki/tls/certs/,
''
+ lib.concatMapStringsSep "\n" etcRule [
"ssl/certs/ca-certificates.crt"
"ssl/certs/ca-bundle.crt"
"pki/tls/certs/ca-bundle.crt"
{
path = "ssl/trust";
trail = "/";
}
{
path = "ssl/trust";
trail = "/*";
}
{
path = "ssl/trust/anchors";
trail = "/";
}
{
path = "ssl/trust/anchors";
trail = "/**";
}
{
path = "pki/trust";
trail = "/";
}
{
path = "pki/trust";
trail = "/*";
}
{
path = "pki/trust/anchors";
trail = "/";
}
{
path = "pki/trust/anchors";
trail = "/**";
}
];
"abstractions/ssl_keys" = ''
# security.acme NixOS module
r /var/lib/acme/*/full.pem,
r /var/lib/acme/*/key.pem,
'';
"abstractions/vulkan" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/vulkan"
${etcRule {
path = "vulkan/icd.d";
trail = "/";
}}
${etcRule {
path = "vulkan/icd.d";
trail = "/*.json";
}}
'';
"abstractions/winbind" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/winbind"
${etcRule {
path = "samba";
trail = "/smb.conf";
}}
${etcRule {
path = "samba";
trail = "/dhcp.conf";
}}
'';
"abstractions/X" = ''
include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/X"
${etcRule {
path = "X11/cursors";
trail = "/";
}}
${etcRule {
path = "X11/cursors";
trail = "/**";
}}
'';
};
}

View File

@@ -0,0 +1,7 @@
{ config, pkgs, ... }:
let
apparmor = config.security.apparmor;
in
{
config.security.apparmor.packages = [ pkgs.apparmor-profiles ];
}

View File

@@ -0,0 +1,132 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.audit;
failureModes = {
silent = 0;
printk = 1;
panic = 2;
};
# The order of the fixed rules is determined by augenrules(8)
rules = pkgs.writeTextDir "audit.rules" ''
-D
-b ${toString cfg.backlogLimit}
-f ${toString failureModes.${cfg.failureMode}}
-r ${toString cfg.rateLimit}
${lib.concatLines cfg.rules}
-e ${if cfg.enable == "lock" then "2" else "1"}
'';
in
{
options = {
security.audit = {
enable = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.enum [
"silent"
"printk"
"panic"
];
default = "printk";
description = "How to handle critical errors in the auditing system";
};
backlogLimit = lib.mkOption {
type = lib.types.int;
# Significantly increase from the kernel default of 64 because a
# normal systems generates way more logs.
default = 1024;
description = ''
The maximum number of outstanding audit buffers allowed; exceeding this is
considered a failure and handled in a manner specified by failureMode.
'';
};
rateLimit = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.listOf lib.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 = lib.mkIf (cfg.enable == "lock" || cfg.enable) {
boot.kernelParams = [
# A lot of audit events happen before the systemd service starts. Thus
# enable it via the kernel commandline to have the audit subsystem ready
# as soon as the kernel starts.
"audit=1"
# Also set the backlog limit because the kernel default is too small to
# capture all of them before the service starts.
"audit_backlog_limit=${toString cfg.backlogLimit}"
];
environment.systemPackages = [ pkgs.audit ];
# upstream contains a audit-rules.service, which uses augenrules.
# That script does not handle cleanup correctly and insists on loading from /etc/audit.
# So, instead we have our own service for loading rules.
systemd.services.audit-rules-nixos = {
description = "Load Audit Rules";
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
unitConfig = {
DefaultDependencies = false;
ConditionVirtualization = "!container";
ConditionKernelCommandLine = [
"!audit=0"
"!audit=off"
];
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${lib.getExe' pkgs.audit "auditctl"} -R ${rules}/audit.rules";
ExecStopPost = [
# Disable auditing
"${lib.getExe' pkgs.audit "auditctl"} -e 0"
# Delete all rules
"${lib.getExe' pkgs.audit "auditctl"} -D"
];
};
};
};
}

View File

@@ -0,0 +1,279 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.auditd;
settingsType =
with lib.types;
nullOr (oneOf [
bool
nonEmptyStr
path
int
]);
pluginOptions = lib.types.submodule {
options = {
active = lib.mkEnableOption "Whether to enable this plugin";
direction = lib.mkOption {
type = lib.types.enum [
"in"
"out"
];
default = "out";
description = ''
The option is dictated by the plugin. In or out are the only choices.
You cannot make a plugin operate in a way it wasn't designed just by
changing this option. This option is to give a clue to the event dispatcher
about which direction events flow.
::: {.note}
Inbound events are not supported yet.
:::
'';
};
path = lib.mkOption {
type = lib.types.path;
description = "This is the absolute path to the plugin executable.";
};
type = lib.mkOption {
type = lib.types.enum [ "always" ];
readOnly = true;
default = "always";
description = ''
This tells the dispatcher how the plugin wants to be run. There is only
one valid option, `always`, which means the plugin is external and should
always be run. The default is `always` since there are no more builtin plugins.
'';
};
args = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr);
default = null;
description = ''
This allows you to pass arguments to the child program.
Generally plugins do not take arguments and have their own
config file that instructs them how they should be configured.
'';
};
format = lib.mkOption {
type = lib.types.enum [
"binary"
"string"
];
default = "string";
description = ''
Binary passes the data exactly as the audit event dispatcher gets it from
the audit daemon. The string option tells the dispatcher to completely change
the event into a string suitable for parsing with the audit parsing library.
'';
};
settings = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
freeformType = lib.types.attrsOf settingsType;
}
);
default = null;
description = "Plugin-specific config file to link to /etc/audit/<plugin>.conf";
};
};
};
prepareConfigValue =
v:
if lib.isBool v then
(if v then "yes" else "no")
else if lib.isList v then
lib.concatStringsSep " " (map prepareConfigValue v)
else
builtins.toString v;
prepareConfigText =
conf:
lib.concatLines (
lib.mapAttrsToList (k: v: if v == null then "#${k} =" else "${k} = ${prepareConfigValue v}") conf
);
in
{
options.security.auditd = {
enable = lib.mkEnableOption "the Linux Audit daemon";
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = lib.types.attrsOf settingsType;
options = {
# space_left needs to be larger than admin_space_left, yet they default to be the same if left open.
space_left = lib.mkOption {
type = lib.types.either lib.types.int (lib.types.strMatching "[0-9]+%");
default = 75;
description = ''
If the free space in the filesystem containing log_file drops below this value, the audit daemon takes the action specified by
{option}`space_left_action`. If the value of {option}`space_left` is specified as a whole number, it is interpreted as an absolute size in mebibytes
(MiB). If the value is specified as a number between 1 and 99 followed by a percentage sign (e.g., 5%), the audit daemon calculates
the absolute size in megabytes based on the size of the filesystem containing {option}`log_file`. (E.g., if the filesystem containing
{option}`log_file` is 2 gibibytes in size, and {option}`space_left` is set to 25%, then the audit daemon sets {option}`space_left` to approximately 500 mebibytes.
::: {.note}
This calculation is performed when the audit daemon starts, so if you resize the filesystem containing {option}`log_file` while the
audit daemon is running, you should send the audit daemon SIGHUP to re-read the configuration file and recalculate the correct per
centage.
:::
'';
};
admin_space_left = lib.mkOption {
type = lib.types.either lib.types.int (lib.types.strMatching "[0-9]+%");
default = 50;
description = ''
This is a numeric value in mebibytes (MiB) that tells the audit daemon when to perform a configurable action because the system is running
low on disk space. This should be considered the last chance to do something before running out of disk space. The numeric value for
this parameter should be lower than the number for {option}`space_left`. You may also append a percent sign (e.g. 1%) to the number to have
the audit daemon calculate the number based on the disk partition size.
'';
};
};
};
default = { };
description = "auditd configuration file contents. See {auditd.conf} for supported values.";
};
plugins = lib.mkOption {
type = lib.types.attrsOf pluginOptions;
default = { };
defaultText = lib.literalExpression ''
{
af_unix = {
path = lib.getExe' pkgs.audit "audisp-af_unix";
args = [
"0640"
"/var/run/audispd_events"
"string"
];
format = "binary";
};
remote = {
path = lib.getExe' pkgs.audit "audisp-remote";
settings = { };
};
filter = {
path = lib.getExe' pkgs.audit "audisp-filter";
args = [
"allowlist"
"/etc/audit/audisp-filter.conf"
(lib.getExe' pkgs.audit "audisp-syslog")
"LOG_USER"
"LOG_INFO"
"interpret"
];
settings = { };
};
syslog = {
path = lib.getExe' pkgs.audit "audisp-syslog";
args = [ "LOG_INFO" ];
};
}
'';
description = "Plugin definitions to register with auditd";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
let
cfg' = cfg.settings;
in
(
(lib.isInt cfg'.space_left && lib.isInt cfg'.admin_space_left)
-> cfg'.space_left > cfg'.admin_space_left
)
&& (
let
get_percent = s: lib.toInt (lib.strings.removeSuffix "%" s);
in
(lib.isString cfg'.space_left && lib.isString cfg'.admin_space_left)
-> (get_percent cfg'.space_left) > (get_percent cfg'.admin_space_left)
);
message = "`security.auditd.settings.space_left` must be larger than `security.auditd.settings.admin_space_left`";
}
];
# Starting the userspace daemon should also enable audit in the kernel
security.audit.enable = lib.mkDefault true;
# setting this to anything other than /etc/audit/plugins.d will break, so we pin it here
security.auditd.settings.plugin_dir = "/etc/audit/plugins.d";
environment.etc = {
"audit/auditd.conf".text = prepareConfigText cfg.settings;
}
// (lib.mapAttrs' (
pluginName: pluginDefinitionConfigValue:
lib.nameValuePair "audit/plugins.d/${pluginName}.conf" {
text = prepareConfigText (lib.removeAttrs pluginDefinitionConfigValue [ "settings" ]);
}
) cfg.plugins)
// (lib.mapAttrs' (
pluginName: pluginDefinitionConfigValue:
lib.nameValuePair "audit/audisp-${pluginName}.conf" {
text = prepareConfigText pluginDefinitionConfigValue.settings;
}
) (lib.filterAttrs (_: v: v.settings != null) cfg.plugins));
security.auditd.plugins = {
af_unix = {
path = lib.getExe' pkgs.audit "audisp-af_unix";
args = [
"0640"
"/run/audit/audispd_events"
"string"
];
format = "binary";
};
remote = {
path = lib.getExe' pkgs.audit "audisp-remote";
settings = { };
};
filter = {
path = lib.getExe' pkgs.audit "audisp-filter";
args = [
"allowlist"
"/etc/audit/audisp-filter.conf"
(lib.getExe' pkgs.audit "audisp-syslog")
"LOG_USER"
"LOG_INFO"
"interpret"
];
settings = { };
};
syslog = {
path = lib.getExe' pkgs.audit "audisp-syslog";
args = [ "LOG_INFO" ];
};
};
systemd.packages = [ pkgs.audit.out ];
systemd.services.auditd = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
# https://github.com/linux-audit/audit-userspace/pull/501
# set up audit directories using systemd service instead of tmpfiles
LogsDirectory = "audit";
LogsDirectoryMode = "0700";
RuntimeDirectory = "audit";
RuntimeDirectoryMode = "0755";
ExecStart = [
# the upstream unit does not allow symlinks, so clear and rewrite the ExecStart
""
"${lib.getExe' pkgs.audit "auditd"} -l -s nochange"
];
};
};
};
}

View File

@@ -0,0 +1,117 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.pki;
cacertPackage = pkgs.cacert.override {
blacklist = cfg.caCertificateBlacklist;
extraCertificateFiles = cfg.certificateFiles;
extraCertificateStrings = cfg.certificates;
};
caBundleName = if cfg.useCompatibleBundle then "ca-no-trust-rules-bundle.crt" else "ca-bundle.crt";
caBundle = "${cacertPackage}/etc/ssl/certs/${caBundleName}";
in
{
options = {
security.pki.installCACerts = lib.mkEnableOption "installing CA certificates to the system" // {
default = true;
internal = true;
};
security.pki.useCompatibleBundle = lib.mkEnableOption ''
usage of a compatibility bundle.
Such a bundle consists exclusively of `BEGIN CERTIFICATE` and no `BEGIN TRUSTED CERTIFICATE`,
which is an OpenSSL specific PEM format.
It is known to be incompatible with certain software stacks.
Nevertheless, enabling this will strip all additional trust rules provided by the
certificates themselves. This can have security consequences depending on your usecases
'';
security.pki.certificateFiles = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
example = lib.literalExpression ''[ "''${pkgs.dn42-cacert}/etc/ssl/certs/dn42-ca.crt" ]'';
description = ''
A list of files containing trusted root certificates in PEM
format. These are concatenated to form
{file}`/etc/ssl/certs/ca-certificates.crt`, which is
used by many programs that use OpenSSL, such as
{command}`curl` and {command}`git`.
'';
};
security.pki.certificates = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = lib.literalExpression ''
[ '''
NixOS.org
=========
-----BEGIN CERTIFICATE-----
MIIGUDCCBTigAwIBAgIDD8KWMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ
TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0
...
-----END CERTIFICATE-----
'''
]
'';
description = ''
A list of trusted root certificates in PEM format.
'';
};
security.pki.caCertificateBlacklist = lib.mkOption {
type = lib.types.listOf lib.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
{file}`/etc/ssl/certs/ca-certificates.crt`. Use the
names from that file.
'';
};
security.pki.caBundle = lib.mkOption {
type = lib.types.path;
readOnly = true;
description = ''
(Read-only) the path to the final bundle of certificate authorities as a single file.
'';
};
};
config = lib.mkMerge [
(lib.mkIf cfg.installCACerts {
# 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";
})
{ security.pki.caBundle = caBundle; }
];
}

View File

@@ -0,0 +1,43 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.chromiumSuidSandbox;
sandbox = pkgs.chromium.sandbox;
in
{
imports = [
(lib.mkRenamedOptionModule
[ "programs" "unity3d" "enable" ]
[ "security" "chromiumSuidSandbox" "enable" ]
)
];
options.security.chromiumSuidSandbox.enable = lib.mkOption {
type = lib.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 = lib.mkIf cfg.enable {
environment.systemPackages = [ sandbox ];
security.wrappers.${sandbox.passthru.sandboxExecutableName} = {
setuid = true;
owner = "root";
group = "root";
source = "${sandbox}/bin/${sandbox.passthru.sandboxExecutableName}";
};
};
}

View File

@@ -0,0 +1,38 @@
{ config, lib, ... }:
let
cfg = config.security;
in
{
options = {
security.lsm = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
A list of the LSMs to initialize in order.
'';
};
};
config = lib.mkMerge [
{
# We set the default LSM's here due to them not being present if set when enabling AppArmor.
security.lsm = [
"landlock"
"yama"
"bpf"
];
}
(lib.mkIf (lib.lists.length cfg.lsm > 0) {
assertions = [
{
assertion = builtins.length (lib.filter (lib.hasPrefix "security=") config.boot.kernelParams) == 0;
message = "security parameter in boot.kernelParams cannot be used when security.lsm is used";
}
];
boot.kernelParams = [
"lsm=${lib.concatStringsSep "," cfg.lsm}"
];
})
];
}

View File

@@ -0,0 +1,210 @@
{
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`.
'';
};
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
`config.security.dhparams.params.«name».path`.
::: {.note}
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.
:::
::: {.warning}
If you are removing all dhparams from this list, you
have to leave {option}`security.dhparams.enable` 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` is
`true`.
:::
::: {.note}
**For module implementers:** It's recommended
to not set a specific bit size here, so that users can easily
override this by setting
{option}`security.dhparams.defaultBitSize`.
:::
'';
};
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`. Otherwise the files are
created within the Nix store.
::: {.note}
If this is `false` the resulting store
path will be non-deterministic and will be rebuilt every time the
`openssl` package changes.
:::
'';
};
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`.
'';
};
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` is
`true`.
'';
};
};
};
config = lib.mkIf (cfg.enable && cfg.stateful) {
systemd.services = {
dhparams-init = {
description = "Clean Up Old Diffie-Hellman Parameters";
# Clean up even when no DH params is set
wantedBy = [ "multi-user.target" ];
serviceConfig.RemainAfterExit = true;
serviceConfig.Type = "oneshot";
script = ''
if [ ! -d ${cfg.path} ]; then
mkdir -p ${cfg.path}
fi
# Remove old dhparams
for file in ${cfg.path}/*; do
if [ ! -f "$file" ]; then
continue
fi
${lib.concatStrings (
lib.mapAttrsToList (
name:
{ bits, path, ... }:
''
if [ "$file" = ${lib.escapeShellArg path} ] && \
${pkgs.openssl}/bin/openssl dhparam -in "$file" -text \
| head -n 1 | grep "(${toString bits} bit)" > /dev/null; then
continue
fi
''
) cfg.params
)}
rm "$file"
done
# TODO: Ideally this would be removing the *former* cfg.path, though
# this does not seem really important as changes to it are quite
# unlikely
rmdir --ignore-fail-on-non-empty ${cfg.path}
'';
};
}
// lib.mapAttrs' (
name:
{ bits, path, ... }:
lib.nameValuePair "dhparams-gen-${name}" {
description = "Generate Diffie-Hellman Parameters for ${name}";
after = [ "dhparams-init.service" ];
before = [ "${name}.service" ];
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExists = "!${path}";
serviceConfig.Type = "oneshot";
script = ''
mkdir -p ${lib.escapeShellArg cfg.path}
${pkgs.openssl}/bin/openssl dhparam -out ${lib.escapeShellArg path} \
${toString bits}
'';
}
) cfg.params;
};
meta.maintainers = with lib.maintainers; [ ekleog ];
}

View File

@@ -0,0 +1,300 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.doas;
inherit (pkgs) doas;
mkUsrString = user: toString user;
mkGrpString = group: ":${toString group}";
mkOpts =
rule:
lib.concatStringsSep " " [
(lib.optionalString rule.noPass "nopass")
(lib.optionalString rule.noLog "nolog")
(lib.optionalString rule.persist "persist")
(lib.optionalString rule.keepEnv "keepenv")
"setenv { SSH_AUTH_SOCK TERMINFO TERMINFO_DIRS ${lib.concatStringsSep " " rule.setEnv} }"
];
mkArgs =
rule:
if (rule.args == null) then
""
else if (lib.length rule.args == 0) then
"args"
else
"args ${lib.concatStringsSep " " rule.args}";
mkRule =
rule:
let
opts = mkOpts rule;
as = lib.optionalString (rule.runAs != null) "as ${rule.runAs}";
cmd = lib.optionalString (rule.cmd != null) "cmd ${rule.cmd}";
args = mkArgs rule;
in
lib.optionals (lib.length cfg.extraRules > 0) [
(lib.optionalString (lib.length rule.users > 0) (
map (usr: "permit ${opts} ${mkUsrString usr} ${as} ${cmd} ${args}") rule.users
))
(lib.optionalString (lib.length rule.groups > 0) (
map (grp: "permit ${opts} ${mkGrpString grp} ${as} ${cmd} ${args}") rule.groups
))
];
in
{
###### interface
options.security.doas = {
enable = lib.mkOption {
type = with lib.types; bool;
default = false;
description = ''
Whether to enable the {command}`doas` command, which allows
non-root users to execute commands as root.
'';
};
wheelNeedsPassword = lib.mkOption {
type = with lib.types; bool;
default = true;
description = ''
Whether users of the `wheel` group must provide a password to
run commands as super user via {command}`doas`.
'';
};
extraRules = lib.mkOption {
default = [ ];
description = ''
Define specific rules to be set in the
{file}`/etc/doas.conf` file. More specific rules should
come after more general ones in order to yield the expected behavior.
You can use `mkBefore` and/or `mkAfter` to ensure
this is the case when configuration options are merged. Be aware that
this option cannot be used to override the behaviour allowing
passwordless operation for root.
'';
example = lib.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 lib.types;
listOf (submodule {
options = {
noPass = lib.mkOption {
type = with types; bool;
default = false;
description = ''
If `true`, the user is not required to enter a
password.
'';
};
noLog = lib.mkOption {
type = with types; bool;
default = false;
description = ''
If `true`, successful executions will not be logged
to
{manpage}`syslogd(8)`.
'';
};
persist = lib.mkOption {
type = with types; bool;
default = false;
description = ''
If `true`, do not ask for a password again for some
time after the user successfully authenticates.
'';
};
keepEnv = lib.mkOption {
type = with types; bool;
default = false;
description = ''
If `true`, environment variables other than those
listed in
{manpage}`doas(1)`
are kept when creating the environment for the new process.
'';
};
setEnv = lib.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
`variable=value`. If the first character of
`value` 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 `setenv { SSH_AUTH_SOCK }` by
default. To prevent `SSH_AUTH_SOCK` from being
inherited, add `"-SSH_AUTH_SOCK"` anywhere in this
list.
'';
};
users = lib.mkOption {
type = with types; listOf (either str int);
default = [ ];
description = "The usernames / UIDs this rule should apply for.";
};
groups = lib.mkOption {
type = with types; listOf (either str int);
default = [ ];
description = "The groups / GIDs this rule should apply for.";
};
runAs = lib.mkOption {
type = with types; nullOr str;
default = null;
description = ''
Which user or group the specified command is allowed to run as.
When set to `null` (the default), all users are
allowed.
A user can be specified using just the username:
`"foo"`. It is also possible to only allow running as
a specific group with `":bar"`.
'';
};
cmd = lib.mkOption {
type = with types; nullOr str;
default = null;
description = ''
The command the user is allowed to run. When set to
`null` (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 = lib.mkOption {
type = with types; nullOr (listOf str);
default = null;
description = ''
Arguments that must be provided to the command. When set to
`[]`, the command must be run without any arguments.
'';
};
};
});
};
extraConfig = lib.mkOption {
type = with lib.types; lines;
default = "";
description = ''
Extra configuration text appended to {file}`doas.conf`. Be aware that
this option cannot be used to override the behaviour allowing
passwordless operation for root.
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
security.doas.extraRules = lib.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"`.
# extraRules
${lib.concatStringsSep "\n" (lib.lists.flatten (map mkRule cfg.extraRules))}
# extraConfig
${cfg.extraConfig}
# "root" is allowed to do anything.
permit nopass keepenv root
'';
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 lib.maintainers; [ cole-h ];
}

View File

@@ -0,0 +1,276 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.duosec;
boolToStr = b: if b then "yes" else "no";
configFilePam = ''
[duo]
ikey=${cfg.integrationKey}
host=${cfg.host}
${lib.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 = [
(lib.mkRenamedOptionModule [ "security" "duosec" "group" ] [ "security" "duosec" "groups" ])
(lib.mkRenamedOptionModule [ "security" "duosec" "ikey" ] [ "security" "duosec" "integrationKey" ])
(lib.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 = lib.mkOption {
type = lib.types.bool;
default = false;
description = "If enabled, protect SSH logins with Duo Security.";
};
pam.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "If enabled, protect logins with Duo Security using PAM support.";
};
integrationKey = lib.mkOption {
type = lib.types.str;
description = "Integration key.";
};
secretKeyFile = lib.mkOption {
type = lib.types.nullOr lib.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 = lib.mkOption {
type = lib.types.str;
description = "Duo API hostname.";
};
groups = lib.mkOption {
type = lib.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
<https://duo.com/docs/duounix> for details.
'';
};
failmode = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Include information such as the command to be executed in
the Duo Push message.
'';
};
autopush = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If `true`, Duo Unix will automatically send
a push login request to the users phone, falling back on a
phone call if push is unavailable. If
`false`, the user will be prompted to
choose an authentication method. When configured with
`autopush = yes`, we recommend setting
`prompts = 1`.
'';
};
motd = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Print the contents of `/etc/motd` to screen
after a successful login.
'';
};
prompts = lib.mkOption {
type = lib.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 `prompts = 1`, the user
will have to successfully authenticate on the first prompt,
whereas if `prompts = 2`, if the user
enters incorrect information at the initial prompt, he/she
will be prompted to authenticate again.
When configured with `autopush = true`, we
recommend setting `prompts = 1`.
'';
};
acceptEnvFactor = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Look for factor selection or passcode in the
`$DUO_PASSCODE` 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 = lib.mkOption {
type = lib.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
`fallbackLocalIP = yes` 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 = lib.mkOption {
type = lib.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 = lib.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";
};
systemd.services.login-duo = lib.mkIf cfg.ssh.enable {
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
script = ''
if test -f "${cfg.secretKeyFile}"; then
mkdir -p /etc/duo
chmod 0755 /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
'';
};
systemd.services.pam-duo = lib.mkIf cfg.ssh.enable {
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
script = ''
if test -f "${cfg.secretKeyFile}"; then
mkdir -p /etc/duo
chmod 0755 /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 = lib.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
${lib.optionalString (!cfg.allowTcpForwarding) ''
AllowTcpForwarding no
''}
''
);
};
}

View File

@@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.googleOsLogin;
package = pkgs.google-guest-oslogin;
in
{
options = {
security.googleOsLogin.enable = lib.mkOption {
type = lib.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 = lib.mkIf cfg.enable {
security.pam.services.sshd = {
makeHomeDir = true;
googleOsLoginAccountVerification = true;
googleOsLoginAuthentication = true;
};
security.sudo.extraConfig = ''
#includedir /run/google-sudoers.d
'';
security.sudo-rs.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 = "root";
};
}

View File

@@ -0,0 +1,289 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.security.ipa;
pyBool = x: if x then "True" else "False";
ldapConf = pkgs.writeText "ldap.conf" ''
# Turning this off breaks GSSAPI used with krb5 when rdns = false
SASL_NOCANON on
URI ldaps://${cfg.server}
BASE ${cfg.basedn}
TLS_CACERT /etc/ipa/ca.crt
'';
nssDb =
pkgs.runCommand "ipa-nssdb"
{
nativeBuildInputs = [ pkgs.nss.tools ];
}
''
mkdir -p $out
certutil -d $out -N --empty-password
certutil -d $out -A --empty-password -n "${cfg.realm} IPA CA" -t CT,C,C -i ${cfg.certificate}
'';
in
{
options = {
security.ipa = {
enable = mkEnableOption "FreeIPA domain integration";
certificate = mkOption {
type = types.package;
description = ''
IPA server CA certificate.
Use `nix-prefetch-url http://$server/ipa/config/ca.crt` to
obtain the file and the hash.
'';
example = literalExpression ''
pkgs.fetchurl {
url = "http://ipa.example.com/ipa/config/ca.crt";
hash = lib.fakeHash;
};
'';
};
domain = mkOption {
type = types.str;
example = "example.com";
description = "Domain of the IPA server.";
};
realm = mkOption {
type = types.str;
example = "EXAMPLE.COM";
description = "Kerberos realm.";
};
server = mkOption {
type = types.str;
example = "ipa.example.com";
description = "IPA Server hostname.";
};
basedn = mkOption {
type = types.str;
example = "dc=example,dc=com";
description = "Base DN to use when performing LDAP operations.";
};
offlinePasswords = mkOption {
type = types.bool;
default = true;
description = "Whether to store offline passwords when the server is down.";
};
cacheCredentials = mkOption {
type = types.bool;
default = true;
description = "Whether to cache credentials.";
};
ipaHostname = mkOption {
type = types.str;
example = "myworkstation.example.com";
default =
if config.networking.domain != null then
config.networking.fqdn
else
"${config.networking.hostName}.${cfg.domain}";
defaultText = literalExpression ''
if config.networking.domain != null then config.networking.fqdn
else "''${networking.hostName}.''${security.ipa.domain}"
'';
description = "Fully-qualified hostname used to identify this host in the IPA domain.";
};
ifpAllowedUids = mkOption {
type = types.listOf types.str;
default = [ "root" ];
description = "A list of users allowed to access the ifp dbus interface.";
};
dyndns = {
enable = mkOption {
type = types.bool;
default = true;
description = "Whether to enable FreeIPA automatic hostname updates.";
};
interface = mkOption {
type = types.str;
example = "eth0";
default = "*";
description = "Network interface to perform hostname updates through.";
};
};
chromiumSupport = mkOption {
type = types.bool;
default = true;
description = "Whether to whitelist the FreeIPA domain in Chromium.";
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = !config.security.krb5.enable;
message = "krb5 must be disabled through `security.krb5.enable` for FreeIPA integration to work.";
}
{
assertion = !config.users.ldap.enable;
message = "ldap must be disabled through `users.ldap.enable` for FreeIPA integration to work.";
}
];
environment.systemPackages = with pkgs; [
krb5Full
freeipa
];
environment.etc = {
"ipa/default.conf".text = ''
[global]
basedn = ${cfg.basedn}
realm = ${cfg.realm}
domain = ${cfg.domain}
server = ${cfg.server}
host = ${config.networking.hostName}
xmlrpc_uri = https://${cfg.server}/ipa/xml
enable_ra = True
'';
"ipa/nssdb".source = nssDb;
"krb5.conf".text = ''
[libdefaults]
default_realm = ${cfg.realm}
dns_lookup_realm = false
dns_lookup_kdc = true
rdns = false
ticket_lifetime = 24h
forwardable = true
udp_preference_limit = 0
[realms]
${cfg.realm} = {
kdc = ${cfg.server}:88
master_kdc = ${cfg.server}:88
admin_server = ${cfg.server}:749
default_domain = ${cfg.domain}
pkinit_anchors = FILE:/etc/ipa/ca.crt
}
[domain_realm]
.${cfg.domain} = ${cfg.realm}
${cfg.domain} = ${cfg.realm}
${cfg.server} = ${cfg.realm}
[dbmodules]
${cfg.realm} = {
db_library = ${pkgs.freeipa}/lib/krb5/plugins/kdb/ipadb.so
}
'';
"ldap.conf".source = ldapConf;
"chromium/policies/managed/freeipa.json" = mkIf cfg.chromiumSupport {
text = builtins.toJSON {
AuthServerWhitelist = "*.${cfg.domain}";
};
};
};
systemd.services."ipa-activation" = {
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
# libcurl requires a hard copy of the certificate
if ! ${pkgs.diffutils}/bin/diff ${cfg.certificate} /etc/ipa/ca.crt > /dev/null 2>&1; then
rm -f /etc/ipa/ca.crt
cp ${cfg.certificate} /etc/ipa/ca.crt
fi
if [ ! -f /etc/krb5.keytab ]; then
cat <<EOF
In order to complete FreeIPA integration, please join the domain by completing the following steps:
1. Authenticate as an IPA user authorized to join new hosts, e.g. kinit admin@${cfg.realm}
2. Join the domain and obtain the keytab file: ipa-join
3. Install the keytab file: sudo install -m 600 krb5.keytab /etc/
4. Restart sssd systemd service: sudo systemctl restart sssd
EOF
# let service fail, to raise awareness
exit 1
fi
'';
};
services.sssd = {
enable = true;
config = ''
[domain/${cfg.domain}]
id_provider = ipa
auth_provider = ipa
access_provider = ipa
chpass_provider = ipa
ipa_domain = ${cfg.domain}
ipa_server = _srv_, ${cfg.server}
ipa_hostname = ${cfg.ipaHostname}
cache_credentials = ${pyBool cfg.cacheCredentials}
krb5_store_password_if_offline = ${pyBool cfg.offlinePasswords}
${optionalString ((toLower cfg.domain) != (toLower cfg.realm)) "krb5_realm = ${cfg.realm}"}
dyndns_update = ${pyBool cfg.dyndns.enable}
dyndns_iface = ${cfg.dyndns.interface}
ldap_tls_cacert = /etc/ipa/ca.crt
ldap_user_extra_attrs = mail:mail, sn:sn, givenname:givenname, telephoneNumber:telephoneNumber, lock:nsaccountlock
[sssd]
services = nss, sudo, pam, ssh, ifp
domains = ${cfg.domain}
[nss]
homedir_substring = /home
[pam]
pam_pwd_expiration_warning = 3
pam_verbosity = 3
[sudo]
[autofs]
[ssh]
[pac]
[ifp]
user_attributes = +mail, +telephoneNumber, +givenname, +sn, +lock
allowed_uids = ${concatStringsSep ", " cfg.ifpAllowedUids}
'';
};
networking.timeServers = singleton cfg.server;
security.pki.certificateFiles = singleton cfg.certificate;
};
}

View File

@@ -0,0 +1,146 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkEnableOption
mkPackageOption
mkOption
types
mkIf
maintainers
;
cfg = config.security.isolate;
configFile = pkgs.writeText "isolate-config.cf" ''
box_root=${cfg.boxRoot}
lock_root=${cfg.lockRoot}
cg_root=${cfg.cgRoot}
first_uid=${toString cfg.firstUid}
first_gid=${toString cfg.firstGid}
num_boxes=${toString cfg.numBoxes}
restricted_init=${if cfg.restrictedInit then "1" else "0"}
${cfg.extraConfig}
'';
isolate = pkgs.symlinkJoin {
name = "isolate-wrapped-${pkgs.isolate.version}";
paths = [ pkgs.isolate ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/isolate \
--set ISOLATE_CONFIG_FILE ${configFile}
wrapProgram $out/bin/isolate-cg-keeper \
--set ISOLATE_CONFIG_FILE ${configFile}
'';
};
in
{
options.security.isolate = {
enable = mkEnableOption ''
Sandbox for securely executing untrusted programs
'';
package = mkPackageOption pkgs "isolate-unwrapped" { };
boxRoot = mkOption {
type = types.path;
default = "/var/lib/isolate/boxes";
description = ''
All sandboxes are created under this directory.
To avoid symlink attacks, this directory and all its ancestors
must be writeable only by root.
'';
};
lockRoot = mkOption {
type = types.path;
default = "/run/isolate/locks";
description = ''
Directory where lock files are created.
'';
};
cgRoot = mkOption {
type = types.str;
default = "auto:/run/isolate/cgroup";
description = ''
Control group which subgroups are placed under.
Either an explicit path to a subdirectory in cgroupfs, or "auto:file" to read
the path from "file", where it is put by `isolate-cg-helper`.
'';
};
firstUid = mkOption {
type = types.numbers.between 1000 65533;
default = 60000;
description = ''
Start of block of UIDs reserved for sandboxes.
'';
};
firstGid = mkOption {
type = types.numbers.between 1000 65533;
default = 60000;
description = ''
Start of block of GIDs reserved for sandboxes.
'';
};
numBoxes = mkOption {
type = types.numbers.between 1000 65533;
default = 1000;
description = ''
Number of UIDs and GIDs to reserve, starting from
{option}`firstUid` and {option}`firstGid`.
'';
};
restrictedInit = mkOption {
type = types.bool;
default = false;
description = ''
If true, only root can create sandboxes.
'';
};
extraConfig = mkOption {
type = types.str;
default = "";
description = ''
Extra configuration to append to the configuration file.
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = [
isolate
];
systemd.services.isolate = {
description = "Isolate control group hierarchy daemon";
wantedBy = [ "multi-user.target" ];
documentation = [ "man:isolate(1)" ];
serviceConfig = {
Type = "notify";
ExecStart = "${isolate}/bin/isolate-cg-keeper";
Slice = "isolate.slice";
Delegate = true;
};
};
systemd.slices.isolate = {
description = "Isolate Sandbox Slice";
};
};
meta.maintainers = with maintainers; [ virchau13 ];
}

View File

@@ -0,0 +1,125 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkIf
mkOption
mkPackageOption
mkRemovedOptionModule
;
inherit (lib.types) bool;
mkRemovedOptionModule' = name: reason: mkRemovedOptionModule [ "krb5" name ] reason;
mkRemovedOptionModuleCfg =
name:
mkRemovedOptionModule' name ''
The option `krb5.${name}' has been removed. Use
`security.krb5.settings.${name}' for structured configuration.
'';
cfg = config.security.krb5;
format = import ./krb5-conf-format.nix { inherit pkgs lib; } { };
in
{
imports = [
(mkRemovedOptionModuleCfg "libdefaults")
(mkRemovedOptionModuleCfg "realms")
(mkRemovedOptionModuleCfg "domain_realm")
(mkRemovedOptionModuleCfg "capaths")
(mkRemovedOptionModuleCfg "appdefaults")
(mkRemovedOptionModuleCfg "plugins")
(mkRemovedOptionModuleCfg "config")
(mkRemovedOptionModuleCfg "extraConfig")
(mkRemovedOptionModule' "kerberos" ''
The option `krb5.kerberos' has been moved to `security.krb5.package'.
'')
];
options = {
security.krb5 = {
enable = mkOption {
default = false;
description = "Enable and configure Kerberos utilities";
type = bool;
};
package = mkPackageOption pkgs "krb5" {
example = "heimdal";
};
settings = mkOption {
default = { };
type = format.type;
description = ''
Structured contents of the {file}`krb5.conf` file. See
{manpage}`krb5.conf(5)` for details about configuration.
'';
example = {
include = [ "/run/secrets/secret-krb5.conf" ];
includedir = [ "/run/secrets/secret-krb5.conf.d" ];
libdefaults = {
default_realm = "ATHENA.MIT.EDU";
};
realms = {
"ATHENA.MIT.EDU" = {
admin_server = "athena.mit.edu";
kdc = [
"athena01.mit.edu"
"athena02.mit.edu"
];
};
};
domain_realm = {
"mit.edu" = "ATHENA.MIT.EDU";
};
logging = {
kdc = "SYSLOG:NOTICE";
admin_server = "SYSLOG:NOTICE";
default = "SYSLOG:NOTICE";
};
};
};
};
};
config = {
assertions = mkIf (cfg.enable || config.services.kerberos_server.enable) [
(
let
implementation = cfg.package.passthru.implementation or "<NOT SET>";
in
{
assertion = lib.elem implementation [
"krb5"
"heimdal"
];
message = ''
`security.krb5.package` must be one of:
- krb5
- heimdal
Currently chosen implementation: ${implementation}
'';
}
)
];
environment = mkIf cfg.enable {
systemPackages = [ cfg.package ];
etc."krb5.conf".source = format.generate "krb5.conf" cfg.settings;
};
};
meta.maintainers = builtins.attrValues {
inherit (lib.maintainers) dblsaiko h7x4;
};
}

View File

@@ -0,0 +1,220 @@
{ pkgs, lib, ... }:
# Based on
# - https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html
# - https://manpages.debian.org/unstable/heimdal-docs/krb5.conf.5heimdal.en.html
let
inherit (lib)
boolToString
concatMapStringsSep
concatStringsSep
filter
isAttrs
isBool
isList
mapAttrsToList
mkOption
singleton
splitString
;
inherit (lib.types)
attrsOf
bool
coercedTo
either
enum
int
listOf
oneOf
path
str
submodule
;
in
{
enableKdcACLEntries ? false,
}:
rec {
sectionType =
let
relation = oneOf [
(listOf (attrsOf value))
(attrsOf value)
value
];
value = either (listOf atom) atom;
atom = oneOf [
int
str
bool
];
in
attrsOf relation;
type =
let
aclEntry = submodule {
options = {
principal = mkOption {
type = str;
description = "Which principal the rule applies to";
};
access = mkOption {
type = coercedTo str singleton (
listOf (enum [
"all"
"add"
"cpw"
"delete"
"get-keys"
"get"
"list"
"modify"
])
);
default = "all";
description = ''
The changes the principal is allowed to make.
:::{.important}
The "all" permission does not imply the "get-keys" permission. This
is consistent with the behavior of both MIT Kerberos and Heimdal.
:::
:::{.warning}
Value "all" is allowed as a list member only if it appears alone
or accompanied by "get-keys". Any other combination involving
"all" will raise an exception.
:::
'';
};
target = mkOption {
type = str;
default = "*";
description = "The principals that 'access' applies to.";
};
};
};
realm = submodule (
{ name, ... }:
{
freeformType = sectionType;
options = {
acl = mkOption {
type = listOf aclEntry;
default = [
{
principal = "*/admin";
access = "all";
}
{
principal = "admin";
access = "all";
}
];
description = ''
The privileges granted to a user.
'';
};
};
}
);
in
submodule {
freeformType = attrsOf sectionType;
options = {
include = mkOption {
default = [ ];
description = ''
Files to include in the Kerberos configuration.
'';
type = coercedTo path singleton (listOf path);
};
includedir = mkOption {
default = [ ];
description = ''
Directories containing files to include in the Kerberos configuration.
'';
type = coercedTo path singleton (listOf path);
};
module = mkOption {
default = [ ];
description = ''
Modules to obtain Kerberos configuration from.
'';
type = coercedTo path singleton (listOf path);
};
}
// (lib.optionalAttrs enableKdcACLEntries {
realms = mkOption {
type = attrsOf realm;
description = ''
The realm(s) to serve keys for.
'';
};
});
};
generate =
let
indent = str: concatMapStringsSep "\n" (line: " " + line) (splitString "\n" str);
formatToplevel =
args@{
include ? [ ],
includedir ? [ ],
module ? [ ],
...
}:
let
sections = removeAttrs args [
"include"
"includedir"
"module"
];
in
concatStringsSep "\n" (
filter (x: x != "") [
(concatStringsSep "\n" (mapAttrsToList formatSection sections))
(concatMapStringsSep "\n" (m: "module ${m}") module)
(concatMapStringsSep "\n" (i: "include ${i}") include)
(concatMapStringsSep "\n" (i: "includedir ${i}") includedir)
]
);
formatSection = name: section: ''
[${name}]
${indent (concatStringsSep "\n" (mapAttrsToList formatRelation section))}
'';
formatRelation =
name: relation:
if isAttrs relation then
''
${name} = {
${indent (concatStringsSep "\n" (mapAttrsToList formatValue relation))}
}''
else if isList relation then
concatMapStringsSep "\n" (formatRelation name) relation
else
formatValue name relation;
formatValue =
name: value:
if isList value then concatMapStringsSep "\n" (formatAtom name) value else formatAtom name value;
formatAtom =
name: atom:
let
v = if isBool atom then boolToString atom else toString atom;
in
"${name} = ${v}";
in
name: value:
pkgs.writeText name ''
${formatToplevel value}
'';
}

View File

@@ -0,0 +1,61 @@
{ config, lib, ... }:
{
meta = {
maintainers = [ lib.maintainers.joachifm ];
};
options = {
security.lockKernelModules = lib.mkOption {
type = lib.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`.
'';
};
};
config = lib.mkIf config.security.lockKernelModules {
boot.kernelModules = lib.concatMap (
x:
lib.optionals (x.device != null) (
if x.fsType == "vfat" then
[
"vfat"
"nls-cp437"
"nls-iso8859-1"
]
else
[ x.fsType ]
)
) 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 = ''
${config.systemd.package}/bin/udevadm settle
echo -n 1 >/proc/sys/kernel/modules_disabled
'';
};
};
}

View File

@@ -0,0 +1,149 @@
{ config, lib, ... }:
{
meta = {
maintainers = [ lib.maintainers.joachifm ];
};
imports = [
(lib.mkRenamedOptionModule
[ "security" "virtualization" "flushL1DataCache" ]
[ "security" "virtualisation" "flushL1DataCache" ]
)
];
options = {
security.allowUserNamespaces = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to prevent replacing the running kernel image.
'';
};
security.allowSimultaneousMultithreading = lib.mkOption {
type = lib.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 [](#opt-security.virtualisation.flushL1DataCache))
versus malicious VM guests (SMT could "bring back" previously flushed
data).
'';
};
security.forcePageTableIsolation = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"never"
"cond"
"always"
]
);
default = null;
description = ''
Whether the hypervisor should flush the L1 data cache before
entering guests.
See also [](#opt-security.allowSimultaneousMultithreading).
- `null`: uses the kernel default
- `"never"`: disables L1 data cache flushing entirely.
May be appropriate if all guests are trusted.
- `"cond"`: flushes L1 data cache only for pre-determined
code paths. May leak information about the host address space
layout.
- `"always"`: flushes L1 data cache every time the hypervisor
enters the guest. May incur significant performance cost.
'';
};
};
config = lib.mkMerge [
(lib.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`.";
}
];
})
(lib.mkIf config.security.unprivilegedUsernsClone {
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = lib.mkDefault true;
})
(lib.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" = lib.mkDefault true;
})
(lib.mkIf (!config.security.allowSimultaneousMultithreading) {
boot.kernelParams = [ "nosmt" ];
})
(lib.mkIf config.security.forcePageTableIsolation {
boot.kernelParams = [ "pti=on" ];
})
(lib.mkIf (config.security.virtualisation.flushL1DataCache != null) {
boot.kernelParams = [
"kvm-intel.vmentry_l1d_flush=${config.security.virtualisation.flushL1DataCache}"
];
})
];
}

View File

@@ -0,0 +1,50 @@
# This module provides configuration for the OATH PAM modules.
{ lib, ... }:
{
options = {
security.pam.oath = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable the OATH (one-time password) PAM module.
'';
};
digits = lib.mkOption {
type = lib.types.enum [
6
7
8
];
default = 6;
description = ''
Specify the lib.length of the one-time password in number of
digits.
'';
};
window = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.path;
default = "/etc/users.oath";
description = ''
Set the path to file where the user's credentials are
stored. This file must not be world readable!
'';
};
};
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
{
config,
lib,
pkgs,
...
}:
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 = lib.any (svc: svc.enable && svc.pamMount) (
lib.attrValues config.security.pam.services
);
in
{
options = {
security.pam.mount = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable PAM mount system to mount filesystems on user login.
'';
};
extraVolumes = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
List of volume definitions for pam_mount.
For more information, visit <https://pam-mount.sourceforge.net/pam_mount.conf.5.html>.
'';
};
additionalSearchPaths = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.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.
'';
};
cryptMountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = lib.literalExpression ''
[ "allow_discard" ]
'';
description = ''
Global mount options that apply to every crypt volume.
You can define volume-specific options in the volume definitions.
'';
};
fuseMountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = lib.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 = lib.mkOption {
type = lib.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 <https://pam-mount.sourceforge.net/pam_mount.conf.5.html>.
'';
};
logoutWait = lib.mkOption {
type = lib.types.int;
default = 0;
description = ''
Amount of microseconds to wait until killing remaining processes after
final logout.
For more information, visit <https://pam-mount.sourceforge.net/pam_mount.conf.5.html>.
'';
};
logoutHup = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Kill remaining processes after logout by sending a SIGHUP.
'';
};
logoutTerm = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Kill remaining processes after logout by sending a SIGTERM.
'';
};
logoutKill = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Kill remaining processes after logout by sending a SIGKILL.
'';
};
createMountPoints = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Create mountpoints for volumes if they do not exist.
'';
};
removeCreatedMountPoints = lib.mkOption {
type = lib.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 = lib.mkIf (cfg.enable || anyPamMount) {
environment.systemPackages = [ pkgs.pam_mount ];
environment.etc."security/pam_mount.conf.xml" = {
source =
let
extraUserVolumes = lib.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 ${lib.concatStringsSep " " (lib.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>${lib.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 -->
<!-- the comma in front of the options is necessary for empty options -->
<fusemount>${pkgs.fuse}/bin/mount.fuse %(VOLUME) %(MNTPT) -o ,${
lib.concatStringsSep "," (cfg.fuseMountOptions ++ [ "%(OPTIONS)" ])
}'</fusemount>
<fuseumount>${pkgs.fuse}/bin/fusermount -u %(MNTPT)</fuseumount>
<!-- the comma in front of the options is necessary for empty options -->
<cryptmount>${pkgs.pam_mount}/bin/mount.crypt -o ,${
lib.concatStringsSep "," (cfg.cryptMountOptions ++ [ "%(OPTIONS)" ])
} %(VOLUME) %(MNTPT)</cryptmount>
<cryptumount>${pkgs.pam_mount}/bin/umount.crypt %(MNTPT)</cryptumount>
<pmvarrun>${pkgs.pam_mount}/bin/pmvarrun -u %(USER) -o %(OPERATION)</pmvarrun>
${lib.optionalString oflRequired "<ofl>${fake_ofl}/bin/fake_ofl %(SIGNAL) %(MNTPT)</ofl>"}
${lib.concatStrings (map userVolumeEntry (lib.attrValues extraUserVolumes))}
${lib.concatStringsSep "\n" cfg.extraVolumes}
</pam_mount>
'';
};
};
}

View File

@@ -0,0 +1,125 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.please;
ini = pkgs.formats.ini { };
in
{
options.security.please = {
enable = lib.mkEnableOption ''
please, a Sudo clone which allows a users to execute a command or edit a
file as another user
'';
package = lib.mkPackageOption pkgs "please" { };
wheelNeedsPassword = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether users of the `wheel` group must provide a password to run
commands or edit files with {command}`please` and
{command}`pleaseedit` respectively.
'';
};
settings = lib.mkOption {
type = ini.type;
default = { };
example = {
jim_run_any_as_root = {
name = "jim";
type = "run";
target = "root";
rule = ".*";
require_pass = false;
};
jim_edit_etc_hosts_as_root = {
name = "jim";
type = "edit";
target = "root";
rule = "/etc/hosts";
editmode = 644;
require_pass = true;
};
};
description = ''
Please configuration. Refer to
<https://github.com/edneville/please/blob/master/please.ini.md> for
details.
'';
};
};
config = lib.mkIf cfg.enable {
security.wrappers =
let
owner = "root";
group = "root";
setuid = true;
in
{
please = {
source = "${cfg.package}/bin/please";
inherit owner group setuid;
};
pleaseedit = {
source = "${cfg.package}/bin/pleaseedit";
inherit owner group setuid;
};
};
security.please.settings = rec {
# The "wheel" group is allowed to do anything by default but this can be
# overridden.
wheel_run_as_any = {
type = "run";
group = true;
name = "wheel";
target = ".*";
rule = ".*";
require_pass = cfg.wheelNeedsPassword;
};
wheel_edit_as_any = wheel_run_as_any // {
type = "edit";
};
wheel_list_as_any = wheel_run_as_any // {
type = "list";
};
};
environment = {
systemPackages = [ cfg.package ];
etc."please.ini".source = ini.generate "please.ini" (
cfg.settings
// rec {
# The "root" user is allowed to do anything by default and this cannot
# be overridden.
root_run_as_any = {
type = "run";
name = "root";
target = ".*";
rule = ".*";
require_pass = false;
};
root_edit_as_any = root_run_as_any // {
type = "edit";
};
root_list_as_any = root_run_as_any // {
type = "list";
};
}
);
};
security.pam.services.please = {
sshAgentAuth = true;
usshAuth = true;
};
};
}

View File

@@ -0,0 +1,124 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.polkit;
in
{
options = {
security.polkit.enable = lib.mkEnableOption "polkit";
security.polkit.package = lib.mkPackageOption pkgs "polkit" { };
security.polkit.debug = lib.mkEnableOption "debug logs from polkit. This is required in order to see log messages from rule definitions";
security.polkit.extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
example = ''
/* Log authorization checks. */
polkit.addRule(function(action, subject) {
// Make sure to set { security.polkit.debug = true; } in configuration.nix
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:
<https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html#polkit-rules>
'';
};
security.polkit.adminIdentities = lib.mkOption {
type = lib.types.listOf lib.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 `auth_admin`
value). By default, this is all users in the `wheel` group.
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
cfg.package.bin
cfg.package.out
];
systemd.packages = [ cfg.package.out ];
systemd.services.polkit.serviceConfig.ExecStart = [
""
"${cfg.package.out}/lib/polkit-1/polkitd ${lib.optionalString (!cfg.debug) "--no-debug"}"
];
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 [${lib.concatStringsSep ", " (map (i: "\"${i}\"") cfg.adminIdentities)}];
});
${cfg.extraConfig}
''; # TODO: validation on compilation (at least against typos)
services.dbus.packages = [ cfg.package.out ];
security.pam.services.polkit-1 = { };
security.wrappers = {
pkexec = {
setuid = true;
owner = "root";
group = "root";
source = "${cfg.package.bin}/bin/pkexec";
};
polkit-agent-helper-1 = {
setuid = true;
owner = "root";
group = "root";
source = "${cfg.package.out}/lib/polkit-1/polkit-agent-helper-1";
};
};
systemd.tmpfiles.rules = [
# Probably no more needed, clean up
"R /var/lib/polkit-1"
"R /var/lib/PolicyKit"
];
users.users.polkituser = {
description = "PolKit daemon";
uid = config.ids.uids.polkituser;
group = "polkituser";
};
users.groups.polkituser = { };
};
}

View File

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

View File

@@ -0,0 +1,75 @@
# A module for rtkit, a DBus system service that hands out realtime
# scheduling priority to processes that ask for it.
{
config,
lib,
pkgs,
utils,
...
}:
with lib;
let
cfg = config.security.rtkit;
package = pkgs.rtkit;
in
{
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, PulseAudio and PipeWire use this to
acquire realtime priority.
'';
};
security.rtkit.args = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Command-line options for `rtkit-daemon`.
'';
example = [
"--our-realtime-priority=29"
"--max-realtime-priority=28"
];
};
};
config = mkIf cfg.enable {
security.polkit.enable = true;
# To make polkit pickup rtkit policies
environment.systemPackages = [ package ];
services.dbus.packages = [ package ];
systemd.packages = [ package ];
systemd.services.rtkit-daemon = {
serviceConfig.ExecStart = [
"" # Resets command from upstream unit.
"${package}/libexec/rtkit-daemon ${utils.escapeSystemdExecArgs cfg.args}"
];
};
users.users.rtkit = {
isSystemUser = true;
group = "rtkit";
description = "RealtimeKit daemon";
};
users.groups.rtkit = { };
};
}

View File

@@ -0,0 +1,50 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.security.soteria;
in
{
options.security.soteria = {
enable = lib.mkEnableOption null // {
description = ''
Whether to enable Soteria, a Polkit authentication agent
for any desktop environment.
::: {.note}
You should only enable this if you are on a Desktop Environment that
does not provide a graphical polkit authentication agent, or you are on
a standalone window manager or Wayland compositor.
:::
'';
};
package = lib.mkPackageOption pkgs "soteria" { };
};
config = lib.mkIf cfg.enable {
security.polkit.enable = true;
environment.systemPackages = [ cfg.package ];
systemd.user.services.polkit-soteria = {
description = "Soteria, Polkit authentication agent for any desktop environment";
wantedBy = [ "graphical-session.target" ];
wants = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
script = lib.getExe cfg.package;
serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = 1;
TimeoutStopSec = 10;
};
};
};
meta.maintainers = with lib.maintainers; [ johnrtitor ];
}

View File

@@ -0,0 +1,338 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.sudo-rs;
toUserString = user: if (lib.isInt user) then "#${toString user}" else "${user}";
toGroupString = group: if (lib.isInt group) then "%#${toString group}" else "%${group}";
toCommandOptionsString =
options: "${lib.concatStringsSep ":" options}${lib.optionalString (lib.length options != 0) ":"} ";
toCommandsString =
commands:
lib.concatStringsSep ", " (
map (
command:
if (lib.isString command) then
command
else
"${toCommandOptionsString command.options}${command.command}"
) commands
);
in
{
###### interface
options.security.sudo-rs = {
defaultOptions = lib.mkOption {
type = with lib.types; listOf str;
default = [ "SETENV" ];
description = ''
Options used for the default rules, granting `root` and the
`wheel` group permission to run any command as any user.
'';
};
enable = lib.mkEnableOption ''
a memory-safe implementation of the {command}`sudo` command,
which allows non-root users to execute commands as root
'';
package = lib.mkPackageOption pkgs "sudo-rs" { };
wheelNeedsPassword = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether users of the `wheel` group must
provide a password to run commands as super user via {command}`sudo`.
'';
};
execWheelOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Only allow members of the `wheel` group to execute sudo by
setting the executable's permissions accordingly.
This prevents users that are not members of `wheel` from
exploiting vulnerabilities in sudo such as CVE-2021-3156.
'';
};
configFile = lib.mkOption {
type = lib.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
{file}`sudoers` file.
'';
};
extraRules = lib.mkOption {
description = ''
Define specific rules to be in the {file}`sudoers` file.
More specific rules should come after more general ones in order to
yield the expected behavior. You can use `lib.mkBefore`/`lib.mkAfter` to ensure
this is the case when configuration options are merged.
'';
default = [ ];
example = lib.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 lib.types;
listOf (submodule {
options = {
users = lib.mkOption {
type = with lib.types; listOf (either str int);
description = ''
The usernames / UIDs this rule should apply for.
'';
default = [ ];
};
groups = lib.mkOption {
type = with lib.types; listOf (either str int);
description = ''
The groups / GIDs this rule should apply for.
'';
default = [ ];
};
host = lib.mkOption {
type = lib.types.str;
default = "ALL";
description = ''
For what host this rule should apply.
'';
};
runAs = lib.mkOption {
type = with lib.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: `"foo"`.
It is also possible to specify a user/group combination using `"foo:bar"`
or to only allow running as a specific group with `":bar"`.
'';
};
commands = lib.mkOption {
description = ''
The commands for which the rule should apply.
'';
type =
with lib.types;
listOf (
either str (submodule {
options = {
command = lib.mkOption {
type = with lib.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 `""` used as the argument,
not allowing arguments to the command at all.
'';
};
options = lib.mkOption {
type =
with lib.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 [sudo manual](https://www.sudo.ws/man/1.7.10/sudoers.man.html).
'';
default = [ ];
};
};
})
);
};
};
});
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra configuration text appended to {file}`sudoers`.
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !config.security.sudo.enable;
message = "`security.sudo` and `security.sudo-rs` cannot both be enabled";
}
];
security.sudo.enable = lib.mkDefault false;
security.sudo-rs.extraRules =
let
defaultRule =
{
users ? [ ],
groups ? [ ],
opts ? [ ],
}:
[
{
inherit users groups;
commands = [
{
command = "ALL";
options = opts ++ cfg.defaultOptions;
}
];
}
];
in
lib.mkMerge [
# This is ordered before users' `lib.mkBefore` rules,
# so as not to introduce unexpected changes.
(lib.mkOrder 400 (defaultRule {
users = [ "root" ];
}))
# This is ordered to show before (most) other rules, but
# late-enough for a user to `lib.mkBefore` it.
(lib.mkOrder 600 (defaultRule {
groups = [ "wheel" ];
opts = (lib.optional (!cfg.wheelNeedsPassword) "NOPASSWD");
}))
];
security.sudo-rs.configFile = lib.concatStringsSep "\n" (
lib.filter (s: s != "") [
''
# Don't edit this file. Set the NixOS options security.sudo-rs.configFile
# or security.sudo-rs.extraRules instead.
''
(lib.pipe cfg.extraRules [
(lib.filter (rule: lib.length rule.commands != 0))
(map (rule: [
(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)
]))
lib.flatten
(lib.concatStringsSep "\n")
])
"\n"
(lib.optionalString (cfg.extraConfig != "") ''
# extraConfig
${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 = lib.getExe cfg.package;
inherit
owner
group
setuid
permissions
;
};
sudoedit = {
source = lib.getExe' cfg.package "sudoedit";
inherit
owner
group
setuid
permissions
;
};
};
environment.systemPackages = [ cfg.package ];
security.pam.services = {
su-l = {
rootOK = true;
forwardXAuth = true;
logFailures = true;
};
sudo = {
sshAgentAuth = true;
usshAuth = true;
};
sudo-i = {
sshAgentAuth = true;
usshAuth = true;
};
};
environment.etc.sudoers = {
source = pkgs.runCommand "sudoers" {
src = pkgs.writeText "sudoers-in" cfg.configFile;
preferLocalBuild = true;
} "${pkgs.buildPackages.sudo-rs}/bin/visudo -f $src -c && cp $src $out";
mode = "0440";
};
};
meta.maintainers = [ lib.maintainers.nicoo ];
}

View File

@@ -0,0 +1,341 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.security.sudo;
toUserString = user: if (lib.isInt user) then "#${toString user}" else "${user}";
toGroupString = group: if (lib.isInt group) then "%#${toString group}" else "%${group}";
toCommandOptionsString =
options: "${lib.concatStringsSep ":" options}${lib.optionalString (lib.length options != 0) ":"} ";
toCommandsString =
commands:
lib.concatStringsSep ", " (
map (
command:
if (lib.isString command) then
command
else
"${toCommandOptionsString command.options}${command.command}"
) commands
);
in
{
###### interface
options.security.sudo = {
defaultOptions = lib.mkOption {
type = with lib.types; listOf str;
default = [ "SETENV" ];
description = ''
Options used for the default rules, granting `root` and the
`wheel` group permission to run any command as any user.
'';
};
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable the {command}`sudo` command, which
allows non-root users to execute commands as root.
'';
};
package = lib.mkPackageOption pkgs "sudo" { };
wheelNeedsPassword = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether users of the `wheel` group must
provide a password to run commands as super user via {command}`sudo`.
'';
};
execWheelOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Only allow members of the `wheel` group to execute sudo by
setting the executable's permissions accordingly.
This prevents users that are not members of `wheel` from
exploiting vulnerabilities in sudo such as CVE-2021-3156.
'';
};
configFile = lib.mkOption {
type = lib.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
{file}`sudoers` file.
'';
};
extraRules = lib.mkOption {
description = ''
Define specific rules to be in the {file}`sudoers` 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 = lib.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 lib.types;
listOf (submodule {
options = {
users = lib.mkOption {
type = with types; listOf (either str int);
description = ''
The usernames / UIDs this rule should apply for.
'';
default = [ ];
};
groups = lib.mkOption {
type = with types; listOf (either str int);
description = ''
The groups / GIDs this rule should apply for.
'';
default = [ ];
};
host = lib.mkOption {
type = types.str;
default = "ALL";
description = ''
For what host this rule should apply.
'';
};
runAs = lib.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: `"foo"`.
It is also possible to specify a user/group combination using `"foo:bar"`
or to only allow running as a specific group with `":bar"`.
'';
};
commands = lib.mkOption {
description = ''
The commands for which the rule should apply.
'';
type =
with types;
listOf (
either str (submodule {
options = {
command = lib.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 `""` used as the argument,
not allowing arguments to the command at all.
'';
};
options = lib.mkOption {
type =
with types;
listOf (enum [
"NOPASSWD"
"PASSWD"
"NOEXEC"
"EXEC"
"SETENV"
"NOSETENV"
"LOG_INPUT"
"NOLOG_INPUT"
"LOG_OUTPUT"
"NOLOG_OUTPUT"
"MAIL"
"NOMAIL"
"FOLLOW"
"NOFLLOW"
"INTERCEPT"
"NOINTERCEPT"
]);
description = ''
Options for running the command. Refer to the [sudo manual](https://www.sudo.ws/docs/man/1.9.17/sudoers.man/#Tag_Spec).
'';
default = [ ];
};
};
})
);
};
};
});
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra configuration text appended to {file}`sudoers`.
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.package.pname != "sudo-rs";
message = ''
NixOS' `sudo` module does not support `sudo-rs`; see `security.sudo-rs` instead.
'';
}
];
security.sudo.extraRules =
let
defaultRule =
{
users ? [ ],
groups ? [ ],
opts ? [ ],
}:
[
{
inherit users groups;
commands = [
{
command = "ALL";
options = opts ++ cfg.defaultOptions;
}
];
}
];
in
lib.mkMerge [
# This is ordered before users' `mkBefore` rules,
# so as not to introduce unexpected changes.
(lib.mkOrder 400 (defaultRule {
users = [ "root" ];
}))
# This is ordered to show before (most) other rules, but
# late-enough for a user to `mkBefore` it.
(lib.mkOrder 600 (defaultRule {
groups = [ "wheel" ];
opts = (lib.optional (!cfg.wheelNeedsPassword) "NOPASSWD");
}))
];
security.sudo.configFile = lib.concatStringsSep "\n" (
lib.filter (s: s != "") [
''
# Don't edit this file. Set the NixOS options security.sudo.configFile
# or security.sudo.extraRules instead.
''
(lib.pipe cfg.extraRules [
(lib.filter (rule: lib.length rule.commands != 0))
(map (rule: [
(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)
]))
lib.flatten
(lib.concatStringsSep "\n")
])
"\n"
(lib.optionalString (cfg.extraConfig != "") ''
# extraConfig
${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 = [ cfg.package ];
security.pam.services.sudo = {
sshAgentAuth = true;
usshAuth = true;
};
environment.etc.sudoers = {
source =
pkgs.runCommand "sudoers"
{
src = pkgs.writeText "sudoers-in" cfg.configFile;
preferLocalBuild = true;
}
# Make sure that the sudoers file is syntactically valid.
# (currently disabled - NIXOS-66)
"${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out";
mode = "0440";
};
};
}

View File

@@ -0,0 +1,260 @@
{
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 `tmpfs`-based
{manpage}`chroot(2)`.
'';
};
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}
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` to **explicitly** add additional store paths to the
chroot.
:::
'';
};
options.confinement.packages = lib.mkOption {
type = types.listOf (types.either types.str types.package);
default = [ ];
description =
let
mkScOption = optName: "{option}`serviceConfig.${optName}`";
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`.
::: {.note}
The store paths listed in {option}`path` are
**not** included in the closure as
well as paths from other options except those listed
above.
:::
'';
};
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 {file}`/bin/sh` inside
the chroot. If this is set to `null`, no
{file}`/bin/sh` is provided at all.
This is useful for some applications, which for example use the
{manpage}`system(3)` library function to execute commands.
'';
};
options.confinement.mode = lib.mkOption {
type = types.enum [
"full-apivfs"
"chroot-only"
];
default = "full-apivfs";
description = ''
The value `full-apivfs` (the default) sets up
private {file}`/dev`, {file}`/proc`,
{file}`/sys`, {file}`/tmp` and {file}`/var/tmp` file systems
in a separate user name space.
If this is set to `chroot-only`, only the file
system name space is set up along with the call to
{manpage}`chroot(2)`.
In all cases, unless `serviceConfig.PrivateTmp=true` is set,
both {file}`/tmp` and {file}`/var/tmp` paths are added to `InaccessiblePaths=`.
This is to overcome options like `DynamicUser=true`
implying `PrivateTmp=true` without letting it being turned off.
Beware however that giving processes the `CAP_SYS_ADMIN` and `@mount` privileges
can let them undo the effects of `InaccessiblePaths=`.
::: {.note}
This doesn't cover network namespaces and is solely for
file system level isolation.
:::
'';
};
config =
let
inherit (config.confinement) binSh fullUnit;
wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs");
in
lib.mkIf config.confinement.enable {
serviceConfig = {
ReadOnlyPaths = [ "+/" ];
RuntimeDirectory = [ "confinement/${mkPathSafeName name}" ];
RootDirectory = "/run/confinement/${mkPathSafeName name}";
InaccessiblePaths = [
"-+/run/confinement/${mkPathSafeName name}"
];
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.";
}
]
) 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"
''}
# If DynamicUser= is enabled, PrivateTmp=true is implied (and cannot be turned off).
# so disable them unless PrivateTmp=true is explicitely set.
${lib.optionalString (!cfg.serviceConfig.PrivateTmp) ''
echo "InaccessiblePaths=-+/tmp" >> "$serviceFile"
echo "InaccessiblePaths=-+/var/tmp" >> "$serviceFile"
''}
while read storePath; do
if [ -L "$storePath" ]; then
# Currently, systemd can't cope with symlinks in Bind(ReadOnly)Paths,
# so let's just bind-mount the target to that location.
echo "BindReadOnlyPaths=$(readlink -e "$storePath"):$storePath"
elif [ "$storePath" != "$excludedPath" ]; then
echo "BindReadOnlyPaths=$storePath"
fi
done < "$closureInfo/store-paths" >> "$serviceFile"
'';
in
lib.optional cfg.confinement.enable chrootPaths
) config.systemd.services
);
}

View File

@@ -0,0 +1,116 @@
# TPM2 {#module-security-tpm2}
## Introduction {#module-security-tpm2-introduction}
The `tpm2` module allows configuration of a number of programs and services associated with the use of a Trusted Platform Module (TPM).
A TPM is a hardware or firmware device that can be useful for security purposes.
One thing you can do with a TPM is to instruct it to create cryptographic keys which reside inside the TPM hardware, and which cannot be exported, not even to the computer hosting the TPM.
To make those keys useful, the TPM also exposes operations that you can perform using the keys.
So for instance, you can instruct the TPM to encrypt a piece of data using a cryptographic key protected by the TPM, or cryptographically sign a piece of data using a cryptographic key protected by the TPM.
A key stored directly on the TPM is called a resident key.
Because the TPM has very limited storage, it's common to work with another kind of key, called a wrapped key.
When you instruct the TPM to create a wrapped key, it creates a new key, encrypts the secret material using a key stored directly on the TPM, and returns the encrypted secret material along with some metadata to the user, in a file called the key context.
The TPM also supports operations where you provide it with the key context along with whatever you want to encrypt or sign, and it will unwrap the key and do the operation.
Another important concept is attestation.
The idea is that the user of a TPM may want to prove to a third party that a particular key is protected by the TPM hardware.
This is typically done by the manufacturer or distributor of the TPM hardware prior to distribution to the end user, and involves creating a resident key on the TPM, along with a certificate for that key signed by the manufacturer's or distributor's key.
The user can then provide the certificate to the third party along with the public portion of the resident key to prove that the key is protected by the TPM.
Most physical TPMs come with one resident key that also has a certificate.
This key is known as the Endorsement Key, or EK, and the certificate is known as the EK Certificate.
For applications where you want to be able to prove properties of a key to third parties, you will want a key that is wrapped by the EK.
Such keys are described as residing in the Endorsement Hierarchy.
If you do not require attestation, you will generally wrap your keys with the storage root key (SRK).
Such keys are described as residing in the Storage Hierarchy, or the Owners Hierarchy.
### Software Architecture {#module-security-tpm2-introduction-softwarearchitecture}
#### TCTI {#module-security-tpm2-introduction-softwarearchitecture-tcti}
TPM hardware uses a binary protocol called the TPM Command Transmission Interface, or TCTI, to communicate with the host computer.
The TPM kernel driver exposes a character device, typically `/dev/tpm0`, and one way of interacting with the TPM is to read and write the TCTI protocol to that device.
Of course doing that directly in your own code would be quite laborious, and there are a number of software libraries and programs that can help you do it.
The lowest level of these is C library called ESAPI, located inside the `tpm2-tss` package.
#### Resource Managers {#module-security-tpm2-introduction-resourcemanagers}
Another thing you need to know is that the TPM is a stateful device: operations can affect its state, and it's common to perform a sequence of operations where early operations modify the state, and later operations depend on that state to do other operations.
For instance, you may load a wrapped key to a particular storage location, then do an encryption operation using that loaded key.
This makes multi-user access challenging, as two users may modify the state in incompatible ways.
The solution to this is to use a resource manager.
The resource manager can handle multiple different sessions, keep track of the accumulated state of each session, and load and unload state as necessary to interleave multiple different sessions.
There are two resource managers available in the software stack surrounding the TPM: a kernel-space resource manager, and a user-space resource manager.
The kernel-space resource manager is exposed via the tpmrm subsystem.
You can see more info about that subsystem in the sysfs at `/sys/class/tpmrm`.
In a typical system you will also see a `/dev/tpmrm0` device, but it is possible to give it a different name.
The user-space resource manager is contained in the `tpm2-abrmd` package.
It is also frequently referred to as `tabrmd`.
Both resource managers are designed so that they use the same TCTI protocol as a raw tpm device.
For the kernel RM, you access it via a character device (typically `/dev/tpmrm0`).
For the userspace RM, you access it over DBUS.
It is common to refer to a component that is on the "receiving" or "server" side of the TCTI protocol as "a TCTI".
So the raw tpm character device, the kernel RM, and `tabrmd` are all "TCTIs".
The ESAPI library speaks the client side of the TCTI protocol, and can be connected to any server TCTI.
All of the other libraries or programs that work with TPM all use ESAPI under the hood, and so a common characteristic among all these libraries is that you will find you need to configure them in some way as to which TCTI they should be talking to.
#### Higher Level Interfaces {#module-security-tpm2-introduction-hli}
As alluded to previously, there are a number of ways of speaking the client side TCTI that all amount to wrappers around ESAPI. They include:
* FAPI - a C library with an easier API for managing cryptographic algorithms and keys, contained in the `tpm2-tss` package.
* pytss - a python library that wraps ESAPI and FAPI. Contained in the `tpm2-pytss` package.
* tpm2 tools - a command line interface exposed through the programs `tpm2`, which roughly corresponds to the ESAPI, and `tss2` which roughly corresponds to FAPI. Contained in the `tpm2-tools` package.
* pkcs11 - libraries that wrap TPM functionality into a pkcs11 interface. There are variants for the ESAPI, and the FAPI. See the `tpm2-pkcs11`, `tpm2-pkcs11-esapi` and `tpm2-pkcs11-fapi` packages.
* TPM2 OpenSSL - an OpenSSL provider allowing openssl to use the TPM as its cryptographic engine.
## Using the tpm2 NixOS module {#module-security-tpm2-nixosmodule}
A typical configuration is:
```
security.tpm2 = {
enable = true;
abrmd.enable = true;
pkcs11.enable = true;
tctiEnvironment.enable = true;
tctiEnvironment.interface = "tabrmd";
}
```
`enable = true;` is required for any tpm functionality other than the raw character device and kernel resource manager to be available.
`abrmd.enable = true;` causes the tpm2-abrmd program (the user-space resource manager) to run as a systemd service.
Generally you want this because the user-space resource manager gets more frequent updates than the kernel-space RM, and there aren't any kernel RM features that are unavailable in the user-space RM.
`pkcs11.enable = true;` makes the PKCS11 tool and libraries available in the system path.
Generally you want this because it's unlikely to cause problems and it's required by one of the more common TPM use cases, which is protecting an ssh key using the TPM.
`tctiEnvironment.enable = true;` and `tctiEnvironment.interface = "tabrmd";` causes the TPM2TOOLS_TCTI and TPM2_PKCS11_TCTI environment variables to be set properly to use the user-space resource manager.
In other words, it configures all users to use the user-space resource manager for tpm2 tools and tpm2 pkcs11 by default.
### FAPI Configuration {#module-security-tpm2-nixosmodule-fapiconfiguration}
A reasonable FAPI config file is shipped by default and available in `/etc/tpm2-tss/fapi-config.json`.
The `tss2` command line utility in `tpm2-tools` will use this file by default.
If you wish to customize it, you can do so like this:
```
security.tpm2.fapi = {
profileName = "P_RSA2048SHA256";
};
```
This example changes the cryptographic profile from the default P_ECCP256SHA256 to P_RSA2048SHA256.
The profiles specify a number of algorithmic details, but the short version is that P_ECCP256SHA256 uses elliptic curve cryptography over the NIST P256 curve and SHA256 for hashes, while P_RSA2048SHA256 uses RSA 2048 bit keys and SHA256 for hashes.
Probably the most likely option you may want to use in the FAPI config would be `ekCertLess`.
If set to `null` (the default) or `false`, then FAPI calls will fail if the TPM does not have an EK certificate.
Most TPMs that come with physical computers will have an EK cert, so the default setting would not be a problem.
However, virtual TPMs may not have an EK certificate.
For instance, the Nitro TPM provided on some Amazon Web Services virtual machines does not come with an EK Cert.
In such cases, you may wish to set `ekCertLess = true;` so that FAPI is usable without an EK cert.

View File

@@ -0,0 +1,392 @@
{
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 access 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]*", TAG+="systemd", MODE="0660", OWNER="${tssUser}"''}
${
lib.optionalString (
tssUser != null || tssGroup != null
) ''KERNEL=="tpmrm[0-9]*", TAG+="systemd", MODE="0660"''
+ lib.optionalString (tssUser != null) '', OWNER="${tssUser}"''
+ lib.optionalString (tssGroup != null) '', GROUP="${tssGroup}"''
}
'';
fapiConfig = (
pkgs.writeText "fapi-config.json" (
builtins.toJSON (
{
profile_name = cfg.fapi.profileName;
profile_dir = cfg.fapi.profileDir;
user_dir = cfg.fapi.userDir;
system_dir = cfg.fapi.systemDir;
tcti = cfg.fapi.tcti;
system_pcrs = cfg.fapi.systemPcrs;
log_dir = cfg.fapi.logDir;
firmware_log_file = cfg.fapi.firmwareLogFile;
ima_log_file = cfg.fapi.imaLogFile;
}
// lib.optionalAttrs (cfg.fapi.ekCertLess != null) {
ek_cert_less = if cfg.fapi.ekCertLess then "yes" else "no";
}
// lib.optionalAttrs (cfg.fapi.ekFingerprint != null) { ek_fingerprint = cfg.fapi.ekFingerprint; }
)
)
);
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.mkPackageOption pkgs "tpm2-abrmd" { };
};
pkcs11 = {
enable = lib.mkEnableOption ''
TPM2 PKCS#11 tool and shared library in system path
(`/run/current-system/sw/lib/libtpm2_pkcs11.so`)
'';
package = lib.mkOption {
description = "tpm2-pkcs11 package to use";
type = lib.types.package;
default = if cfg.abrmd.enable then pkgs.tpm2-pkcs11.abrmd else pkgs.tpm2-pkcs11;
defaultText = lib.literalExpression "if config.security.tpm2.abrmd.enable then pkgs.tpm2-pkcs11.abrmd else pkgs.tpm2-pkcs11";
};
};
tctiEnvironment = {
enable = lib.mkOption {
description = ''
Set common TCTI environment variables to the specified value.
The variables are
- `TPM2TOOLS_TCTI`
- `TPM2_PKCS11_TCTI`
'';
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
[
tpm2-tools repository](https://github.com/tpm2-software/tpm2-tools/blob/master/man/common/tcti.md#tcti-options).
'';
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
[
tpm2-tools repository](https://github.com/tpm2-software/tpm2-tools/blob/master/man/common/tcti.md#tcti-options).
'';
type = lib.types.str;
default = "bus_name=com.intel.tss2.Tabrmd";
};
};
fapi = {
profileName = lib.mkOption {
description = ''
Name of the default cryptographic profile chosen from the profile_dir directory.
'';
type = lib.types.str;
default = "P_ECCP256SHA256";
};
profileDir = lib.mkOption {
description = ''
Directory that contains all cryptographic profiles known to FAPI.
'';
type = lib.types.str;
default = "${pkgs.tpm2-tss}/etc/tpm2-tss/fapi-profiles/";
defaultText = lib.literalExpression "\${pkgs.tpm2-tss}/etc/fapi-profiles/";
};
userDir = lib.mkOption {
description = ''
The directory where user objects are stored.
'';
type = lib.types.str;
default = "~/.local/share/tpm2-tss/user/keystore/";
};
systemDir = lib.mkOption {
description = ''
The directory where system objects, policies, and imported objects are stored.
'';
type = lib.types.str;
default = "/var/lib/tpm2-tss/keystore";
};
tcti = lib.mkOption {
description = ''
The TCTI which will be used.
An empty string indicates no TCTI is specified by the FAPI config.
If not specified in the FAPI config it can be specified by environment
variable (TPM2TOOLS_TCTI, TPM2_PKCS11_TCTI, etc) or a TCTI will be chosen
by the FAPI library by searching for tabrmd, device, and mssim TCTIs in
that order.
'';
type = lib.types.str;
default = "";
example = "device:/dev/tpmrm0";
};
systemPcrs = lib.mkOption {
description = ''
The PCR registers which are used by the system.
'';
type = lib.types.listOf lib.types.int;
default = [ ];
};
logDir = lib.mkOption {
description = ''
The directory for the event log.
'';
type = lib.types.str;
default = "/var/log/tpm2-tss/eventlog/";
};
ekCertLess = lib.mkOption {
description = ''
A switch to disable Endorsement Key (EK) certificate verification.
A value of null indicates that the generated fapi config file does not
contain a ek_cert_less key. The effect of not having that key at all is
the same as setting its value to false.
A value of false means that the tss2 cli will not work if there is no
EK Cert installed, or if the installed EK Cert can't be validated.
A value of true means that the tss2 cli will work even if there's no EK
cert installed.
'';
type = lib.types.nullOr lib.types.bool;
default = null;
};
ekFingerprint = lib.mkOption {
description = ''
The fingerprint of the endorsement key.
A value of null means that you have chosen not to specify the expected
fingerprint of the EK. You can still have an endorsement key, it just
won't get checked to see if it's fingerprint matches a particular value
before being used.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
firmwareLogFile = lib.mkOption {
description = ''
The binary bios measurements.
'';
type = lib.types.str;
default = "/sys/kernel/security/tpm0/binary_bios_measurements";
};
imaLogFile = lib.mkOption {
description = ''
The binary IMA measurements (Integrity Measurement Architecture).
'';
type = lib.types.str;
default = "/sys/kernel/security/ima/binary_runtime_measurements";
};
};
};
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
}''
)
);
}
{
# This script has the hash of the udev rules in it,
# and also writes that hash to
# /var/lib/tpm2-udev-trigger/hash.txt at the end.
# On each run, it checks to see if the hash embedded in the script
# matches the hash on disk. If they are different, that
# indicates that the udev rules created by this module
# have changed. In that case, a udev change is triggered
# for tpm and tpmrm devices so that the new rules are
# applied at the end of a nixos-rebuild switch or activate
systemd.services."tpm2-udev-trigger" =
let
udevHash =
if cfg.applyUdevRules then (builtins.hashString "md5" (udevRules cfg.tssUser cfg.tssGroup)) else "";
in
{
description = "Trigger udev change for TPM devices";
wants = [ "systemd-udevd.service" ];
after = [
"tpm2.target"
"systemd-udevd.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "tpm2-udev-trigger.sh" ''
stateDir=/var/lib/tpm2-udev-trigger
mkdir -p $stateDir
newHash=${udevHash}
hashFile=$stateDir/hash.txt
# if file exists, read old hash
if [ -f $hashFile ]; then
oldHash="$(< $hashFile)"
else
oldHash=""
fi
if [ "$oldHash" != "$newHash" ]; then
echo "TPM udev rules changed, triggering udev"
${config.systemd.package}/bin/udevadm trigger --subsystem-match=tpm --action=change
${config.systemd.package}/bin/udevadm trigger --subsystem-match=tpmrm --action=change
echo "$newHash" > $hashFile
else
echo "TPM udev rules unchanged, not triggering udev"
fi
'';
};
};
}
(lib.mkIf cfg.abrmd.enable {
systemd.services."tpm2-abrmd" = {
wants = [
"tpm2-udev-trigger.service"
"dev-tpm0.device"
];
after = [
"tpm2-udev-trigger.service"
"dev-tpm0.device"
];
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;
})
{
environment.etc."tpm2-tss/fapi-config.json".source = fapiConfig;
systemd.tmpfiles.rules = [
"d ${cfg.fapi.logDir} 2750 tss ${cfg.tssGroup} -"
"d ${cfg.fapi.systemDir} 2750 root ${cfg.tssGroup} -"
];
}
]
);
meta.doc = ./tpm2.md;
meta.maintainers = with lib.maintainers; [ lschuermann ];
}

View File

@@ -0,0 +1,378 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (config.security) wrapperDir;
wrappers = lib.filterAttrs (name: value: value.enable) config.security.wrappers;
parentWrapperDir = dirOf wrapperDir;
# This is security-sensitive code, and glibc vulns happen from time to time.
# musl is security-focused and generally more minimal, so it's a better choice here.
# The dynamic linker is still a fairly complex piece of code, and the wrappers are
# quite small, so linking it statically is more appropriate.
securityWrapper =
sourceProg:
pkgs.pkgsStatic.callPackage ./wrapper.nix {
inherit sourceProg;
# glibc definitions of insecure environment variables
#
# We extract the single header file we need into its own derivation,
# so that we don't have to pull full glibc sources to build wrappers.
#
# They're taken from pkgs.glibc so that we don't have to keep as close
# an eye on glibc changes. Not every relevant variable is in this header,
# so we maintain a slightly stricter list in wrapper.c itself as well.
unsecvars = lib.overrideDerivation (pkgs.srcOnly pkgs.glibc) (
{ name, ... }:
{
name = "${name}-unsecvars";
installPhase = ''
mkdir $out
cp sysdeps/generic/unsecvars.h $out
'';
}
);
};
fileModeType =
let
# taken from the chmod(1) man page
symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+";
numeric = "[-+=]?[0-7]{0,4}";
mode = "((${symbolic})(,${symbolic})*)|(${numeric})";
in
lib.types.strMatching mode // { description = "file mode string"; };
wrapperType = lib.types.submodule (
{ name, config, ... }:
{
options.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable the wrapper.";
};
options.source = lib.mkOption {
type = lib.types.path;
description = "The absolute path to the program to be wrapped.";
};
options.program = lib.mkOption {
type = with lib.types; nullOr str;
default = name;
description = ''
The name of the wrapper program. Defaults to the attribute name.
'';
};
options.owner = lib.mkOption {
type = lib.types.str;
description = "The owner of the wrapper program.";
};
options.group = lib.mkOption {
type = lib.types.str;
description = "The group of the wrapper program.";
};
options.permissions = lib.mkOption {
type = fileModeType;
default = "u+rx,g+x,o+x";
example = "a+rx";
description = ''
The permissions of the wrapper program. The format is that of a
symbolic or numeric file mode understood by {command}`chmod`.
'';
};
options.capabilities = lib.mkOption {
type = lib.types.commas;
default = "";
description = ''
A comma-separated list of capability clauses to be given to the
wrapper program. The format for capability clauses is described in the
TEXTUAL REPRESENTATION section of the {manpage}`cap_from_text(3)`
manual page. For a list of capabilities supported by the system, check
the {manpage}`capabilities(7)` manual page.
::: {.note}
`cap_setpcap`, which is required for the wrapper
program to be able to raise caps into the Ambient set is NOT raised
to the Ambient set so that the real program cannot modify its own
capabilities!! This may be too restrictive for cases in which the
real program needs cap_setpcap but it at least leans on the side
security paranoid vs. too relaxed.
:::
'';
};
options.setuid = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to add the setuid bit the wrapper program.";
};
options.setgid = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to add the setgid bit the wrapper program.";
};
}
);
###### Activation script for the setcap wrappers
mkSetcapProgram =
{
program,
capabilities,
source,
owner,
group,
permissions,
...
}:
''
cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
# Prevent races
chmod 0000 "$wrapperDir/${program}"
chown ${owner}:${group} "$wrapperDir/${program}"
# Set desired capabilities on the file plus cap_setpcap so
# the wrapper program can elevate the capabilities set on
# its file into the Ambient set.
${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}"
# Set the executable bit
chmod ${permissions} "$wrapperDir/${program}"
'';
###### Activation script for the setuid wrappers
mkSetuidProgram =
{
program,
source,
owner,
group,
setuid,
setgid,
permissions,
...
}:
''
cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
# Prevent races
chmod 0000 "$wrapperDir/${program}"
chown ${owner}:${group} "$wrapperDir/${program}"
chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
'';
mkWrappedPrograms = builtins.map (
opts: if opts.capabilities != "" then mkSetcapProgram opts else mkSetuidProgram opts
) (lib.attrValues wrappers);
in
{
imports = [
(lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
(lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
];
###### interface
options = {
security.enableWrappers = lib.mkEnableOption "SUID/SGID wrappers" // {
default = true;
};
security.wrappers = lib.mkOption {
type = lib.types.attrsOf wrapperType;
default = { };
example = lib.literalExpression ''
{
# a setuid root program
doas =
{ setuid = true;
owner = "root";
group = "root";
source = "''${pkgs.doas}/bin/doas";
};
# a setgid program
locate =
{ setgid = true;
owner = "root";
group = "mlocate";
source = "''${pkgs.locate}/bin/locate";
};
# a program with the CAP_NET_RAW capability
ping =
{ owner = "root";
group = "root";
capabilities = "cap_net_raw+ep";
source = "''${pkgs.iputils.out}/bin/ping";
};
}
'';
description = ''
This option effectively allows adding setuid/setgid bits, capabilities,
changing file ownership and permissions of a program without directly
modifying it. This works by creating a wrapper program in a directory
(not configurable), which is then added to the shell `PATH`.
'';
};
security.wrapperDirSize = lib.mkOption {
default = "50%";
example = "10G";
type = lib.types.str;
description = ''
Size limit for the /run/wrappers tmpfs. Look at {manpage}`mount(8)`, tmpfs size option,
for the accepted syntax. WARNING: don't set to less than 64MB.
'';
};
security.wrapperDir = lib.mkOption {
type = lib.types.path;
default = "/run/wrappers/bin";
internal = true;
description = ''
This option defines the path to the wrapper programs. It
should not be overridden.
'';
};
};
###### implementation
config = lib.mkIf config.security.enableWrappers {
assertions = lib.mapAttrsToList (name: opts: {
assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
message = ''
The security.wrappers.${name} wrapper is not valid:
setuid/setgid and capabilities are mutually exclusive.
'';
}) wrappers;
security.wrappers =
let
mkSetuidRoot = source: {
setuid = true;
owner = "root";
group = "root";
inherit source;
};
in
{
# These are mount related wrappers that require the +s permission.
mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
};
# Make sure our wrapperDir exports to the PATH env variable when
# initializing the shell
environment.extraInit = ''
# Wrappers override other bin directories.
export PATH="${wrapperDir}:$PATH"
'';
security.apparmor.includes = lib.mapAttrs' (
wrapName: wrap:
lib.nameValuePair "nixos/security.wrappers/${wrapName}" ''
include "${
pkgs.apparmorRulesFromClosure { name = "security.wrappers.${wrapName}"; } [
(securityWrapper wrap.source)
]
}"
mrpx ${wrap.source},
''
) wrappers;
systemd.mounts = [
{
where = parentWrapperDir;
what = "tmpfs";
type = "tmpfs";
options = lib.concatStringsSep "," [
"nodev"
"mode=755"
"size=${config.security.wrapperDirSize}"
];
}
];
systemd.services.suid-sgid-wrappers = {
description = "Create SUID/SGID Wrappers";
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
after = [ "systemd-sysusers.service" ];
unitConfig.DefaultDependencies = false;
unitConfig.RequiresMountsFor = [
"/nix/store"
"/run/wrappers"
];
serviceConfig.RestrictSUIDSGID = false;
serviceConfig.Type = "oneshot";
script = ''
chmod 755 "${parentWrapperDir}"
# We want to place the tmpdirs for the wrappers to the parent dir.
wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
chmod a+rx "$wrapperDir"
${lib.concatStringsSep "\n" mkWrappedPrograms}
if [ -L ${wrapperDir} ]; then
# Atomically replace the symlink
# See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
old=$(readlink -f ${wrapperDir})
if [ -e "${wrapperDir}-tmp" ]; then
rm --force --recursive "${wrapperDir}-tmp"
fi
ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
rm --force --recursive "$old"
else
# For initial setup
ln --symbolic "$wrapperDir" "${wrapperDir}"
fi
'';
};
###### wrappers consistency checks
system.checks = lib.singleton (
pkgs.runCommand "ensure-all-wrappers-paths-exist"
{
preferLocalBuild = true;
}
''
# make sure we produce output
mkdir -p $out
echo -n "Checking that Nix store paths of all wrapped programs exist... "
declare -A wrappers
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "wrappers['${n}']='${v.source}'") wrappers)}
for name in "''${!wrappers[@]}"; do
path="''${wrappers[$name]}"
if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
test -t 1 && echo -ne '\033[1;31m'
echo "FAIL"
echo "The path $path does not exist!"
echo 'Please, check the value of `security.wrappers."'$name'".source`.'
test -t 1 && echo -ne '\033[0m'
exit 1
fi
done
echo "OK"
''
);
};
}

View File

@@ -0,0 +1,221 @@
#define _GNU_SOURCE
#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>
// imported from glibc
#include "unsecvars.h"
#ifndef SOURCE_PROG
#error SOURCE_PROG should be defined via preprocessor commandline
#endif
// aborts when false, printing the failed expression
#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))
extern char **environ;
// Wrapper debug variable name
static char *wrapper_debug = "WRAPPER_DEBUG";
#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;
}
// These are environment variable aliases for glibc tunables.
// This list shouldn't grow further, since this is a legacy mechanism.
// Any future tunables are expected to only be accessible through GLIBC_TUNABLES.
//
// They are not included in the glibc-provided UNSECURE_ENVVARS list,
// since any SUID executable ignores them. This wrapper also serves
// executables that are merely granted ambient capabilities, rather than
// being SUID, and hence don't run in secure mode. We'd like them to
// defend those in depth as well, so we clear these explicitly.
//
// Except for MALLOC_CHECK_ (which is marked SXID_ERASE), these are all
// marked SXID_IGNORE (ignored in secure mode), so even the glibc version
// of this wrapper would leave them intact.
#define UNSECURE_ENVVARS_TUNABLES \
"MALLOC_CHECK_\0" \
"MALLOC_TOP_PAD_\0" \
"MALLOC_PERTURB_\0" \
"MALLOC_MMAP_THRESHOLD_\0" \
"MALLOC_TRIM_THRESHOLD_\0" \
"MALLOC_MMAP_MAX_\0" \
"MALLOC_ARENA_MAX\0" \
"MALLOC_ARENA_TEST\0"
int main(int argc, char **argv) {
int debug = getenv(wrapper_debug) != NULL;
// Drop insecure environment variables explicitly
//
// glibc does this automatically in SUID binaries, but we'd like to cover this:
//
// a) before it gets to glibc
// b) in binaries that are only granted ambient capabilities by the wrapper,
// but don't run with an altered effective UID/GID, nor directly gain
// capabilities themselves, and thus don't run in secure mode.
//
// We're using musl, which doesn't drop environment variables in secure mode,
// and we'd also like glibc-specific variables to be covered.
//
// If we don't explicitly unset them, it's quite easy to just set LD_PRELOAD,
// have it passed through to the wrapped program, and gain privileges.
for (char *unsec = UNSECURE_ENVVARS_TUNABLES UNSECURE_ENVVARS; *unsec; unsec = strchr(unsec, 0) + 1) {
if (debug) {
fprintf(stderr, "unsetting %s\n", unsec);
}
unsetenv(unsec);
}
// 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("/proc/self/exe") != 0) {
return 1;
}
char *replacement_argv[2] = {SOURCE_PROG, NULL};
char *old_argv0;
// Replace untrusted or missing argv[0] by the wrapped program path.
// This mitigates vulnerabilities caused by incorrect handling in privileged code.
if (argv[0]) {
old_argv0 = argv[0];
argv[0] = SOURCE_PROG;
} else {
old_argv0 = "«nullptr»";
argv = replacement_argv;
}
execve(SOURCE_PROG, argv, environ);
fprintf(stderr, "%s: cannot run `%s': %s\n",
old_argv0, SOURCE_PROG, strerror(errno));
return 1;
}

View File

@@ -0,0 +1,35 @@
{
stdenv,
unsecvars,
linuxHeaders,
sourceProg,
debug ? false,
}:
# For testing:
# $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { sourceProg = "${pkgs.hello}/bin/hello"; debug = true; }'
stdenv.mkDerivation {
name = "security-wrapper-${baseNameOf sourceProg}";
buildInputs = [ linuxHeaders ];
dontUnpack = true;
CFLAGS = [
''-DSOURCE_PROG="${sourceProg}"''
]
++ (
if debug then
[
"-Werror"
"-Og"
"-g"
]
else
[
"-Wall"
"-O2"
]
);
dontStrip = debug;
installPhase = ''
mkdir -p $out/bin
$CC $CFLAGS ${./wrapper.c} -I${unsecvars} -o $out/bin/security-wrapper
'';
}