diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 43d3d432f0074c3c49b9f409e67d01192336c446..a2cc0f946aae537f3a31fa4ce93c935e0729157d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -302,7 +302,7 @@ root.hints:
     - scripts/update-root-hints.sh
 
 test:valgrind:
-  <<: *test
+  <<: *test_flaky  # lost block in /bin/bash during ta_update
   when: delayed
   start_in: '30 seconds'
   script:
diff --git a/.luacheckrc b/.luacheckrc
index 1729bd4b5ca18851f0d4c7d208438be09072cee7..885fa82205c52ec64a809f275423d5d325387a32 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -1,22 +1,22 @@
 std = 'luajit'
 new_read_globals = {
+	'cache',
+	'event',
 	'help',
-	'quit',
+	'_hint_root_file',
 	'hostname',
+	'map',
+	'modules',
+	'net',
 	'package_version',
-	'user',
-	'verbose',
+	'quit',
 	'resolve',
-	'tojson',
+	'ta_update',
 	'todname',
-	'map',
-	'net',
-	'cache',
-	'modules',
-	'trust_anchors',
+	'tojson',
+	'user',
+	'verbose',
 	'worker',
-	'event',
-	'_hint_root_file',
 	-- Sandbox declarations
 	'kB',
 	'MB',
@@ -31,7 +31,6 @@ new_read_globals = {
 	'warn',
 	'log',
 	'mode',
-	'trust_anchors',
 	'reorder_RR',
 	'option',
 	'env',
@@ -72,10 +71,10 @@ ignore = {
 }
 
 -- Sandbox can set global variables
-files['daemon/lua'].ignore = {'111', '121', '122'}
-files['daemon/lua/kres-gen.lua'].ignore = {'631'} -- Allow overly long lines
+files['**/daemon/lua'].ignore = {'111', '121', '122'}
+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', '121', '122'}
-files['daemon/**/*.test.lua'].ignore = {'111', '112', '113', '121', '122'}
+files['**/modules/**/*.test.lua'].ignore = {'111', '112', '113', '121', '122'}
+files['**/daemon/**/*.test.lua'].ignore = {'111', '112', '113', '121', '122'}
diff --git a/NEWS b/NEWS
index a82954d00a82058c31a55ddbc3c382e3dd214529..f6a02544163c83d90ea354c1f1f035a1f33f98a0 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,9 @@ Incompatible changes
 --------------------
 - see upgrading guide:
   https://knot-resolver.readthedocs.io/en/v4.0.0/upgrading.html#upgrade-from-3-to-4
+- configuration: trust_anchors aliases .file, .config() and .negative were removed (!788)
+- configuration: trust_anchors.keyfile_default is no longer accessible (!788)
+- daemon: -k/--keyfile and -K/--keyfile-ro options were removed
 - meson build system is now used for builds (!771)
 - build with embedded LMBD is no longer supported
 - default modules dir location has changed
@@ -33,6 +36,7 @@ Bugfixes
 - policy.RPZ: log problems from zone-file level of parser as well (#453)
 - fix flushing of messages to logs in some cases (!781)
 - fix fallback when SERVFAIL or REFUSED is received from upstream (!784)
+- fix crash when dealing with unknown TA key algorhitm (#449)
 
 Module API changes
 ------------------
diff --git a/ci/respdiff/kresd.config b/ci/respdiff/kresd.config
index c733601a06794ccf30eb0292e4ed3732ac69d083..fb5a33d29c0be2963cdb5f2d0b92600c9038af9c 100644
--- a/ci/respdiff/kresd.config
+++ b/ci/respdiff/kresd.config
@@ -5,7 +5,7 @@ net.listen('127.0.0.1', 8853, { tls = true })
 net.ipv6=false
 
 -- Auto-maintain root TA
-trust_anchors.file = '.local/etc/knot-resolver/root.keys'
+trust_anchors.add_file('.local/etc/knot-resolver/root.keys')
 
 -- Large cache size, so we don't need to flush often
 -- This can be larger than available RAM, least frequently accessed
diff --git a/daemon/README.rst b/daemon/README.rst
index 2bcd35bd92c4fa0e7399b7d5c45d6404dfbcc9b2..3d76095bd2a4dfc0cb99be9bec71a5e09bbc23ef 100644
--- a/daemon/README.rst
+++ b/daemon/README.rst
@@ -305,7 +305,7 @@ Environment
          net = { '127.0.0.1', '::1' }
          -- unprivileged
          cache.size = 100*MB
-         trust_anchors.file = 'root.key'
+         trust_anchors.add_file('root.key')
 
    Example output:
 
@@ -391,14 +391,14 @@ add the following snippet to your configuration file.
 .. code-block:: lua
 
    -- turns off DNSSEC validation
-   trust_anchors.keyfile_default = nil
+   trust_anchors.remove('.')
 
 The resolver supports DNSSEC including :rfc:`5011` automated DNSSEC TA updates
 and :rfc:`7646` negative trust anchors.  Depending on your distribution, DNSSEC
 trust anchors should be either maintained in accordance with the distro-wide
 policy, or automatically maintained by the resolver itself.
 
-.. function:: trust_anchors.add_file(keyfile, readonly)
+.. function:: trust_anchors.add_file(keyfile[, readonly = false])
 
    :param string keyfile: path to the file.
    :param readonly: if true, do not attempt to update the file.
@@ -421,21 +421,22 @@ policy, or automatically maintained by the resolver itself.
 
       [ ta ] key: 19036 state: Valid
 
-.. function:: trust_anchors.config(keyfile, readonly)
+.. function:: trust_anchors.remove(zonename)
 
-   Alias for `add_file`.  It is also equivalent to CLI parameter ``-k <keyfile>``
-   and ``trust_anchors.file = keyfile``.
+   Remove specified trust anchor from trusted key set. Removing trust anchor for the root zone effectivelly disables DNSSEC validation (unless you configured another trust anchor).
 
-.. envvar:: trust_anchors.keyfile_default = keyfile_default
+   .. code-block:: lua
+
+      > trust_anchors.remove('.')
+      true
 
-   Set by ``keyfile_default`` option during compilation. This can be explicitly
-   set to ``nil`` to disable DNSSEC validation.
+   If you want to disable DNSSEC validation for a particular domain but keep it enabled for the rest of DNS tree, use :func:`trust_anchors.set_insecure`.
 
 .. envvar:: trust_anchors.hold_down_time = 30 * day
 
    :return: int (default: 30 * day)
 
-   Modify RFC5011 hold-down timer to given value. Example: ``30 * sec``
+   Modify RFC5011 hold-down timer to given value. Intended only for testing purposes. Example: ``30 * sec``
 
 .. envvar:: trust_anchors.refresh_time = nil
 
@@ -443,6 +444,7 @@ policy, or automatically maintained by the resolver itself.
 
    Modify RFC5011 refresh timer to given value (not set by default), this will force trust anchors
    to be updated every N seconds periodically instead of relying on RFC5011 logic and TTLs.
+   Intended only for testing purposes.
    Example: ``10 * sec``
 
 .. envvar:: trust_anchors.keep_removed = 0
@@ -457,16 +459,15 @@ policy, or automatically maintained by the resolver itself.
 
    :param table nta_list: List of domain names (text format) representing NTAs.
 
-   When you use a domain name as an NTA, DNSSEC validation will be turned off at/below these names.
+   When you use a domain name as an *negative trust anchor* (NTA), DNSSEC validation will be turned off at/below these names.
    Each function call replaces the previous NTA set. You can find the current active set in ``trust_anchors.insecure`` variable.
-
-   .. tip:: Use the `trust_anchors.negative = {}` alias for easier configuration.
+   If you want to disable DNSSEC validation completely use :func:`trust_anchors.remove` function instead.
 
    Example output:
 
    .. code-block:: lua
 
-      > trust_anchors.negative = { 'bad.boy', 'example.com' }
+      > trust_anchors.set_insecure({ 'bad.boy', 'example.com' })
       > trust_anchors.insecure
       [1] => bad.boy
       [2] => example.com
@@ -481,6 +482,8 @@ policy, or automatically maintained by the resolver itself.
    Inserts DS/DNSKEY record(s) into current keyset. These will not be managed or updated, use it only for testing
    or if you have a specific use case for not using a keyfile.
 
+   .. note:: Static keys are very error-prone and should not be used in production. Use :func:`trust_anchors.add_file` instead.
+
    Example output:
 
    .. code-block:: lua
@@ -631,7 +634,7 @@ Example:
 
 	$ kresd-query.lua www.sub.nic.cz 'assert(kres.dname2str(req:resolved().zone_cut.name) == "nic.cz.")' && echo "yes"
 	yes
-	$ kresd-query.lua -C 'trust_anchors.config("root.keys")' nic.cz 'assert(req:resolved().flags.DNSSEC_WANT)'
+	$ kresd-query.lua -C 'trust_anchors.add_file("root.keys")' nic.cz 'assert(req:resolved().flags.DNSSEC_WANT)'
 	$ echo $?
 	0
 
diff --git a/daemon/bindings/impl.c b/daemon/bindings/impl.c
index 2db2895d2ffa3499385941bd40121d4f4f559ff1..21a8d7876eea140287fa30b8999fcd189b614e5a 100644
--- a/daemon/bindings/impl.c
+++ b/daemon/bindings/impl.c
@@ -71,17 +71,17 @@ void kr_bindings_register(lua_State *L)
 
 void lua_error_p(lua_State *L, const char *fmt, ...)
 {
+	/* Add a stack trace and throw the result as a lua error. */
+	luaL_traceback(L, L, "error occured here (config filename:lineno is at the bottom, if config is involved):", 0);
 	/* Push formatted custom message, prepended with "ERROR: ". */
-	lua_pushliteral(L, "ERROR: ");
+	lua_pushliteral(L, "\nERROR: ");
 	{
 		va_list args;
 		va_start(args, fmt);
 		lua_pushvfstring(L, fmt, args);
 		va_end(args);
 	}
-	lua_concat(L, 2);
-	/* Add a stack trace and throw the result as a lua error. */
-	luaL_traceback(L, L, lua_tostring(L, -1), 0);
+	lua_concat(L, 3);
 	lua_error(L);
 	/* TODO: we might construct a little more friendly trace by using luaL_where().
 	 * In particular, in case the error happens in a function that was called
diff --git a/daemon/cache.test/clear.test.lua b/daemon/cache.test/clear.test.lua
index e7e96fd81c8ae37cdc4bd828bd701799efae9647..ed13186dcd29e7dc0162be0d421da328f6510971 100644
--- a/daemon/cache.test/clear.test.lua
+++ b/daemon/cache.test/clear.test.lua
@@ -32,7 +32,7 @@ ev = event.after(0, function () return 1 end)
 
 
 -- Import fake root zone; avoid interference with configured keyfile_default.
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
 trust_anchors.add('. IN DS 48409 8 2 3D63A0C25BCE86621DE63636F11B35B908EFE8E9381E0E3E9DEFD89EA952C27D')
 
 local function check_answer(desc, qname, qtype, expected_rcode)
diff --git a/daemon/lua/config.lua.in b/daemon/lua/config.lua
similarity index 75%
rename from daemon/lua/config.lua.in
rename to daemon/lua/config.lua
index 26f756a5c8a495b8bf4cf57f518868c04242d982..66e9c35e88f6bc1854ebebffd48d9b5023a184cf 100644
--- a/daemon/lua/config.lua.in
+++ b/daemon/lua/config.lua
@@ -31,13 +31,3 @@ end
 if require('ffi').C.kr_zonecut_is_empty(kres.context().root_hints) 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, @unmanaged@)
-	else
-		panic("cannot open default trust anchor file:'%s'",
-		      trust_anchors.keyfile_default
-		)
-	end
-end
diff --git a/daemon/lua/meson.build b/daemon/lua/meson.build
index d11bf7731a4f94d210f87faa7d0306ce3fc08bc5..b2e51e20aa85614941e0871e6583b173c18d5388 100644
--- a/daemon/lua/meson.build
+++ b/daemon/lua/meson.build
@@ -15,9 +15,10 @@ trust_anchors = configure_file(
   output: 'trust_anchors.lua',
   configuration: ta_config,
 )
-config_lua = configure_file(
-  input: 'config.lua.in',
-  output: 'config.lua',
+
+sandbox = configure_file(
+  input: 'sandbox.lua.in',
+  output: 'sandbox.lua',
   configuration: ta_config,
 )
 
@@ -27,10 +28,10 @@ run_target(  # run manually to re-generate kres-gen.lua
 )
 
 lua_src = [
-  config_lua,
+  files('config.lua'),
   files('kres.lua'),
   files('kres-gen.lua'),
-  files('sandbox.lua'),
+  sandbox,
   trust_anchors,
   files('zonefile.lua'),
 ]
diff --git a/daemon/lua/sandbox.lua b/daemon/lua/sandbox.lua.in
similarity index 97%
rename from daemon/lua/sandbox.lua
rename to daemon/lua/sandbox.lua.in
index 017e3a39936ce61df81ec218c6f80b77ca869e25..562d1d7dcdc47006c558774b3fdb8335d24d071d 100644
--- a/daemon/lua/sandbox.lua
+++ b/daemon/lua/sandbox.lua.in
@@ -1,3 +1,4 @@
+local debug = require('debug')
 local ffi = require('ffi')
 
 -- Units
@@ -14,7 +15,9 @@ day = 24 * hour
 
 -- Logging
 function panic(fmt, ...)
-        error(string.format('error: '..fmt, ...))
+        print(debug.traceback('error occured here (config filename:lineno is '
+                .. 'at the bottom, if config is involved):', 2))
+        error(string.format('ERROR: '.. fmt, ...), 0)
 end
 function warn(fmt, ...)
         io.stderr:write(string.format(fmt..'\n', ...))
@@ -316,6 +319,7 @@ end
 
 -- Load embedded modules
 trust_anchors = require('trust_anchors')
+modules.load('ta_update')
 modules.load('ta_signal_query')
 modules.load('policy')
 modules.load('priming')
@@ -324,6 +328,9 @@ modules.load('detect_time_jump')
 modules.load('ta_sentinel')
 modules.load('edns_keepalive')
 
+-- Load keyfile_default
+trust_anchors.add_file('@keyfile_default@', @unmanaged@)
+
 -- Interactive command evaluation
 function eval_cmd(line, raw)
 	-- Compatibility sandbox code loading
diff --git a/daemon/lua/trust_anchors.lua.in b/daemon/lua/trust_anchors.lua.in
index 2497fbcb8762c92b8841c26246bfbb52d9ea3b0f..331f15b4ff468f9f2e929019b5c56bcc4c7b0a07 100644
--- a/daemon/lua/trust_anchors.lua.in
+++ b/daemon/lua/trust_anchors.lua.in
@@ -5,6 +5,23 @@ local C = ffi.C
 
 local trust_anchors -- the public pseudo-module, exported as global variable
 
+-- RFC5011 state table
+local key_state = {
+	Start = 'Start', AddPend = 'AddPend', Valid = 'Valid',
+	Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed'
+}
+
+local function upgrade_required(msg)
+	if msg then
+		msg = msg .. '\n'
+	else
+		msg = ''
+	end
+	panic('Configuration upgrade required: ' .. msg .. 'Please refer to ' ..
+		'https://knot-resolver.readthedocs.io/en/stable/upgrading.html')
+end
+
+-- TODO: Move bootstrap to a separate module or even its own binary
 -- Fetch over HTTPS with peert cert checked
 local function https_fetch(url, ca)
 	local ssl_ok, https = pcall(require, 'ssl.https')
@@ -14,11 +31,11 @@ local function https_fetch(url, ca)
 	end
 	local resp = {}
 	local r, c = https.request{
-	       url = url,
-	       cafile = ca,
-	       verify = {'peer', 'fail_if_no_peer_cert' },
-	       protocol = 'tlsv1_2',
-	       sink = ltn12.sink.table(resp),
+		url = url,
+		cafile = ca,
+		verify = {'peer', 'fail_if_no_peer_cert' },
+		protocol = 'tlsv1_2',
+		sink = ltn12.sink.table(resp),
 	}
 	if r == nil then return r, c end
 	return resp[1]
@@ -43,8 +60,8 @@ local function keydigest_is_valid(valid_from, valid_until)
 	local err = ffi.C.kr_strptime_diff(
 		format, time_now, time2utc(valid_from), time_diff)
 	if (err ~= nil) then
-	       error(string.format('failed to process "validFrom" constraint: %s',
-				   ffi.string(err)))
+		error(string.format('failed to process "validFrom" constraint: %s',
+			ffi.string(err)))
 	end
 	local from_ok = time_diff[0] > 0
 
@@ -55,7 +72,7 @@ local function keydigest_is_valid(valid_from, valid_until)
 			format, time_now, time2utc(valid_until), time_diff)
 		if (err ~= nil) then
 			error(string.format('failed to process "validUntil" constraint: %s',
-					    ffi.string(err)))
+				ffi.string(err)))
 		end
 		until_ok = time_diff[0] < 0
 	end
@@ -73,19 +90,19 @@ local function parse_xml_keydigest(attrs, inside, output)
 	local valid_attrs = {id = true, validFrom = true, validUntil = true}
 	for key, _ in pairs(fields) do
 		assert(valid_attrs[key],
-		       string.format('unsupported KeyDigest attribute "%s" found in "%s"',
-				     key, attrs))
+			string.format('unsupported KeyDigest attribute "%s" found in "%s"',
+				key, attrs))
 	end
 
 	_, n = string.gsub(inside, "<([%w]+).->([^<]+)</[%w]+>", function (k, v) fields[k] = v end)
 	assert(n >= 1,
 		string.format('error parsing KeyDigest XML elements from "%s"',
-			      inside))
+			inside))
 	local mandatory_elements = {'KeyTag', 'Algorithm', 'DigestType', 'Digest'}
 	for _, key in ipairs(mandatory_elements) do
 		assert(fields[key],
 			string.format('mandatory element %s is missing in "%s"',
-				      key, inside))
+				key, inside))
 	end
 	assert(n == 4, string.format('found %d elements but expected 4 in %s', n, inside))
 	table.insert(output, fields)  -- append to list of parsed keydigests
@@ -101,7 +118,7 @@ local function generate_ds(keydigests)
 			rrset = rrset .. '\n' .. rr
 		else
 			log('[ ta ] skipping trust anchor "%s" ' ..
-			    'because it is outside of validity range', rr)
+				'because it is outside of validity range', rr)
 		end
 	end
 	return rrset
@@ -113,8 +130,8 @@ local function assert_str_match(str, pattern, expected)
 		count = count + 1
 	end
 	assert(count == expected,
-	       string.format('expected %d occurences of "%s" but got %d in "%s"',
-			     expected, pattern, count, str))
+		string.format('expected %d occurences of "%s" but got %d in "%s"',
+			expected, pattern, count, str))
 end
 
 -- Fetch root anchors in XML over HTTPS, returning a zone-file-style string
@@ -145,181 +162,21 @@ local function bootstrap(url, ca)
 		return false, string.format('[ ta ] no valid trust anchors found at "%s"', url)
 	end
 	local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n'
-			 .. '       You SHOULD verify them manually against original source:\n'
-			 .. '       https://www.iana.org/dnssec/files\n'
-			 .. '[ ta ] Current root trust anchors are:'
-			 .. rrset
+			.. '       You SHOULD verify them manually against original source:\n'
+			.. '       https://www.iana.org/dnssec/files\n'
+			.. '[ ta ] Bootstrapped root trust anchors are:'
+			.. rrset
 	return rrset, msg
 end
 
--- RFC5011 state table
-local key_state = {
-	Start = 'Start', AddPend = 'AddPend', Valid = 'Valid',
-	Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed'
-}
-
--- Find key in current keyset
-local function ta_find(keyset, rr)
-	local rr_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
-	assert(rr_tag >= 0 and rr_tag <= 65535, string.format('invalid RR: %s: %s',
-	       kres.rr2str(rr), ffi.string(C.knot_strerror(rr_tag))))
-	for i, ta in ipairs(keyset) do
-		-- Match key owner and content
-		local ta_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
-		assert(ta_tag >= 0 and ta_tag <= 65535, string.format('invalid RR: %s: %s',
-		       kres.rr2str(ta), ffi.string(C.knot_strerror(ta_tag))))
-		if ta.owner == rr.owner then
-			if ta.type == rr.type then
-				if rr.type == kres.type.DNSKEY then
-					if C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then
-						return ta
-					end
-				elseif rr.type == kres.type.DS and ta.rdata == rr.rdata then
-					return ta
-				end
-			-- DNSKEY superseding DS, inexact match
-			elseif rr.type == kres.type.DNSKEY and ta.type == kres.type.DS then
-				if ta.key_tag == rr_tag then
-					keyset[i] = rr -- Replace current DS
-					rr.state = ta.state
-					rr.key_tag = ta.key_tag
-					return rr
-				end
-			-- DS key matching DNSKEY, inexact match
-			elseif rr.type == kres.type.DS and ta.type == kres.type.DNSKEY then
-				if rr_tag == ta_tag then
-					return ta
-				end
-			end
-		end
-	end
-	return nil
-end
-
--- Evaluate TA status of a RR according to RFC5011.  The time is in seconds.
-local function ta_present(keyset, rr, hold_down_time, force_valid)
-	if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then
-		return false -- Ignore
-	end
-	-- Find the key in current key set and check its status
-	local now = os.time()
-	local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata)
-	local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
-	assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s',
-	       kres.rr2str(rr), ffi.string(C.knot_strerror(key_tag))))
-	local ta = ta_find(keyset, rr)
-	if ta then
-		-- Key reappears (KeyPres)
-		if ta.state == key_state.Missing then
-			ta.state = key_state.Valid
-			ta.timer = nil
-		end
-		-- Key is revoked (RevBit)
-		if ta.state == key_state.Valid or ta.state == key_state.Missing then
-			if key_revoked then
-				ta.state = key_state.Revoked
-				ta.timer = now + hold_down_time
-			end
-		end
-		-- Remove hold-down timer expires (RemTime)
-		if ta.state == key_state.Revoked and os.difftime(ta.timer, now) <= 0 then
-			ta.state = key_state.Removed
-			ta.timer = nil
-		end
-		-- Add hold-down timer expires (AddTime)
-		if ta.state == key_state.AddPend and os.difftime(ta.timer, now) <= 0 then
-			ta.state = key_state.Valid
-			ta.timer = nil
-		end
-		if rr.state ~= key_state.Valid or verbose() then
-			log('[ ta ] key: ' .. key_tag .. ' state: '..ta.state)
-		end
-		return true
-	elseif not key_revoked then -- First time seen (NewKey)
-		rr.key_tag = key_tag
-		if force_valid then
-			rr.state = key_state.Valid
-		else
-			rr.state = key_state.AddPend
-			rr.timer = now + hold_down_time
-		end
-		if rr.state ~= key_state.Valid or verbose() then
-			log('[ ta ] key: ' .. key_tag .. ' state: '..rr.state)
-		end
-		table.insert(keyset, rr)
-		return true
-	end
-	return false
-end
-
--- TA is missing in the new key set.  The time is in seconds.
-local function ta_missing(ta, hold_down_time)
-	-- Key is removed (KeyRem)
-	local keep_ta = true
-	local key_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
-	assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s',
-	       kres.rr2str(ta), ffi.string(C.knot_strerror(key_tag))))
-	if ta.state == key_state.Valid then
-		ta.state = key_state.Missing
-		ta.timer = os.time() + hold_down_time
-
-	-- Remove key that is missing for too long
-	elseif ta.state == key_state.Missing and os.difftime(ta.timer, os.time()) <= 0 then
-		ta.state = key_state.Removed
-		log('[ ta ] key: '..key_tag..' removed because missing for too long')
-		keep_ta = false
-
-	-- Purge pending key
-	elseif ta.state == key_state.AddPend then
-		log('[ ta ] key: '..key_tag..' purging')
-		keep_ta = false
-	end
-	log('[ ta ] key: '..key_tag..' state: '..ta.state)
-	return keep_ta
-end
-
-local active_refresh, update -- forwards
-
--- Plan an event for refreshing the root DNSKEYs and re-scheduling itself
-local function refresh_plan(keyset, delay, is_initial)
-	local owner_str = kres.dname2str(keyset.owner) -- maybe fix converting back and forth?
-	keyset.refresh_ev = event.after(delay, function ()
-		resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE',
-		function (pkt)
-			-- Schedule itself with updated timeout
-			local delay_new = active_refresh(keyset, kres.pkt_t(pkt), is_initial)
-			delay_new = keyset.refresh_time or trust_anchors.refresh_time or delay_new
-			log('[ ta ] next refresh for ' .. owner_str .. ' in '
-					.. delay_new/hour .. ' hours')
-			refresh_plan(keyset, delay_new)
-		end)
-	end)
-end
-
--- Refresh the DNSKEYs from the packet, and return time to the next check.
-active_refresh = function (keyset, pkt, is_initial)
-	local retry = true
-	if pkt:rcode() == kres.rcode.NOERROR then
-		local records = pkt:section(kres.section.ANSWER)
-		local new_keys = {}
-		for _, rr in ipairs(records) do
-			if rr.type == kres.type.DNSKEY then
-				table.insert(new_keys, rr)
-			end
-		end
-		update(keyset, new_keys, is_initial)
-		retry = false
-	else
-		warn('[ ta ] active refresh failed for ' .. kres.dname2str(keyset.owner)
-				.. ' with rcode: ' .. pkt:rcode())
-	end
-	-- Calculate refresh/retry timer (RFC 5011, 2.3)
-	local min_ttl = retry and day or 15 * day
-	for _, rr in ipairs(keyset) do -- 10 or 50% of the original TTL
-		min_ttl = math.min(min_ttl, (retry and 100 or 500) * rr.ttl)
-	end
-	return math.max(hour, min_ttl)
+local function bootstrap_write(rrstr, filename)
+	local fname_tmp = filename .. '.lock.' .. tostring(worker.pid);
+	local file = assert(io.open(fname_tmp, 'w'))
+	file:write(rrstr)
+	file:close()
+	assert(os.rename(fname_tmp, filename))
 end
+-- Bootstrap end
 
 -- Update ta.comment and return decorated line representing the RR
 -- This is meant to be in zone-file format.
@@ -335,7 +192,9 @@ end
 
 -- Write keyset to a file.  States and timers are stored in comments.
 local function keyset_write(keyset)
-	if not keyset.filename then return false end -- not to be persisted
+	if not keyset.managed then  -- not to be persistent, this is an error!
+		panic('internal error: keyset_write called for an unmanaged TA')
+	end
 	local fname_tmp = keyset.filename .. '.lock.' .. tostring(worker.pid);
 	local file = assert(io.open(fname_tmp, 'w'))
 	for i = 1, #keyset do
@@ -428,7 +287,7 @@ local function keyset_read(path, str)
 	for _, ta in ipairs(tas) do
 		if ta.owner ~= owner then
 			return nil, string.format("do not mix %s and %s TAs in single file/string",
-							kres.dname2str(ta.owner), kres.dname2str(owner))
+				kres.dname2str(ta.owner), kres.dname2str(owner))
 		end
 	end
 	tas.owner = owner
@@ -456,100 +315,41 @@ local function keyset_publish(keyset)
 	end
 	if count == 0 then
 		warn('[ ta ] ERROR: no anchors are trusted for ' ..
-		     kres.dname2str(keyset.owner) .. ' !')
+			kres.dname2str(keyset.owner) .. ' !')
 	end
 	return count > 0 and not has_error
 end
 
-
--- Update existing keyset; return true if successful.
--- Param `is_initial` (bool): force .NewKey states to .Valid, i.e. init empty keyset.
-update = function (keyset, new_keys, is_initial)
-	if not new_keys then return false end
-
-	-- Filter TAs to be purged from the keyset (KeyRem), in three steps
-	-- 1: copy TAs to be kept to `keepset`
-	local hold_down = (keyset.hold_down_time or trust_anchors.hold_down_time) / 1000
-	local keepset = {}
-	local keep_removed = keyset.keep_removed or trust_anchors.keep_removed
-	for _, ta in ipairs(keyset) do
-		local keep = true
-		if not ta_find(new_keys, ta) then
-			-- Ad-hoc: RFC 5011 doesn't mention removing a Missing key.
-			-- Let's do it after a very long period has elapsed.
-			keep = ta_missing(ta, hold_down * 4)
-		end
-		-- Purge removed keys
-		if ta.state == key_state.Removed then
-			if keep_removed > 0 then
-				keep_removed = keep_removed - 1
-			else
-				keep = false
-			end
+local function add_file(path, unmanaged)
+	local managed = not unmanaged
+	if managed then
+		if not ta_update then
+			panic('[ ta ] automatic update for ' .. path .. ' requested, '
+				.. 'but required module ta_update is not loaded')
 		end
-		if keep then
-			table.insert(keepset, ta)
-		end
-	end
-	-- 2: remove all TAs - other settings etc. will remain in the keyset
-	for i, _ in ipairs(keyset) do
-		keyset[i] = nil
-	end
-	-- 3: move TAs to be kept into the keyset (same indices)
-	for k, ta in pairs(keepset) do
-		keyset[k] = ta
-	end
-
-	-- Evaluate new TAs
-	for _, rr in ipairs(new_keys) do
-		if (rr.type == kres.type.DNSKEY or rr.type == kres.type.DS) and rr.rdata ~= nil then
-			ta_present(keyset, rr, hold_down, is_initial)
-		end
-	end
-
-	-- Store the keyset
-	keyset_write(keyset)
-
-	-- Start using the new TAs.
-	if not keyset_publish(keyset) then
-		-- TODO: try to rebootstrap if for root?
-		return false
-	elseif verbose() then
-		log('[ ta ] refreshed trust anchors for domain ' .. kres.dname2str(keyset.owner) .. ' are:\n'
-		    .. trust_anchors.summary(keyset.owner))
-	end
-
-	return true
-end
-
-local add_file = function (path, unmanaged)
-	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
+	-- Bootstrap TA for root zone if keyfile doesn't exist
+	if managed and not io.open(path, 'r') then
+		if trust_anchors.keysets['\0'] then
+			error(string.format(
+				"[ ta ] keyfile '%s' doesn't exist and root key is already installed, "
+				.. "cannot bootstrap; provide a path to valid file with keys", path))
+		end
 		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
+		local rrstr, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca)
+		if not rrstr then
 			msg = msg .. '\n'
 				.. '[ ta ] Failed to bootstrap root trust anchors!'
 			error(msg)
 		end
 		print(msg)
-		trust_anchors.add(tas)
-		-- Fetch DNSKEY immediately
-		local keyset = trust_anchors.keysets['\0']
-		keyset.filename = path
-		if keyset.refresh_ev then event.cancel(keyset.refresh_ev) end
-		refresh_plan(keyset, 0, true)
-		return
-	end
-	if not unmanaged and path == (trust_anchors.keysets['\0'] or {}).filename then
-		return
+		bootstrap_write(rrstr, path)
+		-- continue as if the keyfile was there
 	end
 
 	-- Parse the file and check its sanity
@@ -557,25 +357,45 @@ local add_file = function (path, unmanaged)
 	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
+	keyset.filename = path
+	keyset.managed = managed
 
 	local owner = keyset.owner
 	local owner_str = kres.dname2str(owner)
-	if trust_anchors.keysets[owner] then
+	local keyset_orig = trust_anchors.keysets[owner]
+	if keyset_orig then
 		warn('[ ta ] warning: overriding previously set trust anchors for ' .. owner_str)
-		local refresh_ev = trust_anchors.keysets[owner].refresh_ev
-		if refresh_ev then event.cancel(refresh_ev) end
+		if keyset_orig.managed and ta_update then
+			ta_update.stop(owner)
+		end
 	end
 	trust_anchors.keysets[owner] = keyset
 
 	-- Replace the TA store used for validation
 	if keyset_publish(keyset) and verbose() then
 		log('[ ta ] installed trust anchors for domain ' .. owner_str .. ' are:\n'
-		    .. trust_anchors.summary(owner))
+			.. trust_anchors.summary(owner))
 	end
 	-- TODO: if failed and for root, try to rebootstrap?
 
-	if not unmanaged then refresh_plan(keyset, 10 * sec, false) end
+	if managed then
+		ta_update.start(owner)
+	end
+end
+
+local function remove(zname)
+	local owner = kres.str2dname(zname)
+	if not trust_anchors.keysets[owner] then
+		return false
+	end
+
+	if ta_update then
+		ta_update.stop(owner)
+	end
+	trust_anchors.keysets[owner] = nil
+	local store = kres.context().trust_anchors
+	C.kr_ta_del(store, owner)
+	return true
 end
 
 local function ta_str(owner)
@@ -609,25 +429,24 @@ trust_anchors = {
 	--   - owner - that dname (for simplicity)
 	--   - [optional] filename in which to persist the state,
 	--     implying unmanaged TA if nil
-	--   - [optional] overrides for global defaults of
-	--     hold_down_time, refresh_time, keep_removed
 	-- The RR tables also contain some additional TA-specific fields.
 	keysets = {},
 
 	-- Documented properties:
 	insecure = {},
-	hold_down_time = 30 * day,
-	refresh_time = nil,
-	keep_removed = 0,
 
 	bootstrap_url = 'https://data.iana.org/root-anchors/root-anchors.xml',
 	bootstrap_ca = '@etc_dir@/icann-ca.pem',
-	keyfile_default = '@keyfile_default@',
 
 	-- Load keys from a file, 5011-managed by default.
 	-- If managed and the file doesn't exist, try bootstrapping the root into it.
 	add_file = add_file,
-	config = add_file,
+	config = function() upgrade_required('trust_anchors.config was removed, use trust_anchors.add_file()') end,
+	remove = remove,
+
+	keyset_publish = keyset_publish,
+	keyset_write = keyset_write,
+	key_state = key_state,
 
 	-- Add DS/DNSKEY record(s) (unmanaged)
 	add = function (keystr)
@@ -638,15 +457,15 @@ trust_anchors = {
 			local keyset_orig = trust_anchors.keysets[owner]
 			-- Set up trust_anchors.keysets[owner]
 			if keyset_orig then
+				if keyset_orig.managed then
+					panic('[ ta ] it is impossible to add an unmanaged TA for zone '
+						.. owner_str .. ' which already has a managed TA')
+				end
 				warn('[ ta ] warning: extending previously set trust anchors for '
 						.. owner_str)
 				for _, ta in ipairs(keyset) do
 					table.insert(keyset_orig, ta)
 				end
-				-- we might also add more warning if it's managed, i.e. has .filename,
-				-- as the next update would overwrite this additional TA
-			else
-				trust_anchors.keysets[owner] = keyset
 			end
 			-- Replace the TA store used for validation
 			if not keyset_publish(keyset) then
@@ -654,6 +473,9 @@ trust_anchors = {
 				-- trust_anchors.keysets[owner] was already updated to the
 				-- (partially) failing state, but I'm not sure how much to improve this
 			end
+			keyset.managed = false
+			trust_anchors.keysets[owner] = keyset
+
 		end
 		if verbose() or err then log('New TA state:\n' .. trust_anchors.summary()) end
 		if err then
@@ -703,11 +525,15 @@ trust_anchors = {
 
 -- Syntactic sugar for TA store
 setmetatable(trust_anchors, {
-	__newindex = function (t,k,v)
-	if     k == 'file' then t.config(v)
-	elseif k == 'negative' then t.set_insecure(v)
-	else   rawset(t, k, v) end
-	end,
+       __newindex = function (t,k,v)
+       if     k == 'file' then
+	       upgrade_required('trust_anchors.file was removed, use trust_anchors.add_file()')
+       elseif k == 'negative' then
+	       upgrade_required('trust_anchors.negative was removed, use trust_anchors.set_insecure()')
+       elseif k == 'keyfile_default' then
+	       upgrade_required('trust_anchors.keyfile_default is now compiled in, see trust_anchors.remove()')
+       else   rawset(t, k, v) end
+       end,
 })
 
 return trust_anchors
diff --git a/daemon/lua/trust_anchors.test/bootstrap.test.lua b/daemon/lua/trust_anchors.test/bootstrap.test.lua
index ea35c487eb11972872040a0943fe95c02edf90cd..8ea20a8ab4273fffba45fc90162a63ab78d1092d 100644
--- a/daemon/lua/trust_anchors.test/bootstrap.test.lua
+++ b/daemon/lua/trust_anchors.test/bootstrap.test.lua
@@ -60,7 +60,7 @@ end
 
 local host = 'https://localhost:8080/'
 -- avoid interference with configured keyfile_default
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
 
 local function test_err_cert()
 	trust_anchors.bootstrap_ca = 'x509/wrongca.pem'
@@ -84,6 +84,7 @@ end
 local function test_ok_xml(testname, testdesc)
 	return function()
 		trust_anchors.bootstrap_url = host .. testname .. '.xml'
+		trust_anchors.remove('.')
 		same(trust_anchors.add_file(testname .. '.keys'), nil, testdesc)
 	end
 end
diff --git a/daemon/lua/trust_anchors.test/root.keys b/daemon/lua/trust_anchors.test/root.keys
new file mode 100644
index 0000000000000000000000000000000000000000..e292b5a7bf0cc4afbefdee17c56b10edbd2126f3
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/root.keys
@@ -0,0 +1 @@
+. IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
diff --git a/daemon/lua/trust_anchors.test/ta.test.lua b/daemon/lua/trust_anchors.test/ta.test.lua
index 97da60da53cacaa048b7853a0e1199926b0d843a..2071152d50ba7cf4cf6a0f5f855d55d2eb017e70 100644
--- a/daemon/lua/trust_anchors.test/ta.test.lua
+++ b/daemon/lua/trust_anchors.test/ta.test.lua
@@ -1,6 +1,18 @@
+trust_anchors.remove('.')
 
 local ffi = require('ffi')
 
+-- count warning messages
+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
+		warn_msg[msg] = warn_msg[msg] + 1
+	end
+end
+
 -- Test that adding a revoked DNSKEY is refused.
 local function test_revoked_key()
 	local ta_c = kres.context().trust_anchors
@@ -21,8 +33,40 @@ local function test_revoked_key()
 	same(root_ta.rrs.count, 1, 'the root TA set contains one RR')
 end
 
+local function test_remove()
+	-- uses root key from the previous test
+	assert(trust_anchors.keysets['\0'], 'root key must be there from previous test')
+	local ta_c = kres.context().trust_anchors
+	local root_ta = ffi.C.kr_ta_get(ta_c, '\0')
+	assert(root_ta ~= nil, 'we got non-NULL TA RRset')
+	assert(root_ta.rrs.count, 1, 'we have a root TA set to be deleted')
+
+	trust_anchors.remove('.')
+
+	same(trust_anchors.keysets['\0'], nil, 'Lua interface does not have the removed key')
+	root_ta = ffi.C.kr_ta_get(ta_c, '\0')
+	same(root_ta == nil, true, 'C interface does not have the removed key')
+end
+
+local function test_add_file()
+	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")
+
+	is(warn_msg[overriding_msg], 0, "No override warning messages at start of test")
+	trust_anchors.add_file('root.keys', true)
+	trust_anchors.add_file('root.keys', true)
+	is(warn_msg[overriding_msg], 1, "Warning message when override trust anchors")
+
+	is(trust_anchors.keysets['\0'][1].key_tag, 20326,
+	   "Loaded KeyTag from root.keys")
+end
 
 return {
-	test_revoked_key()
+	test_revoked_key,
+	test_remove,
+	test_add_file,
 }
 
diff --git a/daemon/main.c b/daemon/main.c
index 4f94c1e180c7873abe2fa39cb9bd4deafdd11e81..aee9d378ffe62bc65e0fcaef5b95942482aa755c 100644
--- a/daemon/main.c
+++ b/daemon/main.c
@@ -58,8 +58,6 @@ struct args {
 	addr_array_t tls_set;
 	fd_array_t fd_set;
 	fd_array_t tls_fd_set;
-	char *keyfile;
-	int keyfile_unmanaged;
 	const char *config;
 	int control_fd;
 	const char *rundir;
@@ -68,16 +66,6 @@ struct args {
 	bool tty_binary_output;
 };
 
-/* lua_pcall helper function */
-static inline char *lua_strerror(int lua_err) {
-	switch (lua_err) {
-	case LUA_ERRRUN: return "a runtime error";
-	case LUA_ERRMEM: return "memory allocation error.";
-	case LUA_ERRERR: return "error while running the error handler function.";
-	default: return "a unknown error";
-	}
-}
-
 /**
  * TTY control: process input and free() the buffer.
  *
@@ -384,8 +372,6 @@ 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 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"
 	       " -f, --forks=N          Start N forks sharing the configuration.\n"
 	       " -q, --quiet            No command prompt in interactive mode.\n"
 	       " -v, --verbose          Run in verbose mode."
@@ -483,31 +469,6 @@ static void free_sd_socket_names(char **socket_names, int count)
 }
 #endif
 
-static int set_keyfile(struct engine *engine, char *keyfile, bool unmanaged)
-{
-	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\n", lua_tostring(engine->L, -1));
-		} else {
-			kr_log_error("[ ta ] keyfile '%s': failed to load (%s)\n",
-					keyfile, lua_strerror(lua_ret));
-		}
-		return lua_ret;
-	}
-
-	lua_settop(engine->L, 0);
-	return kr_ok();
-}
-
-
 static void args_init(struct args *args)
 {
 	memset(args, 0, sizeof(struct args));
@@ -542,8 +503,6 @@ static int parse_args(int argc, char **argv, struct args *args)
 		{"fd",         required_argument, 0, 'S'},
 		{"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'},
 		{"verbose",          no_argument, 0, 'v'},
 		{"quiet",            no_argument, 0, 'q'},
@@ -578,15 +537,6 @@ static int parse_args(int argc, char **argv, struct args *args)
 				return EXIT_FAILURE;
 			}
 			break;
-		case 'K':
-			args->keyfile_unmanaged = 1;
-		case 'k':
-			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 'v':
 			kr_verbose_set(true);
 #ifdef NOVERBOSELOG
@@ -800,10 +750,6 @@ int main(int argc, char **argv)
 		}
 		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;
diff --git a/doc/kresd.8.in b/doc/kresd.8.in
index 524c7eacb8cce449b8bac958eb860529fdb92b5c..8bd87fc9e4fb9898c2760d92d35007733e841ad8 100644
--- a/doc/kresd.8.in
+++ b/doc/kresd.8.in
@@ -22,10 +22,6 @@
 .IR fd ]
 .RB [ \-c | \-\-config
 .IR config ]
-.RB [ \-k | \-\-keyfile
-.IR keyfile ]
-.RB [ \-K | \-\-keyfile\-ro
-.IR keyfile ]
 .RB [ \-f | \-\-forks
 .IR N ]
 .RB [ \-q | \-\-quiet ]
@@ -69,7 +65,7 @@ and start
 .PP
 .nf
 .RS 6n
-$ kresd -a 127.0.0.1 -k root.keys
+$ kresd -a 127.0.0.1
 [system] interactive mode
 >
 .RE
@@ -90,7 +86,7 @@ online documentation.
 $ cat << EOF > config
 modules = { 'policy' }
 policy.add(policy.all(policy.FORWARD('192.168.1.1')))
-$ kresd -a 127.0.0.1 -k root.keys
+$ kresd -a 127.0.0.1
 EOF
 .RE
 .fi
@@ -120,22 +116,6 @@ Set the config file with settings for kresd to read instead of reading the
 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>
-(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.
-@man_managed_keyfile_default@
-.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!
-@man_unmanaged_keyfile_default@
-.TP
 .B \-f\fI N\fR, \fB\-\-forks=\fI<N>
 With this option, the daemon is started in non-interactive mode and instead creates a
 UNIX socket in \fIrundir\fR that the operator can connect to for interactive session.
diff --git a/doc/meson.build b/doc/meson.build
index 8c912829e558d70a79e16a28712a5f790506ea7c..1860427895e84af8085759393c6420e4a7bc3018 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -4,7 +4,6 @@
 man_config = configuration_data()
 man_config.set('version', meson.project_version())
 man_config.set('date', run_command('../scripts/get-date.sh').stdout())
-man_config.set('keyfile_default', keyfile_default)
 
 man_config.set('man_seealso_systemd', '')
 if systemd_files == 'enabled'
@@ -13,16 +12,6 @@ elif systemd_files == 'nosocket'
   man_config.set('man_seealso_systemd', '\\fIkresd.systemd.nosocket(7)\\fR, ')
 endif
 
-man_config.set('man_managed_keyfile_default', '')
-man_config.set('man_unmanaged_keyfile_default', '')
-if managed_ta
-  man_config.set('man_managed_keyfile_default', '''
-Default: "@0@"'''.format(keyfile_default))
-else
-  man_config.set('man_unmanaged_keyfile_default', '''
-Default: "@0@"'''.format(keyfile_default))
-endif
-
 man_kresd = configure_file(
   input: 'kresd.8.in',
   output: 'kresd.8',
diff --git a/doc/upgrading.rst b/doc/upgrading.rst
index 1ea5e19904dfbf161d141a804a05c42c4d367b33..013d41c0f696f4abb520c1eab1679fce30fc99df 100644
--- a/doc/upgrading.rst
+++ b/doc/upgrading.rst
@@ -15,10 +15,34 @@ Users
 
 * DNSSEC validation is now turned on by default. If you need to disable it, see
   :ref:`dnssec-config`.
+* ``-k/--keyfile`` and ``-K/--keyfile-ro`` daemon options were removed. If needed,
+  use ``trust_anchors.add_file()`` in configuration file instead.
 * In case you are using your own custom modules, move them to the new module
   location. The exact location depends on your distribution. Generally, modules previously
   in ``/usr/lib/kdns_modules`` should be moved to ``/usr/lib/knot-resolver/kres_modules``.
 
+Configuration
+~~~~~~~~~~~~~
+
+* ``trust_anchors.file``, ``trust_anchors.config()`` and ``trust_anchors.negative``
+  aliases were removed to avoid duplicity
+
+  .. csv-table::
+     :header: "3.x configuration", "4.x configuration"
+
+     "``trust_anchors.file = path``", "``trust_anchors.add_file(path)``"
+     "``trust_anchors.config(path, readonly)``", "``trust_anchors.add_file(path, readonly)``"
+     "``trust_anchors.negative = nta_set``", "``trust_anchors.set_insecure(nta_set)``"
+
+* ``trust_anchors.keyfile_default`` is no longer accessible and is only possible to set
+  at compile time. To turn off DNSSEC, use ``trust_anchors.remove('.')``.
+
+  .. csv-table::
+     :header: "3.x configuration", "4.x configuration"
+
+     "``trust_anchors.keyfile_default = nil``", "``trust_anchors.remove('.')``"
+
+
 Packagers & Developers
 ----------------------
 
diff --git a/etc/config/config.cluster.in b/etc/config/config.cluster.in
index 709116b04bd3731eacd0b5757e3f431a6035a3df..6984aa0920e80870bdb67b0a45ba6296718283d0 100644
--- a/etc/config/config.cluster.in
+++ b/etc/config/config.cluster.in
@@ -7,7 +7,7 @@
 @config_defaults@
 
 -- To disable DNSSEC validation, uncomment the following line (not recommended)
--- trust_anchors.keyfile_default = nil
+-- trust_anchors.remove('.')
 
 -- Large cache size, so we don't need to flush ever
 -- This can be larger than available RAM, least frequently accessed
diff --git a/etc/config/config.docker.in b/etc/config/config.docker.in
index 4e284af760a334b2cb73da9d3de85a56badcfcfb..1a54f1638adbf28ffb2fdcfbae0afd0def9542d0 100644
--- a/etc/config/config.docker.in
+++ b/etc/config/config.docker.in
@@ -6,7 +6,7 @@ net.listen('0.0.0.0')
 net.listen('0.0.0.0', 853, {tls=true})
 
 -- To disable DNSSEC validation, uncomment the following line (not recommended)
--- trust_anchors.keyfile_default = nil
+-- trust_anchors.remove('.')
 
 -- Load Useful modules
 modules = {
diff --git a/etc/config/config.isp.in b/etc/config/config.isp.in
index 30ddbbe00ec81bac2c3a4f357f4162c300a5e0da..4c29db19aa312d373d61dd5e362fcd7b52978098 100644
--- a/etc/config/config.isp.in
+++ b/etc/config/config.isp.in
@@ -4,7 +4,7 @@
 @config_defaults@
 
 -- To disable DNSSEC validation, uncomment the following line (not recommended)
--- trust_anchors.keyfile_default = nil
+-- trust_anchors.remove('.')
 
 -- Large cache size, so we don't need to flush often
 -- This can be larger than available RAM, least frequently accessed
diff --git a/etc/config/config.personal.in b/etc/config/config.personal.in
index 555edc3590aea005f87458571ea2ef499c4d84b0..6d9844e020fec97b41fcf7f6567b1093be8bf2be 100644
--- a/etc/config/config.personal.in
+++ b/etc/config/config.personal.in
@@ -3,7 +3,7 @@
 @config_defaults@
 
 -- To disable DNSSEC validation, uncomment the following line (not recommended)
--- trust_anchors.keyfile_default = nil
+-- trust_anchors.remove('.')
 
 -- Load useful modules
 modules = {
diff --git a/etc/config/config.splitview.in b/etc/config/config.splitview.in
index ae009ac77402f761201a8d4d9fdf01419ef2e048..60da665d79c4b8ce7684f9e91b14e8f19d65c3cc 100644
--- a/etc/config/config.splitview.in
+++ b/etc/config/config.splitview.in
@@ -4,7 +4,7 @@
 @config_defaults@
 
 -- To disable DNSSEC validation, uncomment the following line (not recommended)
--- trust_anchors.keyfile_default = nil
+-- trust_anchors.remove('.')
 
 -- Load Useful modules
 modules = {
diff --git a/meson.build b/meson.build
index 8cb43d0c85eaffc650b7eb7a36405c257bb5e3fe..73fb06cba87a5addfdaa84e8b8e158521ef718f3 100644
--- a/meson.build
+++ b/meson.build
@@ -108,7 +108,7 @@ unit_tests = [
 
 config_tests = [
   # [name, files(test)]  # or
-  # [name, files(test), [arg1, arg2], should_fail]
+  # [name, files(test), [extra_suites]]
 ]
 
 integr_tests = [
diff --git a/modules/bogus_log/test.integr/kresd_config.j2 b/modules/bogus_log/test.integr/kresd_config.j2
index bf4b1e85f4d542238e212d7c64a11c2068452917..061c841709ab223c7811e14d95ad21ac775ae89f 100644
--- a/modules/bogus_log/test.integr/kresd_config.j2
+++ b/modules/bogus_log/test.integr/kresd_config.j2
@@ -34,6 +34,11 @@ function reply_result(state, req)
 end
 policy.add(policy.pattern(reply_result, 'bogus_log.test.'))
 
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
+
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
         modules.unload('ta_signal_query')
diff --git a/modules/meson.build b/modules/meson.build
index 0c3d7fb0dfad059c7653b097dd30cec593bbf16b..5df7fedd8ca0419ae5936139ea822b084baeffac 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -14,6 +14,7 @@ lua_mod_src = [  # add lua modules without separate meson.build
   files('serve_stale/serve_stale.lua'),
   files('ta_sentinel/ta_sentinel.lua'),
   files('ta_signal_query/ta_signal_query.lua'),
+  files('ta_update/ta_update.lua'),
   files('workarounds/workarounds.lua'),
 ]
 
@@ -22,12 +23,16 @@ config_tests += [
   ['hints', files('hints/tests/hints.test.lua')],
   ['nsid', files('nsid/nsid.test.lua')],
   ['dns64', files('dns64/dns64.test.lua')],
+  ['ta_update', files('ta_update/ta_update.test.lua')],
 ]
 
 integr_tests += [
   ['bogus_log', join_paths(meson.current_source_dir(), 'bogus_log', 'test.integr')],
   ['rebinding', join_paths(meson.current_source_dir(), 'rebinding', 'test.integr')],
   ['serve_stale', join_paths(meson.current_source_dir(), 'serve_stale', 'test.integr')],
+  # NOTE: ta_update may pass in cases when it should fail due to race conditions
+  # To ensure reliability, deckard should introduce a time wait
+  ['ta_update', join_paths(meson.current_source_dir(), 'ta_update', 'ta_update.test.integr')],
 ]
 
 
diff --git a/modules/policy/noipv6.test.integr/kresd_config.j2 b/modules/policy/noipv6.test.integr/kresd_config.j2
index 5648d81eed750332f8b5a2190545f9e92d30a8f9..93099f37e2bf94a359640b433d25219f1b8dab9e 100644
--- a/modules/policy/noipv6.test.integr/kresd_config.j2
+++ b/modules/policy/noipv6.test.integr/kresd_config.j2
@@ -3,7 +3,12 @@ net.ipv6 = false
 policy.add(policy.all(policy.STUB({ '::1:2:3:4', '1.2.3.4' })))
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
 
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
diff --git a/modules/policy/noipvx.test.integr/kresd_config.j2 b/modules/policy/noipvx.test.integr/kresd_config.j2
index 9ee7afd2fb53b6bc7fee31c11a95574fcccf77cc..ce97967970a42fe29449b7b634790b9800eb64fa 100644
--- a/modules/policy/noipvx.test.integr/kresd_config.j2
+++ b/modules/policy/noipvx.test.integr/kresd_config.j2
@@ -4,7 +4,12 @@ net.ipv6 = false
 policy.add(policy.all(policy.STUB({ '::1:2:3:4', '1.2.3.4' })))
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
 
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
diff --git a/modules/policy/test.integr/kresd_config.j2 b/modules/policy/test.integr/kresd_config.j2
index 5ad54fea4c11df895cfc4f9e618cda3b10c86f42..3225c537918968fdd1b110900ebaa298b2314d63 100644
--- a/modules/policy/test.integr/kresd_config.j2
+++ b/modules/policy/test.integr/kresd_config.j2
@@ -2,7 +2,12 @@
 policy.add(policy.suffix(policy.REFUSE, {todname('refuse.example.com')}))
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
 
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
diff --git a/modules/rebinding/test.integr/kresd_config.j2 b/modules/rebinding/test.integr/kresd_config.j2
index 39bc957bde556f2fa24bdd7eb92aa3f1a9a3f7e9..e044351f9872a21be13c71d0ee9ad41b46ffa2a1 100644
--- a/modules/rebinding/test.integr/kresd_config.j2
+++ b/modules/rebinding/test.integr/kresd_config.j2
@@ -1,6 +1,11 @@
 {% raw %}
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
 
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
diff --git a/modules/serve_stale/test.integr/kresd_config.j2 b/modules/serve_stale/test.integr/kresd_config.j2
index b9a242c22ff73729ad333ba8f777a0657a63417d..5beff9e3f7c621ed0dc985f08fe825d56ad5ef0e 100644
--- a/modules/serve_stale/test.integr/kresd_config.j2
+++ b/modules/serve_stale/test.integr/kresd_config.j2
@@ -2,7 +2,12 @@
 modules = { 'serve_stale < cache' }
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
 
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
diff --git a/modules/stats/test.integr/kresd_config.j2 b/modules/stats/test.integr/kresd_config.j2
index b8fd49816581a077ba673a06426a42787620231b..a93ece40b3c139e66add0ec1c230a9dbda982c51 100644
--- a/modules/stats/test.integr/kresd_config.j2
+++ b/modules/stats/test.integr/kresd_config.j2
@@ -52,7 +52,12 @@ policy.add(policy.pattern(reply_result, 'stats.test.'))
 policy.add(policy.all(FWD_TARGET)) -- avoid iteration
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
 
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
diff --git a/modules/ta_update/root.keys b/modules/ta_update/root.keys
new file mode 100644
index 0000000000000000000000000000000000000000..e292b5a7bf0cc4afbefdee17c56b10edbd2126f3
--- /dev/null
+++ b/modules/ta_update/root.keys
@@ -0,0 +1 @@
+. IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
diff --git a/modules/ta_update/ta_update.lua b/modules/ta_update/ta_update.lua
new file mode 100644
index 0000000000000000000000000000000000000000..4c0b47624fb188e4991407b423512f0d4cb30509
--- /dev/null
+++ b/modules/ta_update/ta_update.lua
@@ -0,0 +1,287 @@
+-- Module interface
+local ffi = require('ffi')
+local kres = require('kres')
+local C = ffi.C
+
+assert(trust_anchors, 'ta_update module depends on initialized trust_anchors library')
+local key_state = trust_anchors.key_state
+assert(key_state)
+
+local ta_update = {}
+local tracked_tas = {}  -- zone name (wire) => {event = number}
+
+
+-- Find key in current keyset
+local function ta_find(keyset, rr)
+	local rr_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
+	if rr_tag < 0 or rr_tag > 65535 then
+		warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s',
+			kres.rr2str(rr), ffi.string(C.knot_strerror(rr_tag))))
+		return nil
+	end
+	for i, ta in ipairs(keyset) do
+		-- Match key owner and content
+		local ta_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
+		if ta_tag < 0 or ta_tag > 65535 then
+			warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s',
+				kres.rr2str(ta), ffi.string(C.knot_strerror(ta_tag))))
+		else
+			if ta.owner == rr.owner then
+				if ta.type == rr.type then
+					if rr.type == kres.type.DNSKEY then
+						if C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then
+							return ta
+						end
+					elseif rr.type == kres.type.DS and ta.rdata == rr.rdata then
+						return ta
+					end
+				-- DNSKEY superseding DS, inexact match
+				elseif rr.type == kres.type.DNSKEY and ta.type == kres.type.DS then
+					if ta.key_tag == rr_tag then
+						keyset[i] = rr -- Replace current DS
+						rr.state = ta.state
+						rr.key_tag = ta.key_tag
+						return rr
+					end
+				-- DS key matching DNSKEY, inexact match
+				elseif rr.type == kres.type.DS and ta.type == kres.type.DNSKEY then
+					if rr_tag == ta_tag then
+						return ta
+					end
+				end
+			end
+		end
+	end
+	return nil
+end
+
+-- Evaluate TA status of a RR according to RFC5011.  The time is in seconds.
+local function ta_present(keyset, rr, hold_down_time)
+if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then
+	return false -- Ignore
+end
+-- Attempt to extract key_tag
+local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
+if key_tag < 0 or key_tag > 65535 then
+	warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s',
+		kres.rr2str(rr), ffi.string(C.knot_strerror(key_tag))))
+	return false
+end
+-- Find the key in current key set and check its status
+local now = os.time()
+local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata)
+local ta = ta_find(keyset, rr)
+if ta then
+	-- Key reappears (KeyPres)
+	if ta.state == key_state.Missing then
+		ta.state = key_state.Valid
+		ta.timer = nil
+	end
+	-- Key is revoked (RevBit)
+	if ta.state == key_state.Valid or ta.state == key_state.Missing then
+		if key_revoked then
+			ta.state = key_state.Revoked
+			ta.timer = now + hold_down_time
+		end
+	end
+	-- Remove hold-down timer expires (RemTime)
+	if ta.state == key_state.Revoked and os.difftime(ta.timer, now) <= 0 then
+		ta.state = key_state.Removed
+		ta.timer = nil
+	end
+	-- Add hold-down timer expires (AddTime)
+	if ta.state == key_state.AddPend and os.difftime(ta.timer, now) <= 0 then
+		ta.state = key_state.Valid
+		ta.timer = nil
+	end
+	if rr.state ~= key_state.Valid or verbose() then
+		log('[ta_update] key: ' .. key_tag .. ' state: '..ta.state)
+	end
+	return true
+elseif not key_revoked then -- First time seen (NewKey)
+	rr.key_tag = key_tag
+	return false
+end
+end
+
+-- TA is missing in the new key set.  The time is in seconds.
+local function ta_missing(ta, hold_down_time)
+	-- Key is removed (KeyRem)
+	local keep_ta = true
+	local key_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
+	if key_tag < 0 or key_tag > 65535 then
+		warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s',
+			kres.rr2str(ta), ffi.string(C.knot_strerror(key_tag))))
+		key_tag = ''
+	end
+	if ta.state == key_state.Valid then
+		ta.state = key_state.Missing
+		ta.timer = os.time() + hold_down_time
+
+	-- Remove key that is missing for too long
+	elseif ta.state == key_state.Missing and os.difftime(ta.timer, os.time()) <= 0 then
+		ta.state = key_state.Removed
+		log('[ta_update] key: '..key_tag..' removed because missing for too long')
+		keep_ta = false
+
+	-- Purge pending key
+	elseif ta.state == key_state.AddPend then
+		log('[ta_update] key: '..key_tag..' purging')
+		keep_ta = false
+	end
+	log('[ta_update] key: '..key_tag..' state: '..ta.state)
+	return keep_ta
+end
+
+-- Update existing keyset; return true if successful.
+local function update(keyset, new_keys)
+	if not new_keys then return false end
+	if not keyset.managed then
+		-- this may happen due to race condition during testing in CI (refesh time < query time)
+		return false
+	end
+
+	-- Filter TAs to be purged from the keyset (KeyRem), in three steps
+	-- 1: copy TAs to be kept to `keepset`
+	local hold_down = (keyset.hold_down_time or ta_update.hold_down_time) / 1000
+	local keepset = {}
+	local keep_removed = keyset.keep_removed or ta_update.keep_removed
+	for _, ta in ipairs(keyset) do
+		local keep = true
+		if not ta_find(new_keys, ta) then
+			-- Ad-hoc: RFC 5011 doesn't mention removing a Missing key.
+			-- Let's do it after a very long period has elapsed.
+			keep = ta_missing(ta, hold_down * 4)
+		end
+		-- Purge removed keys
+		if ta.state == key_state.Removed then
+			if keep_removed > 0 then
+				keep_removed = keep_removed - 1
+			else
+				keep = false
+			end
+		end
+		if keep then
+			table.insert(keepset, ta)
+		end
+	end
+	-- 2: remove all TAs - other settings etc. will remain in the keyset
+	for i, _ in ipairs(keyset) do
+		keyset[i] = nil
+	end
+	-- 3: move TAs to be kept into the keyset (same indices)
+	for k, ta in pairs(keepset) do
+		keyset[k] = ta
+	end
+
+	-- Evaluate new TAs
+	for _, rr in ipairs(new_keys) do
+		if (rr.type == kres.type.DNSKEY or rr.type == kres.type.DS) and rr.rdata ~= nil then
+			ta_present(keyset, rr, hold_down)
+		end
+	end
+
+	-- Store the keyset
+	trust_anchors.keyset_write(keyset)
+
+	-- Start using the new TAs.
+	if not trust_anchors.keyset_publish(keyset) then
+		-- TODO: try to rebootstrap if for root?
+		return false
+	elseif verbose() then
+		log('[ta_update] refreshed trust anchors for domain ' .. kres.dname2str(keyset.owner) .. ' are:\n'
+			.. trust_anchors.summary(keyset.owner))
+	end
+
+	return true
+end
+
+-- Refresh the DNSKEYs from the packet, and return time to the next check.
+local function active_refresh(keyset, pkt)
+	local retry = true
+	if pkt:rcode() == kres.rcode.NOERROR then
+		local records = pkt:section(kres.section.ANSWER)
+		local new_keys = {}
+		for _, rr in ipairs(records) do
+			if rr.type == kres.type.DNSKEY then
+				table.insert(new_keys, rr)
+			end
+		end
+		update(keyset, new_keys)
+		retry = false
+	else
+		warn('[ta_update] active refresh failed for ' .. kres.dname2str(keyset.owner)
+			.. ' with rcode: ' .. pkt:rcode())
+	end
+	-- Calculate refresh/retry timer (RFC 5011, 2.3)
+	local min_ttl = retry and day or 15 * day
+	for _, rr in ipairs(keyset) do -- 10 or 50% of the original TTL
+		min_ttl = math.min(min_ttl, (retry and 100 or 500) * rr.ttl)
+	end
+	return math.max(hour, min_ttl)
+end
+
+-- Plan an event for refreshing DNSKEYs and re-scheduling itself
+local function refresh_plan(keyset, delay)
+	local owner = keyset.owner
+	local owner_str = kres.dname2str(keyset.owner)
+	if not tracked_tas[owner] then
+		tracked_tas[owner] = {}
+	end
+	local track_cfg = tracked_tas[owner]
+	if track_cfg.event then  -- restart timer if necessary
+		event.cancel(track_cfg.event)
+	end
+	track_cfg.event = event.after(delay, function ()
+		log('[ta_update] refreshing TA for ' .. owner_str)
+		resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE',
+		function (pkt)
+			-- Schedule itself with updated timeout
+			local delay_new = active_refresh(keyset, kres.pkt_t(pkt))
+			delay_new = keyset.refresh_time or ta_update.refresh_time or delay_new
+			log('[ta_update] next refresh for ' .. owner_str .. ' in '
+				.. delay_new/hour .. ' hours')
+			refresh_plan(keyset, delay_new)
+		end)
+	end)
+end
+
+ta_update = {
+	-- [optional] overrides for global defaults of
+	-- hold_down_time, refresh_time, keep_removed
+	hold_down_time = 30 * day,
+	refresh_time = nil,
+	keep_removed = 0,
+	tracked = tracked_tas, -- debug and visibility, should not be changed by hand
+}
+
+-- start tracking (already loaded) TA with given zone name in wire format
+-- do first refresh immediatelly
+function ta_update.start(zname)
+	local keyset = trust_anchors.keysets[zname]
+	if not keyset then
+		panic('[ta_update] TA must be configured first before tracking it')
+	end
+	if not keyset.managed then
+		panic('[ta_update] TA is configured as unmanaged; remove it and '
+			.. 'add it again as managed using trust_anchors.add_file()')
+	end
+	refresh_plan(keyset, 0)
+end
+
+function ta_update.stop(zname)
+	if tracked_tas[zname] then
+		event.cancel(tracked_tas[zname].event)
+		tracked_tas[zname] = nil
+		trust_anchors.keysets[zname].managed = false
+	end
+end
+
+-- stop all timers
+function ta_update.deinit()
+	for zname, _ in pairs(tracked_tas) do
+		ta_update.stop(zname)
+	end
+end
+
+return ta_update
diff --git a/modules/ta_update/ta_update.test.integr/deckard.yaml b/modules/ta_update/ta_update.test.integr/deckard.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6906eeb1b977f9b50faaac68244766d135035e27
--- /dev/null
+++ b/modules/ta_update/ta_update.test.integr/deckard.yaml
@@ -0,0 +1,12 @@
+programs:
+- name: kresd
+  binary: kresd
+  additional:
+    - -f
+    - "1"
+  templates:
+    - modules/ta_update/ta_update.test.integr/kresd_config.j2
+    - tests/integration/hints_zone.j2
+  configs:
+    - config
+    - hints
diff --git a/tests/integration/kresd_config.j2 b/modules/ta_update/ta_update.test.integr/kresd_config.j2
similarity index 63%
rename from tests/integration/kresd_config.j2
rename to modules/ta_update/ta_update.test.integr/kresd_config.j2
index d7f4cfb663887b674127b0c41b282257447d5359..e1b03a5c8f66308eeecc75e50364e0fc947cfa87 100644
--- a/tests/integration/kresd_config.j2
+++ b/modules/ta_update/ta_update.test.integr/kresd_config.j2
@@ -1,11 +1,30 @@
-net = { '{{SELF_ADDR}}' }
--- hints.root({['k.root-servers.net'] = '{{ROOT_ADDR}}'})
-_hint_root_file('hints')
+{% for TAF in TRUST_ANCHOR_FILES %}
+trust_anchors.add_file('{{TAF}}')
+{% endfor %}
+
+{% raw %}
+
+-- Disable RFC8145 signaling, scenario doesn't provide expected answers
+if ta_signal_query then
+        modules.unload('ta_signal_query')
+end
 
--- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+-- Disable RFC8109 priming, scenario doesn't provide expected answers
+if priming then
+        modules.unload('priming')
+end
 
+-- Disable this module because it make one priming query
+if detect_time_skew then
+        modules.unload('detect_time_skew')
+end
+
+_hint_root_file('hints')
 cache.size = 2*MB
+verbose(true)
+{% endraw %}
+
+net = { '{{SELF_ADDR}}' }
 
 
 {% if QMIN == "false" %}
@@ -14,29 +33,6 @@ option('NO_MINIMIZE', true)
 option('NO_MINIMIZE', false)
 {% endif %}
 
-{% if DO_NOT_QUERY_LOCALHOST == "false" %}
-option('ALLOW_LOCAL', true)
-{% else %}
-option('ALLOW_LOCAL', false)
-{% endif %}
-
-{% if HARDEN_GLUE == "true" %}
-mode('normal')
-{% else %}
-mode('permissive')
-{% endif %}
-
-{% for TAF in TRUST_ANCHOR_FILES %}
-trust_anchors.add_file('{{TAF}}')
-{% endfor %}
-trust_anchors.set_insecure({
-
-{% for DI in NEGATIVE_TRUST_ANCHORS %}
-"{{DI}}",
-{% endfor %}
-})
-
-verbose(true)
 
 -- Self-checks on globals
 assert(help() ~= nil)
diff --git a/modules/ta_update/ta_update.test.integr/rfc5011_unsupported_key_rollover.rpl b/modules/ta_update/ta_update.test.integr/rfc5011_unsupported_key_rollover.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..cf7dba9e93cc2010efa2e68a8417857ad445b878
--- /dev/null
+++ b/modules/ta_update/ta_update.test.integr/rfc5011_unsupported_key_rollover.rpl
@@ -0,0 +1,90 @@
+	trust-anchor: ". IN DS 13876 8 2 240B81A3498168E9F1FF85F83C24B63994D91D0569D7FB13C87E0D59AA8EB2DD"
+	val-override-date: "20190313000000"
+	stub-addr: 193.0.14.129 	# K.ROOT-SERVERS.NET.
+	query-minimization: off
+CONFIG_END
+
+SCENARIO_BEGIN RFC 5011 key rollover to unsupported algorhitm.
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+	ADDRESS 193.0.14.129
+	ADDRESS 2001:7fd::1
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       518400  IN      NS      k.root-servers.net.
+.					      518400 IN	RRSIG	NS 8 0 518400 20190326123543 20190312123543 13191 . kyHWRA9F6SKNXHKbB/roiZIUYvsQXdRzdTYZBWeiHb2puAug4h8NqdU9 yJwOpW7lzZyQILshzThh1NXueSOyJ7VYqxgAqIMiQ7hTKXvgfPsDPZYK hl05XtUZYmXQO5gdXyeKbcsI/oC4yom3IU7wt81Y18CJnlKmbY4hAf7e aDAluhbL4H9/4dXWyVBNKk8aOzHnusWjbyFdb/+UlGVEv62RhXYYMuWy c1v/8uSc1CHSgS9ef1krVkqstJtaob5lysa6Vko08XTsDKmyUJXrhhgz wzmZKaVpthAM58dMm+Twho+tLpQ2HApZUOa6Z7F9Rc2QnNLMJLRl7Iz+ fq7JBg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.					      1814400 IN DNSKEY	257 3 253 AwEAAcVR4S9H/xPz0EZNso6tsX+z/CLhzwsDNbPVQNWddu5YP04iHKkA prBuseYVwswkQm14Jqr7u2oLOMDJ0Vn0tbw7UfBDD9nLlMhi8X3l8X++ T7xzqn99xL+8Ad0L5xQwRR7dlij8SuL0DuNhWpWmKwPDP7mI/oTNSYLD 3U/zm023Wgq+mrx+7w9Or7bh9Fo/bPN54RsTQ3BIg7LM2/wmLKtHZqiR lpyCF5gQ+eUSR6JGDfedjrvl+ywEl7dcmF11MV69pyAeASNo9+mvknwx VearXoz1KcNiCpgNmuu3lBQvoygTVmDw0RvYiWkVUmm/b+mo6hsYz6O+ XSRya8C681c=
+.					      1814400 IN DNSKEY	257 3 8 AwEAAcVR4S9H/xPz0EZNso6tsX+z/CLhzwsDNbPVQNWddu5YP04iHKkA prBuseYVwswkQm14Jqr7u2oLOMDJ0Vn0tbw7UfBDD9nLlMhi8X3l8X++ T7xzqn99xL+8Ad0L5xQwRR7dlij8SuL0DuNhWpWmKwPDP7mI/oTNSYLD 3U/zm023Wgq+mrx+7w9Or7bh9Fo/bPN54RsTQ3BIg7LM2/wmLKtHZqiR lpyCF5gQ+eUSR6JGDfedjrvl+ywEl7dcmF11MV69pyAeASNo9+mvknwx VearXoz1KcNiCpgNmuu3lBQvoygTVmDw0RvYiWkVUmm/b+mo6hsYz6O+ XSRya8C681c=
+.					      1814400 IN DNSKEY	256 3 8 AwEAAZ7wwdoX/a2Va1Wx5tlTF/gVpznA/m1m7jvhnEjHCVE6iGQW3qII +tL87ScygLKV25ATPmfjIIkIIG7/NSx66eo2KiJusDjzUR8BQWcy/SHd k+r8yCifsIYTaKqgtnj91gYPoY22bG4CUt8/v1hl9FWh+C+X6occdmLr uXxeo6UOhORkM9oVcK2tOLgK1oedarg5z663JmQdEjwPkgYS7QazCAHh m3eQF8n6mD1AqKh1O6uNaVmLh3mvaI2K/0E9jRfefHJgWh1v2PfRtqlG j9idQkBZX+3IclEx8BoSXrRxVdehBvyntS+eqgx/YBOnJcdH1kOls/s2 0ZknTVQvOdE=
+.					      1209600 IN RRSIG	DNSKEY 8 0 1814400 20190402125328 20190312125328 13876 . qy0f6TfZls3/njJKIQlpZC3/Zq7e1O7VUFtEDiDCk8vU23PeARcMNDfb Io9VPE4MqUtHDJ7DuHUlSttLwH4KZUK7uoYW74Ii6YlnE+2pci1lj8Bn PlodQiOAhrpeH6BdZe55La5uGFE/GB7w9vbjVf6ytz5HBrdFUFoxg5V/ vUwnZS12eW0JY8HXZ7kdiyr/z9eOIRmUYIZHTXDzT5MJBYAaoDXBqE0j DKwxTn5Wx5/O2KthiRYc0j44hEQBawQnL0upBRmof+iAuUInoMBrk1u8 Ylr7RSbvS69qs8lkWGPC6VSKvAnludzcTW79K5avz3jST6rccSowuFNI oyN5UA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+k.root-servers.net.            IN      AAAA
+SECTION ANSWER
+k.root-servers.net.     3600000 IN      AAAA    2001:7fd::1
+k.root-servers.net.			      1209600 IN RRSIG	AAAA 8 3 3600000 20190326123543 20190312123543 13191 . GNsOgKS3KLLHf7J05LRrLHoWWq8qtcyLoT9x5b4fk6yQvSjbRrYxjxsv kUR9f3RE+dPndevDv/GI5PHQ/UKgWeVQEvyhU5QjgveR/AvULIy3dk8f FzkOd/USy931BrOCJF2Zqzw0pHavjotdDVsoVWwwgjNlrahtKFP+e8Bm qD7C1NVrncv9bmMYNlH/ZrPniXR1pWYt4294rrSUSqoH+tVxmwdwX2kL SU/c/a4p+7ST/+GhsG26QBl0K/OJP7nAwdKP1gQBUoKDRUIzXlPdDIc8 fvDaYPq8iKYA5QHAXy3Fvd4Z02J9iTc1/vTncDJata3CNUk2B295f5F/ uk+a0Q==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+k.root-servers.net.            IN      A
+SECTION ANSWER
+k.root-servers.net.     3600000 IN      A       193.0.14.129
+k.root-servers.net.			      1209600 IN RRSIG	A 8 3 3600000 20190326123543 20190312123543 13191 . fi34mMaQ+cEP1mueazJ3YXBOTKX5FGU9hZGQKMogrKLw4jwItTaxBtl2 CYCMP8B2rX9bAhBNjvqxqT5Lj1LJfomKLi+eVQhGONL3t8TgIFml6Z72 7d2qr/AiGgNH71tF/mbf5xFKrIOx37k0is3nRSmbB1FWMuvpVtlPFBey H1rAF/o69jnI7xvFu4TnQHQM+tG/NeCa1fBTJB2J02gS2XUBgPIk9f1a bkuf7nofj7tEN7+jHv2U3dDFDoMafcYIgzF/wlieqDTorBi9SkT68+nh hpJAG29d0rbG3CWUPI6Tm6El8eH+3hC6B8Emc3T30m3R5E/A4VJsbsOt vKBUIQ==
+ENTRY_END
+
+; QTYPE == RRSIG is not supported, https://tools.ietf.org/html/draft-ietf-dnsop-refuse-any-04#section-7
+ENTRY_BEGIN
+MATCH opcode qtype
+ADJUST copy_id copy_query
+REPLY QR AA REFUSED
+SECTION QUESTION
+.            IN      RRSIG
+ENTRY_END
+RANGE_END
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+. IN NS
+ENTRY_END
+
+
+STEP 10 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qname flags rcode question answer
+REPLY QR RD RA AD DO NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       518400  IN      NS      k.root-servers.net.
+.					      518400 IN	RRSIG	NS 8 0 518400 20190326123543 20190312123543 13191 . kyHWRA9F6SKNXHKbB/roiZIUYvsQXdRzdTYZBWeiHb2puAug4h8NqdU9 yJwOpW7lzZyQILshzThh1NXueSOyJ7VYqxgAqIMiQ7hTKXvgfPsDPZYK hl05XtUZYmXQO5gdXyeKbcsI/oC4yom3IU7wt81Y18CJnlKmbY4hAf7e aDAluhbL4H9/4dXWyVBNKk8aOzHnusWjbyFdb/+UlGVEv62RhXYYMuWy c1v/8uSc1CHSgS9ef1krVkqstJtaob5lysa6Vko08XTsDKmyUJXrhhgz wzmZKaVpthAM58dMm+Twho+tLpQ2HApZUOa6Z7F9Rc2QnNLMJLRl7Iz+ fq7JBg==
+SECTION AUTHORITY
+SECTION ADDITIONAL
+ENTRY_END
+
+SCENARIO_END
diff --git a/modules/ta_update/ta_update.test.lua b/modules/ta_update/ta_update.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..02a2aa4a4fa1634079c197c326d8710e8d3f53a8
--- /dev/null
+++ b/modules/ta_update/ta_update.test.lua
@@ -0,0 +1,79 @@
+-- shorten update interval
+ta_update.refresh_time = 0.3 * sec
+ta_update.hold_down_time = 0.6 * sec
+sleep_time = 0.9
+
+-- prevent build-time config from interfering with the test
+trust_anchors.remove('.')
+
+-- count . IN DNSKEY queries
+counter = 0
+local function counter_func (state, req)
+        local answer = req.answer
+        local qry = req:current()
+        if answer:qclass() == kres.class.IN
+		and qry.stype == kres.type.DNSKEY
+		and kres.dname2wire(qry.sname) == '\0' then
+		counter = counter + 1
+        end
+        return state
+end
+policy.add(policy.all(counter_func))
+
+local function test_ta_update_vs_trust_anchors_dependency()
+	ok(ta_update, 'ta_update module is loaded by default')
+
+	assert(counter == 0, 'test init must work')
+	same(trust_anchors.add_file('root.keys'), nil, 'load managed TA for root zone')
+	same(trust_anchors.keysets['\0'].managed, true, 'managed TA has managed flag')
+	same(type(ta_update.tracked['\0'].event), 'number', 'adding managed TA starts tracking')
+	same(counter, 0, 'TA refresh is only scheduled')
+	worker.sleep(sleep_time)
+	ok(counter > 0, 'TA refresh asked for TA DNSKEY after some time')
+
+	same(ta_update.stop('\0'), nil, 'key tracking can be stopped')
+	same(ta_update.tracked['\0'], nil, 'stopping removed metadata')
+	same(trust_anchors.keysets['\0'].managed, false, 'now unmanaged TA does not have managed flag')
+	counter = 0
+	worker.sleep(sleep_time)
+	same(counter, 0, 'stop() actually prevents further TA refreshes')
+
+	ok(modules.unload('ta_update'), 'module can be unloaded')
+	same(ta_update, nil, 'unloaded module is nil')
+
+	ok(trust_anchors.remove('.'), 'managed root TA can be removed')
+	same(trust_anchors.keysets['\0'], nil, 'TA removal works')
+end
+
+local function test_unloaded()
+	boom(trust_anchors.add_file, {'root.keys', false}, 'managed TA cannot be added without ta_update module')
+
+	counter = 0
+	same(trust_anchors.add_file('root.keys', true), nil, 'unmanaged TA can be added without ta_update module')
+	worker.sleep(sleep_time)
+	ok(counter == 0, 'TA is actually unmanaged')
+
+	ok(trust_anchors.remove('.'), 'unmanaged root TA can be removed')
+	same(trust_anchors.keysets['\0'], nil, 'TA removal works')
+
+end
+
+local function test_reload()
+	ok(modules.load('ta_update'), 'module can be re-loaded')
+	same(trust_anchors.add_file('root.keys', false), nil, 'managed TA can be added after loading ta_update module')
+	same(counter, 0, 'TA refresh is only scheduled')
+	worker.sleep(sleep_time)
+	ok(counter > 0, 'TA refresh asked for TA DNSKEY after some time')
+end
+
+local function test_err_inputs()
+	ok(modules.load('ta_update'), 'make sure module is loaded')
+	boom(ta_update.start, {'\12nonexistent'}, 'nonexistent TA cannot be tracked')
+end
+
+return {
+	test_ta_update_vs_trust_anchors_dependency,
+	test_unloaded,
+	test_reload,
+	test_err_inputs,
+}
diff --git a/modules/view/addr.test.integr/kresd_config.j2 b/modules/view/addr.test.integr/kresd_config.j2
index a92c1423408bfb77dac097b160bef98ae8401488..08a6be20f115ab735fee72ca10653c71bca3ab43 100644
--- a/modules/view/addr.test.integr/kresd_config.j2
+++ b/modules/view/addr.test.integr/kresd_config.j2
@@ -6,7 +6,12 @@ view:addr('127.0.0.0/24', policy.suffix(policy.DENY_MSG("addr 127.0.0.0/24 match
 policy.add(policy.all(policy.FORWARD('1.2.3.4')))
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
 
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
diff --git a/modules/view/tsig.test.integr/kresd_config.j2 b/modules/view/tsig.test.integr/kresd_config.j2
index 9ce0162c0b1dc10e06ad840a1d3106edeccafeb5..c306989f7ec357ecb72d21685a71e4c4ecad6e8d 100644
--- a/modules/view/tsig.test.integr/kresd_config.j2
+++ b/modules/view/tsig.test.integr/kresd_config.j2
@@ -7,6 +7,11 @@ view:tsig('\8testkey1\0', policy.suffix(policy.DENY_MSG("TSIG key testkey1 match
 view:tsig('\7testkey\0', policy.suffix(policy.DENY_MSG("TSIG key testkey matched example"),{"\7example\0"}))
 policy.add(policy.all(policy.FORWARD('1.2.3.4')))
 
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
+
 -- Disable RFC8145 signaling, scenario doesn't provide expected answers
 if ta_signal_query then
         modules.unload('ta_signal_query')
@@ -23,7 +28,7 @@ if detect_time_skew then
 end
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
 
 _hint_root_file('hints')
 cache.size = 2*MB
diff --git a/scripts/kresd-host.lua b/scripts/kresd-host.lua
index 9348716af1d83f3807bc227d095939a0521294c1..bfbcbf19e752d2e914d129d1af279038708efb64 100755
--- a/scripts/kresd-host.lua
+++ b/scripts/kresd-host.lua
@@ -41,10 +41,10 @@ k = 1 while k <= #arg do
 		k = k + 1
 		table.insert(config, arg[k])
 	elseif v == '-D' then
-		table.insert(config, 'trust_anchors.file = "root.keys"')
+		table.insert(config, 'trust_anchors.add_file("root.keys")')
 	elseif v == '-f' then
 		k = k + 1
-		table.insert(config, string.format('trust_anchors.file = "%s"', arg[k]))
+		table.insert(config, string.format('trust_anchors.add_file("%s")', arg[k]))
 	elseif v == '-v' then
 		verbose = true
 	elseif v == '-d' then
diff --git a/scripts/launch-test-instance.sh b/scripts/launch-test-instance.sh
deleted file mode 100755
index 8a933285db4c7e8720e44820f4644b62eb1db07d..0000000000000000000000000000000000000000
--- a/scripts/launch-test-instance.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/sh -e
-export PATH="/usr/lib/ccache:$PATH"
-
-PORT=${1:-$((32767+$(dd if=/dev/urandom count=1 2> /dev/null | cksum | cut -d' ' -f1) % 32768))}
-
-JOBS=$(cat /proc/cpuinfo  | grep processor | wc -l)
-
-WORKDIR=${2:-$(mktemp -d /tmp/knot-resolver.XXXXXX)}
-
-PREFIX=${PREFIX:-$WORKDIR} make clean
-
-CFLAGS=${CFLAGS:-"-O2 -g3"} PREFIX=${PREFIX:-$WORKDIR} make -j ${JOBS} V=1
-
-PREFIX=${PREFIX:-$WORKDIR} make install
-
-install -d -m 0700 ${WORKDIR}/run/kresd
-
-echo "Launching Knot Resolver on port: ${PORT}"
-echo "To debug, use:"
-echo "dig +dnssec +multi +time=60 +retry=1 -p ${PORT} @::1"
-
-LD_LIBRARY_PATH=${WORKDIR}/lib ${WORKDIR}/sbin/kresd -a 127.0.0.1#${PORT} -a ::1#${PORT} -v -k ${ROOT_KEY:-/usr/share/dns/root.key} ${WORKDIR}/run/kresd
-
-if [ "${WORKDIR}" != "${2}" -a "${KEEP_WORKDIR}" != "yes" ]; then
-    rm -r ${WORKDIR}
-fi
diff --git a/tests/config/keyfile/bad_args.test.lua b/tests/config/keyfile/bad_args.test.lua
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/tests/config/keyfile/load_ta.test.lua b/tests/config/keyfile/load_ta.test.lua
deleted file mode 100644
index bfe851b7c6822c2f8033a9325a0b8f58912c3c9b..0000000000000000000000000000000000000000
--- a/tests/config/keyfile/load_ta.test.lua
+++ /dev/null
@@ -1,37 +0,0 @@
--- 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/nonexist1.test.lua b/tests/config/keyfile/nonexist1.test.lua
deleted file mode 100644
index 332919dabbf5dacf692fdc62613505de487d01d5..0000000000000000000000000000000000000000
--- a/tests/config/keyfile/nonexist1.test.lua
+++ /dev/null
@@ -1,2 +0,0 @@
--- simulate building without keyfile_default
-trust_anchors.keyfile_default = nil
diff --git a/tests/config/keyfile/nonexist2.test.lua b/tests/config/keyfile/nonexist2.test.lua
deleted file mode 100644
index e801c6d3269caa42eb42766dad1f4a7770b0b676..0000000000000000000000000000000000000000
--- a/tests/config/keyfile/nonexist2.test.lua
+++ /dev/null
@@ -1,2 +0,0 @@
--- simulate building with keyfile_default
-trust_anchors.keyfile_default = "../../../../tests/config/keyfile/root1.keys"
diff --git a/tests/config/keyfile/root1.keys b/tests/config/keyfile/root1.keys
deleted file mode 100644
index c7343371b9f01ac3ca7540d75044e02a56c86350..0000000000000000000000000000000000000000
--- a/tests/config/keyfile/root1.keys
+++ /dev/null
@@ -1 +0,0 @@
-.                   	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
deleted file mode 100644
index 5e9d6ac6e0e3964863dd4ea98280e1eb1fb5862e..0000000000000000000000000000000000000000
--- a/tests/config/keyfile/root2.keys
+++ /dev/null
@@ -1 +0,0 @@
-.                   	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/meson.build b/tests/config/meson.build
index 7d9a68b0b09356b731fd272fe4898fea64bea0d4..c0dff6a242495b6e11527d3679b497aaa4e49585 100644
--- a/tests/config/meson.build
+++ b/tests/config/meson.build
@@ -1,22 +1,6 @@
 config_tests += [
-  ['basic', files('basic.test.lua'), [], false, ['skip_asan']],
-  ['cache', files('cache.test.lua'), [], false, ['skip_asan']],
-  ['keyfile.bad_args', files('keyfile/bad_args.test.lua'),
-    ['--keyfile-ro', 'root.keys',
-     '--keyfile', 'root.keys'],
-    true,
-  ],
-  ['keyfile.load_ta', files('keyfile/load_ta.test.lua'),
-    ['--keyfile-ro', files('keyfile/root2.keys')]
-  ],
-  ['keyfile.nonexist1', files('keyfile/nonexist1.test.lua'),
-    ['--keyfile-ro', 'nonexist'],
-    true,
-  ],
-  ['keyfile.nonexist2', files('keyfile/nonexist2.test.lua'),
-    ['--keyfile-ro', 'nonexist'],
-    true,
-  ],
+  ['basic', files('basic.test.lua'), ['skip_asan']],
+  ['cache', files('cache.test.lua'), ['skip_asan']],
   ['lru', files('lru.test.lua')],
   ['tls', files('tls.test.lua')],
   ['worker', files('worker.test.lua')],
@@ -27,20 +11,8 @@ run_configtest = find_program('../../scripts/test-config.sh')
 
 
 foreach config_test : config_tests
-  # kresd arguments
-  conftest_args = [
-    '-c', files('test.cfg'),
-    '-f', '1',
-  ]
-  if config_test.length() >= 3
-    conftest_args += config_test[2]
-  endif
-
-  # kresd return code check
-  conftest_should_fail = config_test.length() >= 4 ? config_test[3] : false
-
   # additional suites
-  extra_suites = config_test.length() >= 5 ? config_test[4] : []
+  extra_suites = config_test.length() >= 3 ? config_test[2] : []
 
   # environment variables for test
   conftest_env = environment()
@@ -53,12 +25,14 @@ foreach config_test : config_tests
   test(
     'config.' + config_test[0],
     run_configtest,
-    args: conftest_args,
+    args: [
+	'-c', files('test.cfg'),
+	'-f', '1',
+    ],
     env: conftest_env,
     suite: [
       'postinstall',
       'config',
     ] + extra_suites,
-    should_fail: conftest_should_fail,
   )
 endforeach
diff --git a/tests/integration/deckard b/tests/integration/deckard
index 04dd33b568b173ad7e6c01604f61999b091191d1..6edfef4f65e846f3aef0cbf3b80e88f39658a1c9 160000
--- a/tests/integration/deckard
+++ b/tests/integration/deckard
@@ -1 +1 @@
-Subproject commit 04dd33b568b173ad7e6c01604f61999b091191d1
+Subproject commit 6edfef4f65e846f3aef0cbf3b80e88f39658a1c9
diff --git a/tests/pytests/templates/kresd.conf.j2 b/tests/pytests/templates/kresd.conf.j2
index 141f4389a66d47372a81b9601288a5abb146ca52..72f0ee1a7f9262a97018a77f525d418792e55fd6 100644
--- a/tests/pytests/templates/kresd.conf.j2
+++ b/tests/pytests/templates/kresd.conf.j2
@@ -43,7 +43,8 @@ policy.add(policy.suffix(policy.PASS, {todname('test.')}))
 {% endif %}
 
 -- make sure DNSSEC is turned off for tests
-trust_anchors.keyfile_default = nil
+trust_anchors.remove('.')
+modules.unload("ta_update")
 modules.unload("ta_signal_query")
 modules.unload("priming")
 modules.unload("detect_time_skew")