{ config, lib, pkgs, ... }: let inherit (builtins) head match readFile; inherit (lib) types; inherit (config.environment) etc; cfg = config.security.apparmor; mkDisableOption = descr: lib.mkEnableOption descr // { default=true; example=false; }; in { imports = [ (lib.mkRemovedOptionModule [ "security" "apparmor" "profiles" ] "Please use the new security.apparmor.policies.") apparmor/profiles.nix ]; options = { security.apparmor = { enable = lib.mkEnableOption "Whether to enable the AppArmor Mandatory Access Control system."; policies = lib.mkOption { description = '' AppArmor policies. ''; type = types.attrsOf (types.submodule ({ name, config, ... }: { options = { enable = mkDisableOption "Whether to load the profile into the kernel."; enforce = mkDisableOption "Whether to enforce the policy or only complain in the logs."; profile = lib.mkOption { description = "The policy of the profile."; type = types.lines; apply = pkgs.writeText name; }; }; })); default = {}; }; includes = lib.mkOption { type = types.attrsOf types.lines; default = []; description = '' List of paths to be added to AppArmor's searched paths when resolving absolute #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 '' Whether to enable 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 = mkDisableOption '' Whether to kill processes which have an AppArmor profile enabled (in policies) but are not confined (because AppArmor can only confine new processes). ''; }; }; config = lib.mkIf cfg.enable { environment.systemPackages = [ pkgs.apparmor-utils ]; environment.etc."apparmor.d".source = pkgs.linkFarm "apparmor.d" ( lib.mapAttrsToList (name: p: {inherit name; path=p.profile;}) cfg.policies ++ 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; environment.etc."apparmor/logprof.conf".text = '' [settings] profiledir = /etc/apparmor.d inactive_profiledir = ${pkgs.apparmor-profiles}/share/apparmor/extra-profiles logfiles = /var/log/audit/audit.log /var/log/syslog /var/log/messages parser = ${pkgs.apparmor-parser}/bin/apparmor_parser ldd = ${pkgs.glibc.bin}/bin/ldd logger = ${pkgs.utillinux}/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 directory locations to look for #includes # # each name should be a valid directory containing possible #include # candidate files under the profile dir which by default is /etc/apparmor.d. # # So an entry of my-includes will allow /etc/apparmor.d/my-includes to # be used by the yast UI and profiling tools as a source of #include # files. custom_includes = [qualifiers] ${pkgs.runtimeShell} = icnu ${pkgs.bashInteractive}/bin/sh = icnu ${pkgs.bashInteractive}/bin/bash = icnu '' + head (match "^.*\\[qualifiers](.*)" # Drop the original [settings] section. (readFile "${pkgs.apparmor-utils}/etc/apparmor/logprof.conf")); boot.kernelParams = [ "apparmor=1" "security=apparmor" ]; systemd.services.apparmor = { after = [ "local-fs.target" "systemd-journald-audit.socket" ]; before = [ "sysinit.target" ]; wantedBy = [ "multi-user.target" ]; restartTriggers = [ etc."apparmor/parser.conf".source etc."apparmor.d".source ]; 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 # It works by: # - Adding or replacing into the kernel profiles enabled in cfg.policies # (because AppArmor can do that without stopping the processes already confined). # - Removing from the kernel any profile whose name is not # one of the names within the content of the profiles in cfg.policies. # - Killing the processes which are unconfined but now have a profile loaded # (because AppArmor can only confine new processes). reloadIfChanged = true; # Avoid searchs in /usr/share/locale/ environment.LANG="C"; serviceConfig = let enabledPolicies = lib.attrValues (lib.filterAttrs (n: p: p.enable) cfg.policies); unloadDisabledProfiles = pkgs.writeShellScript "apparmor-remove" '' set -eux enabledProfiles=$(mktemp) loadedProfiles=$(mktemp) trap "rm -f $enabledProfiles $loadedProfiles" EXIT ${pkgs.apparmor-parser}/bin/apparmor_parser --names /dev/null ${ lib.concatMapStrings (p: "\\\n "+p.profile) enabledPolicies} | sort -u >"$enabledProfiles" sed -e "s/ (\(enforce\|complain\))$//" /sys/kernel/security/apparmor/profiles | sort -u >"$loadedProfiles" comm -23 "$loadedProfiles" "$enabledProfiles" | while IFS=$'\n\r' read -r profile do printf %s "$profile" >/sys/kernel/security/apparmor/.remove done ''; killUnconfinedConfinables = pkgs.writeShellScript "apparmor-kill" '' set -eux ${pkgs.apparmor-utils}/bin/aa-status --json | ${pkgs.jq}/bin/jq --raw-output '.processes | .[] | .[] | select (.status == "unconfined") | .pid' | xargs --verbose --no-run-if-empty --delimiter='\n' \ kill ''; commonOpts = p: "--verbose --show-cache ${lib.optionalString (!p.enforce) "--complain "}${p.profile}"; in { Type = "oneshot"; RemainAfterExit = "yes"; ExecStartPre = "${pkgs.apparmor-utils}/bin/aa-teardown"; ExecStart = map (p: "${pkgs.apparmor-parser}/bin/apparmor_parser --add ${commonOpts p}") enabledPolicies; ExecStartPost = lib.optional cfg.killUnconfinedConfinables killUnconfinedConfinables; ExecReload = map (p: "${pkgs.apparmor-parser}/bin/apparmor_parser --replace ${commonOpts p}") enabledPolicies ++ [ unloadDisabledProfiles ] ++ lib.optional cfg.killUnconfinedConfinables killUnconfinedConfinables; ExecStop = "${pkgs.apparmor-utils}/bin/aa-teardown"; CacheDirectory = [ "apparmor" ]; CacheDirectoryMode = "0700"; }; }; }; meta.maintainers = with lib.maintainers; [ julm ]; }