From bb6c788cf9e97a961a0de81537ed83eb76b5fe97 Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 22 Sep 2026 22:53:23 +0200 Subject: [PATCH] fix(daemon): rsync read-only module default; warn on unenforced security keys --- CHANGELOG.md | 18 ++++ README.md | 14 ++- src/shared/daemon_conf.c | 97 +++++++++++++++-- src/shared/daemon_conf.h | 28 +++-- tests/integration/test_daemon.py | 16 ++- tests/test_daemon_conf.c | 172 +++++++++++++++++++++++++++++++ 6 files changed, 322 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd7502d..2853154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,24 @@ of 157 rows. - **Daemon umask no longer forced to `0`.** `daemonize()` now sets the conventional `022`, so implied parent directories created without `-p` are no longer world-writable `0777`. +- **Daemon modules are read-only by default.** A `--daemon` module is now + served read-only unless it sets `read only = no` (or rsync's `write only = + yes`), matching rsync: a real `rsyncd.conf` that omits `read only` is no + longer silently writable. A global `read only` still sets the default for + later modules, and an explicit module value wins. This is a behavior change + for existing FastSync-native configs that relied on the old writable default; + add `read only = no` to keep them writable. An rsync `write only = yes` is + mapped to writability (FastSync is push-only, so a module can never be read + from the network). +- **Accepted-but-unenforced rsync security keys now warn at startup.** The + rsync keys FastSync recognizes but does not implement — `secrets file`, + `refuse options`, `exclude`/`include`/`filter`, `max size`/`min size`, + `pre-xfer exec`/`post-xfer exec`, `incoming chmod`/`outgoing chmod`, + `name converter`, `use chroot`, `uid`/`gid`, and the rest of the + access-control set — load for migration compatibility but now emit a + `WARN` naming the key (and module) so an operator does not believe the + restriction is enforced. `auth users`/`secrets file` stay fail-closed: a + module declaring `auth users` still requires a FastSync credential store. - **Credentials and signal handling hardened.** Secret files are opened with `O_NOFOLLOW|O_NONBLOCK` (while allowing fd-backed store paths and bound-waiting a FIFO read for ~3 s so a slow process substitution works but a connected-but- diff --git a/README.md b/README.md index c40469f..897b1b5 100644 --- a/README.md +++ b/README.md @@ -768,9 +768,17 @@ and `address`, the global section accepts: - `hosts allow` / `hosts deny` — comma- and/or whitespace-separated host access patterns. -A `[module]` requires `path`, and may also set `read only`, `client owner`, -`auth users`, `max connections` (0 = unlimited; enforced per module across all -connection children), and its own `hosts allow`/`hosts deny`. +A `[module]` requires `path`, and may also set `read only`, `write only`, +`client owner`, `auth users`, `max connections` (0 = unlimited; enforced per +module across all connection children), and its own `hosts allow`/`hosts deny`. + +Like rsync, a module is **read-only by default**: a bare `[module]` with only a +`path` refuses a write transfer. Opt a module into writability explicitly with +`read only = no` or `write only = yes`; a global `read only` value in the +section before the first `[module]` sets the default for later modules, and a +module's own `read only`/`write only = yes` always wins over it. An +rsync-style `write only = yes` is mapped to writability because FastSync is +push-only (a module can never be read from the network). The per-host cap and the shared auth lockout identify a source by its numeric peer IP. **Loopback peers (127.0.0.0/8, IPv6 `::1`) are exempt**: every local diff --git a/src/shared/daemon_conf.c b/src/shared/daemon_conf.c index 3b0a351..5144668 100644 --- a/src/shared/daemon_conf.c +++ b/src/shared/daemon_conf.c @@ -1,5 +1,6 @@ #include "daemon_conf.h" #include "credentials.h" +#include "log.h" #include "utils.h" #include #include @@ -77,11 +78,13 @@ static const char* const kRsyncInertGlobalKeys[] = { /* rsync 3.4.1 rsyncd.conf MODULE keys accepted in a [module] section that have * no FastSync equivalent (accepted-and-documented inert). Keys with a FastSync - * meaning (`path`, `read only`, `auth users`, `max connections`, + * meaning (`path`, `read only`, `write only`, `auth users`, `max connections`, * `hosts allow`/`hosts deny`, `client owner`) are handled by apply_module_key * before this list is consulted. Security-relevant keys (`exclude`, `filter`, * `secrets file`, `refuse options`, ...) are inert, so a daemon-side filter or - * rsync secrets file is NOT enforced: see RSYNC_COMPAT.md for the residual. */ + * rsync secrets file is NOT enforced: each is loudly warned about at load time + * (see kRsyncUnenforcedModuleSecurityKeys) and documented as a residual in + * RSYNC_COMPAT.md. */ static const char* const kRsyncInertModuleKeys[] = { "comment", "use chroot", @@ -110,7 +113,6 @@ static const char* const kRsyncInertModuleKeys[] = { "numeric ids", "fake super", "munge symlinks", - "write only", "list", "dont compress", "charset", @@ -132,8 +134,57 @@ static const char* const kRsyncInertModuleKeys[] = { "ignore nonreadable", }; +/* Subset of the inert rsync keys whose intent is access control (data + * visibility, credential source, transfer hooks, daemon privilege), plus the + * global keys that shape the daemon's privilege/identity. These load for + * rsync-config compatibility, but because FastSync ignores them an operator + * migrating a hardened rsyncd.conf must not believe the restriction applies. + * The loader emits one LOG_LEVEL_WARNING per occurrence naming the key (and the + * module, for a module key). `write only` is deliberately absent: it is mapped + * onto writability instead (FastSync is push-only, so a write-only module is + * simply writable). */ +static const char* const kRsyncUnenforcedModuleSecurityKeys[] = { + "secrets file", + "auth digest", + "refuse options", + "exclude", + "include", + "exclude from", + "include from", + "filter", + "max size", + "min size", + "pre-xfer exec", + "post-xfer exec", + "incoming chmod", + "outgoing chmod", + "name converter", + "use chroot", + "daemon chroot", + "uid", + "gid", + "daemon uid", + "daemon gid", + "munge symlinks", + "fake super", + "strict modes", + "proxy protocol", + "proxy protocol hosts", +}; + +static const char* const kRsyncUnenforcedGlobalSecurityKeys[] = { + "use chroot", + "uid", + "gid", + "strict modes", +}; + #define kRsyncInertGlobalCount (sizeof(kRsyncInertGlobalKeys) / sizeof(kRsyncInertGlobalKeys[0])) #define kRsyncInertModuleCount (sizeof(kRsyncInertModuleKeys) / sizeof(kRsyncInertModuleKeys[0])) +#define kRsyncUnenforcedModuleSecurityCount \ + (sizeof(kRsyncUnenforcedModuleSecurityKeys) / sizeof(kRsyncUnenforcedModuleSecurityKeys[0])) +#define kRsyncUnenforcedGlobalSecurityCount \ + (sizeof(kRsyncUnenforcedGlobalSecurityKeys) / sizeof(kRsyncUnenforcedGlobalSecurityKeys[0])) static bool parse_bool_value(const char* value, bool* out) { if (strcasecmp(value, "yes") == 0 || strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0) { @@ -352,7 +403,10 @@ DaemonConf* daemon_conf_create(void) { if (!conf) return NULL; conf->global.port = DAEMON_CONF_DEFAULT_PORT; - conf->global.read_only_default = false; + /* rsync modules are READ-ONLY unless `read only = no` (or `write only = yes`) + * is set, so FastSync must default the same way: a migrated rsyncd.conf that + * omits `read only` is served read-only, never writable. */ + conf->global.read_only_default = true; conf->global.max_connections = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS; conf->global.auth_failure_delay_ms = DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS; conf->global.max_connections_per_host = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS_PER_HOST; @@ -483,8 +537,14 @@ static bool apply_global_key(DaemonConf* conf, char* key, const char* value, boo return store_host_list(&conf->global.hosts_deny, &conf->global.hosts_deny_count, value, "hosts deny", NULL, replace_hosts, err, err_size); /* A recognized rsync global key with no FastSync equivalent loads inert. */ - if (key_in_list(key, kRsyncInertGlobalKeys, kRsyncInertGlobalCount)) + if (key_in_list(key, kRsyncInertGlobalKeys, kRsyncInertGlobalCount)) { + if (key_in_list(key, kRsyncUnenforcedGlobalSecurityKeys, kRsyncUnenforcedGlobalSecurityCount)) + log_message(LOG_LEVEL_WARNING, + "daemon config: global key '%s' is accepted for rsync compatibility but is NOT " + "enforced by FastSync; the restriction it expresses will not be applied", + key); return true; + } set_error(err, err_size, "unknown global key '%s'", key); return false; } @@ -516,6 +576,25 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char* module->read_only_explicit = true; return true; } + /* rsync's `write only = yes` makes the module client-writable. FastSync has + * no read/pull path, so mapping it to writability is the exact + * security-relevant effect; set `read_only_explicit` so a global default + * cannot override the module's explicit choice. `write only = no` is the + * rsync default and leaves the module's read-only state untouched. */ + if (key_equals(key, "write only")) { + bool parsed; + if (!parse_bool_value(value, &parsed)) { + set_error(err, err_size, + "module '%s': 'write only' must be yes/no (or true/false/1/0), got '%s'", + module->name, value); + return false; + } + if (parsed) { + module->read_only = false; + module->read_only_explicit = true; + } + return true; + } if (key_equals(key, "client owner")) { bool parsed; if (!parse_bool_value(value, &parsed)) { @@ -584,8 +663,14 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char* return store_host_list(&module->hosts_deny, &module->hosts_deny_count, value, "hosts deny", module->name, false, err, err_size); /* A recognized rsync module key with no FastSync equivalent loads inert. */ - if (key_in_list(key, kRsyncInertModuleKeys, kRsyncInertModuleCount)) + if (key_in_list(key, kRsyncInertModuleKeys, kRsyncInertModuleCount)) { + if (key_in_list(key, kRsyncUnenforcedModuleSecurityKeys, kRsyncUnenforcedModuleSecurityCount)) + log_message(LOG_LEVEL_WARNING, + "daemon config: module '%s' key '%s' is accepted for rsync compatibility but is " + "NOT enforced by FastSync; the restriction it expresses will not be applied", + module->name, key); return true; + } set_error(err, err_size, "unknown key '%s' in module '%s'", key, module->name); return false; } diff --git a/src/shared/daemon_conf.h b/src/shared/daemon_conf.h index 81af6db..49e7a30 100644 --- a/src/shared/daemon_conf.h +++ b/src/shared/daemon_conf.h @@ -21,12 +21,16 @@ * rsync compatibility: to reduce the divergence from rsync 3.4.1's rsyncd.conf * grammar, the parser also ACCEPTS the common rsync GLOBAL and MODULE keys. * Keys with a FastSync equivalent are mapped onto it (the native spellings are - * unchanged). Keys with no FastSync equivalent are accepted and documented as - * inert (they load successfully but have no effect) rather than failing the - * whole config; the accepted inert set is listed in kRsyncInertGlobalKeys / - * kRsyncInertModuleKeys in daemon_conf.c and in RSYNC_COMPAT.md. A key - * outside both the FastSync-native grammar and the recognized rsync subset is - * still rejected as unknown. */ + * unchanged; `read only` defaults to yes like rsync, and `write only = yes` + * opts a module into writability). Keys with no FastSync equivalent are + * accepted and documented as inert (they load successfully but have no effect) + * rather than failing the whole config; the accepted inert set is listed in + * kRsyncInertGlobalKeys / kRsyncInertModuleKeys in daemon_conf.c and in + * RSYNC_COMPAT.md. Every inert key whose intent is access control is loudly + * warned about at load time (kRsyncUnenforced*SecurityKeys) so an operator + * migrating a hardened rsyncd.conf is never misled into believing the + * restriction is enforced. A key outside both the FastSync-native grammar and + * the recognized rsync subset is still rejected as unknown. */ /* A daemon module's configured root is used exactly like the standalone * server's --destination-root: the daemon confines every connection that @@ -55,10 +59,11 @@ typedef struct DaemonModule { char* path; /* module root (daemon-side authorized root) */ bool read_only; /* `read only = yes/no`; defaults to the global `read only` default (rsync allows it in the global section), which is - itself default no */ - bool read_only_explicit; /* set when this module set its own `read only`, so a - later global default (from a `--dparam read only=`) - does not override it */ + itself default YES (rsync modules are read-only unless + `read only = no` / `write only = yes` opts in) */ + bool read_only_explicit; /* set when this module set its own `read only` or + `write only = yes`, so a later global default (from a + `--dparam read only=`) does not override it */ bool client_owner; /* `client owner = yes/no`; default no. Per-module opt-in that lets this module's clients choose ownership (--numeric-ids/--chown/--usermap/--groupmap/--fake-super/ @@ -85,7 +90,8 @@ typedef struct DaemonConfGlobals { char* address; /* `address` (optional bind address), may be NULL */ bool read_only_default; /* global `read only` default for modules defined after it (rsync allows the module key in the - global section); default no */ + global section); default YES to match rsync's + read-only modules */ int max_connections; /* `max connections`, default DAEMON_CONF_DEFAULT_MAX_CONNECTIONS (100) */ int auth_failure_delay_ms; /* `auth failure delay`, milliseconds; default diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index 2b9917a..e424300 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -229,6 +229,7 @@ def daemon_env(): "\n" "[files]\n" "path = %s\n" + "read only = no\n" "\n" "[readonly]\n" "path = %s\n" @@ -236,18 +237,22 @@ def daemon_env(): "\n" "[locked]\n" "path = %s\n" + "read only = no\n" "auth users = alice\n" "\n" "[team]\n" "path = %s\n" + "read only = no\n" "auth users = alice,bob\n" "\n" "[owner]\n" "path = %s\n" + "read only = no\n" "client owner = yes\n" "\n" "[denied]\n" "path = %s\n" + "read only = no\n" "hosts deny = 127.0.0.1\n" % (config_port, FILES_MODULE, READONLY_MODULE, AUTH_MODULE, TEAM_MODULE, OWNER_MODULE, DENIED_MODULE)) @@ -266,7 +271,8 @@ def daemon_env(): global DETACH_PORT DETACH_PORT = _find_free_port() with open(DETACH_CONF, "w") as f: - f.write("port = %d\n\n[detach]\npath = %s\n" % (DETACH_PORT, DETACH_MODULE)) + f.write("port = %d\n\n[detach]\npath = %s\nread only = no\n" + % (DETACH_PORT, DETACH_MODULE)) yield _kill_by_cmdline_marker(DETACH_CONF) @@ -353,7 +359,8 @@ class TestDaemonModuleSelection: the fix regresses.""" port = _find_free_port() with open(UMASK_CONF, "w") as f: - f.write("port = %d\n\n[files]\npath = %s\n" % (port, FILES_MODULE)) + f.write("port = %d\n\n[files]\npath = %s\nread only = no\n" + % (port, FILES_MODULE)) sub = os.path.join(FILES_MODULE, "umask_check") shutil.rmtree(sub, ignore_errors=True) os.makedirs(sub, exist_ok=True) @@ -1128,7 +1135,8 @@ class TestDaemonMotd: motd_line = "motd file = %s\n" % motd_path if motd_path else "" os.makedirs(self.MOTD_MODULE, exist_ok=True) with open(self.MOTD_CONF, "w") as f: - f.write("port = %d\n%s\n[files]\npath = %s\n" % (port, motd_line, self.MOTD_MODULE)) + f.write("port = %d\n%s\n[files]\npath = %s\nread only = no\n" + % (port, motd_line, self.MOTD_MODULE)) d = DaemonManager() d.start(self.MOTD_CONF, port_override=port) return d, port @@ -1365,6 +1373,7 @@ class TestDaemonConnectionLimits: "\n" "[locked]\n" "path = %s\n" + "read only = no\n" "auth users = alice\n" % (port, AUTH_MODULE)) d = DaemonManager() @@ -1402,6 +1411,7 @@ class TestDaemonConnectionLimits: "\n" "[files]\n" "path = %s\n" + "read only = no\n" "max connections = 2\n" % (port, FILES_MODULE)) d = DaemonManager() diff --git a/tests/test_daemon_conf.c b/tests/test_daemon_conf.c index fde819c..f83465e 100644 --- a/tests/test_daemon_conf.c +++ b/tests/test_daemon_conf.c @@ -1,6 +1,7 @@ #include "test_daemon_conf.h" #include "credentials.h" #include "daemon_conf.h" +#include "log.h" #include "test_utils.h" #include #include @@ -31,6 +32,9 @@ static void test_daemon_conf_create_defaults() { EXPECT_EQ_INT(conf->global.port, DAEMON_CONF_DEFAULT_PORT); EXPECT_NULL(conf->global.motd_file); EXPECT_NULL(conf->global.address); + /* rsync modules are read-only unless they opt in, so the default must be + * true. */ + EXPECT_TRUE(conf->global.read_only_default); EXPECT_EQ_INT(conf->global.max_connections, DAEMON_CONF_DEFAULT_MAX_CONNECTIONS); EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS); EXPECT_EQ_INT(conf->global.max_connections_per_host, @@ -802,6 +806,172 @@ static void test_daemon_conf_dparam_rsync_keys() { daemon_conf_free(conf); } +/* rsync modules default to READ-ONLY; `read only = no` / `write only = yes` + * opt a module into writability, and an explicit module value wins over a + * global default. */ +static void test_daemon_conf_read_only_default_and_opt_in() { + char* path; + char err[256]; + DaemonConf* conf; + + /* A module that never mentions read only/write only is READ-ONLY, matching + * rsync (a migrated rsyncd.conf must not be served writable). */ + EXPECT_EQ_INT(write_conf("[m]\npath = /x\n", &path), 0); + conf = daemon_conf_load(path, err, sizeof(err)); + free(path); + EXPECT_NOT_NULL(conf); + EXPECT_TRUE(conf->modules[0].read_only); + EXPECT_FALSE(conf->modules[0].read_only_explicit); + daemon_conf_free(conf); + + /* `read only = no` opts in to writable. */ + EXPECT_EQ_INT(write_conf("[m]\npath = /x\nread only = no\n", &path), 0); + conf = daemon_conf_load(path, err, sizeof(err)); + free(path); + EXPECT_NOT_NULL(conf); + EXPECT_FALSE(conf->modules[0].read_only); + EXPECT_TRUE(conf->modules[0].read_only_explicit); + daemon_conf_free(conf); + + /* `write only = yes` opts in to writable (FastSync is push-only). */ + EXPECT_EQ_INT(write_conf("[m]\npath = /x\nwrite only = yes\n", &path), 0); + conf = daemon_conf_load(path, err, sizeof(err)); + free(path); + EXPECT_NOT_NULL(conf); + EXPECT_FALSE(conf->modules[0].read_only); + EXPECT_TRUE(conf->modules[0].read_only_explicit); + daemon_conf_free(conf); + + /* `write only = no` is rsync's default and does not undo read-only. */ + EXPECT_EQ_INT(write_conf("[m]\npath = /x\nwrite only = no\n", &path), 0); + conf = daemon_conf_load(path, err, sizeof(err)); + free(path); + EXPECT_NOT_NULL(conf); + EXPECT_TRUE(conf->modules[0].read_only); + EXPECT_FALSE(conf->modules[0].read_only_explicit); + daemon_conf_free(conf); + + /* A global `read only = no` is the default for later modules; an explicit + * module `read only`/`write only = yes` still wins. */ + EXPECT_EQ_INT(write_conf("read only = no\n[a]\npath = /a\n" + "[b]\npath = /b\nread only = yes\n" + "[c]\npath = /c\nwrite only = yes\n", + &path), + 0); + conf = daemon_conf_load(path, err, sizeof(err)); + free(path); + EXPECT_NOT_NULL(conf); + EXPECT_FALSE(conf->global.read_only_default); + EXPECT_FALSE(conf->modules[0].read_only); + EXPECT_TRUE(conf->modules[1].read_only); + EXPECT_FALSE(conf->modules[2].read_only); + daemon_conf_free(conf); + + /* A global `read only = yes` keeps modules without an explicit value + * read-only. */ + EXPECT_EQ_INT(write_conf("read only = yes\n[a]\npath = /a\n" + "[b]\npath = /b\nread only = no\n" + "[c]\npath = /c\nwrite only = yes\n", + &path), + 0); + conf = daemon_conf_load(path, err, sizeof(err)); + free(path); + EXPECT_NOT_NULL(conf); + EXPECT_TRUE(conf->global.read_only_default); + EXPECT_TRUE(conf->modules[0].read_only); + EXPECT_FALSE(conf->modules[1].read_only); + EXPECT_FALSE(conf->modules[2].read_only); + daemon_conf_free(conf); + + /* An invalid `write only` value is a clear parse error. */ + EXPECT_EQ_INT(write_conf("[m]\npath = /x\nwrite only = maybe\n", &path), 0); + conf = daemon_conf_load(path, err, sizeof(err)); + free(path); + EXPECT_NULL(conf); + EXPECT_TRUE(strstr(err, "write only") != NULL); +} + +/* Security-relevant rsync keys are accepted for migration but have no FastSync + * effect, so loading must warn loudly (naming the key and module) rather than + * letting an operator believe the restriction is enforced. */ +static void test_daemon_conf_unenforced_security_keys_warned() { + char* path; + char err[256]; + EXPECT_EQ_INT(write_conf("use chroot = yes\n" + "uid = nobody\n" + "[m]\n" + "path = /x\n" + "read only = no\n" + "secrets file = /etc/rsyncd.secrets\n" + "refuse options = delete\n" + "exclude = *.tmp\n" + "max size = 1M\n" + "pre-xfer exec = /bin/true\n" + "incoming chmod = F644\n" + "name converter = sh\n", + &path), + 0); + + set_log_level(LOG_LEVEL_WARNING); + FILE* log_capture = tmpfile(); + EXPECT_NOT_NULL(log_capture); + log_set_file(log_capture); + DaemonConf* conf = daemon_conf_load(path, err, sizeof(err)); + fflush(log_capture); + rewind(log_capture); + log_set_file(NULL); + free(path); + + /* Inert security keys must never fail the load. */ + EXPECT_NOT_NULL(conf); + EXPECT_FALSE(conf->modules[0].read_only); + + bool saw_secrets = false; + bool saw_refuse = false; + bool saw_filter = false; + bool saw_size = false; + bool saw_hook = false; + bool saw_chmod = false; + bool saw_converter = false; + bool saw_global_chroot = false; + bool saw_global_uid = false; + char line[512]; + while (fgets(line, sizeof(line), log_capture) != NULL) { + if (strstr(line, "NOT enforced") == NULL) + continue; + if (strstr(line, "module 'm'") && strstr(line, "secrets file")) + saw_secrets = true; + if (strstr(line, "module 'm'") && strstr(line, "refuse options")) + saw_refuse = true; + if (strstr(line, "module 'm'") && strstr(line, "exclude")) + saw_filter = true; + if (strstr(line, "module 'm'") && strstr(line, "max size")) + saw_size = true; + if (strstr(line, "module 'm'") && strstr(line, "pre-xfer exec")) + saw_hook = true; + if (strstr(line, "module 'm'") && strstr(line, "incoming chmod")) + saw_chmod = true; + if (strstr(line, "module 'm'") && strstr(line, "name converter")) + saw_converter = true; + if (strstr(line, "global key 'use chroot'")) + saw_global_chroot = true; + if (strstr(line, "global key 'uid'")) + saw_global_uid = true; + } + fclose(log_capture); + + EXPECT_TRUE(saw_secrets); + EXPECT_TRUE(saw_refuse); + EXPECT_TRUE(saw_filter); + EXPECT_TRUE(saw_size); + EXPECT_TRUE(saw_hook); + EXPECT_TRUE(saw_chmod); + EXPECT_TRUE(saw_converter); + EXPECT_TRUE(saw_global_chroot); + EXPECT_TRUE(saw_global_uid); + daemon_conf_free(conf); +} + void test_daemon_conf() { test_daemon_conf_create_defaults(); test_daemon_conf_full_parse(); @@ -826,4 +996,6 @@ void test_daemon_conf() { test_daemon_conf_rsync_unknown_keys_rejected(); test_daemon_conf_rsync_read_only_invalid(); test_daemon_conf_dparam_rsync_keys(); + test_daemon_conf_read_only_default_and_opt_in(); + test_daemon_conf_unenforced_security_keys_warned(); } \ No newline at end of file