diff --git a/.luacheckrc b/.luacheckrc
index 29e59a28e9edbde7acda7cba4852009b0214a69b..a6a3210868879005af1db100a6ab9a12a478ad69 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -76,4 +76,4 @@ files['daemon/lua/kres-gen.lua'].ignore = {'631'} -- Allow overly long lines
 -- Tests and scripts can use global variables
 files['scripts'].ignore = {'111', '112', '113'}
 files['tests'].ignore = {'111', '112', '113'}
-files['modules/*/*_test.lua'].ignore = {'111', '112', '113', '122'}
\ No newline at end of file
+files['modules/**/*.test.lua'].ignore = {'111', '112', '113', '122'}
diff --git a/config.mk b/config.mk
index 0023b4b5cbc1a3904d57f78de08a0a84fed26fca..00dd3805c9ad67f08b01194db9d1ddcd02896233 100644
--- a/config.mk
+++ b/config.mk
@@ -23,6 +23,7 @@ ROOTHINTS ?= $(ETCDIR)/root.hints
 COVERAGE_STAGE ?= gcov
 COVERAGE_STATSDIR ?= $(CURDIR)/coverage.stats
 TOPSRCDIR := $(CURDIR)
+KEYFILE_DEFAULT ?=
 
 # Tools
 CC      ?= cc
diff --git a/daemon/README.rst b/daemon/README.rst
index c7979012231767c2bfcad273ea39db7ff5510979..c3dc6e3179e1b24f91d8b458e64327a5a8fc19f6 100644
--- a/daemon/README.rst
+++ b/daemon/README.rst
@@ -22,9 +22,12 @@ To enable it, you need to provide trusted root keys. Bootstrapping of the keys i
    $ kresd -k root-new.keys # File for root keys
    [ ta ] keyfile 'root-new.keys': doesn't exist, bootstrapping
    [ ta ] Root trust anchors bootstrapped over https with pinned certificate.
-          You may want to verify them manually, as described on:
-          https://data.iana.org/root-anchors/old/draft-icann-dnssec-trust-anchor.html#sigs
-   [ ta ] next refresh for . in 23.912361111111 hours
+          You SHOULD verify them manually against original source:
+          https://www.iana.org/dnssec/files
+   [ ta ] Current root trust anchors are:
+   . 0 IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5
+   . 0 IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
+   [ ta ] next refresh for . in 24 hours
 
 Alternatively, you can set it in configuration file with ``trust_anchors.file = 'root.keys'``. If the file doesn't exist, it will be automatically populated with root keys validated using root anchors retrieved over HTTPS.
 
@@ -58,6 +61,8 @@ The root anchors bootstrap may fail for various reasons, in this case you need t
 
 You've just enabled DNSSEC!
 
+.. note:: Bootstrapping and automatic update need write access to keyfile direcory. If you want to manage root anchors manually you should use ``trust_anchors.add_file('root.keys', true)``.
+
 CLI interface
 =============
 
diff --git a/daemon/daemon.mk b/daemon/daemon.mk
index a455c2c25386a21d2aa532f82fa389deb9d600c9..cf28be0ff010c8e11a22bea21914d6454ef93962 100644
--- a/daemon/daemon.mk
+++ b/daemon/daemon.mk
@@ -56,6 +56,7 @@ daemon-install: kresd-install bindings-install
 ifneq ($(SED),)
 	$(SED) -e "s/@VERSION@/$(VERSION)/" -e "s/@DATE@/$(date)/" \
 		-e "s|@MODULEDIR@|$(MODULEDIR)|" \
+		-e "s|@KEYFILE_DEFAULT@|$(KEYFILE_DEFAULT)|" \
 		doc/kresd.8.in > doc/kresd.8
 	$(INSTALL) -d -m 0755 $(DESTDIR)$(MANDIR)/man8/
 	$(INSTALL) -m 0644 doc/kresd.8 $(DESTDIR)$(MANDIR)/man8/
@@ -65,7 +66,7 @@ daemon-clean: kresd-clean
 		daemon/lua/zonefile.lua
 
 daemon/lua/trust_anchors.lua: daemon/lua/trust_anchors.lua.in
-	@$(call quiet,SED,$<) -e "s|@ETCDIR@|$(ETCDIR)|g" $< > $@
+	@$(call quiet,SED,$<) -e "s|@ETCDIR@|$(ETCDIR)|g;s|@KEYFILE_DEFAULT@|$(KEYFILE_DEFAULT)|g" $< > $@
 
 LIBZSCANNER_COMMENTS := \
 	$(shell pkg-config libzscanner --atleast-version=2.4.2 && echo true || echo false)
diff --git a/daemon/engine.c b/daemon/engine.c
index 8eb782442a533abe6c80a8b20bc7133ca294594e..b7e69db7bc5155500a35b55ecaf59d9a181dbedf 100644
--- a/daemon/engine.c
+++ b/daemon/engine.c
@@ -812,9 +812,8 @@ int engine_ipc(struct engine *engine, const char *expr)
 	}
 }
 
-static int engine_loadconf(struct engine *engine, const char *config_path)
+int engine_load_sandbox(struct engine *engine)
 {
-	int ret = 0;
 	/* Init environment */
 	static const char sandbox_bytecode[] = {
 		#include "daemon/lua/sandbox.inc"
@@ -824,22 +823,13 @@ static int engine_loadconf(struct engine *engine, const char *config_path)
 		lua_pop(engine->L, 1);
 		return kr_error(ENOEXEC);
 	}
-	/* Load config file */
-	if (config_path) {
-		if (strcmp(config_path, "-") == 0) {
-			return ret; /* No config and no defaults. */
-		}
-		ret = l_dosandboxfile(engine->L, config_path);
-	}
-	if (ret == 0) {
-		/* Load defaults */
-		static const char config_bytecode[] = {
-			#include "daemon/lua/config.inc"
-		};
-		ret = l_dobytecode(engine->L, config_bytecode, sizeof(config_bytecode), "config");
-	}
+	return kr_ok();
+}
 
