/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include "af-list.h" #include "analyze.h" #include "analyze-security.h" #include "analyze-verify.h" #include "bus-error.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; bool device_allow_non_empty; Set *system_call_architectures; bool system_call_filter_allow_list; Hashmap *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->supplementary_groups); set_free(i->system_call_architectures); hashmap_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 = data; assert(b); 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; } 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; } #if HAVE_SECCOMP static bool syscall_names_in_filter(Hashmap *s, bool allow_list, const SyscallFilterSet *f, const char **ret_offending_syscall) { const char *syscall; NULSTR_FOREACH(syscall, f->value) { int id; 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 */ id = seccomp_syscall_resolve_name(syscall); if (id < 0) continue; if (hashmap_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 && hashmap_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 (info->device_allow_non_empty) { d = strdup("Service has a device ACL with some special devices"); 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), }, { .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, }, #if HAVE_SECCOMP { .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 = userdata; int r; uint64_t namespaces; assert(bus); assert(member); assert(m); assert(info); 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 = userdata; int r; uint32_t umask; assert(bus); assert(member); assert(m); assert(info); 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 = userdata; int r; assert(bus); assert(member); assert(m); assert(info); 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; /* The actual ExecContext stores the system call id as the map value, which we don't * need. So we assign NULL to all values here. */ r = hashmap_put_strdup(&info->system_call_filter, name, NULL); 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; size_t n = 0; 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; n++; } info->device_allow_non_empty = n > 0; 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 (c->syscall_archs) { info->system_call_architectures = set_copy(c->syscall_archs); if (!info->system_call_architectures) return log_oom(); } info->system_call_filter_allow_list = c->syscall_allow_list; if (c->syscall_filter) { info->system_call_filter = hashmap_copy(c->syscall_filter); if (!info->system_call_filter) return log_oom(); } } 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); info->device_allow_non_empty = !LIST_IS_EMPTY(g->device_allow); } *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, UnitFileScope 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; char **filename; 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, UnitFileScope 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; char **i; r = sd_bus_call_method( bus, "org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "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 { char **i; 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); }