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.
+
+ --security-policy=PATH
+
+ 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 --security-policy=
+ 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
+ }
+ }
+
+
+
+
+
+
--iterations=NUMBER
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),