diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index a2f9154791..932218d80e 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -792,6 +792,323 @@ Service b@0.service not loaded, b.socket cannot be started. as well and its default value is 100. + + + + With security, allow the user to define a custom set of + requirements formatted as a JSON file against which to compare the specified unit file(s) + and determine their overall exposure level to security threats. + + + Accepted Assessment Test Identifiers + + + + + + Assessment Test Identifier + + + + + UserOrDynamicUser + + + SupplementaryGroups + + + PrivateMounts + + + PrivateDevices + + + PrivateTmp + + + PrivateNetwork + + + PrivateUsers + + + ProtectControlGroups + + + ProtectKernelModules + + + ProtectKernelTunables + + + ProtectKernelLogs + + + ProtectClock + + + ProtectHome + + + ProtectHostname + + + ProtectSystem + + + RootDirectoryOrRootImage + + + LockPersonality + + + MemoryDenyWriteExecute + + + NoNewPrivileges + + + CapabilityBoundingSet_CAP_SYS_ADMIN + + + CapabilityBoundingSet_CAP_SET_UID_GID_PCAP + + + CapabilityBoundingSet_CAP_SYS_PTRACE + + + CapabilityBoundingSet_CAP_SYS_TIME + + + CapabilityBoundingSet_CAP_NET_ADMIN + + + CapabilityBoundingSet_CAP_SYS_RAWIO + + + CapabilityBoundingSet_CAP_SYS_MODULE + + + CapabilityBoundingSet_CAP_AUDIT + + + CapabilityBoundingSet_CAP_SYSLOG + + + CapabilityBoundingSet_CAP_SYS_NICE_RESOURCE + + + CapabilityBoundingSet_CAP_MKNOD + + + CapabilityBoundingSet_CAP_CHOWN_FSETID_SETFCAP + + + CapabilityBoundingSet_CAP_DAC_FOWNER_IPC_OWNER + + + CapabilityBoundingSet_CAP_KILL + + + CapabilityBoundingSet_CAP_NET_BIND_SERVICE_BROADCAST_RAW + + + CapabilityBoundingSet_CAP_SYS_BOOT + + + CapabilityBoundingSet_CAP_MAC + + + CapabilityBoundingSet_CAP_LINUX_IMMUTABLE + + + CapabilityBoundingSet_CAP_IPC_LOCK + + + CapabilityBoundingSet_CAP_SYS_CHROOT + + + CapabilityBoundingSet_CAP_BLOCK_SUSPEND + + + CapabilityBoundingSet_CAP_WAKE_ALARM + + + CapabilityBoundingSet_CAP_LEASE + + + CapabilityBoundingSet_CAP_SYS_TTY_CONFIG + + + UMask + + + KeyringMode + + + ProtectProc + + + ProcSubset + + + NotifyAccess + + + RemoveIPC + + + Delegate + + + RestrictRealtime + + + RestrictSUIDSGID + + + RestrictNamespaces_CLONE_NEWUSER + + + RestrictNamespaces_CLONE_NEWNS + + + RestrictNamespaces_CLONE_NEWIPC + + + RestrictNamespaces_CLONE_NEWPID + + + RestrictNamespaces_CLONE_NEWCGROUP + + + RestrictNamespaces_CLONE_NEWUTS + + + RestrictNamespaces_CLONE_NEWNET + + + RestrictAddressFamilies_AF_INET_INET6 + + + RestrictAddressFamilies_AF_UNIX + + + RestrictAddressFamilies_AF_NETLINK + + + RestrictAddressFamilies_AF_PACKET + + + RestrictAddressFamilies_OTHER + + + SystemCallArchitectures + + + SystemCallFilter_swap + + + SystemCallFilter_obsolete + + + SystemCallFilter_clock + + + SystemCallFilter_cpu_emulation + + + SystemCallFilter_debug + + + SystemCallFilter_mount + + + SystemCallFilter_module + + + SystemCallFilter_raw_io + + + SystemCallFilter_reboot + + + SystemCallFilter_privileged + + + SystemCallFilter_resources + + + IPAddressDeny + + + DeviceAllow + + + AmbientCapabilities + + + +
+ + + JSON Policy + The JSON file passed as a path parameter to + has a top-level JSON object, with keys being the assessment test identifiers mentioned + above. The values in the file should be JSON objects with one or more of the + following fields: description_na (string), description_good (string), description_bad + (string), weight (unsigned integer), and range (unsigned integer). If any of these fields + corresponding to a specific id of the unit file is missing from the JSON object, the + default built-in field value corresponding to that same id is used for security analysis + as default. The weight and range fields are used in determining the overall exposure level + of the unit files so by allowing users to manipulate these fields, 'security' gives them + the option to decide for themself which ids are more important and hence, should have a greater + effect on the exposure level. + + + { + "PrivateDevices": + { + "description_good": "Service has no access to hardware devices", + "description_bad": "Service potentially has access to hardware devices", + "weight": 1000, + "range": 1 + }, + "PrivateMounts": + { + "description_good": "Service cannot install system mounts", + "description_bad": "Service may install system mounts", + "weight": 1000, + "range": 1 + }, + "PrivateNetwork": + { + "description_good": "Service has no access to the host's network", + "description_bad": "Service has access to the host's network", + "weight": 2500, + "range": 1 + }, + "PrivateTmp": + { + "description_good": "Service has no access to other software's temporary files", + "description_bad": "Service has access to other software's temporary files", + "weight": 1000, + "range": 1 + }, + "PrivateUsers": + { + "description_good": "Service does not have access to other users", + "description_bad": "Service has access to other users", + "weight": 1000, + "range": 1 + } + } + + +
+
+ + diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze index 6f33d53cfc..07d069a6d7 100644 --- a/shell-completion/bash/systemd-analyze +++ b/shell-completion/bash/systemd-analyze @@ -144,7 +144,7 @@ _systemd_analyze() { elif __contains_word "$verb" ${VERBS[SECURITY]}; then if [[ $cur = -* ]]; then - comps='--help --version --no-pager --system --user -H --host -M --machine --offline --threshold' + comps='--help --version --no-pager --system --user -H --host -M --machine --offline --threshold --security-policy' else if __contains_word "--user" ${COMP_WORDS[*]}; then mode=--user diff --git a/shell-completion/zsh/_systemd-analyze b/shell-completion/zsh/_systemd-analyze index f91357cb61..3b77c3b938 100644 --- a/shell-completion/zsh/_systemd-analyze +++ b/shell-completion/zsh/_systemd-analyze @@ -92,6 +92,7 @@ _arguments \ '--recursive-errors=[When verifying a unit, control dependency verification]:MODE' \ '--offline=[Perform a security review of the specified unit file(s)]:BOOL' \ '--threshold=[Set a value to compare the overall security exposure level with]: NUMBER' \ + '--security-policy=[Allow user to use customized requirements to compare unit file(s) against]: PATH' \ '--no-pager[Do not pipe output into a pager]' \ '--man=[Do (not) check for existence of man pages]:boolean:(1 0)' \ '--order[When generating graph for dot, show only order]' \ diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c index 24500e3a5b..6c886e731c 100644 --- a/src/analyze/analyze-security.c +++ b/src/analyze/analyze-security.c @@ -2481,8 +2481,16 @@ static int offline_security_checks(char **filenames, UnitFileScope scope, bool c return r; } -int analyze_security(sd_bus *bus, char **units, UnitFileScope scope, bool check_man, bool run_generators, - bool offline, unsigned threshold, const char *root, AnalyzeSecurityFlags flags) { +int analyze_security(sd_bus *bus, + char **units, + JsonVariant *policy, + UnitFileScope scope, + bool check_man, + bool run_generators, + bool offline, + unsigned threshold, + const char *root, + AnalyzeSecurityFlags flags) { _cleanup_(table_unrefp) Table *overview_table = NULL; int ret = 0, r; diff --git a/src/analyze/analyze-security.h b/src/analyze/analyze-security.h index 57a93afbef..8ad5a689f5 100644 --- a/src/analyze/analyze-security.h +++ b/src/analyze/analyze-security.h @@ -5,6 +5,7 @@ #include "sd-bus.h" +#include "json.h" #include "unit-file.h" typedef enum AnalyzeSecurityFlags { @@ -13,5 +14,13 @@ typedef enum AnalyzeSecurityFlags { ANALYZE_SECURITY_ONLY_LONG_RUNNING = 1 << 2, } AnalyzeSecurityFlags; -int analyze_security(sd_bus *bus, char **units, UnitFileScope scope, bool check_man, bool run_generators, - bool offline, unsigned threshold, const char *root, AnalyzeSecurityFlags flags); +int analyze_security(sd_bus *bus, + char **units, + JsonVariant *policy, + UnitFileScope scope, + bool check_man, + bool run_generators, + bool offline, + unsigned threshold, + const char *root, + AnalyzeSecurityFlags flags); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 9bc7e606e8..816532f69e 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -91,6 +91,7 @@ static bool arg_man = true; static bool arg_generators = false; static char *arg_root = NULL; static char *arg_image = NULL; +static char *arg_security_policy = NULL; static bool arg_offline = false; static unsigned arg_threshold = 100; static unsigned arg_iterations = 1; @@ -100,6 +101,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_dot_from_patterns, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_dot_to_patterns, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_security_policy, freep); typedef struct BootTimes { usec_t firmware_time; @@ -2154,7 +2156,9 @@ static int do_verify(int argc, char *argv[], void *userdata) { static int do_security(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *policy = NULL; int r; + unsigned line, column; r = acquire_bus(&bus, NULL); if (r < 0) @@ -2162,7 +2166,35 @@ static int do_security(int argc, char *argv[], void *userdata) { (void) pager_open(arg_pager_flags); - return analyze_security(bus, strv_skip(argv, 1), arg_scope, arg_man, arg_generators, arg_offline, arg_threshold, arg_root, 0); + if (arg_security_policy) { + r = json_parse_file(/*f=*/ NULL, arg_security_policy, /*flags=*/ 0, &policy, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse '%s' at %u:%u: %m", arg_security_policy, line, column); + } else { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *pp = NULL; + + r = search_and_fopen_nulstr("systemd-analyze-security.policy", "re", /*root=*/ NULL, CONF_PATHS_NULSTR("systemd"), &f, &pp); + if (r < 0 && r != -ENOENT) + return r; + + if (f != NULL) { + r = json_parse_file(f, pp, /*flags=*/ 0, &policy, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON policy: %m", pp, line, column); + } + } + + return analyze_security(bus, + strv_skip(argv, 1), + policy, + arg_scope, + arg_man, + arg_generators, + arg_offline, + arg_threshold, + arg_root, + /*flags=*/ 0); } static int help(int argc, char *argv[], void *userdata) { @@ -2214,6 +2246,8 @@ static int help(int argc, char *argv[], void *userdata) { " --threshold=N Exit with a non-zero status when overall\n" " exposure level is over threshold value\n" " --version Show package version\n" + " --security-policy=PATH Use custom JSON security policy instead\n" + " of built-in one\n" " --no-pager Do not pipe output into a pager\n" " --system Operate on system systemd instance\n" " --user Operate on user systemd instance\n" @@ -2266,6 +2300,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_RECURSIVE_ERRORS, ARG_OFFLINE, ARG_THRESHOLD, + ARG_SECURITY_POLICY, }; static const struct option options[] = { @@ -2278,6 +2313,7 @@ static int parse_argv(int argc, char *argv[]) { { "recursive-errors", required_argument, NULL, ARG_RECURSIVE_ERRORS }, { "offline", required_argument, NULL, ARG_OFFLINE }, { "threshold", required_argument, NULL, ARG_THRESHOLD }, + { "security-policy", required_argument, NULL, ARG_SECURITY_POLICY }, { "system", no_argument, NULL, ARG_SYSTEM }, { "user", no_argument, NULL, ARG_USER }, { "global", no_argument, NULL, ARG_GLOBAL }, @@ -2409,6 +2445,12 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_SECURITY_POLICY: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_security_policy); + if (r < 0) + return r; + break; + case ARG_ITERATIONS: r = safe_atou(optarg, &arg_iterations); if (r < 0) @@ -2447,6 +2489,10 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --user is not supported for cat-config right now."); + if (arg_security_policy && !streq_ptr(argv[optind], "security")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --security-policy= is only supported for security."); + if ((arg_root || arg_image) && (!STRPTR_IN_SET(argv[optind], "cat-config", "verify")) && (!(streq_ptr(argv[optind], "security") && arg_offline))) return log_error_errno(SYNTHETIC_ERRNO(EINVAL),