From 4f80cfca5ebf9743c13b648017b773090f57b86b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 13 Jul 2022 10:38:53 +0200 Subject: [PATCH 1/4] pid1: add mechanism for conditionalizing units/network/netdev/link based on credentials passed in This is useful when provisioning systems via nspawn/qemu and running specific services only if specific data is passed into the system. --- docs/CREDENTIALS.md | 6 +++ man/systemd.link.xml | 12 +++++ man/systemd.netdev.xml | 1 + man/systemd.network.xml | 1 + man/systemd.unit.xml | 13 ++++++ src/core/load-fragment-gperf.gperf.in | 2 + src/network/netdev/netdev-gperf.gperf | 1 + src/network/networkd-network-gperf.gperf | 1 + src/shared/condition.c | 44 ++++++++++++++++++ src/shared/condition.h | 1 + src/test/test-condition.c | 59 ++++++++++++++++++++++++ src/udev/net/link-config-gperf.gperf | 1 + test/units/testsuite-54.sh | 6 +++ 13 files changed, 148 insertions(+) diff --git a/docs/CREDENTIALS.md b/docs/CREDENTIALS.md index bbd92ad3c9..4ba3784469 100644 --- a/docs/CREDENTIALS.md +++ b/docs/CREDENTIALS.md @@ -395,3 +395,9 @@ in `/etc/credstore/`, `/run/credstore/`, `/usr/lib/credstore/`. `LoadCredentialEncrypted=` will also search `/etc/credstore.encrypted/` and similar directories. These directories are hence a great place to store credentials to load on the system. + +## Conditionalizing Services + +Sometimes it makes sense to conditionalize system services and invoke them only +if the right system credential is passed to the system. use the +`ConditionCredential=` and `AssertCredential=` unit file settings for that. diff --git a/man/systemd.link.xml b/man/systemd.link.xml index de23b941ad..d933632393 100644 --- a/man/systemd.link.xml +++ b/man/systemd.link.xml @@ -269,6 +269,18 @@ + + Credential= + + Checks whether the specified credential was passed to the + systemd-networkd.service service. See System and Service Credentials for details. When + prefixed with an exclamation mark (!), the result is negated. If an empty + string is assigned, the previously assigned value is cleared. + + + + Architecture= diff --git a/man/systemd.netdev.xml b/man/systemd.netdev.xml index 7d3a4f95c8..c3578fc2da 100644 --- a/man/systemd.netdev.xml +++ b/man/systemd.netdev.xml @@ -217,6 +217,7 @@ + diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 516a42e25a..70d2c34a40 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -135,6 +135,7 @@ + diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 55f32f3272..ea95ba8869 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1330,6 +1330,19 @@ + + ConditionCredential= + + ConditionCredential= may be used to check whether a credential + by the specified name was passed into the service manager. See System and Service Credentials for details about + credentials. If used in services for the system service manager this may be used to conditionalize + services based on system credentials passed in. If used in services for the per-user service + manager this may be used to conditionalize services based on credentials passed into the + unit@.service service instance belonging to the user. The argument must be a + valid credential name. + + ConditionEnvironment= diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 7817c20c0b..54c1c0bb56 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -332,6 +332,7 @@ Unit.ConditionVirtualization, config_parse_unit_condition_string, Unit.ConditionHost, config_parse_unit_condition_string, CONDITION_HOST, offsetof(Unit, conditions) Unit.ConditionKernelCommandLine, config_parse_unit_condition_string, CONDITION_KERNEL_COMMAND_LINE, offsetof(Unit, conditions) Unit.ConditionKernelVersion, config_parse_unit_condition_string, CONDITION_KERNEL_VERSION, offsetof(Unit, conditions) +Unit.ConditionCredential, config_parse_unit_condition_string, CONDITION_CREDENTIAL, offsetof(Unit, conditions) Unit.ConditionSecurity, config_parse_unit_condition_string, CONDITION_SECURITY, offsetof(Unit, conditions) Unit.ConditionCapability, config_parse_unit_condition_string, CONDITION_CAPABILITY, offsetof(Unit, conditions) Unit.ConditionACPower, config_parse_unit_condition_string, CONDITION_AC_POWER, offsetof(Unit, conditions) @@ -363,6 +364,7 @@ Unit.AssertVirtualization, config_parse_unit_condition_string, Unit.AssertHost, config_parse_unit_condition_string, CONDITION_HOST, offsetof(Unit, asserts) Unit.AssertKernelCommandLine, config_parse_unit_condition_string, CONDITION_KERNEL_COMMAND_LINE, offsetof(Unit, asserts) Unit.AssertKernelVersion, config_parse_unit_condition_string, CONDITION_KERNEL_VERSION, offsetof(Unit, asserts) +Unit.AssertCredential, config_parse_unit_condition_string, CONDITION_CREDENTIAL, offsetof(Unit, asserts) Unit.AssertSecurity, config_parse_unit_condition_string, CONDITION_SECURITY, offsetof(Unit, asserts) Unit.AssertCapability, config_parse_unit_condition_string, CONDITION_CAPABILITY, offsetof(Unit, asserts) Unit.AssertACPower, config_parse_unit_condition_string, CONDITION_AC_POWER, offsetof(Unit, asserts) diff --git a/src/network/netdev/netdev-gperf.gperf b/src/network/netdev/netdev-gperf.gperf index 55ad60ddc8..162664ecf1 100644 --- a/src/network/netdev/netdev-gperf.gperf +++ b/src/network/netdev/netdev-gperf.gperf @@ -45,6 +45,7 @@ Match.Host, config_parse_net_condition, Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(NetDev, conditions) Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(NetDev, conditions) Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(NetDev, conditions) +Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(NetDev, conditions) Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(NetDev, conditions) Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(NetDev, conditions) NetDev.Description, config_parse_string, 0, offsetof(NetDev, description) diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 0b0c8da27b..13d521e37a 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -62,6 +62,7 @@ Match.Host, config_parse_net_condition, Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(Network, conditions) Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(Network, conditions) Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(Network, conditions) +Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(Network, conditions) Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(Network, conditions) Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(Network, conditions) Link.MACAddress, config_parse_hw_addr, 0, offsetof(Network, hw_addr) diff --git a/src/shared/condition.c b/src/shared/condition.c index 640dd96eb2..2fc22c3714 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -22,6 +22,7 @@ #include "cgroup-util.h" #include "condition.h" #include "cpu-set-util.h" +#include "creds-util.h" #include "efi-api.h" #include "env-file.h" #include "env-util.h" @@ -140,6 +141,46 @@ static int condition_test_kernel_command_line(Condition *c, char **env) { return false; } +static int condition_test_credential(Condition *c, char **env) { + int (*gd)(const char **ret); + int r; + + assert(c); + assert(c->parameter); + assert(c->type == CONDITION_CREDENTIAL); + + /* For now we'll do a very simple existance check and are happy with either a regular or an encrypted + * credential. Given that we check the syntax of the argument we have the option to later maybe allow + * contents checks too without breaking compatibility, but for now let's be minimalistic. */ + + if (!credential_name_valid(c->parameter)) /* credentials with invalid names do not exist */ + return false; + + FOREACH_POINTER(gd, get_credentials_dir, get_encrypted_credentials_dir) { + _cleanup_free_ char *j = NULL; + const char *cd; + + r = gd(&cd); + if (r == -ENXIO) /* no env var set */ + continue; + if (r < 0) + return r; + + j = path_join(cd, c->parameter); + if (!j) + return -ENOMEM; + + if (laccess(j, F_OK) >= 0) + return true; /* yay! */ + if (errno != ENOENT) + return -errno; + + /* not found in this dir */ + } + + return false; +} + typedef enum { /* Listed in order of checking. Note that some comparators are prefixes of others, hence the longest * should be listed first. */ @@ -1099,6 +1140,7 @@ int condition_test(Condition *c, char **env) { [CONDITION_FILE_IS_EXECUTABLE] = condition_test_file_is_executable, [CONDITION_KERNEL_COMMAND_LINE] = condition_test_kernel_command_line, [CONDITION_KERNEL_VERSION] = condition_test_kernel_version, + [CONDITION_CREDENTIAL] = condition_test_credential, [CONDITION_VIRTUALIZATION] = condition_test_virtualization, [CONDITION_SECURITY] = condition_test_security, [CONDITION_CAPABILITY] = condition_test_capability, @@ -1218,6 +1260,7 @@ static const char* const condition_type_table[_CONDITION_TYPE_MAX] = { [CONDITION_HOST] = "ConditionHost", [CONDITION_KERNEL_COMMAND_LINE] = "ConditionKernelCommandLine", [CONDITION_KERNEL_VERSION] = "ConditionKernelVersion", + [CONDITION_CREDENTIAL] = "ConditionCredential", [CONDITION_SECURITY] = "ConditionSecurity", [CONDITION_CAPABILITY] = "ConditionCapability", [CONDITION_AC_POWER] = "ConditionACPower", @@ -1255,6 +1298,7 @@ static const char* const assert_type_table[_CONDITION_TYPE_MAX] = { [CONDITION_HOST] = "AssertHost", [CONDITION_KERNEL_COMMAND_LINE] = "AssertKernelCommandLine", [CONDITION_KERNEL_VERSION] = "AssertKernelVersion", + [CONDITION_CREDENTIAL] = "AssertCredential", [CONDITION_SECURITY] = "AssertSecurity", [CONDITION_CAPABILITY] = "AssertCapability", [CONDITION_AC_POWER] = "AssertACPower", diff --git a/src/shared/condition.h b/src/shared/condition.h index 2bbb7fa7f4..54cc904feb 100644 --- a/src/shared/condition.h +++ b/src/shared/condition.h @@ -14,6 +14,7 @@ typedef enum ConditionType { CONDITION_HOST, CONDITION_KERNEL_COMMAND_LINE, CONDITION_KERNEL_VERSION, + CONDITION_CREDENTIAL, CONDITION_SECURITY, CONDITION_CAPABILITY, CONDITION_AC_POWER, diff --git a/src/test/test-condition.c b/src/test/test-condition.c index fb82f44d04..56b5ad88a2 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -15,7 +15,9 @@ #include "condition.h" #include "cpu-set-util.h" #include "efi-loader.h" +#include "env-util.h" #include "errno-util.h" +#include "fs-util.h" #include "hostname-util.h" #include "id128-util.h" #include "ima-util.h" @@ -24,14 +26,17 @@ #include "macro.h" #include "nulstr-util.h" #include "os-util.h" +#include "path-util.h" #include "process-util.h" #include "psi-util.h" +#include "rm-rf.h" #include "selinux-util.h" #include "set.h" #include "smack-util.h" #include "string-util.h" #include "strv.h" #include "tests.h" +#include "tmpfile-util.h" #include "tomoyo-util.h" #include "udev-util.h" #include "uid-alloc-range.h" @@ -460,6 +465,60 @@ TEST(condition_test_kernel_version) { condition_free(condition); } +TEST(condition_test_credential) { + _cleanup_(rm_rf_physical_and_freep) char *n1 = NULL, *n2 = NULL; + _cleanup_free_ char *d1 = NULL, *d2 = NULL, *j = NULL; + Condition *condition; + + assert_se(free_and_strdup(&d1, getenv("CREDENTIALS_DIRECTORY")) >= 0); + assert_se(free_and_strdup(&d2, getenv("ENCRYPTED_CREDENTIALS_DIRECTORY")) >= 0); + + assert_se(unsetenv("CREDENTIALS_DIRECTORY") >= 0); + assert_se(unsetenv("ENCRYPTED_CREDENTIALS_DIRECTORY") >= 0); + + condition = condition_new(CONDITION_CREDENTIAL, "definitelymissing", /* trigger= */ false, /* negate= */ false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + /* invalid */ + condition = condition_new(CONDITION_CREDENTIAL, "..", /* trigger= */ false, /* negate= */ false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + assert_se(mkdtemp_malloc(NULL, &n1) >= 0); + assert_se(mkdtemp_malloc(NULL, &n2) >= 0); + + assert_se(setenv("CREDENTIALS_DIRECTORY", n1, /* overwrite= */ true) >= 0); + assert_se(setenv("ENCRYPTED_CREDENTIALS_DIRECTORY", n2, /* overwrite= */ true) >= 0); + + condition = condition_new(CONDITION_CREDENTIAL, "stillmissing", /* trigger= */ false, /* negate= */ false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + assert_se(j = path_join(n1, "existing")); + assert_se(touch(j) >= 0); + assert_se(j); + condition = condition_new(CONDITION_CREDENTIAL, "existing", /* trigger= */ false, /* negate= */ false); + assert_se(condition); + assert_se(condition_test(condition, environ) > 0); + condition_free(condition); + free(j); + + assert_se(j = path_join(n2, "existing-encrypted")); + assert_se(touch(j) >= 0); + assert_se(j); + condition = condition_new(CONDITION_CREDENTIAL, "existing-encrypted", /* trigger= */ false, /* negate= */ false); + assert_se(condition); + assert_se(condition_test(condition, environ) > 0); + condition_free(condition); + + assert_se(set_unset_env("CREDENTIALS_DIRECTORY", d1, /* overwrite= */ true) >= 0); + assert_se(set_unset_env("ENCRYPTED_CREDENTIALS_DIRECTORY", d2, /* overwrite= */ true) >= 0); +} + #if defined(__i386__) || defined(__x86_64__) TEST(condition_test_cpufeature) { Condition *condition; diff --git a/src/udev/net/link-config-gperf.gperf b/src/udev/net/link-config-gperf.gperf index 96280148c7..240f16e251 100644 --- a/src/udev/net/link-config-gperf.gperf +++ b/src/udev/net/link-config-gperf.gperf @@ -34,6 +34,7 @@ Match.Host, config_parse_net_condition, Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(LinkConfig, conditions) Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(LinkConfig, conditions) Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(LinkConfig, conditions) +Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(LinkConfig, conditions) Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(LinkConfig, conditions) Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(LinkConfig, conditions) Link.Description, config_parse_string, 0, offsetof(LinkConfig, description) diff --git a/test/units/testsuite-54.sh b/test/units/testsuite-54.sh index 151a7987d5..771b041bf1 100755 --- a/test/units/testsuite-54.sh +++ b/test/units/testsuite-54.sh @@ -58,6 +58,12 @@ if [ "$expected_credential" != "" ] ; then # Combine it with a fallback (which should have no effect, given the cred should be passed down) [ "$(systemd-run -p LoadCredential="$expected_credential" -p SetCredential="$expected_credential":zzz --pipe --wait systemd-creds cat "$expected_credential")" = "$expected_value" ] + + # This should succeed + systemd-run -p AssertCredential="$expected_credential" -p Type=oneshot true + + # And this should fail + systemd-run -p AssertCredential="undefinedcredential" -p Type=oneshot true && { echo 'unexpected success'; exit 1; } fi # Verify that the creds are immutable From 2c7b8f3dd540cbee3492c972c94dfb13b9a2903e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 14 Jul 2022 10:34:54 +0200 Subject: [PATCH 2/4] update TODO --- TODO | 5 ----- 1 file changed, 5 deletions(-) diff --git a/TODO b/TODO index f15ccafb11..1cd0a709ac 100644 --- a/TODO +++ b/TODO @@ -299,11 +299,6 @@ Features: cloud-init/ignitation and similar can parameterize the host with data they acquire. -* Add ConditionCredentialExists= or so, that allows conditionalizing services - depending on whether a specific system credential is set. Usecase: a service - similar to the ssh keygen service that installs any SSH host key supplied via - system credentials into /etc/ssh. - * drop support for kernels that lack ambient capabilities support (i.e. make 4.3 new baseline). Then drop support for "!!" modifier for ExecStart= which is only supported for such old kernels From 351f7d5143d2403a2678c7b9797606ce30837109 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 14 Jul 2022 10:41:15 +0200 Subject: [PATCH 3/4] fuzz: add ConditionCredential= to fuzz files, and sort their sections --- test/fuzz/fuzz-link-parser/directives.link | 21 ++++++++-------- .../fuzz/fuzz-netdev-parser/directives.netdev | 3 ++- test/fuzz/fuzz-network-parser/directives | 25 ++++++++++--------- .../fuzz-unit-file/directives-all.service | 5 +++- test/fuzz/fuzz-unit-file/directives.service | 2 ++ 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/test/fuzz/fuzz-link-parser/directives.link b/test/fuzz/fuzz-link-parser/directives.link index d6c6cc6f2e..a1c797a297 100644 --- a/test/fuzz/fuzz-link-parser/directives.link +++ b/test/fuzz/fuzz-link-parser/directives.link @@ -1,18 +1,19 @@ [Match] -MACAddress= -PermanentMACAddress= -OriginalName= -Path= +Architecture= +Credential= Driver= -Type= -Kind= -Property= +Firmware= Host= -Virtualization= KernelCommandLine= KernelVersion= -Architecture= -Firmware= +Kind= +MACAddress= +OriginalName= +Path= +PermanentMACAddress= +Property= +Type= +Virtualization= [Link] Description= MACAddressPolicy= diff --git a/test/fuzz/fuzz-netdev-parser/directives.netdev b/test/fuzz/fuzz-netdev-parser/directives.netdev index d97f81512b..d5f3228065 100644 --- a/test/fuzz/fuzz-netdev-parser/directives.netdev +++ b/test/fuzz/fuzz-netdev-parser/directives.netdev @@ -24,11 +24,12 @@ Mode= SourceMACAddress= [Match] Architecture= +Credential= Firmware= Host= +KernelCommandLine= KernelVersion= Virtualization= -KernelCommandLine= [GENEVE] DestinationPort= TTL= diff --git a/test/fuzz/fuzz-network-parser/directives b/test/fuzz/fuzz-network-parser/directives index ea0de6660c..b7e8f16834 100644 --- a/test/fuzz/fuzz-network-parser/directives +++ b/test/fuzz/fuzz-network-parser/directives @@ -15,23 +15,24 @@ ProxyARP= ProxyARPWiFi= MulticastRouter= [Match] -KernelVersion= -Type= -Kind= -Driver= Architecture= -Firmware= -Path= -WLANInterfaceType= -SSID= BSSID= -Name= -Property= -Virtualization= -KernelCommandLine= +Credential= +Driver= +Firmware= Host= +KernelCommandLine= +KernelVersion= +Kind= MACAddress= +Name= +Path= PermanentMACAddress= +Property= +SSID= +Type= +Virtualization= +WLANInterfaceType= [Link] ActivationPolicy= RequiredForOnline= diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service index 487001b6b7..621fb1cf1b 100644 --- a/test/fuzz/fuzz-unit-file/directives-all.service +++ b/test/fuzz/fuzz-unit-file/directives-all.service @@ -10,9 +10,10 @@ Also= AmbientCapabilities= AssertACPower= AssertArchitecture= +AssertCPUPressure= AssertCapability= AssertControlGroupController= -AssertCPUPressure= +AssertCredential= AssertDirectoryNotEmpty= AssertFileIsExecutable= AssertFileNotEmpty= @@ -59,6 +60,7 @@ ConditionACPower= ConditionArchitecture= ConditionCapability= ConditionControlGroupController= +ConditionCredential= ConditionCPUPressure= ConditionDirectoryNotEmpty= ConditionFileIsExecutable= @@ -481,6 +483,7 @@ InitialCongestionWindow= InputKey= InvertRule= KernelCommandLine= +Credential= KernelVersion= Key= Kind= diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service index 3c33d947fe..056edb9f17 100644 --- a/test/fuzz/fuzz-unit-file/directives.service +++ b/test/fuzz/fuzz-unit-file/directives.service @@ -9,6 +9,7 @@ AssertCPUPressure= AssertCPUs= AssertCapability= AssertControlGroupController= +AssertCredential= AssertDirectoryNotEmpty= AssertEnvironment= AssertFileIsExecutable= @@ -47,6 +48,7 @@ ConditionCPUs= ConditionFirmware= ConditionCapability= ConditionControlGroupController= +ConditionCredential= ConditionDirectoryNotEmpty= ConditionEnvironment= ConditionFileIsExecutable= From 462511c8c6c4d05e5008f7de121e6011e00d07c3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 15 Jul 2022 10:53:23 +0200 Subject: [PATCH 4/4] man: fix copy/paste typo --- man/systemd.link.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/man/systemd.link.xml b/man/systemd.link.xml index d933632393..bb4cd227e6 100644 --- a/man/systemd.link.xml +++ b/man/systemd.link.xml @@ -228,7 +228,7 @@ Matches against the hostname or machine ID of the host. See ConditionHost= in systemd.unit5 for details. When prefixed with an exclamation mark (!), the result is negated. - If an empty string is assigned, then previously assigned value is cleared. + If an empty string is assigned, the previously assigned value is cleared. @@ -240,7 +240,7 @@ whether it is a specific implementation. See ConditionVirtualization= in systemd.unit5 for details. When prefixed with an exclamation mark (!), the result is negated. - If an empty string is assigned, then previously assigned value is cleared. + If an empty string is assigned, the previously assigned value is cleared. @@ -252,7 +252,7 @@ ConditionKernelCommandLine= in systemd.unit5 for details. When prefixed with an exclamation mark (!), the result is negated. - If an empty string is assigned, then previously assigned value is cleared. + If an empty string is assigned, the previously assigned value is cleared. @@ -264,7 +264,7 @@ expression. See ConditionKernelVersion= in systemd.unit5 for details. When prefixed with an exclamation mark (!), the result is negated. - If an empty string is assigned, then previously assigned value is cleared. + If an empty string is assigned, the previously assigned value is cleared. @@ -288,7 +288,7 @@ ConditionArchitecture= in systemd.unit5 for details. When prefixed with an exclamation mark (!), the result is negated. - If an empty string is assigned, then previously assigned value is cleared. + If an empty string is assigned, the previously assigned value is cleared. @@ -300,7 +300,7 @@ ConditionFirmware= in systemd.unit5 for details. When prefixed with an exclamation mark (!), the result is negated. - If an empty string is assigned, then previously assigned value is cleared. + If an empty string is assigned, the previously assigned value is cleared.