| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include <sys/utsname.h> |
| |
| #include "af-list.h" |
| #include "analyze.h" |
| #include "analyze-security.h" |
| #include "analyze-verify.h" |
| #include "bus-error.h" |
| #include "bus-locator.h" |
| #include "bus-map-properties.h" |
| #include "bus-unit-util.h" |
| #include "bus-util.h" |
| #include "copy.h" |
| #include "env-util.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "format-table.h" |
| #include "in-addr-prefix-util.h" |
| #include "locale-util.h" |
| #include "macro.h" |
| #include "manager.h" |
| #include "missing_capability.h" |
| #include "missing_sched.h" |
| #include "mkdir.h" |
| #include "nulstr-util.h" |
| #include "parse-util.h" |
| #include "path-util.h" |
| #include "pretty-print.h" |
| #if HAVE_SECCOMP |
| # include "seccomp-util.h" |
| #endif |
| #include "service.h" |
| #include "set.h" |
| #include "stdio-util.h" |
| #include "strv.h" |
| #include "terminal-util.h" |
| #include "unit-def.h" |
| #include "unit-name.h" |
| #include "unit-serialize.h" |
| |
| typedef struct SecurityInfo { |
| char *id; |
| char *type; |
| char *load_state; |
| char *fragment_path; |
| bool default_dependencies; |
| |
| uint64_t ambient_capabilities; |
| uint64_t capability_bounding_set; |
| |
| char *user; |
| char **supplementary_groups; |
| bool dynamic_user; |
| |
| bool ip_address_deny_all; |
| bool ip_address_allow_localhost; |
| bool ip_address_allow_other; |
| |
| bool ip_filters_custom_ingress; |
| bool ip_filters_custom_egress; |
| |
| char *keyring_mode; |
| char *protect_proc; |
| char *proc_subset; |
| bool lock_personality; |
| bool memory_deny_write_execute; |
| bool no_new_privileges; |
| char *notify_access; |
| bool protect_hostname; |
| |
| bool private_devices; |
| bool private_mounts; |
| bool private_network; |
| bool private_tmp; |
| bool private_users; |
| |
| bool protect_control_groups; |
| bool protect_kernel_modules; |
| bool protect_kernel_tunables; |
| bool protect_kernel_logs; |
| bool protect_clock; |
| |
| char *protect_home; |
| char *protect_system; |
| |
| bool remove_ipc; |
| |
| bool restrict_address_family_inet; |
| bool restrict_address_family_unix; |
| bool restrict_address_family_netlink; |
| bool restrict_address_family_packet; |
| bool restrict_address_family_other; |
| |
| unsigned long long restrict_namespaces; |
| bool restrict_realtime; |
| bool restrict_suid_sgid; |
| |
| char *root_directory; |
| char *root_image; |
| |
| bool delegate; |
| char *device_policy; |
| char **device_allow; |
| |
| Set *system_call_architectures; |
| |
| bool system_call_filter_allow_list; |
| Set *system_call_filter; |
| |
| mode_t _umask; |
| } SecurityInfo; |
| |
| struct security_assessor { |
| const char *id; |
| const char *json_field; |
| const char *description_good; |
| const char *description_bad; |
| const char *description_na; |
| const char *url; |
| uint64_t weight; |
| uint64_t range; |
| int (*assess)( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description); |
| size_t offset; |
| uint64_t parameter; |
| bool default_dependencies_only; |
| }; |
| |
| static SecurityInfo *security_info_new(void) { |
| SecurityInfo *info = new(SecurityInfo, 1); |
| if (!info) |
| return NULL; |
| |
| *info = (SecurityInfo) { |
| .default_dependencies = true, |
| .capability_bounding_set = UINT64_MAX, |
| .restrict_namespaces = UINT64_MAX, |
| ._umask = 0002, |
| }; |
| |
| return info; |
| } |
| |
| static SecurityInfo *security_info_free(SecurityInfo *i) { |
| if (!i) |
| return NULL; |
| |
| free(i->id); |
| free(i->type); |
| free(i->load_state); |
| free(i->fragment_path); |
| |
| free(i->user); |
| |
| free(i->protect_home); |
| free(i->protect_system); |
| |
| free(i->root_directory); |
| free(i->root_image); |
| |
| free(i->keyring_mode); |
| free(i->protect_proc); |
| free(i->proc_subset); |
| free(i->notify_access); |
| |
| free(i->device_policy); |
| strv_free(i->device_allow); |
| |
| strv_free(i->supplementary_groups); |
| set_free(i->system_call_architectures); |
| set_free(i->system_call_filter); |
| |
| return mfree(i); |
| } |
| |
| DEFINE_TRIVIAL_CLEANUP_FUNC(SecurityInfo*, security_info_free); |
| |
| static bool security_info_runs_privileged(const SecurityInfo *i) { |
| assert(i); |
| |
| if (STRPTR_IN_SET(i->user, "0", "root")) |
| return true; |
| |
| if (i->dynamic_user) |
| return false; |
| |
| return isempty(i->user); |
| } |
| |
| static int assess_bool( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| const bool *b = ASSERT_PTR(data); |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = a->parameter ? *b : !*b; |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static int assess_user( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| _cleanup_free_ char *d = NULL; |
| uint64_t b; |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (streq_ptr(info->user, NOBODY_USER_NAME)) { |
| d = strdup("Service runs under as '" NOBODY_USER_NAME "' user, which should not be used for services"); |
| b = 9; |
| } else if (info->dynamic_user && !STR_IN_SET(info->user, "0", "root")) { |
| d = strdup("Service runs under a transient non-root user identity"); |
| b = 0; |
| } else if (info->user && !STR_IN_SET(info->user, "0", "root", "")) { |
| d = strdup("Service runs under a static non-root user identity"); |
| b = 0; |
| } else { |
| *ret_badness = 10; |
| *ret_description = NULL; |
| return 0; |
| } |
| |
| if (!d) |
| return log_oom(); |
| |
| *ret_badness = b; |
| *ret_description = TAKE_PTR(d); |
| |
| return 0; |
| } |
| |
| static int assess_protect_home( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| const char *description; |
| uint64_t badness; |
| char *copy; |
| int r; |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| badness = 10; |
| description = "Service has full access to home directories"; |
| |
| r = parse_boolean(info->protect_home); |
| if (r < 0) { |
| if (streq_ptr(info->protect_home, "read-only")) { |
| badness = 5; |
| description = "Service has read-only access to home directories"; |
| } else if (streq_ptr(info->protect_home, "tmpfs")) { |
| badness = 1; |
| description = "Service has access to fake empty home directories"; |
| } |
| } else if (r > 0) { |
| badness = 0; |
| description = "Service has no access to home directories"; |
| } |
| |
| copy = strdup(description); |
| if (!copy) |
| return log_oom(); |
| |
| *ret_badness = badness; |
| *ret_description = copy; |
| |
| return 0; |
| } |
| |
| static int assess_protect_system( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| const char *description; |
| uint64_t badness; |
| char *copy; |
| int r; |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| badness = 10; |
| description = "Service has full access to the OS file hierarchy"; |
| |
| r = parse_boolean(info->protect_system); |
| if (r < 0) { |
| if (streq_ptr(info->protect_system, "full")) { |
| badness = 3; |
| description = "Service has very limited write access to the OS file hierarchy"; |
| } else if (streq_ptr(info->protect_system, "strict")) { |
| badness = 0; |
| description = "Service has strict read-only access to the OS file hierarchy"; |
| } |
| } else if (r > 0) { |
| badness = 5; |
| description = "Service has limited write access to the OS file hierarchy"; |
| } |
| |
| copy = strdup(description); |
| if (!copy) |
| return log_oom(); |
| |
| *ret_badness = badness; |
| *ret_description = copy; |
| |
| return 0; |
| } |
| |
| static int assess_root_directory( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = |
| empty_or_root(info->root_directory) && |
| empty_or_root(info->root_image); |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static int assess_capability_bounding_set( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = !!(info->capability_bounding_set & a->parameter); |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static int assess_umask( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| char *copy = NULL; |
| const char *d; |
| uint64_t b; |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (!FLAGS_SET(info->_umask, 0002)) { |
| d = "Files created by service are world-writable by default"; |
| b = 10; |
| } else if (!FLAGS_SET(info->_umask, 0004)) { |
| d = "Files created by service are world-readable by default"; |
| b = 5; |
| } else if (!FLAGS_SET(info->_umask, 0020)) { |
| d = "Files created by service are group-writable by default"; |
| b = 2; |
| } else if (!FLAGS_SET(info->_umask, 0040)) { |
| d = "Files created by service are group-readable by default"; |
| b = 1; |
| } else { |
| d = "Files created by service are accessible only by service's own user by default"; |
| b = 0; |
| } |
| |
| copy = strdup(d); |
| if (!copy) |
| return log_oom(); |
| |
| *ret_badness = b; |
| *ret_description = copy; |
| |
| return 0; |
| } |
| |
| static int assess_keyring_mode( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = !streq_ptr(info->keyring_mode, "private"); |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static int assess_protect_proc( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (streq_ptr(info->protect_proc, "noaccess")) |
| *ret_badness = 1; |
| else if (STRPTR_IN_SET(info->protect_proc, "invisible", "ptraceable")) |
| *ret_badness = 0; |
| else |
| *ret_badness = 3; |
| |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static int assess_proc_subset( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = !streq_ptr(info->proc_subset, "pid"); |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static int assess_notify_access( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = streq_ptr(info->notify_access, "all"); |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static int assess_remove_ipc( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (security_info_runs_privileged(info)) |
| *ret_badness = UINT64_MAX; |
| else |
| *ret_badness = !info->remove_ipc; |
| |
| *ret_description = NULL; |
| return 0; |
| } |
| |
| static int assess_supplementary_groups( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (security_info_runs_privileged(info)) |
| *ret_badness = UINT64_MAX; |
| else |
| *ret_badness = !strv_isempty(info->supplementary_groups); |
| |
| *ret_description = NULL; |
| return 0; |
| } |
| |
| static int assess_restrict_namespaces( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = !!(info->restrict_namespaces & a->parameter); |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| #if HAVE_SECCOMP |
| |
| static int assess_system_call_architectures( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| char *d; |
| uint64_t b; |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (set_isempty(info->system_call_architectures)) { |
| b = 10; |
| d = strdup("Service may execute system calls with all ABIs"); |
| } else if (set_contains(info->system_call_architectures, "native") && |
| set_size(info->system_call_architectures) == 1) { |
| b = 0; |
| d = strdup("Service may execute system calls only with native ABI"); |
| } else { |
| b = 8; |
| d = strdup("Service may execute system calls with multiple ABIs"); |
| } |
| |
| if (!d) |
| return log_oom(); |
| |
| *ret_badness = b; |
| *ret_description = d; |
| |
| return 0; |
| } |
| |
| static bool syscall_names_in_filter(Set *s, bool allow_list, const SyscallFilterSet *f, const char **ret_offending_syscall) { |
| NULSTR_FOREACH(syscall, f->value) { |
| if (syscall[0] == '@') { |
| const SyscallFilterSet *g; |
| |
| assert_se(g = syscall_filter_set_find(syscall)); |
| if (syscall_names_in_filter(s, allow_list, g, ret_offending_syscall)) |
| return true; /* bad! */ |
| |
| continue; |
| } |
| |
| /* Let's see if the system call actually exists on this platform, before complaining */ |
| if (seccomp_syscall_resolve_name(syscall) < 0) |
| continue; |
| |
| if (set_contains(s, syscall) == allow_list) { |
| log_debug("Offending syscall filter item: %s", syscall); |
| if (ret_offending_syscall) |
| *ret_offending_syscall = syscall; |
| return true; /* bad! */ |
| } |
| } |
| |
| *ret_offending_syscall = NULL; |
| return false; |
| } |
| |
| static int assess_system_call_filter( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(a); |
| assert(info); |
| assert(ret_badness); |
| assert(ret_description); |
| |
| assert(a->parameter < _SYSCALL_FILTER_SET_MAX); |
| const SyscallFilterSet *f = syscall_filter_sets + a->parameter; |
| |
| _cleanup_free_ char *d = NULL; |
| uint64_t b; |
| int r; |
| |
| if (!info->system_call_filter_allow_list && set_isempty(info->system_call_filter)) { |
| r = free_and_strdup(&d, "Service does not filter system calls"); |
| b = 10; |
| } else { |
| bool bad; |
| const char *offender = NULL; |
| |
| log_debug("Analyzing system call filter, checking against: %s", f->name); |
| bad = syscall_names_in_filter(info->system_call_filter, info->system_call_filter_allow_list, f, &offender); |
| log_debug("Result: %s", bad ? "bad" : "good"); |
| |
| if (info->system_call_filter_allow_list) { |
| if (bad) { |
| r = asprintf(&d, "System call allow list defined for service, and %s is included " |
| "(e.g. %s is allowed)", |
| f->name, offender); |
| b = 9; |
| } else { |
| r = asprintf(&d, "System call allow list defined for service, and %s is not included", |
| f->name); |
| b = 0; |
| } |
| } else { |
| if (bad) { |
| r = asprintf(&d, "System call deny list defined for service, and %s is not included " |
| "(e.g. %s is allowed)", |
| f->name, offender); |
| b = 10; |
| } else { |
| r = asprintf(&d, "System call deny list defined for service, and %s is included", |
| f->name); |
| b = 0; |
| } |
| } |
| } |
| if (r < 0) |
| return log_oom(); |
| |
| *ret_badness = b; |
| *ret_description = TAKE_PTR(d); |
| |
| return 0; |
| } |
| |
| #endif |
| |
| static int assess_ip_address_allow( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| char *d = NULL; |
| uint64_t b; |
| |
| assert(info); |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (info->ip_filters_custom_ingress || info->ip_filters_custom_egress) { |
| d = strdup("Service defines custom ingress/egress IP filters with BPF programs"); |
| b = 0; |
| } else if (!info->ip_address_deny_all) { |
| d = strdup("Service does not define an IP address allow list"); |
| b = 10; |
| } else if (info->ip_address_allow_other) { |
| d = strdup("Service defines IP address allow list with non-localhost entries"); |
| b = 5; |
| } else if (info->ip_address_allow_localhost) { |
| d = strdup("Service defines IP address allow list with only localhost entries"); |
| b = 2; |
| } else { |
| d = strdup("Service blocks all IP address ranges"); |
| b = 0; |
| } |
| |
| if (!d) |
| return log_oom(); |
| |
| *ret_badness = b; |
| *ret_description = d; |
| |
| return 0; |
| } |
| |
| static int assess_device_allow( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| char *d = NULL; |
| uint64_t b; |
| |
| assert(info); |
| assert(ret_badness); |
| assert(ret_description); |
| |
| if (STRPTR_IN_SET(info->device_policy, "strict", "closed")) { |
| |
| if (!strv_isempty(info->device_allow)) { |
| _cleanup_free_ char *join = NULL; |
| |
| join = strv_join(info->device_allow, " "); |
| if (!join) |
| return log_oom(); |
| |
| d = strjoin("Service has a device ACL with some special devices: ", join); |
| b = 5; |
| } else { |
| d = strdup("Service has a minimal device ACL"); |
| b = 0; |
| } |
| } else { |
| d = strdup("Service has no device ACL"); |
| b = 10; |
| } |
| |
| if (!d) |
| return log_oom(); |
| |
| *ret_badness = b; |
| *ret_description = d; |
| |
| return 0; |
| } |
| |
| static int assess_ambient_capabilities( |
| const struct security_assessor *a, |
| const SecurityInfo *info, |
| const void *data, |
| uint64_t *ret_badness, |
| char **ret_description) { |
| |
| assert(ret_badness); |
| assert(ret_description); |
| |
| *ret_badness = info->ambient_capabilities != 0; |
| *ret_description = NULL; |
| |
| return 0; |
| } |
| |
| static const struct security_assessor security_assessor_table[] = { |
| { |
| .id = "User=/DynamicUser=", |
| .json_field = "UserOrDynamicUser", |
| .description_bad = "Service runs as root user", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#User=", |
| .weight = 2000, |
| .range = 10, |
| .assess = assess_user, |
| }, |
| { |
| .id = "SupplementaryGroups=", |
| .json_field = "SupplementaryGroups", |
| .description_good = "Service has no supplementary groups", |
| .description_bad = "Service runs with supplementary groups", |
| .description_na = "Service runs as root, option does not matter", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SupplementaryGroups=", |
| .weight = 200, |
| .range = 1, |
| .assess = assess_supplementary_groups, |
| }, |
| { |
| .id = "PrivateDevices=", |
| .json_field = "PrivateDevices", |
| .description_good = "Service has no access to hardware devices", |
| .description_bad = "Service potentially has access to hardware devices", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, private_devices), |
| }, |
| { |
| .id = "PrivateMounts=", |
| .json_field = "PrivateMounts", |
| .description_good = "Service cannot install system mounts", |
| .description_bad = "Service may install system mounts", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateMounts=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, private_mounts), |
| }, |
| { |
| .id = "PrivateNetwork=", |
| .json_field = "PrivateNetwork", |
| .description_good = "Service has no access to the host's network", |
| .description_bad = "Service has access to the host's network", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateNetwork=", |
| .weight = 2500, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, private_network), |
| }, |
| { |
| .id = "PrivateTmp=", |
| .json_field = "PrivateTmp", |
| .description_good = "Service has no access to other software's temporary files", |
| .description_bad = "Service has access to other software's temporary files", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, private_tmp), |
| .default_dependencies_only = true, |
| }, |
| { |
| .id = "PrivateUsers=", |
| .json_field = "PrivateUsers", |
| .description_good = "Service does not have access to other users", |
| .description_bad = "Service has access to other users", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateUsers=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, private_users), |
| }, |
| { |
| .id = "ProtectControlGroups=", |
| .json_field = "ProtectControlGroups", |
| .description_good = "Service cannot modify the control group file system", |
| .description_bad = "Service may modify the control group file system", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, protect_control_groups), |
| }, |
| { |
| .id = "ProtectKernelModules=", |
| .json_field = "ProtectKernelModules", |
| .description_good = "Service cannot load or read kernel modules", |
| .description_bad = "Service may load or read kernel modules", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, protect_kernel_modules), |
| }, |
| { |
| .id = "ProtectKernelTunables=", |
| .json_field = "ProtectKernelTunables", |
| .description_good = "Service cannot alter kernel tunables (/proc/sys, …)", |
| .description_bad = "Service may alter kernel tunables", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, protect_kernel_tunables), |
| }, |
| { |
| .id = "ProtectKernelLogs=", |
| .json_field = "ProtectKernelLogs", |
| .description_good = "Service cannot read from or write to the kernel log ring buffer", |
| .description_bad = "Service may read from or write to the kernel log ring buffer", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelLogs=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, protect_kernel_logs), |
| }, |
| { |
| .id = "ProtectClock=", |
| .json_field = "ProtectClock", |
| .description_good = "Service cannot write to the hardware clock or system clock", |
| .description_bad = "Service may write to the hardware clock or system clock", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectClock=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, protect_clock), |
| }, |
| { |
| .id = "ProtectHome=", |
| .json_field = "ProtectHome", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_protect_home, |
| .default_dependencies_only = true, |
| }, |
| { |
| .id = "ProtectHostname=", |
| .json_field = "ProtectHostname", |
| .description_good = "Service cannot change system host/domainname", |
| .description_bad = "Service may change system host/domainname", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHostname=", |
| .weight = 50, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, protect_hostname), |
| }, |
| { |
| .id = "ProtectSystem=", |
| .json_field = "ProtectSystem", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectSystem=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_protect_system, |
| .default_dependencies_only = true, |
| }, |
| { |
| .id = "RootDirectory=/RootImage=", |
| .json_field = "RootDirectoryOrRootImage", |
| .description_good = "Service has its own root directory/image", |
| .description_bad = "Service runs within the host's root directory", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RootDirectory=", |
| .weight = 200, |
| .range = 1, |
| .assess = assess_root_directory, |
| .default_dependencies_only = true, |
| }, |
| { |
| .id = "LockPersonality=", |
| .json_field = "LockPersonality", |
| .description_good = "Service cannot change ABI personality", |
| .description_bad = "Service may change ABI personality", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality=", |
| .weight = 100, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, lock_personality), |
| }, |
| { |
| .id = "MemoryDenyWriteExecute=", |
| .json_field = "MemoryDenyWriteExecute", |
| .description_good = "Service cannot create writable executable memory mappings", |
| .description_bad = "Service may create writable executable memory mappings", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#MemoryDenyWriteExecute=", |
| .weight = 100, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, memory_deny_write_execute), |
| }, |
| { |
| .id = "NoNewPrivileges=", |
| .json_field = "NoNewPrivileges", |
| .description_good = "Service processes cannot acquire new privileges", |
| .description_bad = "Service processes may acquire new privileges", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#NoNewPrivileges=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, no_new_privileges), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_ADMIN", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_ADMIN", |
| .description_good = "Service has no administrator privileges", |
| .description_bad = "Service has administrator privileges", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = UINT64_C(1) << CAP_SYS_ADMIN, |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SET(UID|GID|PCAP)", |
| .json_field = "CapabilityBoundingSet_CAP_SET_UID_GID_PCAP", |
| .description_good = "Service cannot change UID/GID identities/capabilities", |
| .description_bad = "Service may change UID/GID identities/capabilities", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SETUID)| |
| (UINT64_C(1) << CAP_SETGID)| |
| (UINT64_C(1) << CAP_SETPCAP), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_PTRACE", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_PTRACE", |
| .description_good = "Service has no ptrace() debugging abilities", |
| .description_bad = "Service has ptrace() debugging abilities", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_PTRACE), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_TIME", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_TIME", |
| .description_good = "Service processes cannot change the system clock", |
| .description_bad = "Service processes may change the system clock", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = UINT64_C(1) << CAP_SYS_TIME, |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_NET_ADMIN", |
| .json_field = "CapabilityBoundingSet_CAP_NET_ADMIN", |
| .description_good = "Service has no network configuration privileges", |
| .description_bad = "Service has network configuration privileges", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_NET_ADMIN), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_RAWIO", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_RAWIO", |
| .description_good = "Service has no raw I/O access", |
| .description_bad = "Service has raw I/O access", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_RAWIO), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_MODULE", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_MODULE", |
| .description_good = "Service cannot load kernel modules", |
| .description_bad = "Service may load kernel modules", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_MODULE), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_AUDIT_*", |
| .json_field = "CapabilityBoundingSet_CAP_AUDIT", |
| .description_good = "Service has no audit subsystem access", |
| .description_bad = "Service has audit subsystem access", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_AUDIT_CONTROL) | |
| (UINT64_C(1) << CAP_AUDIT_READ) | |
| (UINT64_C(1) << CAP_AUDIT_WRITE), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYSLOG", |
| .json_field = "CapabilityBoundingSet_CAP_SYSLOG", |
| .description_good = "Service has no access to kernel logging", |
| .description_bad = "Service has access to kernel logging", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYSLOG), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_(NICE|RESOURCE)", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_NICE_RESOURCE", |
| .description_good = "Service has no privileges to change resource use parameters", |
| .description_bad = "Service has privileges to change resource use parameters", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_NICE) | |
| (UINT64_C(1) << CAP_SYS_RESOURCE), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_MKNOD", |
| .json_field = "CapabilityBoundingSet_CAP_MKNOD", |
| .description_good = "Service cannot create device nodes", |
| .description_bad = "Service may create device nodes", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_MKNOD), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_(CHOWN|FSETID|SETFCAP)", |
| .json_field = "CapabilityBoundingSet_CAP_CHOWN_FSETID_SETFCAP", |
| .description_good = "Service cannot change file ownership/access mode/capabilities", |
| .description_bad = "Service may change file ownership/access mode/capabilities unrestricted", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_CHOWN) | |
| (UINT64_C(1) << CAP_FSETID) | |
| (UINT64_C(1) << CAP_SETFCAP), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_(DAC_*|FOWNER|IPC_OWNER)", |
| .json_field = "CapabilityBoundingSet_CAP_DAC_FOWNER_IPC_OWNER", |
| .description_good = "Service cannot override UNIX file/IPC permission checks", |
| .description_bad = "Service may override UNIX file/IPC permission checks", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_DAC_OVERRIDE) | |
| (UINT64_C(1) << CAP_DAC_READ_SEARCH) | |
| (UINT64_C(1) << CAP_FOWNER) | |
| (UINT64_C(1) << CAP_IPC_OWNER), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_KILL", |
| .json_field = "CapabilityBoundingSet_CAP_KILL", |
| .description_good = "Service cannot send UNIX signals to arbitrary processes", |
| .description_bad = "Service may send UNIX signals to arbitrary processes", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_KILL), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_NET_(BIND_SERVICE|BROADCAST|RAW)", |
| .json_field = "CapabilityBoundingSet_CAP_NET_BIND_SERVICE_BROADCAST_RAW)", |
| .description_good = "Service has no elevated networking privileges", |
| .description_bad = "Service has elevated networking privileges", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_NET_BIND_SERVICE) | |
| (UINT64_C(1) << CAP_NET_BROADCAST) | |
| (UINT64_C(1) << CAP_NET_RAW), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_BOOT", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_BOOT", |
| .description_good = "Service cannot issue reboot()", |
| .description_bad = "Service may issue reboot()", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 100, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_BOOT), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_MAC_*", |
| .json_field = "CapabilityBoundingSet_CAP_MAC", |
| .description_good = "Service cannot adjust SMACK MAC", |
| .description_bad = "Service may adjust SMACK MAC", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 100, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_MAC_ADMIN)| |
| (UINT64_C(1) << CAP_MAC_OVERRIDE), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE", |
| .json_field = "CapabilityBoundingSet_CAP_LINUX_IMMUTABLE", |
| .description_good = "Service cannot mark files immutable", |
| .description_bad = "Service may mark files immutable", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 75, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_LINUX_IMMUTABLE), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_IPC_LOCK", |
| .json_field = "CapabilityBoundingSet_CAP_IPC_LOCK", |
| .description_good = "Service cannot lock memory into RAM", |
| .description_bad = "Service may lock memory into RAM", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 50, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_IPC_LOCK), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_CHROOT", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_CHROOT", |
| .description_good = "Service cannot issue chroot()", |
| .description_bad = "Service may issue chroot()", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 50, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_CHROOT), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_BLOCK_SUSPEND", |
| .json_field = "CapabilityBoundingSet_CAP_BLOCK_SUSPEND", |
| .description_good = "Service cannot establish wake locks", |
| .description_bad = "Service may establish wake locks", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 25, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_BLOCK_SUSPEND), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_WAKE_ALARM", |
| .json_field = "CapabilityBoundingSet_CAP_WAKE_ALARM", |
| .description_good = "Service cannot program timers that wake up the system", |
| .description_bad = "Service may program timers that wake up the system", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 25, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_WAKE_ALARM), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_LEASE", |
| .json_field = "CapabilityBoundingSet_CAP_LEASE", |
| .description_good = "Service cannot create file leases", |
| .description_bad = "Service may create file leases", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 25, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_LEASE), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_TTY_CONFIG", |
| .description_good = "Service cannot issue vhangup()", |
| .description_bad = "Service may issue vhangup()", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 25, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_TTY_CONFIG), |
| }, |
| { |
| .id = "CapabilityBoundingSet=~CAP_SYS_PACCT", |
| .json_field = "CapabilityBoundingSet_CAP_SYS_PACCT", |
| .description_good = "Service cannot use acct()", |
| .description_bad = "Service may use acct()", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", |
| .weight = 25, |
| .range = 1, |
| .assess = assess_capability_bounding_set, |
| .parameter = (UINT64_C(1) << CAP_SYS_PACCT), |
| }, |
| { |
| .id = "UMask=", |
| .json_field = "UMask", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#UMask=", |
| .weight = 100, |
| .range = 10, |
| .assess = assess_umask, |
| }, |
| { |
| .id = "KeyringMode=", |
| .json_field = "KeyringMode", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#KeyringMode=", |
| .description_good = "Service doesn't share key material with other services", |
| .description_bad = "Service shares key material with other service", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_keyring_mode, |
| }, |
| { |
| .id = "ProtectProc=", |
| .json_field = "ProtectProc", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectProc=", |
| .description_good = "Service has restricted access to process tree (/proc hidepid=)", |
| .description_bad = "Service has full access to process tree (/proc hidepid=)", |
| .weight = 1000, |
| .range = 3, |
| .assess = assess_protect_proc, |
| }, |
| { |
| .id = "ProcSubset=", |
| .json_field = "ProcSubset", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProcSubset=", |
| .description_good = "Service has no access to non-process /proc files (/proc subset=)", |
| .description_bad = "Service has full access to non-process /proc files (/proc subset=)", |
| .weight = 10, |
| .range = 1, |
| .assess = assess_proc_subset, |
| }, |
| { |
| .id = "NotifyAccess=", |
| .json_field = "NotifyAccess", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#NotifyAccess=", |
| .description_good = "Service child processes cannot alter service state", |
| .description_bad = "Service child processes may alter service state", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_notify_access, |
| }, |
| { |
| .id = "RemoveIPC=", |
| .json_field = "RemoveIPC", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RemoveIPC=", |
| .description_good = "Service user cannot leave SysV IPC objects around", |
| .description_bad = "Service user may leave SysV IPC objects around", |
| .description_na = "Service runs as root, option does not apply", |
| .weight = 100, |
| .range = 1, |
| .assess = assess_remove_ipc, |
| .offset = offsetof(SecurityInfo, remove_ipc), |
| }, |
| { |
| .id = "Delegate=", |
| .json_field = "Delegate", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Delegate=", |
| .description_good = "Service does not maintain its own delegated control group subtree", |
| .description_bad = "Service maintains its own delegated control group subtree", |
| .weight = 100, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, delegate), |
| .parameter = true, /* invert! */ |
| }, |
| { |
| .id = "RestrictRealtime=", |
| .json_field = "RestrictRealtime", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime=", |
| .description_good = "Service realtime scheduling access is restricted", |
| .description_bad = "Service may acquire realtime scheduling", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, restrict_realtime), |
| }, |
| { |
| .id = "RestrictSUIDSGID=", |
| .json_field = "RestrictSUIDSGID", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictSUIDSGID=", |
| .description_good = "SUID/SGID file creation by service is restricted", |
| .description_bad = "Service may create SUID/SGID files", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, restrict_suid_sgid), |
| }, |
| { |
| .id = "RestrictNamespaces=~user", |
| .json_field = "RestrictNamespaces_user", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", |
| .description_good = "Service cannot create user namespaces", |
| .description_bad = "Service may create user namespaces", |
| .weight = 1500, |
| .range = 1, |
| .assess = assess_restrict_namespaces, |
| .parameter = CLONE_NEWUSER, |
| }, |
| { |
| .id = "RestrictNamespaces=~mnt", |
| .json_field = "RestrictNamespaces_mnt", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", |
| .description_good = "Service cannot create file system namespaces", |
| .description_bad = "Service may create file system namespaces", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_restrict_namespaces, |
| .parameter = CLONE_NEWNS, |
| }, |
| { |
| .id = "RestrictNamespaces=~ipc", |
| .json_field = "RestrictNamespaces_ipc", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", |
| .description_good = "Service cannot create IPC namespaces", |
| .description_bad = "Service may create IPC namespaces", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_restrict_namespaces, |
| .parameter = CLONE_NEWIPC, |
| }, |
| { |
| .id = "RestrictNamespaces=~pid", |
| .json_field = "RestrictNamespaces_pid", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", |
| .description_good = "Service cannot create process namespaces", |
| .description_bad = "Service may create process namespaces", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_restrict_namespaces, |
| .parameter = CLONE_NEWPID, |
| }, |
| { |
| .id = "RestrictNamespaces=~cgroup", |
| .json_field = "RestrictNamespaces_cgroup", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", |
| .description_good = "Service cannot create cgroup namespaces", |
| .description_bad = "Service may create cgroup namespaces", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_restrict_namespaces, |
| .parameter = CLONE_NEWCGROUP, |
| }, |
| { |
| .id = "RestrictNamespaces=~net", |
| .json_field = "RestrictNamespaces_net", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", |
| .description_good = "Service cannot create network namespaces", |
| .description_bad = "Service may create network namespaces", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_restrict_namespaces, |
| .parameter = CLONE_NEWNET, |
| }, |
| { |
| .id = "RestrictNamespaces=~uts", |
| .json_field = "RestrictNamespaces_uts", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", |
| .description_good = "Service cannot create hostname namespaces", |
| .description_bad = "Service may create hostname namespaces", |
| .weight = 100, |
| .range = 1, |
| .assess = assess_restrict_namespaces, |
| .parameter = CLONE_NEWUTS, |
| }, |
| { |
| .id = "RestrictAddressFamilies=~AF_(INET|INET6)", |
| .json_field = "RestrictAddressFamilies_AF_INET_INET6", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", |
| .description_good = "Service cannot allocate Internet sockets", |
| .description_bad = "Service may allocate Internet sockets", |
| .weight = 1500, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, restrict_address_family_inet), |
| }, |
| { |
| .id = "RestrictAddressFamilies=~AF_UNIX", |
| .json_field = "RestrictAddressFamilies_AF_UNIX", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", |
| .description_good = "Service cannot allocate local sockets", |
| .description_bad = "Service may allocate local sockets", |
| .weight = 25, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, restrict_address_family_unix), |
| }, |
| { |
| .id = "RestrictAddressFamilies=~AF_NETLINK", |
| .json_field = "RestrictAddressFamilies_AF_NETLINK", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", |
| .description_good = "Service cannot allocate netlink sockets", |
| .description_bad = "Service may allocate netlink sockets", |
| .weight = 200, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, restrict_address_family_netlink), |
| }, |
| { |
| .id = "RestrictAddressFamilies=~AF_PACKET", |
| .json_field = "RestrictAddressFamilies_AF_PACKET", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", |
| .description_good = "Service cannot allocate packet sockets", |
| .description_bad = "Service may allocate packet sockets", |
| .weight = 1000, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, restrict_address_family_packet), |
| }, |
| { |
| .id = "RestrictAddressFamilies=~…", |
| .json_field = "RestrictAddressFamilies_OTHER", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", |
| .description_good = "Service cannot allocate exotic sockets", |
| .description_bad = "Service may allocate exotic sockets", |
| .weight = 1250, |
| .range = 1, |
| .assess = assess_bool, |
| .offset = offsetof(SecurityInfo, restrict_address_family_other), |
| }, |
| #if HAVE_SECCOMP |
| { |
| .id = "SystemCallArchitectures=", |
| .json_field = "SystemCallArchitectures", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallArchitectures=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_architectures, |
| }, |
| { |
| .id = "SystemCallFilter=~@swap", |
| .json_field = "SystemCallFilter_swap", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_SWAP, |
| }, |
| { |
| .id = "SystemCallFilter=~@obsolete", |
| .json_field = "SystemCallFilter_obsolete", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 250, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_OBSOLETE, |
| }, |
| { |
| .id = "SystemCallFilter=~@clock", |
| .json_field = "SystemCallFilter_clock", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_CLOCK, |
| }, |
| { |
| .id = "SystemCallFilter=~@cpu-emulation", |
| .json_field = "SystemCallFilter_cpu_emulation", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 250, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_CPU_EMULATION, |
| }, |
| { |
| .id = "SystemCallFilter=~@debug", |
| .json_field = "SystemCallFilter_debug", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_DEBUG, |
| }, |
| { |
| .id = "SystemCallFilter=~@mount", |
| .json_field = "SystemCallFilter_mount", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_MOUNT, |
| }, |
| { |
| .id = "SystemCallFilter=~@module", |
| .json_field = "SystemCallFilter_module", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_MODULE, |
| }, |
| { |
| .id = "SystemCallFilter=~@raw-io", |
| .json_field = "SystemCallFilter_raw_io", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_RAW_IO, |
| }, |
| { |
| .id = "SystemCallFilter=~@reboot", |
| .json_field = "SystemCallFilter_reboot", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_REBOOT, |
| }, |
| { |
| .id = "SystemCallFilter=~@privileged", |
| .json_field = "SystemCallFilter_privileged", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 700, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_PRIVILEGED, |
| }, |
| { |
| .id = "SystemCallFilter=~@resources", |
| .json_field = "SystemCallFilter_resources", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", |
| .weight = 700, |
| .range = 10, |
| .assess = assess_system_call_filter, |
| .parameter = SYSCALL_FILTER_SET_RESOURCES, |
| }, |
| #endif |
| { |
| .id = "IPAddressDeny=", |
| .json_field = "IPAddressDeny", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#IPAddressDeny=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_ip_address_allow, |
| }, |
| { |
| .id = "DeviceAllow=", |
| .json_field = "DeviceAllow", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#DeviceAllow=", |
| .weight = 1000, |
| .range = 10, |
| .assess = assess_device_allow, |
| }, |
| { |
| .id = "AmbientCapabilities=", |
| .json_field = "AmbientCapabilities", |
| .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#AmbientCapabilities=", |
| .description_good = "Service process does not receive ambient capabilities", |
| .description_bad = "Service process receives ambient capabilities", |
| .weight = 500, |
| .range = 1, |
| .assess = assess_ambient_capabilities, |
| }, |
| }; |
| |
| static JsonVariant* security_assessor_find_in_policy(const struct security_assessor *a, JsonVariant *policy, const char *name) { |
| JsonVariant *item; |
| assert(a); |
| |
| if (!policy) |
| return NULL; |
| if (!json_variant_is_object(policy)) { |
| log_debug("Specified policy is not a JSON object, ignoring."); |
| return NULL; |
| } |
| |
| item = json_variant_by_key(policy, a->json_field); |
| if (!item) |
| return NULL; |
| if (!json_variant_is_object(item)) { |
| log_debug("Item for '%s' in policy JSON object is not an object, ignoring.", a->id); |
| return NULL; |
| } |
| |
| return name ? json_variant_by_key(item, name) : item; |
| } |
| |
| static uint64_t access_weight(const struct security_assessor *a, JsonVariant *policy) { |
| JsonVariant *val; |
| |
| assert(a); |
| |
| val = security_assessor_find_in_policy(a, policy, "weight"); |
| if (val) { |
| if (json_variant_is_unsigned(val)) |
| return json_variant_unsigned(val); |
| log_debug("JSON field 'weight' of policy for %s is not an unsigned integer, ignoring.", a->id); |
| } |
| |
| return a->weight; |
| } |
| |
| static uint64_t access_range(const struct security_assessor *a, JsonVariant *policy) { |
| JsonVariant *val; |
| |
| assert(a); |
| |
| val = security_assessor_find_in_policy(a, policy, "range"); |
| if (val) { |
| if (json_variant_is_unsigned(val)) |
| return json_variant_unsigned(val); |
| log_debug("JSON field 'range' of policy for %s is not an unsigned integer, ignoring.", a->id); |
| } |
| |
| return a->range; |
| } |
| |
| static const char *access_description_na(const struct security_assessor *a, JsonVariant *policy) { |
| JsonVariant *val; |
| |
| assert(a); |
| |
| val = security_assessor_find_in_policy(a, policy, "description_na"); |
| if (val) { |
| if (json_variant_is_string(val)) |
| return json_variant_string(val); |
| log_debug("JSON field 'description_na' of policy for %s is not a string, ignoring.", a->id); |
| } |
| |
| return a->description_na; |
| } |
| |
| static const char *access_description_good(const struct security_assessor *a, JsonVariant *policy) { |
| JsonVariant *val; |
| |
| assert(a); |
| |
| val = security_assessor_find_in_policy(a, policy, "description_good"); |
| if (val) { |
| if (json_variant_is_string(val)) |
| return json_variant_string(val); |
| log_debug("JSON field 'description_good' of policy for %s is not a string, ignoring.", a->id); |
| } |
| |
| return a->description_good; |
| } |
| |
| static const char *access_description_bad(const struct security_assessor *a, JsonVariant *policy) { |
| JsonVariant *val; |
| |
| assert(a); |
| |
| val = security_assessor_find_in_policy(a, policy, "description_bad"); |
| if (val) { |
| if (json_variant_is_string(val)) |
| return json_variant_string(val); |
| log_debug("JSON field 'description_bad' of policy for %s is not a string, ignoring.", a->id); |
| } |
| |
| return a->description_bad; |
| } |
| |
| static int assess(const SecurityInfo *info, |
| Table *overview_table, |
| AnalyzeSecurityFlags flags, |
| unsigned threshold, |
| JsonVariant *policy, |
| PagerFlags pager_flags, |
| JsonFormatFlags json_format_flags) { |
| |
| static const struct { |
| uint64_t exposure; |
| const char *name; |
| const char *color; |
| SpecialGlyph smiley; |
| } badness_table[] = { |
| { 100, "DANGEROUS", ANSI_HIGHLIGHT_RED, SPECIAL_GLYPH_DEPRESSED_SMILEY }, |
| { 90, "UNSAFE", ANSI_HIGHLIGHT_RED, SPECIAL_GLYPH_UNHAPPY_SMILEY }, |
| { 75, "EXPOSED", ANSI_HIGHLIGHT_YELLOW, SPECIAL_GLYPH_SLIGHTLY_UNHAPPY_SMILEY }, |
| { 50, "MEDIUM", NULL, SPECIAL_GLYPH_NEUTRAL_SMILEY }, |
| { 10, "OK", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_SLIGHTLY_HAPPY_SMILEY }, |
| { 1, "SAFE", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_HAPPY_SMILEY }, |
| { 0, "PERFECT", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_ECSTATIC_SMILEY }, |
| }; |
| |
| uint64_t badness_sum = 0, weight_sum = 0, exposure; |
| _cleanup_(table_unrefp) Table *details_table = NULL; |
| size_t i; |
| int r; |
| |
| if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) { |
| details_table = table_new(" ", "name", "json_field", "description", "weight", "badness", "range", "exposure"); |
| if (!details_table) |
| return log_oom(); |
| |
| r = table_set_json_field_name(details_table, 0, "set"); |
| if (r < 0) |
| return log_error_errno(r, "Failed to set JSON field name of column 0: %m"); |
| |
| (void) table_set_sort(details_table, (size_t) 3, (size_t) 1); |
| (void) table_set_reverse(details_table, 3, true); |
| |
| if (getenv_bool("SYSTEMD_ANALYZE_DEBUG") <= 0) |
| (void) table_set_display(details_table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 7); |
| } |
| |
| for (i = 0; i < ELEMENTSOF(security_assessor_table); i++) { |
| const struct security_assessor *a = security_assessor_table + i; |
| _cleanup_free_ char *d = NULL; |
| uint64_t badness; |
| void *data; |
| uint64_t weight = access_weight(a, policy); |
| uint64_t range = access_range(a, policy); |
| |
| data = (uint8_t *) info + a->offset; |
| |
| if (a->default_dependencies_only && !info->default_dependencies) { |
| badness = UINT64_MAX; |
| d = strdup("Service runs in special boot phase, option is not appropriate"); |
| if (!d) |
| return log_oom(); |
| } else if (weight == 0) { |
| badness = UINT64_MAX; |
| d = strdup("Option excluded by policy, skipping"); |
| if (!d) |
| return log_oom(); |
| } else { |
| r = a->assess(a, info, data, &badness, &d); |
| if (r < 0) |
| return r; |
| } |
| |
| assert(range > 0); |
| |
| if (badness != UINT64_MAX) { |
| assert(badness <= range); |
| |
| badness_sum += DIV_ROUND_UP(badness * weight, range); |
| weight_sum += weight; |
| } |
| |
| if (details_table) { |
| const char *description, *color = NULL; |
| int checkmark; |
| |
| if (badness == UINT64_MAX) { |
| checkmark = -1; |
| description = access_description_na(a, policy); |
| color = NULL; |
| } else if (badness == a->range) { |
| checkmark = 0; |
| description = access_description_bad(a, policy); |
| color = ansi_highlight_red(); |
| } else if (badness == 0) { |
| checkmark = 1; |
| description = access_description_good(a, policy); |
| color = ansi_highlight_green(); |
| } else { |
| checkmark = 0; |
| description = NULL; |
| color = ansi_highlight_red(); |
| } |
| |
| if (d) |
| description = d; |
| |
| if (checkmark < 0) { |
| r = table_add_many(details_table, TABLE_EMPTY); |
| if (r < 0) |
| return table_log_add_error(r); |
| } else { |
| r = table_add_many(details_table, |
| TABLE_BOOLEAN_CHECKMARK, checkmark > 0, |
| TABLE_SET_MINIMUM_WIDTH, 1, |
| TABLE_SET_MAXIMUM_WIDTH, 1, |
| TABLE_SET_ELLIPSIZE_PERCENT, 0, |
| TABLE_SET_COLOR, color); |
| if (r < 0) |
| return table_log_add_error(r); |
| } |
| |
| r = table_add_many(details_table, |
| TABLE_STRING, a->id, TABLE_SET_URL, a->url, |
| TABLE_STRING, a->json_field, |
| TABLE_STRING, description, |
| TABLE_UINT64, weight, TABLE_SET_ALIGN_PERCENT, 100, |
| TABLE_UINT64, badness, TABLE_SET_ALIGN_PERCENT, 100, |
| TABLE_UINT64, range, TABLE_SET_ALIGN_PERCENT, 100, |
| TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100); |
| if (r < 0) |
| return table_log_add_error(r); |
| } |
| } |
| |
| assert(weight_sum > 0); |
| |
| if (details_table) { |
| size_t row; |
| |
| for (row = 1; row < table_get_rows(details_table); row++) { |
| char buf[DECIMAL_STR_MAX(uint64_t) + 1 + DECIMAL_STR_MAX(uint64_t) + 1]; |
| const uint64_t *weight, *badness, *range; |
| TableCell *cell; |
| uint64_t x; |
| |
| assert_se(weight = table_get_at(details_table, row, 4)); |
| assert_se(badness = table_get_at(details_table, row, 5)); |
| assert_se(range = table_get_at(details_table, row, 6)); |
| |
| if (*badness == UINT64_MAX || *badness == 0) |
| continue; |
| |
| assert_se(cell = table_get_cell(details_table, row, 7)); |
| |
| x = DIV_ROUND_UP(DIV_ROUND_UP(*badness * *weight * 100U, *range), weight_sum); |
| xsprintf(buf, "%" PRIu64 ".%" PRIu64, x / 10, x % 10); |
| |
| r = table_update(details_table, cell, TABLE_STRING, buf); |
| if (r < 0) |
| return log_error_errno(r, "Failed to update cell in table: %m"); |
| } |
| |
| if (json_format_flags & JSON_FORMAT_OFF) { |
| r = table_hide_column_from_display(details_table, (size_t) 2); |
| if (r < 0) |
| return log_error_errno(r, "Failed to set columns to display: %m"); |
| } |
| |
| r = table_print_with_pager(details_table, json_format_flags, pager_flags, /* show_header= */true); |
| if (r < 0) |
| return log_error_errno(r, "Failed to output table: %m"); |
| } |
| |
| exposure = DIV_ROUND_UP(badness_sum * 100U, weight_sum); |
| |
| for (i = 0; i < ELEMENTSOF(badness_table); i++) |
| if (exposure >= badness_table[i].exposure) |
| break; |
| |
| assert(i < ELEMENTSOF(badness_table)); |
| |
| if (details_table && (json_format_flags & JSON_FORMAT_OFF)) { |
| _cleanup_free_ char *clickable = NULL; |
| const char *name; |
| |
| /* If we shall output the details table, also print the brief summary underneath */ |
| |
| if (info->fragment_path) { |
| r = terminal_urlify_path(info->fragment_path, info->id, &clickable); |
| if (r < 0) |
| return log_oom(); |
| |
| name = clickable; |
| } else |
| name = info->id; |
| |
| printf("\n%s %sOverall exposure level for %s%s: %s%" PRIu64 ".%" PRIu64 " %s%s %s\n", |
| special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), |
| ansi_highlight(), |
| name, |
| ansi_normal(), |
| colors_enabled() ? strempty(badness_table[i].color) : "", |
| exposure / 10, exposure % 10, |
| badness_table[i].name, |
| ansi_normal(), |
| special_glyph(badness_table[i].smiley)); |
| } |
| |
| fflush(stdout); |
| |
| if (overview_table) { |
| char buf[DECIMAL_STR_MAX(uint64_t) + 1 + DECIMAL_STR_MAX(uint64_t) + 1]; |
| _cleanup_free_ char *url = NULL; |
| |
| if (info->fragment_path) { |
| r = file_url_from_path(info->fragment_path, &url); |
| if (r < 0) |
| return log_error_errno(r, "Failed to generate URL from path: %m"); |
| } |
| |
| xsprintf(buf, "%" PRIu64 ".%" PRIu64, exposure / 10, exposure % 10); |
| |
| r = table_add_many(overview_table, |
| TABLE_STRING, info->id, |
| TABLE_SET_URL, url, |
| TABLE_STRING, buf, |
| TABLE_SET_ALIGN_PERCENT, 100, |
| TABLE_STRING, badness_table[i].name, |
| TABLE_SET_COLOR, strempty(badness_table[i].color), |
| TABLE_STRING, special_glyph(badness_table[i].smiley)); |
| if (r < 0) |
| return table_log_add_error(r); |
| } |
| |
| /* Return error when overall exposure level is over threshold */ |
| if (exposure > threshold) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static int property_read_restrict_namespaces( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = ASSERT_PTR(userdata); |
| int r; |
| uint64_t namespaces; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_read(m, "t", &namespaces); |
| if (r < 0) |
| return r; |
| |
| info->restrict_namespaces = (unsigned long long) namespaces; |
| |
| return 0; |
| } |
| |
| static int property_read_umask( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = ASSERT_PTR(userdata); |
| int r; |
| uint32_t umask; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_read(m, "u", &umask); |
| if (r < 0) |
| return r; |
| |
| info->_umask = (mode_t) umask; |
| |
| return 0; |
| } |
| |
| static int property_read_restrict_address_families( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = userdata; |
| int allow_list, r; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_enter_container(m, 'r', "bas"); |
| if (r < 0) |
| return r; |
| |
| r = sd_bus_message_read(m, "b", &allow_list); |
| if (r < 0) |
| return r; |
| |
| info->restrict_address_family_inet = |
| info->restrict_address_family_unix = |
| info->restrict_address_family_netlink = |
| info->restrict_address_family_packet = |
| info->restrict_address_family_other = allow_list; |
| |
| r = sd_bus_message_enter_container(m, 'a', "s"); |
| if (r < 0) |
| return r; |
| |
| for (;;) { |
| const char *name; |
| |
| r = sd_bus_message_read(m, "s", &name); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| break; |
| |
| if (STR_IN_SET(name, "AF_INET", "AF_INET6")) |
| info->restrict_address_family_inet = !allow_list; |
| else if (streq(name, "AF_UNIX")) |
| info->restrict_address_family_unix = !allow_list; |
| else if (streq(name, "AF_NETLINK")) |
| info->restrict_address_family_netlink = !allow_list; |
| else if (streq(name, "AF_PACKET")) |
| info->restrict_address_family_packet = !allow_list; |
| else |
| info->restrict_address_family_other = !allow_list; |
| } |
| |
| r = sd_bus_message_exit_container(m); |
| if (r < 0) |
| return r; |
| |
| return sd_bus_message_exit_container(m); |
| } |
| |
| static int property_read_syscall_archs( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = ASSERT_PTR(userdata); |
| int r; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_enter_container(m, 'a', "s"); |
| if (r < 0) |
| return r; |
| |
| for (;;) { |
| const char *name; |
| |
| r = sd_bus_message_read(m, "s", &name); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| break; |
| |
| r = set_put_strdup(&info->system_call_architectures, name); |
| if (r < 0) |
| return r; |
| } |
| |
| return sd_bus_message_exit_container(m); |
| } |
| |
| static int property_read_system_call_filter( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = userdata; |
| int allow_list, r; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_enter_container(m, 'r', "bas"); |
| if (r < 0) |
| return r; |
| |
| r = sd_bus_message_read(m, "b", &allow_list); |
| if (r < 0) |
| return r; |
| |
| info->system_call_filter_allow_list = allow_list; |
| |
| r = sd_bus_message_enter_container(m, 'a', "s"); |
| if (r < 0) |
| return r; |
| |
| for (;;) { |
| const char *name; |
| |
| r = sd_bus_message_read(m, "s", &name); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| break; |
| |
| /* ignore errno or action after colon */ |
| r = set_put_strndup(&info->system_call_filter, name, strchrnul(name, ':') - name); |
| if (r < 0) |
| return r; |
| } |
| |
| r = sd_bus_message_exit_container(m); |
| if (r < 0) |
| return r; |
| |
| return sd_bus_message_exit_container(m); |
| } |
| |
| static int property_read_ip_address_allow( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = userdata; |
| bool deny_ipv4 = false, deny_ipv6 = false; |
| int r; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_enter_container(m, 'a', "(iayu)"); |
| if (r < 0) |
| return r; |
| |
| for (;;) { |
| const void *data; |
| size_t size; |
| int32_t family; |
| uint32_t prefixlen; |
| |
| r = sd_bus_message_enter_container(m, 'r', "iayu"); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| break; |
| |
| r = sd_bus_message_read(m, "i", &family); |
| if (r < 0) |
| return r; |
| |
| r = sd_bus_message_read_array(m, 'y', &data, &size); |
| if (r < 0) |
| return r; |
| |
| r = sd_bus_message_read(m, "u", &prefixlen); |
| if (r < 0) |
| return r; |
| |
| r = sd_bus_message_exit_container(m); |
| if (r < 0) |
| return r; |
| |
| if (streq(member, "IPAddressAllow")) { |
| union in_addr_union u; |
| |
| if (family == AF_INET && size == 4 && prefixlen == 8) |
| memcpy(&u.in, data, size); |
| else if (family == AF_INET6 && size == 16 && prefixlen == 128) |
| memcpy(&u.in6, data, size); |
| else { |
| info->ip_address_allow_other = true; |
| continue; |
| } |
| |
| if (in_addr_is_localhost(family, &u)) |
| info->ip_address_allow_localhost = true; |
| else |
| info->ip_address_allow_other = true; |
| } else { |
| assert(streq(member, "IPAddressDeny")); |
| |
| if (family == AF_INET && size == 4 && prefixlen == 0) |
| deny_ipv4 = true; |
| else if (family == AF_INET6 && size == 16 && prefixlen == 0) |
| deny_ipv6 = true; |
| } |
| } |
| |
| info->ip_address_deny_all = deny_ipv4 && deny_ipv6; |
| |
| return sd_bus_message_exit_container(m); |
| } |
| |
| static int property_read_ip_filters( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = userdata; |
| _cleanup_(strv_freep) char **l = NULL; |
| int r; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_read_strv(m, &l); |
| if (r < 0) |
| return r; |
| |
| if (streq(member, "IPIngressFilterPath")) |
| info->ip_filters_custom_ingress = !strv_isempty(l); |
| else if (streq(member, "IPEgressFilterPath")) |
| info->ip_filters_custom_egress = !strv_isempty(l); |
| |
| return 0; |
| } |
| |
| static int property_read_device_allow( |
| sd_bus *bus, |
| const char *member, |
| sd_bus_message *m, |
| sd_bus_error *error, |
| void *userdata) { |
| |
| SecurityInfo *info = userdata; |
| int r; |
| |
| assert(bus); |
| assert(member); |
| assert(m); |
| |
| r = sd_bus_message_enter_container(m, 'a', "(ss)"); |
| if (r < 0) |
| return r; |
| |
| for (;;) { |
| const char *name, *policy; |
| |
| r = sd_bus_message_read(m, "(ss)", &name, &policy); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| break; |
| |
| r = strv_extendf(&info->device_allow, "%s:%s", name, policy); |
| if (r < 0) |
| return r; |
| } |
| |
| return sd_bus_message_exit_container(m); |
| } |
| |
| static int acquire_security_info(sd_bus *bus, const char *name, SecurityInfo *info, AnalyzeSecurityFlags flags) { |
| |
| static const struct bus_properties_map security_map[] = { |
| { "AmbientCapabilities", "t", NULL, offsetof(SecurityInfo, ambient_capabilities) }, |
| { "CapabilityBoundingSet", "t", NULL, offsetof(SecurityInfo, capability_bounding_set) }, |
| { "DefaultDependencies", "b", NULL, offsetof(SecurityInfo, default_dependencies) }, |
| { "Delegate", "b", NULL, offsetof(SecurityInfo, delegate) }, |
| { "DeviceAllow", "a(ss)", property_read_device_allow, 0 }, |
| { "DevicePolicy", "s", NULL, offsetof(SecurityInfo, device_policy) }, |
| { "DynamicUser", "b", NULL, offsetof(SecurityInfo, dynamic_user) }, |
| { "FragmentPath", "s", NULL, offsetof(SecurityInfo, fragment_path) }, |
| { "IPAddressAllow", "a(iayu)", property_read_ip_address_allow, 0 }, |
| { "IPAddressDeny", "a(iayu)", property_read_ip_address_allow, 0 }, |
| { "IPIngressFilterPath", "as", property_read_ip_filters, 0 }, |
| { "IPEgressFilterPath", "as", property_read_ip_filters, 0 }, |
| { "Id", "s", NULL, offsetof(SecurityInfo, id) }, |
| { "KeyringMode", "s", NULL, offsetof(SecurityInfo, keyring_mode) }, |
| { "ProtectProc", "s", NULL, offsetof(SecurityInfo, protect_proc) }, |
| { "ProcSubset", "s", NULL, offsetof(SecurityInfo, proc_subset) }, |
| { "LoadState", "s", NULL, offsetof(SecurityInfo, load_state) }, |
| { "LockPersonality", "b", NULL, offsetof(SecurityInfo, lock_personality) }, |
| { "MemoryDenyWriteExecute", "b", NULL, offsetof(SecurityInfo, memory_deny_write_execute) }, |
| { "NoNewPrivileges", "b", NULL, offsetof(SecurityInfo, no_new_privileges) }, |
| { "NotifyAccess", "s", NULL, offsetof(SecurityInfo, notify_access) }, |
| { "PrivateDevices", "b", NULL, offsetof(SecurityInfo, private_devices) }, |
| { "PrivateMounts", "b", NULL, offsetof(SecurityInfo, private_mounts) }, |
| { "PrivateNetwork", "b", NULL, offsetof(SecurityInfo, private_network) }, |
| { "PrivateTmp", "b", NULL, offsetof(SecurityInfo, private_tmp) }, |
| { "PrivateUsers", "b", NULL, offsetof(SecurityInfo, private_users) }, |
| { "ProtectControlGroups", "b", NULL, offsetof(SecurityInfo, protect_control_groups) }, |
| { "ProtectHome", "s", NULL, offsetof(SecurityInfo, protect_home) }, |
| { "ProtectHostname", "b", NULL, offsetof(SecurityInfo, protect_hostname) }, |
| { "ProtectKernelModules", "b", NULL, offsetof(SecurityInfo, protect_kernel_modules) }, |
| { "ProtectKernelTunables", "b", NULL, offsetof(SecurityInfo, protect_kernel_tunables) }, |
| { "ProtectKernelLogs", "b", NULL, offsetof(SecurityInfo, protect_kernel_logs) }, |
| { "ProtectClock", "b", NULL, offsetof(SecurityInfo, protect_clock) }, |
| { "ProtectSystem", "s", NULL, offsetof(SecurityInfo, protect_system) }, |
| { "RemoveIPC", "b", NULL, offsetof(SecurityInfo, remove_ipc) }, |
| { "RestrictAddressFamilies", "(bas)", property_read_restrict_address_families, 0 }, |
| { "RestrictNamespaces", "t", property_read_restrict_namespaces, 0 }, |
| { "RestrictRealtime", "b", NULL, offsetof(SecurityInfo, restrict_realtime) }, |
| { "RestrictSUIDSGID", "b", NULL, offsetof(SecurityInfo, restrict_suid_sgid) }, |
| { "RootDirectory", "s", NULL, offsetof(SecurityInfo, root_directory) }, |
| { "RootImage", "s", NULL, offsetof(SecurityInfo, root_image) }, |
| { "SupplementaryGroups", "as", NULL, offsetof(SecurityInfo, supplementary_groups) }, |
| { "SystemCallArchitectures", "as", property_read_syscall_archs, 0 }, |
| { "SystemCallFilter", "(as)", property_read_system_call_filter, 0 }, |
| { "Type", "s", NULL, offsetof(SecurityInfo, type) }, |
| { "UMask", "u", property_read_umask, 0 }, |
| { "User", "s", NULL, offsetof(SecurityInfo, user) }, |
| {} |
| }; |
| |
| _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; |
| _cleanup_free_ char *path = NULL; |
| int r; |
| |
| /* Note: this mangles *info on failure! */ |
| |
| assert(bus); |
| assert(name); |
| assert(info); |
| |
| path = unit_dbus_path_from_name(name); |
| if (!path) |
| return log_oom(); |
| |
| r = bus_map_all_properties( |
| bus, |
| "org.freedesktop.systemd1", |
| path, |
| security_map, |
| BUS_MAP_STRDUP | BUS_MAP_BOOLEAN_AS_BOOL, |
| &error, |
| NULL, |
| info); |
| if (r < 0) |
| return log_error_errno(r, "Failed to get unit properties: %s", bus_error_message(&error, r)); |
| |
| if (!streq_ptr(info->load_state, "loaded")) { |
| |
| if (FLAGS_SET(flags, ANALYZE_SECURITY_ONLY_LOADED)) |
| return -EMEDIUMTYPE; |
| |
| if (streq_ptr(info->load_state, "not-found")) |
| log_error("Unit %s not found, cannot analyze.", name); |
| else if (streq_ptr(info->load_state, "masked")) |
| log_error("Unit %s is masked, cannot analyze.", name); |
| else |
| log_error("Unit %s not loaded properly, cannot analyze.", name); |
| |
| return -EINVAL; |
| } |
| |
| if (FLAGS_SET(flags, ANALYZE_SECURITY_ONLY_LONG_RUNNING) && streq_ptr(info->type, "oneshot")) |
| return -EMEDIUMTYPE; |
| |
| if (info->private_devices || |
| info->private_tmp || |
| info->protect_control_groups || |
| info->protect_kernel_tunables || |
| info->protect_kernel_modules || |
| !streq_ptr(info->protect_home, "no") || |
| !streq_ptr(info->protect_system, "no") || |
| info->root_image) |
| info->private_mounts = true; |
| |
| if (info->protect_kernel_modules) |
| info->capability_bounding_set &= ~(UINT64_C(1) << CAP_SYS_MODULE); |
| |
| if (info->protect_kernel_logs) |
| info->capability_bounding_set &= ~(UINT64_C(1) << CAP_SYSLOG); |
| |
| if (info->protect_clock) |
| info->capability_bounding_set &= ~((UINT64_C(1) << CAP_SYS_TIME) | |
| (UINT64_C(1) << CAP_WAKE_ALARM)); |
| |
| if (info->private_devices) |
| info->capability_bounding_set &= ~((UINT64_C(1) << CAP_MKNOD) | |
| (UINT64_C(1) << CAP_SYS_RAWIO)); |
| |
| return 0; |
| } |
| |
| static int analyze_security_one(sd_bus *bus, |
| const char *name, |
| Table *overview_table, |
| AnalyzeSecurityFlags flags, |
| unsigned threshold, |
| JsonVariant *policy, |
| PagerFlags pager_flags, |
| JsonFormatFlags json_format_flags) { |
| |
| _cleanup_(security_info_freep) SecurityInfo *info = security_info_new(); |
| if (!info) |
| return log_oom(); |
| |
| int r; |
| |
| assert(bus); |
| assert(name); |
| |
| r = acquire_security_info(bus, name, info, flags); |
| if (r == -EMEDIUMTYPE) /* Ignore this one because not loaded or Type is oneshot */ |
| return 0; |
| if (r < 0) |
| return r; |
| |
| r = assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags); |
| if (r < 0) |
| return r; |
| |
| return 0; |
| } |
| |
| /* Refactoring SecurityInfo so that it can make use of existing struct variables instead of reading from dbus */ |
| static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, SecurityInfo **ret_info) { |
| assert(ret_info); |
| |
| _cleanup_(security_info_freep) SecurityInfo *info = security_info_new(); |
| if (!info) |
| return log_oom(); |
| |
| if (u) { |
| if (u->id) { |
| info->id = strdup(u->id); |
| if (!info->id) |
| return log_oom(); |
| } |
| if (unit_type_to_string(u->type)) { |
| info->type = strdup(unit_type_to_string(u->type)); |
| if (!info->type) |
| return log_oom(); |
| } |
| if (unit_load_state_to_string(u->load_state)) { |
| info->load_state = strdup(unit_load_state_to_string(u->load_state)); |
| if (!info->load_state) |
| return log_oom(); |
| } |
| if (u->fragment_path) { |
| info->fragment_path = strdup(u->fragment_path); |
| if (!info->fragment_path) |
| return log_oom(); |
| } |
| info->default_dependencies = u->default_dependencies; |
| if (u->type == UNIT_SERVICE && notify_access_to_string(SERVICE(u)->notify_access)) { |
| info->notify_access = strdup(notify_access_to_string(SERVICE(u)->notify_access)); |
| if (!info->notify_access) |
| return log_oom(); |
| } |
| } |
| |
| if (c) { |
| info->ambient_capabilities = c->capability_ambient_set; |
| info->capability_bounding_set = c->capability_bounding_set; |
| if (c->user) { |
| info->user = strdup(c->user); |
| if (!info->user) |
| return log_oom(); |
| } |
| if (c->supplementary_groups) { |
| info->supplementary_groups = strv_copy(c->supplementary_groups); |
| if (!info->supplementary_groups) |
| return log_oom(); |
| } |
| info->dynamic_user = c->dynamic_user; |
| if (exec_keyring_mode_to_string(c->keyring_mode)) { |
| info->keyring_mode = strdup(exec_keyring_mode_to_string(c->keyring_mode)); |
| if (!info->keyring_mode) |
| return log_oom(); |
| } |
| if (protect_proc_to_string(c->protect_proc)) { |
| info->protect_proc = strdup(protect_proc_to_string(c->protect_proc)); |
| if (!info->protect_proc) |
| return log_oom(); |
| } |
| if (proc_subset_to_string(c->proc_subset)) { |
| info->proc_subset = strdup(proc_subset_to_string(c->proc_subset)); |
| if (!info->proc_subset) |
| return log_oom(); |
| } |
| info->lock_personality = c->lock_personality; |
| info->memory_deny_write_execute = c->memory_deny_write_execute; |
| info->no_new_privileges = c->no_new_privileges; |
| info->protect_hostname = c->protect_hostname; |
| info->private_devices = c->private_devices; |
| info->private_mounts = c->private_mounts; |
| info->private_network = c->private_network; |
| info->private_tmp = c->private_tmp; |
| info->private_users = c->private_users; |
| info->protect_control_groups = c->protect_control_groups; |
| info->protect_kernel_modules = c->protect_kernel_modules; |
| info->protect_kernel_tunables = c->protect_kernel_tunables; |
| info->protect_kernel_logs = c->protect_kernel_logs; |
| info->protect_clock = c->protect_clock; |
| if (protect_home_to_string(c->protect_home)) { |
| info->protect_home = strdup(protect_home_to_string(c->protect_home)); |
| if (!info->protect_home) |
| return log_oom(); |
| } |
| if (protect_system_to_string(c->protect_system)) { |
| info->protect_system = strdup(protect_system_to_string(c->protect_system)); |
| if (!info->protect_system) |
| return log_oom(); |
| } |
| info->remove_ipc = c->remove_ipc; |
| info->restrict_address_family_inet = |
| info->restrict_address_family_unix = |
| info->restrict_address_family_netlink = |
| info->restrict_address_family_packet = |
| info->restrict_address_family_other = |
| c->address_families_allow_list; |
| |
| void *key; |
| SET_FOREACH(key, c->address_families) { |
| int family = PTR_TO_INT(key); |
| if (family == 0) |
| continue; |
| if (IN_SET(family, AF_INET, AF_INET6)) |
| info->restrict_address_family_inet = !c->address_families_allow_list; |
| else if (family == AF_UNIX) |
| info->restrict_address_family_unix = !c->address_families_allow_list; |
| else if (family == AF_NETLINK) |
| info->restrict_address_family_netlink = !c->address_families_allow_list; |
| else if (family == AF_PACKET) |
| info->restrict_address_family_packet = !c->address_families_allow_list; |
| else |
| info->restrict_address_family_other = !c->address_families_allow_list; |
| } |
| |
| info->restrict_namespaces = c->restrict_namespaces; |
| info->restrict_realtime = c->restrict_realtime; |
| info->restrict_suid_sgid = c->restrict_suid_sgid; |
| if (c->root_directory) { |
| info->root_directory = strdup(c->root_directory); |
| if (!info->root_directory) |
| return log_oom(); |
| } |
| if (c->root_image) { |
| info->root_image = strdup(c->root_image); |
| if (!info->root_image) |
| return log_oom(); |
| } |
| info->_umask = c->umask; |
| |
| #if HAVE_SECCOMP |
| SET_FOREACH(key, c->syscall_archs) { |
| const char *name; |
| |
| name = seccomp_arch_to_string(PTR_TO_UINT32(key) - 1); |
| if (!name) |
| continue; |
| |
| if (set_put_strdup(&info->system_call_architectures, name) < 0) |
| return log_oom(); |
| } |
| |
| info->system_call_filter_allow_list = c->syscall_allow_list; |
| |
| void *id, *num; |
| HASHMAP_FOREACH_KEY(num, id, c->syscall_filter) { |
| _cleanup_free_ char *name = NULL; |
| |
| if (info->system_call_filter_allow_list && PTR_TO_INT(num) >= 0) |
| continue; |
| |
| name = seccomp_syscall_resolve_num_arch(SCMP_ARCH_NATIVE, PTR_TO_INT(id) - 1); |
| if (!name) |
| continue; |
| |
| if (set_ensure_consume(&info->system_call_filter, &string_hash_ops_free, TAKE_PTR(name)) < 0) |
| return log_oom(); |
| } |
| #endif |
| } |
| |
| if (g) { |
| info->delegate = g->delegate; |
| if (cgroup_device_policy_to_string(g->device_policy)) { |
| info->device_policy = strdup(cgroup_device_policy_to_string(g->device_policy)); |
| if (!info->device_policy) |
| return log_oom(); |
| } |
| |
| struct in_addr_prefix *i; |
| bool deny_ipv4 = false, deny_ipv6 = false; |
| |
| SET_FOREACH(i, g->ip_address_deny) { |
| if (i->family == AF_INET && i->prefixlen == 0) |
| deny_ipv4 = true; |
| else if (i->family == AF_INET6 && i->prefixlen == 0) |
| deny_ipv6 = true; |
| } |
| info->ip_address_deny_all = deny_ipv4 && deny_ipv6; |
| |
| info->ip_address_allow_localhost = info->ip_address_allow_other = false; |
| SET_FOREACH(i, g->ip_address_allow) { |
| if (in_addr_is_localhost(i->family, &i->address)) |
| info->ip_address_allow_localhost = true; |
| else |
| info->ip_address_allow_other = true; |
| } |
| |
| info->ip_filters_custom_ingress = !strv_isempty(g->ip_filters_ingress); |
| info->ip_filters_custom_egress = !strv_isempty(g->ip_filters_egress); |
| |
| LIST_FOREACH(device_allow, a, g->device_allow) |
| if (strv_extendf(&info->device_allow, |
| "%s:%s%s%s", |
| a->path, |
| a->r ? "r" : "", a->w ? "w" : "", a->m ? "m" : "") < 0) |
| return log_oom(); |
| } |
| |
| *ret_info = TAKE_PTR(info); |
| |
| return 0; |
| } |
| |
| static int offline_security_check(Unit *u, |
| unsigned threshold, |
| JsonVariant *policy, |
| PagerFlags pager_flags, |
| JsonFormatFlags json_format_flags) { |
| |
| _cleanup_(table_unrefp) Table *overview_table = NULL; |
| AnalyzeSecurityFlags flags = 0; |
| _cleanup_(security_info_freep) SecurityInfo *info = NULL; |
| int r; |
| |
| assert(u); |
| |
| if (DEBUG_LOGGING) |
| unit_dump(u, stdout, "\t"); |
| |
| r = get_security_info(u, unit_get_exec_context(u), unit_get_cgroup_context(u), &info); |
| if (r < 0) |
| return r; |
| |
| return assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags); |
| } |
| |
| static int offline_security_checks(char **filenames, |
| JsonVariant *policy, |
| LookupScope scope, |
| bool check_man, |
| bool run_generators, |
| unsigned threshold, |
| const char *root, |
| const char *profile, |
| PagerFlags pager_flags, |
| JsonFormatFlags json_format_flags) { |
| |
| const ManagerTestRunFlags flags = |
| MANAGER_TEST_RUN_MINIMAL | |
| MANAGER_TEST_RUN_ENV_GENERATORS | |
| MANAGER_TEST_RUN_IGNORE_DEPENDENCIES | |
| run_generators * MANAGER_TEST_RUN_GENERATORS; |
| |
| _cleanup_(manager_freep) Manager *m = NULL; |
| Unit *units[strv_length(filenames)]; |
| _cleanup_free_ char *var = NULL; |
| int r, k; |
| size_t count = 0; |
| |
| if (strv_isempty(filenames)) |
| return 0; |
| |
| /* set the path */ |
| r = verify_generate_path(&var, filenames); |
| if (r < 0) |
| return log_error_errno(r, "Failed to generate unit load path: %m"); |
| |
| assert_se(set_unit_path(var) >= 0); |
| |
| r = manager_new(scope, flags, &m); |
| if (r < 0) |
| return log_error_errno(r, "Failed to initialize manager: %m"); |
| |
| log_debug("Starting manager..."); |
| |
| r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root); |
| if (r < 0) |
| return r; |
| |
| if (profile) { |
| /* Ensure the temporary directory is in the search path, so that we can add drop-ins. */ |
| r = strv_extend(&m->lookup_paths.search_path, m->lookup_paths.temporary_dir); |
| if (r < 0) |
| return log_oom(); |
| } |
| |
| log_debug("Loading remaining units from the command line..."); |
| |
| STRV_FOREACH(filename, filenames) { |
| _cleanup_free_ char *prepared = NULL; |
| |
| log_debug("Handling %s...", *filename); |
| |
| k = verify_prepare_filename(*filename, &prepared); |
| if (k < 0) { |
| log_warning_errno(k, "Failed to prepare filename %s: %m", *filename); |
| if (r == 0) |
| r = k; |
| continue; |
| } |
| |
| /* When a portable image is analyzed, the profile is what provides a good chunk of |
| * the security-related settings, but they are obviously not shipped with the image. |
| * This allows to take them in consideration. */ |
| if (profile) { |
| _cleanup_free_ char *unit_name = NULL, *dropin = NULL, *profile_path = NULL; |
| |
| r = path_extract_filename(prepared, &unit_name); |
| if (r < 0) |
| return log_oom(); |
| |
| dropin = strjoin(m->lookup_paths.temporary_dir, "/", unit_name, ".d/profile.conf"); |
| if (!dropin) |
| return log_oom(); |
| (void) mkdir_parents(dropin, 0755); |
| |
| if (!is_path(profile)) { |
| r = find_portable_profile(profile, unit_name, &profile_path); |
| if (r < 0) |
| return log_error_errno(r, "Failed to find portable profile %s: %m", profile); |
| profile = profile_path; |
| } |
| |
| r = copy_file(profile, dropin, 0, 0644, 0, 0, 0); |
| if (r < 0) |
| return log_error_errno(r, "Failed to copy: %m"); |
| } |
| |
| k = manager_load_startable_unit_or_warn(m, NULL, prepared, &units[count]); |
| if (k < 0) { |
| if (r == 0) |
| r = k; |
| continue; |
| } |
| |
| count++; |
| } |
| |
| for (size_t i = 0; i < count; i++) { |
| k = offline_security_check(units[i], threshold, policy, pager_flags, json_format_flags); |
| if (k < 0 && r == 0) |
| r = k; |
| } |
| |
| return r; |
| } |
| |
| static int analyze_security(sd_bus *bus, |
| char **units, |
| JsonVariant *policy, |
| LookupScope scope, |
| bool check_man, |
| bool run_generators, |
| bool offline, |
| unsigned threshold, |
| const char *root, |
| const char *profile, |
| JsonFormatFlags json_format_flags, |
| PagerFlags pager_flags, |
| AnalyzeSecurityFlags flags) { |
| |
| _cleanup_(table_unrefp) Table *overview_table = NULL; |
| int ret = 0, r; |
| |
| assert(!!bus != offline); |
| |
| if (offline) |
| return offline_security_checks(units, policy, scope, check_man, run_generators, threshold, root, profile, pager_flags, json_format_flags); |
| |
| if (strv_length(units) != 1) { |
| overview_table = table_new("unit", "exposure", "predicate", "happy"); |
| if (!overview_table) |
| return log_oom(); |
| } |
| |
| if (strv_isempty(units)) { |
| _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; |
| _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; |
| _cleanup_strv_free_ char **list = NULL; |
| size_t n = 0; |
| |
| r = bus_call_method( |
| bus, |
| bus_systemd_mgr, |
| "ListUnits", |
| &error, |
| &reply, |
| NULL); |
| if (r < 0) |
| return log_error_errno(r, "Failed to list units: %s", bus_error_message(&error, r)); |
| |
| r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)"); |
| if (r < 0) |
| return bus_log_parse_error(r); |
| |
| for (;;) { |
| UnitInfo info; |
| char *copy = NULL; |
| |
| r = bus_parse_unit_info(reply, &info); |
| if (r < 0) |
| return bus_log_parse_error(r); |
| if (r == 0) |
| break; |
| |
| if (!endswith(info.id, ".service")) |
| continue; |
| |
| if (!GREEDY_REALLOC(list, n + 2)) |
| return log_oom(); |
| |
| copy = strdup(info.id); |
| if (!copy) |
| return log_oom(); |
| |
| list[n++] = copy; |
| list[n] = NULL; |
| } |
| |
| strv_sort(list); |
| |
| flags |= ANALYZE_SECURITY_SHORT|ANALYZE_SECURITY_ONLY_LOADED|ANALYZE_SECURITY_ONLY_LONG_RUNNING; |
| |
| STRV_FOREACH(i, list) { |
| r = analyze_security_one(bus, *i, overview_table, flags, threshold, policy, pager_flags, json_format_flags); |
| if (r < 0 && ret >= 0) |
| ret = r; |
| } |
| |
| } else |
| STRV_FOREACH(i, units) { |
| _cleanup_free_ char *mangled = NULL, *instance = NULL; |
| const char *name; |
| |
| if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT) && i != units) { |
| putc('\n', stdout); |
| fflush(stdout); |
| } |
| |
| r = unit_name_mangle(*i, 0, &mangled); |
| if (r < 0) |
| return log_error_errno(r, "Failed to mangle unit name '%s': %m", *i); |
| |
| if (!endswith(mangled, ".service")) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), |
| "Unit %s is not a service unit, refusing.", |
| *i); |
| |
| if (unit_name_is_valid(mangled, UNIT_NAME_TEMPLATE)) { |
| r = unit_name_replace_instance(mangled, "test-instance", &instance); |
| if (r < 0) |
| return log_oom(); |
| |
| name = instance; |
| } else |
| name = mangled; |
| |
| r = analyze_security_one(bus, name, overview_table, flags, threshold, policy, pager_flags, json_format_flags); |
| if (r < 0 && ret >= 0) |
| ret = r; |
| } |
| |
| if (overview_table) { |
| if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) { |
| putc('\n', stdout); |
| fflush(stdout); |
| } |
| |
| r = table_print_with_pager(overview_table, json_format_flags, pager_flags, /* show_header= */true); |
| if (r < 0) |
| return log_error_errno(r, "Failed to output table: %m"); |
| } |
| return ret; |
| } |
| |
| int verb_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; |
| |
| if (!arg_offline) { |
| r = acquire_bus(&bus, NULL); |
| if (r < 0) |
| return bus_log_connect_error(r, arg_transport); |
| } |
| |
| pager_open(arg_pager_flags); |
| |
| 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) { |
| 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, |
| arg_profile, |
| arg_json_format_flags, |
| arg_pager_flags, |
| /*flags=*/ 0); |
| } |