-	/* Evaluate */
+int engine_loadconf(struct engine *engine, const char *config_path)
+{
+	assert(config_path != NULL);
+	int ret = l_dosandboxfile(engine->L, config_path);
 	if (ret != 0) {
 		fprintf(stderr, "%s\n", lua_tostring(engine->L, -1));
 		lua_pop(engine->L, 1);
@@ -847,14 +837,22 @@ static int engine_loadconf(struct engine *engine, const char *config_path)
 	return ret;
 }
 
-int engine_start(struct engine *engine, const char *config_path)
+int engine_load_defaults(struct engine *engine)
 {
-	/* Load configuration. */
-	int ret = engine_loadconf(engine, config_path);
+	/* Load defaults */
+	static const char config_bytecode[] = {
+		#include "daemon/lua/config.inc"
+	};
+	int ret = l_dobytecode(engine->L, config_bytecode, sizeof(config_bytecode), "config");
 	if (ret != 0) {
-		return ret;
+		fprintf(stderr, "%s\n", lua_tostring(engine->L, -1));
+		lua_pop(engine->L, 1);
 	}
+	return ret;
+}
 
+int engine_start(struct engine *engine)
+{
 	/* Clean up stack and restart GC */
 	lua_settop(engine->L, 0);
 	lua_gc(engine->L, LUA_GCCOLLECT, 0);
diff --git a/daemon/engine.h b/daemon/engine.h
index 25ee30a124602895f241313b20b423e21660cf92..dea87c118d6e1a6cce9dec4b7a261cc32afbe479 100644
--- a/daemon/engine.h
+++ b/daemon/engine.h
@@ -83,12 +83,13 @@ int engine_pcall(struct lua_State *L, int argc);
 
 int engine_ipc(struct engine *engine, const char *expr);
 
-/** Start the lua engine and execute the config.
- *
- * @note Special path "-" means that even default config won't be done
- *       (like listening on localhost).
- */
-int engine_start(struct engine *engine, const char *config_path);
+
+int engine_load_sandbox(struct engine *engine);
+int engine_loadconf(struct engine *engine, const char *config_path);
+int engine_load_defaults(struct engine *engine);
+
+/** Start the lua engine and execute the config. */
+int engine_start(struct engine *engine);
 void engine_stop(struct engine *engine);
 int engine_register(struct engine *engine, const char *name, const char *precedence, const char* ref);
 int engine_unregister(struct engine *engine, const char *name);
diff --git a/daemon/lua/config.lua b/daemon/lua/config.lua
index cc6e2f2f23afbc2a75066c3881024dfa44068761..7abf181845cad1a975e6e8c1d7633b3806dbeaf9 100644
--- a/daemon/lua/config.lua
+++ b/daemon/lua/config.lua
@@ -18,3 +18,13 @@ end
 if kres.context().root_hints.nsset.root == nil then
 	_hint_root_file()
 end
+
+if not trust_anchors.keysets['\0'] and trust_anchors.keyfile_default then
+	if io.open(trust_anchors.keyfile_default, 'r') then
+		trust_anchors.config(trust_anchors.keyfile_default, true)
+	else
+		panic("cannot open default trust anchor file:'%s'",
+		      trust_anchors.keyfile_default
+		)
+	end
+end
diff --git a/daemon/lua/trust_anchors.lua.in b/daemon/lua/trust_anchors.lua.in
index d21a34d2c95648ae40216f8206f8568281e1ee5c..7c9e27dad61be687dbbd09132a459368d5457aad 100644
--- a/daemon/lua/trust_anchors.lua.in
+++ b/daemon/lua/trust_anchors.lua.in
@@ -42,8 +42,10 @@ local function bootstrap(url, ca)
 		return false, string.format('[ ta ] failed to get any record from "%s"', url)
 	end
 	local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n'
-			 .. '       You may want to verify them manually, as described on:\n'
-			 .. '       https://data.iana.org/root-anchors/old/draft-icann-dnssec-trust-anchor.html#sigs'
+			 .. '       You SHOULD verify them manually against original source:\n'
+			 .. '       https://www.iana.org/dnssec/files\n'
+			 .. '[ ta ] Current root trust anchors are:'
+			 .. rr
 	return rr, msg
 end
 
@@ -367,9 +369,16 @@ update = function (keyset, new_keys, is_initial)
 end
 
 local add_file = function (path, unmanaged)
-	-- Bootstrap if requested and keyfile doesn't exist
+	if not unmanaged then
+		if not io.open(path .. '.lock', 'w') then
+			error("[ ta ] ERROR: write access needed to keyfile dir '"..path.."'")
+		end
+		os.remove(path .. ".lock")
+	end
 
+	-- Bootstrap if requested and keyfile doesn't exist
 	if not unmanaged and not io.open(path, 'r') then
+		log("[ ta ] keyfile '%s': doesn't exist, bootstrapping", path);
 		local tas, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca)
 		if not tas then
 			msg = msg .. '\n'
@@ -395,13 +404,14 @@ local add_file = function (path, unmanaged)
 
 	-- Parse the file and check its sanity
 	local keyset, err = keyset_read(path)
-	if not unmanaged then keyset.filename = path end
 	if not keyset then
 		panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err)
 	end
+	if not unmanaged then keyset.filename = path end
 	if not keyset[1] then
 		panic("[ ta ] ERROR: failed to read anchors from '%s'", path)
 	end
+	if not unmanaged then keyset.filename = path end
 	local owner = keyset[1].owner
 	for _, ta in ipairs(keyset) do
 		if ta.owner ~= owner then
