diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index a2f9154791..932218d80e 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -792,6 +792,323 @@ Service b@0.service not loaded, b.socket cannot be started. as well and its default value is 100. + + + + With security, allow the user to define a custom set of + requirements formatted as a JSON file against which to compare the specified unit file(s) + and determine their overall exposure level to security threats. + + + Accepted Assessment Test Identifiers + + + + + + Assessment Test Identifier + + + + + UserOrDynamicUser + + + SupplementaryGroups + + + PrivateMounts + + + PrivateDevices + + + PrivateTmp + + + PrivateNetwork + + + PrivateUsers + + + ProtectControlGroups + + + ProtectKernelModules + + + ProtectKernelTunables + + + ProtectKernelLogs + + + ProtectClock + + + ProtectHome + + + ProtectHostname + + + ProtectSystem + + + RootDirectoryOrRootImage + + + LockPersonality + + + MemoryDenyWriteExecute + + + NoNewPrivileges + + + CapabilityBoundingSet_CAP_SYS_ADMIN + + + CapabilityBoundingSet_CAP_SET_UID_GID_PCAP + + + CapabilityBoundingSet_CAP_SYS_PTRACE + + + CapabilityBoundingSet_CAP_SYS_TIME + + + CapabilityBoundingSet_CAP_NET_ADMIN + + + CapabilityBoundingSet_CAP_SYS_RAWIO + + + CapabilityBoundingSet_CAP_SYS_MODULE + + + CapabilityBoundingSet_CAP_AUDIT + + + CapabilityBoundingSet_CAP_SYSLOG + + + CapabilityBoundingSet_CAP_SYS_NICE_RESOURCE + + + CapabilityBoundingSet_CAP_MKNOD + + + CapabilityBoundingSet_CAP_CHOWN_FSETID_SETFCAP + + + CapabilityBoundingSet_CAP_DAC_FOWNER_IPC_OWNER + + + CapabilityBoundingSet_CAP_KILL + + + CapabilityBoundingSet_CAP_NET_BIND_SERVICE_BROADCAST_RAW + + + CapabilityBoundingSet_CAP_SYS_BOOT + + + CapabilityBoundingSet_CAP_MAC + + + CapabilityBoundingSet_CAP_LINUX_IMMUTABLE + + + CapabilityBoundingSet_CAP_IPC_LOCK + + + CapabilityBoundingSet_CAP_SYS_CHROOT + + + CapabilityBoundingSet_CAP_BLOCK_SUSPEND + + + CapabilityBoundingSet_CAP_WAKE_ALARM + + + CapabilityBoundingSet_CAP_LEASE + + + CapabilityBoundingSet_CAP_SYS_TTY_CONFIG + + + UMask + + + KeyringMode + + + ProtectProc + + + ProcSubset + + + NotifyAccess + + + RemoveIPC + + + Delegate + + + RestrictRealtime + + + RestrictSUIDSGID + + + RestrictNamespaces_CLONE_NEWUSER + + + RestrictNamespaces_CLONE_NEWNS + + + RestrictNamespaces_CLONE_NEWIPC + + + RestrictNamespaces_CLONE_NEWPID + + + RestrictNamespaces_CLONE_NEWCGROUP + + + RestrictNamespaces_CLONE_NEWUTS + + + RestrictNamespaces_CLONE_NEWNET + + + RestrictAddressFamilies_AF_INET_INET6 + + + RestrictAddressFamilies_AF_UNIX + + + RestrictAddressFamilies_AF_NETLINK + + + RestrictAddressFamilies_AF_PACKET + + + RestrictAddressFamilies_OTHER + + + SystemCallArchitectures + + + SystemCallFilter_swap + + + SystemCallFilter_obsolete + + + SystemCallFilter_clock + + + SystemCallFilter_cpu_emulation + + + SystemCallFilter_debug + + + SystemCallFilter_mount + + + SystemCallFilter_module + + + SystemCallFilter_raw_io + + + SystemCallFilter_reboot + + + SystemCallFilter_privileged + + + SystemCallFilter_resources + + + IPAddressDeny + + + DeviceAllow + + + AmbientCapabilities + + + +
+ + + JSON Policy + The JSON file passed as a path parameter to + has a top-level JSON object, with keys being the assessment test identifiers mentioned + above. The values in the file should be JSON objects with one or more of the + following fields: description_na (string), description_good (string), description_bad + (string), weight (unsigned integer), and range (unsigned integer). If any of these fields + corresponding to a specific id of the unit file is missing from the JSON object, the + default built-in field value corresponding to that same id is used for security analysis + as default. The weight and range fields are used in determining the overall exposure level + of the unit files so by allowing users to manipulate these fields, 'security' gives them + the option to decide for themself which ids are more important and hence, should have a greater + effect on the exposure level. + + + { + "PrivateDevices": + { + "description_good": "Service has no access to hardware devices", + "description_bad": "Service potentially has access to hardware devices", + "weight": 1000, + "range": 1 + }, + "PrivateMounts": + { + "description_good": "Service cannot install system mounts", + "description_bad": "Service may install system mounts", + "weight": 1000, + "range": 1 + }, + "PrivateNetwork": + { + "description_good": "Service has no access to the host's network", + "description_bad": "Service has access to the host's network", + "weight": 2500, + "range": 1 + }, + "PrivateTmp": + { + "description_good": "Service has no access to other software's temporary files", + "description_bad": "Service has access to other software's temporary files", + "weight": 1000, + "range": 1 + }, + "PrivateUsers": + { + "description_good": "Service does not have access to other users", + "description_bad": "Service has access to other users", + "weight": 1000, + "range": 1 + } + } + + +
+
+ + diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze index 6f33d53cfc..07d069a6d7 100644 --- a/shell-completion/bash/systemd-analyze +++ b/shell-completion/bash/systemd-analyze @@ -144,7 +144,7 @@ _systemd_analyze() { elif __contains_word "$verb" ${VERBS[SECURITY]}; then if [[ $cur = -* ]]; then - comps='--help --version --no-pager --system --user -H --host -M --machine --offline --threshold' + comps='--help --version --no-pager --system --user -H --host -M --machine --offline --threshold --security-policy' else if __contains_word "--user" ${COMP_WORDS[*]}; then mode=--user diff --git a/shell-completion/zsh/_systemd-analyze b/shell-completion/zsh/_systemd-analyze index f91357cb61..3b77c3b938 100644 --- a/shell-completion/zsh/_systemd-analyze +++ b/shell-completion/zsh/_systemd-analyze @@ -92,6 +92,7 @@ _arguments \ '--recursive-errors=[When verifying a unit, control dependency verification]:MODE' \ '--offline=[Perform a security review of the specified unit file(s)]:BOOL' \ '--threshold=[Set a value to compare the overall security exposure level with]: NUMBER' \ + '--security-policy=[Allow user to use customized requirements to compare unit file(s) against]: PATH' \ '--no-pager[Do not pipe output into a pager]' \ '--man=[Do (not) check for existence of man pages]:boolean:(1 0)' \ '--order[When generating graph for dot, show only order]' \ diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c index 24500e3a5b..63d1998ed3 100644 --- a/src/analyze/analyze-security.c +++ b/src/analyze/analyze-security.c @@ -107,6 +107,7 @@ typedef struct SecurityInfo { struct security_assessor { const char *id; + const char *json_field; const char *description_good; const char *description_bad; const char *description_na; @@ -754,6 +755,7 @@ static int assess_ambient_capabilities( 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, @@ -762,6 +764,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -772,6 +775,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -782,6 +786,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -792,6 +797,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -802,6 +808,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -813,6 +820,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -823,6 +831,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -833,6 +842,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -843,6 +853,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -853,6 +864,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -863,6 +875,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -873,6 +886,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "ProtectHome=", + .json_field = "ProtectHome", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=", .weight = 1000, .range = 10, @@ -881,6 +895,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -891,6 +906,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "ProtectSystem=", + .json_field = "ProtectSystem", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectSystem=", .weight = 1000, .range = 10, @@ -899,6 +915,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -909,6 +926,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -919,6 +937,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -929,6 +948,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -939,6 +959,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -949,6 +970,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -961,6 +983,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -971,6 +994,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -981,6 +1005,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -991,6 +1016,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1001,6 +1027,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1011,6 +1038,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1023,6 +1051,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1033,6 +1062,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1044,6 +1074,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1054,6 +1085,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1066,6 +1098,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1079,6 +1112,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1089,6 +1123,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1101,6 +1136,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1111,6 +1147,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1122,6 +1159,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1132,6 +1170,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1142,6 +1181,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1152,6 +1192,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1162,6 +1203,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1172,6 +1214,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1182,6 +1225,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1192,6 +1236,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=", @@ -1202,6 +1247,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "UMask=", + .json_field = "UMask", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#UMask=", .weight = 100, .range = 10, @@ -1209,6 +1255,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1218,6 +1265,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=)", @@ -1227,6 +1275,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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=)", @@ -1236,6 +1285,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1245,6 +1295,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1256,6 +1307,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1267,6 +1319,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1277,6 +1330,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1287,6 +1341,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "RestrictNamespaces=~CLONE_NEWUSER", + .json_field = "RestrictNamespaces_CLONE_NEWUSER", .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", @@ -1297,6 +1352,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "RestrictNamespaces=~CLONE_NEWNS", + .json_field = "RestrictNamespaces_CLONE_NEWNS", .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", @@ -1307,6 +1363,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "RestrictNamespaces=~CLONE_NEWIPC", + .json_field = "RestrictNamespaces_CLONE_NEWIPC", .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", @@ -1317,6 +1374,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "RestrictNamespaces=~CLONE_NEWPID", + .json_field = "RestrictNamespaces_CLONE_NEWPID", .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", @@ -1327,6 +1385,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "RestrictNamespaces=~CLONE_NEWCGROUP", + .json_field = "RestrictNamespaces_CLONE_NEWCGROUP", .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", @@ -1337,6 +1396,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "RestrictNamespaces=~CLONE_NEWNET", + .json_field = "RestrictNamespaces_CLONE_NEWNET", .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", @@ -1347,6 +1407,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "RestrictNamespaces=~CLONE_NEWUTS", + .json_field = "RestrictNamespaces_CLONE_NEWUTS", .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", @@ -1357,6 +1418,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1367,6 +1429,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1377,6 +1440,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1387,6 +1451,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1397,6 +1462,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1407,6 +1473,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallArchitectures=", + .json_field = "SystemCallArchitectures", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallArchitectures=", .weight = 1000, .range = 10, @@ -1415,6 +1482,7 @@ static const struct security_assessor security_assessor_table[] = { #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, @@ -1423,6 +1491,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@obsolete", + .json_field = "SystemCallFilter_obsolete", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 250, .range = 10, @@ -1431,6 +1500,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@clock", + .json_field = "SystemCallFilter_clock", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 1000, .range = 10, @@ -1439,6 +1509,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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, @@ -1447,6 +1518,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@debug", + .json_field = "SystemCallFilter_debug", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 1000, .range = 10, @@ -1455,6 +1527,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@mount", + .json_field = "SystemCallFilter_mount", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 1000, .range = 10, @@ -1463,6 +1536,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@module", + .json_field = "SystemCallFilter_module", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 1000, .range = 10, @@ -1471,6 +1545,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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, @@ -1479,6 +1554,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@reboot", + .json_field = "SystemCallFilter_reboot", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 1000, .range = 10, @@ -1487,6 +1563,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@privileged", + .json_field = "SystemCallFilter_privileged", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 700, .range = 10, @@ -1495,6 +1572,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "SystemCallFilter=~@resources", + .json_field = "SystemCallFilter_resources", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", .weight = 700, .range = 10, @@ -1504,6 +1582,7 @@ static const struct security_assessor security_assessor_table[] = { #endif { .id = "IPAddressDeny=", + .json_field = "IPAddressDeny", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#IPAddressDeny=", .weight = 1000, .range = 10, @@ -1511,6 +1590,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .id = "DeviceAllow=", + .json_field = "DeviceAllow", .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#DeviceAllow=", .weight = 1000, .range = 10, @@ -1518,6 +1598,7 @@ static const struct security_assessor security_assessor_table[] = { }, { .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", @@ -1527,7 +1608,109 @@ static const struct security_assessor security_assessor_table[] = { }, }; -static int assess(const SecurityInfo *info, Table *overview_table, AnalyzeSecurityFlags flags, unsigned threshold) { +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("Specificied 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) { + static const struct { uint64_t exposure; const char *name; @@ -1565,6 +1748,8 @@ static int assess(const SecurityInfo *info, Table *overview_table, AnalyzeSecuri _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; @@ -1579,29 +1764,30 @@ static int assess(const SecurityInfo *info, Table *overview_table, AnalyzeSecuri return r; } - assert(a->range > 0); + assert(range > 0); if (badness != UINT64_MAX) { - assert(badness <= a->range); + assert(badness <= range); - badness_sum += DIV_ROUND_UP(badness * a->weight, a->range); - weight_sum += a->weight; + badness_sum += DIV_ROUND_UP(badness * weight, range); + weight_sum += weight; } if (details_table) { const char *checkmark, *description, *color = NULL; + const char *id = a->id; if (badness == UINT64_MAX) { checkmark = " "; - description = a->description_na; + description = access_description_na(a, policy); color = NULL; } else if (badness == a->range) { checkmark = special_glyph(SPECIAL_GLYPH_CROSS_MARK); - description = a->description_bad; + description = access_description_bad(a, policy); color = ansi_highlight_red(); } else if (badness == 0) { checkmark = special_glyph(SPECIAL_GLYPH_CHECK_MARK); - description = a->description_good; + description = access_description_good(a, policy); color = ansi_highlight_green(); } else { checkmark = special_glyph(SPECIAL_GLYPH_CROSS_MARK); @@ -1612,17 +1798,20 @@ static int assess(const SecurityInfo *info, Table *overview_table, AnalyzeSecuri if (d) description = d; + if (json_variant_by_key(policy, a->json_field) != NULL) + id = a->json_field; + r = table_add_many(details_table, TABLE_STRING, checkmark, TABLE_SET_MINIMUM_WIDTH, 1, TABLE_SET_MAXIMUM_WIDTH, 1, TABLE_SET_ELLIPSIZE_PERCENT, 0, TABLE_SET_COLOR, color, - TABLE_STRING, a->id, TABLE_SET_URL, a->url, + TABLE_STRING, id, TABLE_SET_URL, a->url, TABLE_STRING, description, - TABLE_UINT64, a->weight, TABLE_SET_ALIGN_PERCENT, 100, + TABLE_UINT64, weight, TABLE_SET_ALIGN_PERCENT, 100, TABLE_UINT64, badness, TABLE_SET_ALIGN_PERCENT, 100, - TABLE_UINT64, a->range, 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); @@ -2192,8 +2381,12 @@ static int acquire_security_info(sd_bus *bus, const char *name, SecurityInfo *in return 0; } -static int analyze_security_one(sd_bus *bus, const char *name, Table *overview_table, - AnalyzeSecurityFlags flags, unsigned threshold) { +static int analyze_security_one(sd_bus *bus, + const char *name, + Table *overview_table, + AnalyzeSecurityFlags flags, + unsigned threshold, + JsonVariant *policy) { _cleanup_(security_info_freep) SecurityInfo *info = security_info_new(); if (!info) @@ -2210,7 +2403,7 @@ static int analyze_security_one(sd_bus *bus, const char *name, Table *overview_t if (r < 0) return r; - r = assess(info, overview_table, flags, threshold); + r = assess(info, overview_table, flags, threshold, policy); if (r < 0) return r; @@ -2396,7 +2589,7 @@ static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, Security return 0; } -static int offline_security_check(Unit *u, unsigned threshold) { +static int offline_security_check(Unit *u, unsigned threshold, JsonVariant *policy) { _cleanup_(table_unrefp) Table *overview_table = NULL; AnalyzeSecurityFlags flags = 0; _cleanup_(security_info_freep) SecurityInfo *info = NULL; @@ -2411,10 +2604,17 @@ static int offline_security_check(Unit *u, unsigned threshold) { if (r < 0) return r; - return assess(info, overview_table, flags, threshold); + return assess(info, overview_table, flags, threshold, policy); } -static int offline_security_checks(char **filenames, UnitFileScope scope, bool check_man, bool run_generators, unsigned threshold, const char *root) { +static int offline_security_checks(char **filenames, + JsonVariant *policy, + UnitFileScope scope, + bool check_man, + bool run_generators, + unsigned threshold, + const char *root) { + const ManagerTestRunFlags flags = MANAGER_TEST_RUN_MINIMAL | MANAGER_TEST_RUN_ENV_GENERATORS | @@ -2473,7 +2673,7 @@ static int offline_security_checks(char **filenames, UnitFileScope scope, bool c } for (size_t i = 0; i < count; i++) { - k = offline_security_check(units[i], threshold); + k = offline_security_check(units[i], threshold, policy); if (k < 0 && r == 0) r = k; } @@ -2481,8 +2681,16 @@ static int offline_security_checks(char **filenames, UnitFileScope scope, bool c return r; } -int analyze_security(sd_bus *bus, char **units, UnitFileScope scope, bool check_man, bool run_generators, - bool offline, unsigned threshold, const char *root, AnalyzeSecurityFlags flags) { +int analyze_security(sd_bus *bus, + char **units, + JsonVariant *policy, + UnitFileScope scope, + bool check_man, + bool run_generators, + bool offline, + unsigned threshold, + const char *root, + AnalyzeSecurityFlags flags) { _cleanup_(table_unrefp) Table *overview_table = NULL; int ret = 0, r; @@ -2490,7 +2698,7 @@ int analyze_security(sd_bus *bus, char **units, UnitFileScope scope, bool check_ assert(bus); if (offline) - return offline_security_checks(units, scope, check_man, run_generators, threshold, root); + return offline_security_checks(units, policy, scope, check_man, run_generators, threshold, root); if (strv_length(units) != 1) { overview_table = table_new("unit", "exposure", "predicate", "happy"); @@ -2550,7 +2758,7 @@ int analyze_security(sd_bus *bus, char **units, UnitFileScope scope, bool check_ 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); + r = analyze_security_one(bus, *i, overview_table, flags, threshold, policy); if (r < 0 && ret >= 0) ret = r; } @@ -2585,7 +2793,7 @@ int analyze_security(sd_bus *bus, char **units, UnitFileScope scope, bool check_ } else name = mangled; - r = analyze_security_one(bus, name, overview_table, flags, threshold); + r = analyze_security_one(bus, name, overview_table, flags, threshold, policy); if (r < 0 && ret >= 0) ret = r; } diff --git a/src/analyze/analyze-security.h b/src/analyze/analyze-security.h index 57a93afbef..8ad5a689f5 100644 --- a/src/analyze/analyze-security.h +++ b/src/analyze/analyze-security.h @@ -5,6 +5,7 @@ #include "sd-bus.h" +#include "json.h" #include "unit-file.h" typedef enum AnalyzeSecurityFlags { @@ -13,5 +14,13 @@ typedef enum AnalyzeSecurityFlags { ANALYZE_SECURITY_ONLY_LONG_RUNNING = 1 << 2, } AnalyzeSecurityFlags; -int analyze_security(sd_bus *bus, char **units, UnitFileScope scope, bool check_man, bool run_generators, - bool offline, unsigned threshold, const char *root, AnalyzeSecurityFlags flags); +int analyze_security(sd_bus *bus, + char **units, + JsonVariant *policy, + UnitFileScope scope, + bool check_man, + bool run_generators, + bool offline, + unsigned threshold, + const char *root, + AnalyzeSecurityFlags flags); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 9bc7e606e8..816532f69e 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -91,6 +91,7 @@ static bool arg_man = true; static bool arg_generators = false; static char *arg_root = NULL; static char *arg_image = NULL; +static char *arg_security_policy = NULL; static bool arg_offline = false; static unsigned arg_threshold = 100; static unsigned arg_iterations = 1; @@ -100,6 +101,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_dot_from_patterns, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_dot_to_patterns, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_security_policy, freep); typedef struct BootTimes { usec_t firmware_time; @@ -2154,7 +2156,9 @@ static int do_verify(int argc, char *argv[], void *userdata) { static int do_security(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *policy = NULL; int r; + unsigned line, column; r = acquire_bus(&bus, NULL); if (r < 0) @@ -2162,7 +2166,35 @@ static int do_security(int argc, char *argv[], void *userdata) { (void) pager_open(arg_pager_flags); - return analyze_security(bus, strv_skip(argv, 1), arg_scope, arg_man, arg_generators, arg_offline, arg_threshold, arg_root, 0); + if (arg_security_policy) { + r = json_parse_file(/*f=*/ NULL, arg_security_policy, /*flags=*/ 0, &policy, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse '%s' at %u:%u: %m", arg_security_policy, line, column); + } else { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *pp = NULL; + + r = search_and_fopen_nulstr("systemd-analyze-security.policy", "re", /*root=*/ NULL, CONF_PATHS_NULSTR("systemd"), &f, &pp); + if (r < 0 && r != -ENOENT) + return r; + + if (f != NULL) { + r = json_parse_file(f, pp, /*flags=*/ 0, &policy, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON policy: %m", pp, line, column); + } + } + + return analyze_security(bus, + strv_skip(argv, 1), + policy, + arg_scope, + arg_man, + arg_generators, + arg_offline, + arg_threshold, + arg_root, + /*flags=*/ 0); } static int help(int argc, char *argv[], void *userdata) { @@ -2214,6 +2246,8 @@ static int help(int argc, char *argv[], void *userdata) { " --threshold=N Exit with a non-zero status when overall\n" " exposure level is over threshold value\n" " --version Show package version\n" + " --security-policy=PATH Use custom JSON security policy instead\n" + " of built-in one\n" " --no-pager Do not pipe output into a pager\n" " --system Operate on system systemd instance\n" " --user Operate on user systemd instance\n" @@ -2266,6 +2300,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_RECURSIVE_ERRORS, ARG_OFFLINE, ARG_THRESHOLD, + ARG_SECURITY_POLICY, }; static const struct option options[] = { @@ -2278,6 +2313,7 @@ static int parse_argv(int argc, char *argv[]) { { "recursive-errors", required_argument, NULL, ARG_RECURSIVE_ERRORS }, { "offline", required_argument, NULL, ARG_OFFLINE }, { "threshold", required_argument, NULL, ARG_THRESHOLD }, + { "security-policy", required_argument, NULL, ARG_SECURITY_POLICY }, { "system", no_argument, NULL, ARG_SYSTEM }, { "user", no_argument, NULL, ARG_USER }, { "global", no_argument, NULL, ARG_GLOBAL }, @@ -2409,6 +2445,12 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_SECURITY_POLICY: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_security_policy); + if (r < 0) + return r; + break; + case ARG_ITERATIONS: r = safe_atou(optarg, &arg_iterations); if (r < 0) @@ -2447,6 +2489,10 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --user is not supported for cat-config right now."); + if (arg_security_policy && !streq_ptr(argv[optind], "security")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --security-policy= is only supported for security."); + if ((arg_root || arg_image) && (!STRPTR_IN_SET(argv[optind], "cat-config", "verify")) && (!(streq_ptr(argv[optind], "security") && arg_offline))) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), diff --git a/test/TEST-63-ANALYZE/Makefile b/test/TEST-63-ANALYZE/Makefile new file mode 120000 index 0000000000..e9f93b1104 --- /dev/null +++ b/test/TEST-63-ANALYZE/Makefile @@ -0,0 +1 @@ +../TEST-01-BASIC/Makefile \ No newline at end of file diff --git a/test/TEST-63-ANALYZE/test.sh b/test/TEST-63-ANALYZE/test.sh new file mode 100755 index 0000000000..a64a7da258 --- /dev/null +++ b/test/TEST-63-ANALYZE/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +TEST_DESCRIPTION="test analyze" + +# shellcheck source=test/test-functions +. "${TEST_BASE_DIR:?}/test-functions" + +do_test "$@" diff --git a/test/units/testsuite-63.sh b/test/units/testsuite-63.sh new file mode 100755 index 0000000000..207dfeff68 --- /dev/null +++ b/test/units/testsuite-63.sh @@ -0,0 +1,574 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -eux + +systemd-analyze log-level debug + +mkdir -p /tmp/img/usr/lib/systemd/system/ +mkdir -p /tmp/img/opt/ + +touch /tmp/img/opt/script0.sh +chmod +x /tmp/img/opt/script0.sh + +cat </tmp/img/usr/lib/systemd/system/testfile.service +[Service] +ExecStart = /opt/script0.sh +EOF + +set +e +# Default behaviour is to recurse through all dependencies when unit is loaded +systemd-analyze verify --root=/tmp/img/ testfile.service \ + && { echo 'unexpected success'; exit 1; } + +# As above, recurses through all dependencies when unit is loaded +systemd-analyze verify --recursive-errors=yes --root=/tmp/img/ testfile.service \ + && { echo 'unexpected success'; exit 1; } + +# Recurses through unit file and its direct dependencies when unit is loaded +systemd-analyze verify --recursive-errors=one --root=/tmp/img/ testfile.service \ + && { echo 'unexpected success'; exit 1; } + +set -e + +# zero exit status since dependencies are ignored when unit is loaded +systemd-analyze verify --recursive-errors=no --root=/tmp/img/ testfile.service + +rm /tmp/img/usr/lib/systemd/system/testfile.service + +cat </tmp/testfile.service +[Unit] +foo = bar + +[Service] +ExecStart = echo hello +EOF + +cat </tmp/testfile2.service +[Unit] +Requires = testfile.service + +[Service] +ExecStart = echo hello +EOF + +# Zero exit status since no additional dependencies are recursively loaded when the unit file is loaded +systemd-analyze verify --recursive-errors=no /tmp/testfile2.service + +set +e +# Non-zero exit status since all associated dependencies are recusrively loaded when the unit file is loaded +systemd-analyze verify --recursive-errors=yes /tmp/testfile2.service \ + && { echo 'unexpected success'; exit 1; } +set -e + +rm /tmp/testfile.service +rm /tmp/testfile2.service + +cat </tmp/testfile.service +[Service] +ExecStart = echo hello +EOF + + +# Zero exit status since the value used for comparison determine exposure to security threats is by default 100 +systemd-analyze security --offline=true /tmp/testfile.service + +set +e +#The overall exposure level assigned to the unit is greater than the set threshold +systemd-analyze security --threshold=90 --offline=true /tmp/testfile.service \ + && { echo 'unexpected success'; exit 1; } +set -e + +rm /tmp/testfile.service + +cat </tmp/img/usr/lib/systemd/system/testfile.service +[Service] +ExecStart = echo hello +PrivateNetwork = yes +PrivateDevices = yes +PrivateUsers = yes +EOF + +# The new overall exposure level assigned to the unit is less than the set thresholds +# Verifies that the --offline= option works with --root= +systemd-analyze security --threshold=90 --offline=true --root=/tmp/img/ testfile.service + +# Added an additional "INVALID_ID" id to the .json to verify that nothing breaks when input is malformed +# The PrivateNetwork id description and weight was changed to verify that 'security' is actually reading in +# values from the .json file when required. The default weight for "PrivateNetwork" is 2500, and the new weight +# assigned to that id in the .json file is 6000. This increased weight means that when the "PrivateNetwork" key is +# set to 'yes' (as above in the case of testfile.service) in the content of the unit file, the overall exposure +# level for the unit file should decrease to account for that increased weight. +cat </tmp/testfile.json +{"User_Or_DynamicUser": + {"description_bad": "Service runs as root user", + "weight": 2000, + "range": 10 + }, +"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", + "weight": 200, + "range": 1 + }, +"PrivateDevices": + {"description_good": "Service has no access to hardware devices", + "description_bad": "Service potentially has access to hardware devices", + "weight": 1000, + "range": 1 + }, +"PrivateMounts": + {"description_good": "Service cannot install system mounts", + "description_bad": "Service may install system mounts", + "weight": 1000, + "range": 1 + }, +"PrivateNetwork": + {"description_good": "Service doesn't have access to the host's network", + "description_bad": "Service has access to the host's network", + "weight": 6000, + "range": 1 + }, +"PrivateTmp": + {"description_good": "Service has no access to other software's temporary files", + "description_bad": "Service has access to other software's temporary files", + "weight": 1000, + "range": 1 + }, +"PrivateUsers": + {"description_good": "Service does not have access to other users", + "description_bad": "Service has access to other users", + "weight": 1000, + "range": 1 + }, +"ProtectControlGroups": + {"description_good": "Service cannot modify the control group file system", + "description_bad": "Service may modify the control group file system", + "weight": 1000, + "range": 1 + }, +"ProtectKernelModules": + {"description_good": "Service cannot load or read kernel modules", + "description_bad": "Service may load or read kernel modules", + "weight": 1000, + "range": 1 + }, +"ProtectKernelTunables": + {"description_good": "Service cannot alter kernel tunables (/proc/sys, …)", + "description_bad": "Service may alter kernel tunables", + "weight": 1000, + "range": 1 + }, +"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", + "weight": 1000, + "range": 1 + }, +"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", + "weight": 1000, + "range": 1 + }, +"ProtectHome": + {"weight": 1000, + "range": 10 + }, +"ProtectHostname": + {"description_good": "Service cannot change system host/domainname", + "description_bad": "Service may change system host/domainname", + "weight": 50, + "range": 1 + }, +"ProtectSystem": + {"weight": 1000, + "range": 10 + }, +"RootDirectory_Or_RootImage": + {"description_good": "Service has its own root directory/image", + "description_bad": "Service runs within the host's root directory", + "weight": 200, + "range": 1 + }, +"LockPersonality": + {"description_good": "Service cannot change ABI personality", + "description_bad": "Service may change ABI personality", + "weight": 100, + "range": 1 + }, +"MemoryDenyWriteExecute": + {"description_good": "Service cannot create writable executable memory mappings", + "description_bad": "Service may create writable executable memory mappings", + "weight": 100, + "range": 1 + }, +"NoNewPrivileges": + {"description_good": "Service processes cannot acquire new privileges", + "description_bad": "Service processes may acquire new privileges", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_ADMIN": + {"description_good": "Service has no administrator privileges", + "description_bad": "Service has administrator privileges", + "weight": 1500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SET_UID_GID_PCAP": + {"description_good": "Service cannot change UID/GID identities/capabilities", + "description_bad": "Service may change UID/GID identities/capabilities", + "weight": 1500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_PTRACE": + {"description_good": "Service has no ptrace() debugging abilities", + "description_bad": "Service has ptrace() debugging abilities", + "weight": 1500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_TIME": + {"description_good": "Service processes cannot change the system clock", + "description_bad": "Service processes may change the system clock", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_NET_ADMIN": + {"description_good": "Service has no network configuration privileges", + "description_bad": "Service has network configuration privileges", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_RAWIO": + {"description_good": "Service has no raw I/O access", + "description_bad": "Service has raw I/O access", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_MODULE": + {"description_good": "Service cannot load kernel modules", + "description_bad": "Service may load kernel modules", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_AUDIT": + {"description_good": "Service has no audit subsystem access", + "description_bad": "Service has audit subsystem access", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYSLOG": + {"description_good": "Service has no access to kernel logging", + "description_bad": "Service has access to kernel logging", + "weight": 500, + "range": 1 + }, +"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", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_MKNOD": + {"description_good": "Service cannot create device nodes", + "description_bad": "Service may create device nodes", + "weight": 500, + "range": 1 + }, +"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", + "weight": 1000, + "range": 1 + }, +"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", + "weight": 1000, + "range": 1 + }, +"CapabilityBoundingSet_CAP_KILL": + {"description_good": "Service cannot send UNIX signals to arbitrary processes", + "description_bad": "Service may send UNIX signals to arbitrary processes", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_NET_BIND_SERVICE_BROADCAST_RAW": + {"description_good": "Service has no elevated networking privileges", + "description_bad": "Service has elevated networking privileges", + "weight": 500, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_BOOT": + {"description_good": "Service cannot issue reboot()", + "description_bad": "Service may issue reboot()", + "weight": 100, + "range": 1 + }, +"CapabilityBoundingSet_CAP_MAC": + {"description_good": "Service cannot adjust SMACK MAC", + "description_bad": "Service may adjust SMACK MAC", + "weight": 100, + "range": 1 + }, +"CapabilityBoundingSet_CAP_LINUX_IMMUTABLE": + {"description_good": "Service cannot mark files immutable", + "description_bad": "Service may mark files immutable", + "weight": 75, + "range": 1 + }, +"CapabilityBoundingSet_CAP_IPC_LOCK": + {"description_good": "Service cannot lock memory into RAM", + "description_bad": "Service may lock memory into RAM", + "weight": 50, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_CHROOT": + {"description_good": "Service cannot issue chroot()", + "description_bad": "Service may issue chroot()", + "weight": 50, + "range": 1 + }, +"CapabilityBoundingSet_CAP_BLOCK_SUSPEND": + {"description_good": "Service cannot establish wake locks", + "description_bad": "Service may establish wake locks", + "weight": 25, + "range": 1 + }, +"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", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_LEASE": + {"description_good": "Service cannot create file leases", + "description_bad": "Service may create file leases", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_TTY_CONFIG": + {"description_good": "Service cannot issue vhangup()", + "description_bad": "Service may issue vhangup()", + "weight": 25, + "range": 1 + }, +"CapabilityBoundingSet_CAP_SYS_PACCT": + {"description_good": "Service cannot use acct()", + "description_bad": "Service may use acct()", + "weight": 25, + "range": 1 + }, +"UMask": + {"weight": 100, + "range": 10 + }, +"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 + }, +"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 + }, +"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 + }, +"NotifyAccess": + {"description_good": "Service child processes cannot alter service state", + "description_bad": "Service child processes may alter service state", + "weight": 1000, + "range": 1 + }, +"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 + }, +"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 + }, +"RestrictRealtime": + {"description_good": "Service realtime scheduling access is restricted", + "description_bad": "Service may acquire realtime scheduling", + "weight": 500, + "range": 1 + }, +"RestrictSUIDSGID": + {"description_good": "SUID/SGIDfilecreationbyserviceisrestricted", + "description_bad": "ServicemaycreateSUID/SGIDfiles", + "weight": 1000, + "range": 1 + }, +"RestrictNamespaces_CLONE_NEWUSER": + {"description_good": "Servicecannotcreateusernamespaces", + "description_bad": "Servicemaycreateusernamespaces", + "weight": 1500, + "range": 1 + }, +"RestrictNamespaces_CLONE_NEWNS": + {"description_good": "Service cannot create file system namespaces", + "description_bad": "Service may create file system namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_CLONE_NEWIPC": + {"description_good": "Service cannot create IPC namespaces", + "description_bad": "Service may create IPC namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_CLONE_NEWPID": + {"description_good": "Service cannot create process namespaces", + "description_bad": "Service may create process namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_CLONE_NEWCGROUP": + {"description_good": "Service cannot create cgroup namespaces", + "description_bad": "Service may create cgroup namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_CLONE_NEWNET": + {"description_good": "Service cannot create network namespaces", + "description_bad": "Service may create network namespaces", + "weight": 500, + "range": 1 + }, +"RestrictNamespaces_CLONE_NEWUTS": + {"description_good": "Service cannot create hostname namespaces", + "description_bad": "Service may create hostname namespaces", + "weight": 100, + "range": 1 + }, +"RestrictAddressFamilies_AF_INET_INET6": + {"description_good": "Service cannot allocate Internet sockets", + "description_bad": "Service may allocate Internet sockets", + "weight": 1500, + "range": 1 + }, +"RestrictAddressFamilies_AF_UNIX": + {"description_good": "Service cannot allocate local sockets", + "description_bad": "Service may allocate local sockets", + "weight": 25, + "range": 1 + }, +"RestrictAddressFamilies_AF_NETLINK": + {"description_good": "Service cannot allocate netlink sockets", + "description_bad": "Service may allocate netlink sockets", + "weight": 200, + "range": 1 + }, +"RestrictAddressFamilies_AF_PACKET": + {"description_good": "Service cannot allocate packet sockets", + "description_bad": "Service may allocate packet sockets", + "weight": 1000, + "range": 1 + }, +"RestrictAddressFamilies_OTHER": + {"description_good": "Service cannot allocate exotic sockets", + "description_bad": "Service may allocate exotic sockets", + "weight": 1250, + "range": 1 + }, +"SystemCallArchitectures": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_swap": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_obsolete": + {"weight": 250, + "range": 10 + }, +"SystemCallFilter_clock": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_cpu_emulation": + {"weight": 250, + "range": 10 + }, +"SystemCallFilter_debug": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_mount": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_module": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_raw_io": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_reboot": + {"weight": 1000, + "range": 10 + }, +"SystemCallFilter_privileged": + {"weight": 700, + "range": 10 + }, +"SystemCallFilter_resources": + {"weight": 700, + "range": 10 + }, +"IPAddressDeny": + {"weight": 1000, + "range": 10 + }, +"DeviceAllow": + {"weight": 1000, + "range": 10 + }, +"AmbientCapabilities": + {"description_good": "Service process does not receive ambient capabilities", + "description_bad": "Service process receives ambient capabilities", + "weight": 500, + "range": 1 + }, +"INVALID_ID": + {"weight": 1000, + "range": 10 + } +} +EOF + +# Reads in custom security requirements from the parsed .json file and uses these for comparison +systemd-analyze security --threshold=90 --offline=true \ + --security-policy=/tmp/testfile.json \ + --root=/tmp/img/ testfile.service + +set +e +systemd-analyze security --threshold=50 --offline=true \ + --security-policy=/tmp/testfile.json \ + --root=/tmp/img/ testfile.service \ + && { echo 'unexpected success'; exit 1; } +set -e + +rm /tmp/img/usr/lib/systemd/system/testfile.service + +systemd-analyze log-level info + +echo OK >/testok + +exit 0