@@ -446,6 +456,8 @@ trust_anchors = {
 
 	bootstrap_url = 'https://data.iana.org/root-anchors/root-anchors.xml',
 	bootstrap_ca = '@ETCDIR@/icann-ca.pem',
+	-- change empty string to nil
+	keyfile_default = ('@KEYFILE_DEFAULT@' ~= '' and '@KEYFILE_DEFAULT@') or nil,
 
 	-- Load keys from a file, 5011-managed by default.
 	-- If managed and the file doesn't exist, try bootstrapping the root into it.
diff --git a/daemon/main.c b/daemon/main.c
index 4329000185b2534d337055c57df640757143df2c..0d212696285e68050edd761dd2a070b281895569 100644
--- a/daemon/main.c
+++ b/daemon/main.c
@@ -18,7 +18,6 @@
 #include <signal.h>
 #include <stdlib.h>
 #include <string.h>
-#include <signal.h>
 #include <getopt.h>
 #include <libgen.h>
 #include <uv.h>
@@ -39,17 +38,32 @@
 #include "daemon/engine.h"
 #include "daemon/bindings.h"
 #include "daemon/tls.h"
+#include "lib/dnssec/ta.h"
 
 /* We can fork early on Linux 3.9+ and do SO_REUSEPORT for better performance. */
 #if defined(UV_VERSION_HEX) && defined(SO_REUSEPORT) && defined(__linux__)
  #define CAN_FORK_EARLY 1
 #endif
 
-/*
- * Globals
- */
-static bool g_quiet = false;
-static bool g_interactive = true;
+/* @internal Array of ip address shorthand. */
+typedef array_t(char*) addr_array_t;
+
+struct args {
+	int forks;
+	addr_array_t addr_set;
+	addr_array_t tls_set;
+	fd_array_t fd_set;
+	fd_array_t tls_fd_set;
+	char *keyfile;
+	int keyfile_unmanaged;
+	const char *moduledir;
+	const char *config;
+	int control_fd;
+	const char *rundir;
+	bool interactive;
+	bool quiet;
+	bool tty_binary_output;
+};
 
 /* lua_pcall helper function */
 static inline char *lua_strerror(int lua_err) {
@@ -66,9 +80,8 @@ static inline char *lua_strerror(int lua_err) {
  *
  * For parameters see http://docs.libuv.org/en/v1.x/stream.html#c.uv_read_cb
  *
- * - This is just basic read-eval-print; libedit is supported through krsec;
- * - stream->data represents a bool determining binary output mode (used by kresc);
- * - binary output: uint32_t length in network order, followed by that many bytes.
+ * - This is just basic read-eval-print; libedit is supported through kresc;
+ * - stream->data contains program arguments (struct args);
  */
 static void tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf)
 {
@@ -77,6 +90,7 @@ static void tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t
 	/* Set output streams */
 	FILE *out = stdout;
 	uv_os_fd_t stream_fd = 0;
+	struct args *args = stream->data;
 	if (uv_fileno((uv_handle_t *)stream, &stream_fd)) {
 		uv_close((uv_handle_t *)stream, (uv_close_cb) free);
 		free(cmd);
@@ -111,11 +125,9 @@ static void tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t
 			cmd[nread] = '\0';
 		}
 
-		/* Pseudo-command for switching to "binary output";
-		 * beware: void* <-> bool */
-		bool is_binary = stream->data;
+		/* Pseudo-command for switching to "binary output"; */
 		if (strcmp(cmd, "__binary") == 0) {
-			stream->data = (void *)(is_binary = true);
+			args->tty_binary_output = true;
 			goto finish;
 		}
 
@@ -128,7 +140,7 @@ static void tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t
 		}
 
 		/* Simpler output in binary mode */
-		if (is_binary) {
+		if (args->tty_binary_output) {
 			size_t len_s = strlen(message);
 			if (len_s > UINT32_MAX)
 				goto finish;
@@ -140,12 +152,12 @@ static void tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t
 		}
 
 		/* Log to remote socket if connected */
-		const char *delim = g_quiet ? "" : "> ";
+		const char *delim = args->quiet ? "" : "> ";
 		if (stream_fd != STDIN_FILENO) {
 			fprintf(stdout, "%s\n", cmd); /* Duplicate command to logs */
 			if (message)
 				fprintf(out, "%s", message); /* Duplicate output to sender */
-			if (message || !g_quiet)
+			if (message || !args->quiet)
 				fprintf(out, "\n");
 			fprintf(out, "%s", delim);
 		}
@@ -153,7 +165,7 @@ static void tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t
 		FILE *fp_out = ret ? stderr : stdout;
 		if (message)
 			fprintf(fp_out, "%s", message);
-		if (message || !g_quiet)
+		if (message || !args->quiet)
 			fprintf(fp_out, "\n");
 		fprintf(fp_out, "%s", delim);
 		lua_settop(L, 0);
@@ -175,16 +187,17 @@ static void tty_alloc(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) {
 static void tty_accept(uv_stream_t *master, int status)
 {
 	uv_tcp_t *client = malloc(sizeof(*client));
+	struct args *args = master->data;
 	if (client) {
 		 uv_tcp_init(master->loop, client);
 		 if (uv_accept(master, (uv_stream_t *)client) != 0) {
 			free(client);
 			return;
 		 }
-		 client->data = 0;
+		 client->data = args;
 		 uv_read_start((uv_stream_t *)client, tty_alloc, tty_process_input);
 		 /* Write command line */
-		 if (!g_quiet) {
+		 if (!args->quiet) {
 		 	uv_buf_t buf = { "> ", 2 };
 		 	uv_try_write((uv_stream_t *)client, &buf, 1);
 		 }
@@ -347,7 +360,8 @@ static void help(int argc, char *argv[])
 	       " -S, --fd=[fd]          Listen on given fd (handed out by supervisor).\n"
 	       " -T, --tlsfd=[fd]       Listen using TLS on given fd (handed out by supervisor).\n"
 	       " -c, --config=[path]    Config file path (relative to [rundir]) (default: config).\n"
-	       " -k, --keyfile=[path]   File containing trust anchors (DS or DNSKEY).\n"
+	       " -k, --keyfile=[path]   File with root domain trust anchors (DS or DNSKEY), automatically updated.\n"
+	       " -K, --keyfile-ro=[path] File with read-only root domain trust anchors, for use with an external updater.\n"
 	       " -m, --moduledir=[path] Override the default module path (" MODULEDIR ").\n"
 	       " -f, --forks=N          Start N forks sharing the configuration.\n"
 	       " -q, --quiet            Quiet output, no prompt in interactive mode.\n"
@@ -362,23 +376,23 @@ static void help(int argc, char *argv[])
 	       " [rundir]             Path to the working directory (default: .)\n");
 }
 
-static int run_worker(uv_loop_t *loop, struct engine *engine, fd_array_t *ipc_set, bool leader, int control_fd)
+static int run_worker(uv_loop_t *loop, struct engine *engine, fd_array_t *ipc_set, bool leader, struct args *args)
 {
 	/* Control sockets or TTY */
 	auto_free char *sock_file = NULL;
 	uv_pipe_t pipe;
 	uv_pipe_init(loop, &pipe, 0);
-	pipe.data = 0;
-	if (g_interactive) {
-		if (!g_quiet)
+	pipe.data = args;
+	if (args->interactive) {
+		if (!args->quiet)
 			printf("[system] interactive mode\n> ");
 		fflush(stdout);
 		uv_pipe_open(&pipe, 0);
 		uv_read_start((uv_stream_t*) &pipe, tty_alloc, tty_process_input);
 	} else {
 		int pipe_ret = -1;
-		if (control_fd != -1) {
-			pipe_ret = uv_pipe_open(&pipe, control_fd);
+		if (args->control_fd != -1) {
+			pipe_ret = uv_pipe_open(&pipe, args->control_fd);
 		} else {
 			(void) mkdir("tty", S_IRWXU|S_IRWXG);
 			sock_file = afmt("tty/%ld", getpid());
@@ -422,105 +436,49 @@ static void free_sd_socket_names(char **socket_names, int count)
 }
 #endif
 
-static int init_keyfile(struct engine *engine, const char *keyfile)
+static int set_keyfile(struct engine *engine, char *keyfile, bool unmanaged)
 {
-	auto_free char *dirname_storage = strdup(keyfile);
-	if (!dirname_storage) {
-		return kr_error(ENOMEM);
-	}
-
-	/* Resolve absolute path to the keyfile directory */
-	auto_free char *keyfile_dir = malloc(PATH_MAX);
-	if (!keyfile_dir) {
-		return kr_error(ENOMEM);
-	}
-	if (realpath(dirname(dirname_storage), keyfile_dir) == NULL) {
-		kr_log_error("[ ta ]: keyfile '%s' directory: %s\n", keyfile, strerror(errno));
-		return kr_error(ENOTDIR);
-	}
-
-	auto_free char *basename_storage = strdup(keyfile);
-	if (!basename_storage) {
-		return kr_error(ENOMEM);
-	}
-
-	char *_filename = basename(basename_storage);
-	int dirlen = strlen(keyfile_dir);
-	int namelen = strlen(_filename);
-	if (dirlen + 1 + namelen >= PATH_MAX) {
-		kr_log_error("[ ta ]: keyfile '%s' PATH_MAX exceeded\n",
-			     keyfile);
-		return kr_error(ENAMETOOLONG);
-	}
-	keyfile_dir[dirlen++] = '/';
-	keyfile_dir[dirlen] = '\0';
-
-	auto_free char *keyfile_path = malloc(dirlen + namelen + 1);
-	if (!keyfile_path) {
-		return kr_error(ENOMEM);
-	}
-	memcpy(keyfile_path, keyfile_dir, dirlen);
-	memcpy(keyfile_path + dirlen, _filename, namelen + 1);
-
-	int unmanaged = 0;
-
-	/* Note: config has been executed, so access() is OK,
-	 * as we've dropped privileges already if configured. */
-	if (access(keyfile_path, F_OK) != 0) {
-		kr_log_info("[ ta ] keyfile '%s': doesn't exist, bootstrapping\n", keyfile_path);
-		if (access(keyfile_dir, W_OK) != 0) {
-			kr_log_error("[ ta ] keyfile '%s': write access to '%s' needed\n", keyfile_path, keyfile_dir);
-			return kr_error(EPERM);
-		}
-	} else if (access(keyfile_path, R_OK) == 0) {
-		if ((access(keyfile_path, W_OK) != 0) || (access(keyfile_dir, W_OK) != 0)) {
-			kr_log_error("[ ta ] keyfile '%s': not writeable, starting in unmanaged mode\n", keyfile_path);
-			unmanaged = 1;
-		}
-	} else {
-		kr_log_error("[ ta ] keyfile '%s': %s\n", keyfile_path, strerror(errno));
-		return kr_error(EPERM);
-	}
-
-	auto_free char *cmd = afmt("trust_anchors.config('%s',%s)", keyfile_path, unmanaged?"true":"nil");
+	assert(keyfile != NULL);
+	auto_free char *cmd = afmt("trust_anchors.config('%s',%s)",
+				   keyfile, unmanaged ? "true" : "nil");
 	if (!cmd) {
+		kr_log_error("[system] not enough memory\n");
 		return kr_error(ENOMEM);
 	}
-
 	int lua_ret = engine_cmd(engine->L, cmd, false);
 	if (lua_ret != 0) {
 		if (lua_gettop(engine->L) > 0) {
-			kr_log_error("%s", lua_tostring(engine->L, -1));
-			lua_settop(engine->L, 0);
+			kr_log_error("%s\n", lua_tostring(engine->L, -1));
 		} else {
 			kr_log_error("[ ta ] keyfile '%s': failed to load (%s)\n",
-					keyfile_path, lua_strerror(lua_ret));
+					keyfile, lua_strerror(lua_ret));
 		}
-		return kr_error(EIO);
+		return lua_ret;
 	}
 
 	lua_settop(engine->L, 0);
-	return 0;
+	return kr_ok();
 }
 
-int main(int argc, char **argv)
+
+static void args_init(struct args *args)
 {
-	int forks = 1;
-	array_t(char*) addr_set;
-	array_t(char*) tls_set;
-	array_init(addr_set);
-	array_init(tls_set);
-	array_t(int) fd_set;
-	array_init(fd_set);
-	array_t(int) tls_fd_set;
-	array_init(tls_fd_set);
-	char *keyfile = NULL;
-	char *moduledir = MODULEDIR;
-	const char *config = NULL;
-	int control_fd = -1;
+	memset(args, 0, sizeof(struct args));
+	args->forks = -1;
+	array_init(args->addr_set);
+	array_init(args->tls_set);
+	array_init(args->fd_set);
+	array_init(args->tls_fd_set);
+	args->moduledir = MODULEDIR;
+	args->control_fd = -1;
+	args->interactive = true;
+	args->quiet = false;
+}
 
+int parse_args(int argc, char **argv, struct args *args)
+{
 	/* Long options. */
-	int c = 0, li = 0, ret = 0;
+	int c = 0, li = 0;
 	struct option opts[] = {
 		{"addr", required_argument,   0, 'a'},
 		{"tls",  required_argument,   0, 't'},
@@ -528,6 +486,7 @@ int main(int argc, char **argv)
 		{"tlsfd", required_argument,  0, 'T'},
 		{"config", required_argument, 0, 'c'},
 		{"keyfile",required_argument, 0, 'k'},
+		{"keyfile-ro",required_argument, 0, 'K'},
 		{"forks",required_argument,   0, 'f'},
 		{"moduledir", required_argument, 0, 'm'},
 		{"verbose",    no_argument,   0, 'v'},
@@ -536,38 +495,44 @@ int main(int argc, char **argv)
 		{"help",      no_argument,    0, 'h'},
 		{0, 0, 0, 0}
 	};
-	while ((c = getopt_long(argc, argv, "a:t:S:T:c:f:m:k:vqVh", opts, &li)) != -1) {
+	while ((c = getopt_long(argc, argv, "a:t:S:T:c:f:m:K:k:vqVh", opts, &li)) != -1) {
 		switch (c)
 		{
 		case 'a':
-			array_push(addr_set, optarg);
+			array_push(args->addr_set, optarg);
 			break;
 		case 't':
-			array_push(tls_set, optarg);
+			array_push(args->tls_set, optarg);
 			break;
 		case 'S':
-			array_push(fd_set, strtol(optarg, NULL, 10));
+			array_push(args->fd_set, strtol(optarg, NULL, 10));
 			break;
 		case 'T':
-			array_push(tls_fd_set, strtol(optarg, NULL, 10));
+			array_push(args->tls_fd_set, strtol(optarg, NULL, 10));
 			break;
 		case 'c':
-			config = optarg;
+			args->config = optarg;
 			break;
 		case 'f':
-			g_interactive = false;
-			forks = strtol(optarg, NULL, 10);
-			if (forks <= 0) {
+			args->interactive = false;
+			args->forks = strtol(optarg, NULL, 10);
+			if (args->forks <= 0) {
 				kr_log_error("[system] error '-f' requires a positive"
 						" number, not '%s'\n", optarg);
 				return EXIT_FAILURE;
 			}
 			break;
+		case 'K':
+			args->keyfile_unmanaged = 1;
 		case 'k':
-			keyfile = optarg;
+			if (args->keyfile != NULL) {
+				kr_log_error("[system] error only one of '--keyfile' and '--keyfile-ro' allowed\n");
+				return EXIT_FAILURE;
+			}
+			args->keyfile = optarg;
 			break;
 		case 'm':
-			moduledir = optarg;
+			args->moduledir = optarg;
 			break;
 		case 'v':
 			kr_verbose_set(true);
@@ -576,7 +541,7 @@ int main(int argc, char **argv)
 #endif
 			break;
 		case 'q':
-			g_quiet = true;
+			args->quiet = true;
 			break;
 		case 'V':
 			kr_log_info("%s, version %s\n", "Knot DNS Resolver", PACKAGE_VERSION);
@@ -589,6 +554,49 @@ int main(int argc, char **argv)
 			help(argc, argv);
 			return EXIT_FAILURE;
 		}
+	}	
+	if (optind < argc) {
+		args->rundir = argv[optind];
+	}
+	return EXIT_SUCCESS;
+}
+
+static int bind_fds(struct network *net, fd_array_t *fd_set, bool tls) {
+	int ret = 0;
+	for (size_t i = 0; i < fd_set->len; ++i) {
+		ret = network_listen_fd(net, fd_set->at[i], tls);
+		if (ret != 0) {
+			kr_log_error("[system] %slisten on fd=%d %s\n",
+				 tls ? "TLS " : "", fd_set->at[i], kr_strerror(ret));
+			break;
+		}
+	}
+	return ret;
+}
+
+static int bind_sockets(struct network *net, addr_array_t *addr_set, bool tls) {	
+	uint32_t flags = tls ? NET_TCP|NET_TLS : NET_UDP|NET_TCP;
+	int ret = 0;
+	for (size_t i = 0; i < addr_set->len; ++i) {
+		int port = tls ? KR_DNS_TLS_PORT : KR_DNS_PORT;
+		const char *addr = set_addr(addr_set->at[i], &port);
+		ret = network_listen(net, addr, (uint16_t)port, flags);
+		if (ret != 0) {
+			kr_log_error("[system] bind to '%s@%d' %s%s\n", 
+				addr, port, tls ? "(TLS) " : "", kr_strerror(ret));
+			break;
+		}
+	}
+	return ret;
+}
+
+int main(int argc, char **argv)
+{
+	int ret = 0;
+	struct args args;
+	args_init(&args);
+	if ((ret = parse_args(argc, argv, &args)) != EXIT_SUCCESS) {
+		return ret;
 	}
 
 #ifdef HAS_SYSTEMD
@@ -598,45 +606,44 @@ int main(int argc, char **argv)
 	for (int i = 0; i < sd_nsocks; ++i) {
 		int fd = SD_LISTEN_FDS_START + i;
 		/* when run under systemd supervision, do not use interactive mode */
-		g_interactive = false;
-		if (forks != 1) {
+		args.interactive = false;
+		if (args.forks != 1) {
 			kr_log_error("[system] when run under systemd-style supervision, "
-				     "use single-process only (bad: --fork=%d).\n", forks);
+				     "use single-process only (bad: --fork=%d).\n", args.forks);
 			free_sd_socket_names(socket_names, sd_nsocks);
 			return EXIT_FAILURE;
 		}
 		if (!strcasecmp("control",socket_names[i])) {
-			control_fd = fd;
+			args.control_fd = fd;
 		} else if (!strcasecmp("tls",socket_names[i])) {
-			array_push(tls_fd_set, fd);
+			array_push(args.tls_fd_set, fd);
 		} else {
-			array_push(fd_set, fd);
+			array_push(args.fd_set, fd);
 		}
 	}
 	free_sd_socket_names(socket_names, sd_nsocks);
 #endif
 
 	/* Switch to rundir. */
-	if (optind < argc) {
-		const char *rundir = argv[optind];
+	if (args.rundir != NULL) {
 		/* FIXME: access isn't a good way if we start as root and drop privileges later */
-		if (access(rundir, W_OK) != 0) {
-			kr_log_error("[system] rundir '%s': %s\n", rundir, strerror(errno));
+		if (access(args.rundir, W_OK) != 0) {
+			kr_log_error("[system] rundir '%s': %s\n", args.rundir, strerror(errno));
 			return EXIT_FAILURE;
 		}
-		ret = chdir(rundir);
+		ret = chdir(args.rundir);
 		if (ret != 0) {
-			kr_log_error("[system] rundir '%s': %s\n", rundir, strerror(errno));
+			kr_log_error("[system] rundir '%s': %s\n", args.rundir, strerror(errno));
 			return EXIT_FAILURE;
 		}
 	}
 
-	if (config && strcmp(config, "-") != 0 && access(config, R_OK) != 0) {
-		kr_log_error("[system] config '%s': %s\n", config, strerror(errno));
+	if (args.config && strcmp(args.config, "-") != 0 && access(args.config, R_OK) != 0) {
+		kr_log_error("[system] config '%s': %s\n", args.config, strerror(errno));
 		return EXIT_FAILURE;
 	}
-	if (!config && access("config", R_OK) == 0) {
-		config = "config";
+	if (!args.config && access("config", R_OK) == 0) {
+		args.config = "config";
 	}
 
 #ifndef CAN_FORK_EARLY
@@ -654,7 +661,7 @@ int main(int argc, char **argv)
 	fd_array_t ipc_set;
 	array_init(ipc_set);
 	/* Fork subprocesses if requested */
-	int fork_id = fork_workers(&ipc_set, forks);
+	int fork_id = fork_workers(&ipc_set, args.forks);
 	if (fork_id < 0) {
 		return EXIT_FAILURE;
 	}
@@ -673,54 +680,20 @@ int main(int argc, char **argv)
 		return EXIT_FAILURE;
 	}
 	/* Create worker */
-	struct worker_ctx *worker = worker_create(&engine, &pool, fork_id, forks);
+	struct worker_ctx *worker = worker_create(&engine, &pool, fork_id, args.forks);
 	if (!worker) {
 		kr_log_error("[system] not enough memory\n");
 		return EXIT_FAILURE;
 	}
-	/* Bind to passed fds and run */
-	for (size_t i = 0; i < fd_set.len; ++i) {
-		ret = network_listen_fd(&engine.net, fd_set.at[i], false);
-		if (ret != 0) {
-			kr_log_error("[system] listen on fd=%d %s\n", fd_set.at[i], kr_strerror(ret));
-			ret = EXIT_FAILURE;
-			break;
-		}
-	}
-	/* Do the same for TLS */
-	for (size_t i = 0; i < tls_fd_set.len; ++i) {
-		ret = network_listen_fd(&engine.net, tls_fd_set.at[i], true);
-		if (ret != 0) {
-			kr_log_error("[system] TLS listen on fd=%d %s\n", tls_fd_set.at[i], kr_strerror(ret));
-			ret = EXIT_FAILURE;
-			break;
-		}
-	}
-	/* Bind to sockets and run */
-	if (ret == 0) {
-		for (size_t i = 0; i < addr_set.len; ++i) {
-			int port = 53;
-			const char *addr = set_addr(addr_set.at[i], &port);
-			ret = network_listen(&engine.net, addr, (uint16_t)port, NET_UDP|NET_TCP);
-			if (ret != 0) {
-				kr_log_error("[system] bind to '%s@%d' %s\n", addr, port, kr_strerror(ret));
-				ret = EXIT_FAILURE;
-				break;
-			}
-		}
-	}
-	/* Bind to TLS sockets */
-	if (ret == 0) {
-		for (size_t i = 0; i < tls_set.len; ++i) {
-			int port = KR_DNS_TLS_PORT;
-			const char *addr = set_addr(tls_set.at[i], &port);
-			ret = network_listen(&engine.net, addr, (uint16_t)port, NET_TCP|NET_TLS);
-			if (ret != 0) {
-				kr_log_error("[system] bind to '%s@%d' (TLS) %s\n", addr, port, kr_strerror(ret));
-				ret = EXIT_FAILURE;
-				break;
-			}
-		}
+	
+	/* Bind to passed fds and sockets*/
+	if (bind_fds(&engine.net, &args.fd_set, false) != 0 ||
+	    bind_fds(&engine.net, &args.tls_fd_set, true) != 0 ||
+	    bind_sockets(&engine.net, &args.addr_set, false) != 0 ||
+	    bind_sockets(&engine.net, &args.tls_set, true) != 0
+	) {
+		ret = EXIT_FAILURE;
+		goto cleanup;
 	}
 
 	/* Workaround for https://github.com/libuv/libuv/issues/45
@@ -735,7 +708,7 @@ int main(int argc, char **argv)
 		goto cleanup;
 	}
 
-	engine_set_moduledir(&engine, moduledir);
+	engine_set_moduledir(&engine, args.moduledir);
 	
 	/* Block signals. */
 	uv_loop_t *loop = uv_default_loop();
@@ -748,23 +721,34 @@ int main(int argc, char **argv)
 	worker->loop = loop;
 	loop->data = worker;
 
-	ret = engine_start(&engine, config);
-	if (ret != 0) {
+	if (engine_load_sandbox(&engine) != 0) {
 		ret = EXIT_FAILURE;
 		goto cleanup;
 	}
-
-	if (keyfile) {
-		ret = init_keyfile(&engine, keyfile);
-		if (ret != 0) {
-			kr_log_error("[system] failed to initialized keyfile: %s\n", kr_strerror(ret));
+	if (args.config != NULL && strcmp(args.config, "-") != 0) {
+		if(engine_loadconf(&engine, args.config) != 0) {
 			ret = EXIT_FAILURE;
 			goto cleanup;
 		}
+		lua_settop(engine.L, 0);
+	}
+	if (args.keyfile != NULL && set_keyfile(&engine, args.keyfile, args.keyfile_unmanaged) != 0) {
+		ret = EXIT_FAILURE;
+		goto cleanup;
+	}
+	if (args.config == NULL || strcmp(args.config, "-") !=0) {
+		if(engine_load_defaults(&engine) != 0) {
+			ret = EXIT_FAILURE;
+			goto cleanup;
+		}
+	}
+	if (engine_start(&engine) != 0) {
+		ret = EXIT_FAILURE;
+		goto cleanup;
 	}
 
 	/* Run the event loop */
-	ret = run_worker(loop, &engine, &ipc_set, fork_id == 0, control_fd);
+	ret = run_worker(loop, &engine, &ipc_set, fork_id == 0, &args);
 	if (ret != 0) {
 		perror("[system] worker failed");
 		ret = EXIT_FAILURE;
@@ -775,8 +759,8 @@ int main(int argc, char **argv)
 	engine_deinit(&engine);
 	worker_reclaim(worker);
 	mp_delete(pool.ctx);
-	array_clear(addr_set);
-	array_clear(tls_set);
+	array_clear(args.addr_set);
+	array_clear(args.tls_set);
 	kr_crypto_cleanup();
 	return ret;
 }
diff --git a/doc/build.rst b/doc/build.rst
index 0032ce2bcdf76a3e46f12f014d632a74c2ce96c6..6ac99ba759adbf279b0370ac692c000012c117d4 100644
--- a/doc/build.rst
+++ b/doc/build.rst
@@ -197,10 +197,12 @@ All paths are prefixed with ``PREFIX`` variable by default if not specified othe
    "daemon",  "``SBINDIR``", "``$(PREFIX)/sbin``", ""
    "configuration", "``ETCDIR``", "``$(PREFIX)/etc/kresd``", "Configuration file, templates."
    "modules", "``MODULEDIR``", "``$(LIBDIR)/kdns_modules``", "Runtime directory for loading dynamic modules [#]_."
+   "trust anchor file", "``KEYFILE_DEFAULT``", "*(none)*", "Path to read-only trust anchor file, which is used as fallback when no other file is specified. [#]_"
    "work directory", "", "the current directory", "Run directory for daemon. (Only relevant during run time, not e.g. during installation.)"
 
 .. [#] The ``libkres.pc`` is installed in ``$(LIBDIR)/pkgconfig``.
 .. [#] The default moduledir can be changed with `-m` option to `kresd` daemon or by calling `moduledir()` function from lua.
+.. [#] If no other trust anchor is specified by user, the compiled-in path ``KEYFILE_DEFAULT`` must contain a valid trust anchor. This is typically used by distributions which provide DNSSEC root trust anchors as part of distribution package. Users can disable the built-in trust anchor by adding ``trust_anchors.keyfile_default = nil`` to their configuration.
 
 .. note:: Each module is self-contained and may install additional bundled files within ``$(MODULEDIR)/$(modulename)``. These files should be read-only, non-executable.
 
diff --git a/doc/kresd.8.in b/doc/kresd.8.in
index a1be6e24e0d81e0adebaec7e497549fb4d529d90..eb42bbdd06126822f570f8652e95f4b57d7529c7 100644
--- a/doc/kresd.8.in
+++ b/doc/kresd.8.in
@@ -24,6 +24,8 @@
 .IR config ]
 .RB [ \-k | \-\-keyfile
 .IR keyfile ]
+.RB [ \-K | \-\-keyfile-ro
+.IR keyfile ]
 .RB [ \-m | \-\-moduledir
 .IR path ]
 .RB [ \-f | \-\-forks
@@ -121,10 +123,20 @@ file at the default location (\fIconfig\fR). The syntax is
 described in \fIdaemon/README.md\fR.
 .TP
 .B \-k\fI keyfile\fR, \fB\-\-keyfile=\fI<keyfile>
-Use given for keeping root trust anchors. If the file doesn't exist, it will be
-automatically boostrapped from IANA and warning for you will be issued to check it
-before trusting it. The file contains DNSKEY/DS records in presentation format,
-and is compatible with Unbound or BIND9 root key files.
+(Recommended!) Automatically managed root trust anchors file.
+Root trust anchors in this file are managed using standard RFC 5011 (Automated Updates of DNS Security Trust Anchors).
+Kresd needs write access to the directory containing the keyfile.
+
+If the file does not exist, it will be automatically boostrapped from IANA using HTTPS protocol
+and warning that you need to to check the key before trusting it will be issued.
+
+The file contains DNSKEY/DS records in presentation format,
+and is compatible with Unbound and BIND 9 root key files.
+.TP
+.B \-K\fI keyfile\fR, \fB\-\-keyfile-ro=\fI<keyfile>
+(Discouraged) Static root trust anchors file. The file is not updated by kresd. Use of this option is discouraged because it will break your installation when the trust anchor key changes!
+
+Default: "@KEYFILE_DEFAULT@" (can be empty if your distribution did not provide one)
 .TP
 .B \-m\fI path\fR, \fB\-\-moduledir=\fI<path>
 Override the directory that is searched for modules.  Default: @MODULEDIR@
diff --git a/modules/hints/hints_test.lua b/modules/hints/tests/hints.test.lua
similarity index 85%
rename from modules/hints/hints_test.lua
rename to modules/hints/tests/hints.test.lua
index 2b8a1a73609ab70ecbdec0882f0a9254ab5e8a18..b60d2a364ef62236c182ebd6d2863c75eb52372d 100644
--- a/modules/hints/hints_test.lua
+++ b/modules/hints/tests/hints.test.lua
@@ -6,7 +6,7 @@ modules = { 'hints' }
 -- test for default configuration
 local function test_default()
 	-- get loaded root hints and change names to lowercase
-	hints_data = utils.table_keys_to_lower(hints.root())
+	local hints_data = utils.table_keys_to_lower(hints.root())
 
 	-- root hints loaded from default location
 	-- check correct ip address of a.root-server.net
@@ -16,11 +16,11 @@ end
 -- test loading from config file
 local function test_custom()
 	-- load custom root hints file with fake ip address for a.root-server.net
-	err_msg = hints.root_file(TEST_DIR .. 'hints_test.zone')
+	local err_msg = hints.root_file(TEST_DIR .. 'hints_test.zone')
 	same(err_msg, '', 'load root hints from file')
 
 	-- get loaded root hints and change names to lowercase
-	hints_data = utils.table_keys_to_lower(hints.root())
+	local hints_data = utils.table_keys_to_lower(hints.root())
 	isnt(hints_data['a.root-servers.net.'], nil, 'can retrieve root hints')
 
 	-- check loaded ip address of a.root-server.net
@@ -33,4 +33,4 @@ end
 return {
 	test_default,
 	test_custom
-}
\ No newline at end of file
+}
diff --git a/modules/hints/hints_test.zone b/modules/hints/tests/hints_test.zone
similarity index 100%
rename from modules/hints/hints_test.zone
rename to modules/hints/tests/hints_test.zone
diff --git a/modules/policy/policy_test.lua b/modules/policy/policy.test.lua
similarity index 100%
rename from modules/policy/policy_test.lua
rename to modules/policy/policy.test.lua
diff --git a/modules/predict/predict_test.lua b/modules/predict/tests/predict.test.lua
similarity index 100%
rename from modules/predict/predict_test.lua
rename to modules/predict/tests/predict.test.lua
diff --git a/tests/config/basic_test.lua b/tests/config/basic.test.lua
similarity index 100%
rename from tests/config/basic_test.lua
rename to tests/config/basic.test.lua
diff --git a/tests/config/cache_test.lua b/tests/config/cache.test.lua
similarity index 100%
rename from tests/config/cache_test.lua
rename to tests/config/cache.test.lua
diff --git a/tests/config/keyfile/bad_args.args b/tests/config/keyfile/bad_args.args
new file mode 100644
index 0000000000000000000000000000000000000000..6661ce0078691209a9a3fdc85730107bdfee3e94
--- /dev/null
+++ b/tests/config/keyfile/bad_args.args
@@ -0,0 +1 @@
+--keyfile-ro root.keys --keyfile root.keys
\ No newline at end of file
diff --git a/tests/config/keyfile/bad_args.returncode b/tests/config/keyfile/bad_args.returncode
new file mode 100644
index 0000000000000000000000000000000000000000..56a6051ca2b02b04ef92d5150c9ef600403cb1de
--- /dev/null
+++ b/tests/config/keyfile/bad_args.returncode
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/tests/config/keyfile/bad_args.test.lua b/tests/config/keyfile/bad_args.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/config/keyfile/load_ta.args b/tests/config/keyfile/load_ta.args
new file mode 100644
index 0000000000000000000000000000000000000000..2d1897de7882fc62dca4dca0620ab8e9aa9d1c47
--- /dev/null
+++ b/tests/config/keyfile/load_ta.args
@@ -0,0 +1 @@
+--keyfile-ro root2.keys
\ No newline at end of file
diff --git a/tests/config/keyfile/load_ta.test.lua b/tests/config/keyfile/load_ta.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..bfe851b7c6822c2f8033a9325a0b8f58912c3c9b
--- /dev/null
+++ b/tests/config/keyfile/load_ta.test.lua
@@ -0,0 +1,37 @@
+-- test fixtures
+
+-- count warning message, fail with other than allowed message
+warn_msg = {}
+overriding_msg="[ ta ] warning: overriding previously set trust anchors for ."
+warn_msg[overriding_msg] = 0
+function warn(fmt, ...)
+	msg = string.format(fmt, ...)
+	if warn_msg[msg] == nil then
+		fail(string.format("Not allowed warn message: %s", msg))
+	else
+		warn_msg[msg] = warn_msg[msg] + 1
+	end
+end
+
+-- tests
+
+boom(trust_anchors.add_file, {'nonwriteable/root.keys', false},
+     "Managed trust anchor in non-writeable directory")
+
+boom(trust_anchors.add_file, {'nonexist.keys', true},
+     "Nonexist unmanaged trust anchor file")
+
+trust_anchors.add_file('root2.keys', true)
+trust_anchors.add_file('root1.keys', true)
+is(warn_msg[overriding_msg], 1, "Warning message when override trust anchors")
+
+is(trust_anchors.keysets['\0'][1].key_tag, 19036,
+   "Loaded KeyTag from root1.keys")
+
+local function test_loading_from_cmdline()
+	is(trust_anchors.keysets['\0'][1].key_tag , 20326,
+	   "Loaded KeyTag from cmdline file root2.keys")
+	is(warn_msg[overriding_msg], 2, "Warning message when override trust anchors")
+end
+
+return {test_loading_from_cmdline}
diff --git a/tests/config/keyfile/nonexist_keyfile1.args b/tests/config/keyfile/nonexist_keyfile1.args
new file mode 100644
index 0000000000000000000000000000000000000000..6fead684f9c0739184bef3d6babb41a09dd29045
--- /dev/null
+++ b/tests/config/keyfile/nonexist_keyfile1.args
@@ -0,0 +1 @@
+--keyfile-ro nonexist
\ No newline at end of file
diff --git a/tests/config/keyfile/nonexist_keyfile1.returncode b/tests/config/keyfile/nonexist_keyfile1.returncode
new file mode 100644
index 0000000000000000000000000000000000000000..56a6051ca2b02b04ef92d5150c9ef600403cb1de
--- /dev/null
+++ b/tests/config/keyfile/nonexist_keyfile1.returncode
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/tests/config/keyfile/nonexist_keyfile1.test.lua b/tests/config/keyfile/nonexist_keyfile1.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..be73f32f460cee0f936a445729cb5bdc3162b914
--- /dev/null
+++ b/tests/config/keyfile/nonexist_keyfile1.test.lua
@@ -0,0 +1,2 @@
+-- simulate building without KEYFILE_DEFAULT
+trust_anchors.keyfile_default = nil
diff --git a/tests/config/keyfile/nonexist_keyfile2.args b/tests/config/keyfile/nonexist_keyfile2.args
new file mode 100644
index 0000000000000000000000000000000000000000..6fead684f9c0739184bef3d6babb41a09dd29045
--- /dev/null
+++ b/tests/config/keyfile/nonexist_keyfile2.args
@@ -0,0 +1 @@
+--keyfile-ro nonexist
\ No newline at end of file
diff --git a/tests/config/keyfile/nonexist_keyfile2.returncode b/tests/config/keyfile/nonexist_keyfile2.returncode
new file mode 100644
index 0000000000000000000000000000000000000000..56a6051ca2b02b04ef92d5150c9ef600403cb1de
--- /dev/null
+++ b/tests/config/keyfile/nonexist_keyfile2.returncode
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/tests/config/keyfile/nonexist_keyfile2.test.lua b/tests/config/keyfile/nonexist_keyfile2.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..bb2f98bf14b29837728075987332c8f7647c2b50
--- /dev/null
+++ b/tests/config/keyfile/nonexist_keyfile2.test.lua
@@ -0,0 +1,2 @@
+-- simulate building with KEYFILE_DEFAULT
+trust_anchors.keyfile_default = "root1.keys"
diff --git a/tests/config/keyfile/root1.keys b/tests/config/keyfile/root1.keys
new file mode 100644
index 0000000000000000000000000000000000000000..c7343371b9f01ac3ca7540d75044e02a56c86350
--- /dev/null
+++ b/tests/config/keyfile/root1.keys
@@ -0,0 +1 @@
+.                   	172800	DNSKEY	257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjFFVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoXbfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaDX6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpzW5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relSQageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulqQxA+Uk1ihz0= ; Valid: ; KeyTag:19036
diff --git a/tests/config/keyfile/root2.keys b/tests/config/keyfile/root2.keys
new file mode 100644
index 0000000000000000000000000000000000000000..5e9d6ac6e0e3964863dd4ea98280e1eb1fb5862e
--- /dev/null
+++ b/tests/config/keyfile/root2.keys
@@ -0,0 +1 @@
+.                   	172800	DNSKEY	257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU= ; Valid: ; KeyTag:20326
diff --git a/tests/config/runtest.sh b/tests/config/runtest.sh
index 48dad22e2fac474ab86419c881527d83e842db94..11d9b460b73e25679fdad11f25646fa8c5d7f7b5 100755
--- a/tests/config/runtest.sh
+++ b/tests/config/runtest.sh
@@ -1,6 +1,7 @@
 #!/bin/bash -e
-export SOURCE_PATH=$(cd "$(dirname "$0")" && pwd -P)
-export TEST_FILE=${2}
+export SOURCE_PATH="$(cd "$(dirname "$0")" && pwd -P)"
+export TEST_FILE="${2}"
+TEST_DIR="$(dirname $TEST_FILE)"
 export TMP_RUNDIR="$(mktemp -d)"
 export KRESD_NO_LISTEN=1
 function finish {
@@ -8,5 +9,15 @@ function finish {
 }
 trap finish EXIT
 
+
 echo "# $(basename ${TEST_FILE})"
-${DEBUGGER} ${1} -f 1 -c ${SOURCE_PATH}/test.cfg "${TMP_RUNDIR}"
\ No newline at end of file
+cp -a "${TEST_DIR}/"* "${TMP_RUNDIR}/"
+CMDLINE_ARGS="$(cat "${TEST_FILE%.test.lua}.args" 2>/dev/null || echo "")"
+EXPECTED_RETURNCODE="$(cat "${TEST_FILE%.test.lua}.returncode" 2>/dev/null || echo 0)"
+set +e
+${DEBUGGER} ${1} -f 1 -c ${SOURCE_PATH}/test.cfg $CMDLINE_ARGS "${TMP_RUNDIR}"
+RETCODE="$?"
+if [ "$RETCODE" -ne "$EXPECTED_RETURNCODE" ]; then
+	echo "Expected return code '$EXPECTED_RETURNCODE' got '$RETCODE'."
+fi
+test "$RETCODE" -eq "$EXPECTED_RETURNCODE"
diff --git a/tests/config/test_config.mk b/tests/config/test_config.mk
index 28a34bfb481d8dc07da58fedc86dd842b8c4f4b1..58d7d1987d7f988740ca0f52ec1584b05d5182fa 100644
--- a/tests/config/test_config.mk
+++ b/tests/config/test_config.mk
@@ -6,8 +6,10 @@
 # Check return code of kresd. Passed test have to call quit().
 
 tests_config := \
-	$(wildcard modules/*/*_test.lua) \
-	$(wildcard tests/config/*_test.lua)
+	$(wildcard modules/*/*.test.lua) \
+	$(wildcard modules/*/*/*.test.lua) \
+	$(wildcard tests/config/*.test.lua) \
+	$(wildcard tests/config/*/*.test.lua)
 
 define make_config_test
 $(1): check-install-precond
diff --git a/tests/config/test_utils.lua b/tests/config/test_utils.lua
index 3c2c63271e6f5a08f5cb909214e939276135fc76..edec11f713ab3d37b77436d32f56f953bb766d42 100644
--- a/tests/config/test_utils.lua
+++ b/tests/config/test_utils.lua
@@ -38,4 +38,4 @@ function M.not_contains(table, value, message)
 	return contains(fail, pass, table, value, message)
 end
 
-return M
\ No newline at end of file
+return M
diff --git a/tests/config/tests/keyfile/nonexist_keyfile1.args b/tests/config/tests/keyfile/nonexist_keyfile1.args
new file mode 100644
index 0000000000000000000000000000000000000000..6fead684f9c0739184bef3d6babb41a09dd29045
--- /dev/null
+++ b/tests/config/tests/keyfile/nonexist_keyfile1.args
@@ -0,0 +1 @@
+--keyfile-ro nonexist
\ No newline at end of file
diff --git a/tests/config/tests/keyfile/nonexist_keyfile1.returncode b/tests/config/tests/keyfile/nonexist_keyfile1.returncode
new file mode 100644
index 0000000000000000000000000000000000000000..56a6051ca2b02b04ef92d5150c9ef600403cb1de
--- /dev/null
+++ b/tests/config/tests/keyfile/nonexist_keyfile1.returncode
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/tests/config/tests/keyfile/nonexist_keyfile1.test.lua b/tests/config/tests/keyfile/nonexist_keyfile1.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..be73f32f460cee0f936a445729cb5bdc3162b914
--- /dev/null
+++ b/tests/config/tests/keyfile/nonexist_keyfile1.test.lua
@@ -0,0 +1,2 @@
+-- simulate building without KEYFILE_DEFAULT
+trust_anchors.keyfile_default = nil
diff --git a/tests/config/tests/keyfile/nonexist_keyfile2.args b/tests/config/tests/keyfile/nonexist_keyfile2.args
new file mode 100644
index 0000000000000000000000000000000000000000..6fead684f9c0739184bef3d6babb41a09dd29045
--- /dev/null
+++ b/tests/config/tests/keyfile/nonexist_keyfile2.args
@@ -0,0 +1 @@
+--keyfile-ro nonexist
\ No newline at end of file
diff --git a/tests/config/tests/keyfile/nonexist_keyfile2.returncode b/tests/config/tests/keyfile/nonexist_keyfile2.returncode
new file mode 100644
index 0000000000000000000000000000000000000000..56a6051ca2b02b04ef92d5150c9ef600403cb1de
--- /dev/null
+++ b/tests/config/tests/keyfile/nonexist_keyfile2.returncode
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/tests/config/tests/keyfile/nonexist_keyfile2.test.lua b/tests/config/tests/keyfile/nonexist_keyfile2.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..bb2f98bf14b29837728075987332c8f7647c2b50
--- /dev/null
+++ b/tests/config/tests/keyfile/nonexist_keyfile2.test.lua
@@ -0,0 +1,2 @@
+-- simulate building with KEYFILE_DEFAULT
+trust_anchors.keyfile_default = "root1.keys"