diff --git a/distro/tests/manager-packaging/control b/distro/tests/manager-packaging/control
index c820dbe61b78b22ab354bd1ff0ac1c73a2ec83d9..f70a0b33a182816566dd94fb9b963e06230d761b 100644
--- a/distro/tests/manager-packaging/control
+++ b/distro/tests/manager-packaging/control
@@ -13,17 +13,17 @@ Tests: systemd_service.sh
 Tests-Directory: manager/tests/packaging
 Restrictions: needs-root
 {% if distro.match('fedora') -%}
-Depends: knot-utils
+Depends: knot-utils, jq, curl
 {% elif distro.match('debian') or distro.match('ubuntu') -%}
-Depends: knot-dnsutils
+Depends: knot-dnsutils, jq, curl
 {% elif distro.match('arch') -%}
-Depends: knot
+Depends: knot, jq, curl
 {% elif distro.match('opensuse') -%}
-Depends: knot-utils
+Depends: knot-utils, jq, curl
 {% elif distro.match('rocky') -%}
-Depends: knot-utils
+Depends: knot-utils, jq, curl
 {% elif distro.match('centos') -%}
-Depends: knot-utils
+Depends: knot-utils, jq, curl
 {% else -%}
 Depends: unsupported-distro-this-package-does-not-exist-and-the-test-should-fail
 {%- endif %}
diff --git a/manager/knot-resolver.service b/manager/knot-resolver.service
index 8295f3668406590cd8e304061ba09cdf2913ae5d..00be7d48e3872346000e34209c49816701e4cbbd 100644
--- a/manager/knot-resolver.service
+++ b/manager/knot-resolver.service
@@ -6,6 +6,7 @@ Type=notify
 TimeoutStartSec=10s
 ExecStart=/usr/bin/env python3 -m knot_resolver_manager --config=/etc/knot-resolver/config.yml
 KillSignal=SIGINT
+WorkingDirectory=/var/run/knot-resolver/
 # See systemd.service(5) for explanation, why we should replace this with a blocking request
 # ExecReload=/usr/bin/env kill -HUP $MAINPID
 
diff --git a/manager/knot_resolver_manager/kresd_controller/interface.py b/manager/knot_resolver_manager/kresd_controller/interface.py
index 3db894fcb611ad2be09e08c2c0b97f273b42fe7a..081c3221224daae4eaca126aafef65bddd8cad76 100644
--- a/manager/knot_resolver_manager/kresd_controller/interface.py
+++ b/manager/knot_resolver_manager/kresd_controller/interface.py
@@ -163,7 +163,7 @@ class Subprocess:
         reader: asyncio.StreamReader
         writer: Optional[asyncio.StreamWriter] = None
         try:
-            reader, writer = await asyncio.open_unix_connection(f"./control/{self.id}")
+            reader, writer = await asyncio.open_unix_connection(f"./control/{int(self.id)}")
 
             # drop prompt
             _ = await reader.read(2)
diff --git a/manager/knot_resolver_manager/server.py b/manager/knot_resolver_manager/server.py
index 38851a456a711a16550182f9ef54a3d78c3b87f8..edcbda728972705fab8d0df3027164a50ec14e7b 100644
--- a/manager/knot_resolver_manager/server.py
+++ b/manager/knot_resolver_manager/server.py
@@ -7,9 +7,9 @@ import sys
 from http import HTTPStatus
 from pathlib import Path
 from time import time
-from typing import Any, Optional, Set, Union, cast
+from typing import Any, List, Optional, Set, Union, cast
 
-from aiohttp import ETag, web
+from aiohttp import web
 from aiohttp.web import middleware
 from aiohttp.web_app import Application
 from aiohttp.web_response import json_response
@@ -25,6 +25,7 @@ from knot_resolver_manager.datamodel.config_schema import KresConfig
 from knot_resolver_manager.datamodel.management_schema import ManagementSchema
 from knot_resolver_manager.exceptions import CancelStartupExecInsteadException, KresManagerException
 from knot_resolver_manager.kresd_controller import get_best_controller_implementation
+from knot_resolver_manager.utils import ignore_exceptions_optional
 from knot_resolver_manager.utils.async_utils import readfile
 from knot_resolver_manager.utils.functional import Result
 from knot_resolver_manager.utils.modeling import ParsedTree, parse, parse_yaml
@@ -165,28 +166,45 @@ class Server:
         """
         Route handler for changing resolver configuration
         """
+        # There are a lot of local variables in here, but they are usually immutable (almost SSA form :) )
+        # pylint: disable=too-many-locals
 
         # parse the incoming data
         document_path = request.match_info["path"]
-        etags = request.if_match
-        last: ParsedTree = self.config_store.get().get_unparsed_data()
-        update_with: ParsedTree = parse(await request.text(), request.content_type)
+        getheaders = ignore_exceptions_optional(List[str], None, KeyError)(request.headers.getall)
+        etags = getheaders("if-match")
+        not_etags = getheaders("if-none-match")
+        current_config: ParsedTree = self.config_store.get().get_unparsed_data()
+        if request.method == "GET":
+            update_with: Optional[ParsedTree] = None
+        else:
+            update_with = parse(await request.text(), request.content_type)
 
-        if etags is not None and last.etag not in map(str, etags):
-            return web.Response(status=HTTPStatus.PRECONDITION_FAILED)
+        # stop processing if etags
+        def strip_quotes(s: str) -> str:
+            return s.strip('"')
 
-        op = cast(Literal["get", "post", "delete", "patch", "put"], request.method.lower())
-        root, to_return = last.query(op, document_path, update_with)
+        status = HTTPStatus.NOT_MODIFIED if request.method in ("GET", "HEAD") else HTTPStatus.PRECONDITION_FAILED
+        if etags is not None and current_config.etag not in map(strip_quotes, etags):
+            return web.Response(status=status)
+        if not_etags is not None and current_config.etag in map(strip_quotes, not_etags):
+            return web.Response(status=status)
 
-        # validate config
-        config_validated = KresConfig(root)
+        # run query
+        op = cast(Literal["get", "post", "delete", "patch", "put"], request.method.lower())
+        new_config, to_return = current_config.query(op, document_path, update_with)
 
-        # apply config
-        await self.config_store.update(config_validated)
+        # update the config
+        if request.method != "GET":
+            # validate
+            config_validated = KresConfig(new_config)
+            # apply
+            await self.config_store.update(config_validated)
 
         # return success
-        res = web.Response(status=HTTPStatus.OK, text=str(to_return))
-        res.etag = ETag(config_validated.get_unparsed_data().etag)
+        resp_text: Optional[str] = str(to_return) if to_return is not None else None
+        res = web.Response(status=HTTPStatus.OK, text=resp_text)
+        res.headers.add("ETag", f'"{new_config.etag}"')
         return res
 
     async def _handler_metrics(self, _request: web.Request) -> web.Response:
@@ -241,11 +259,11 @@ class Server:
         self.app.add_routes(
             [
                 web.get("/", self._handler_index),
-                web.post(r"/config{path:.*}", self._handler_config_query),
-                web.put(r"/config{path:.*}", self._handler_config_query),
-                web.patch(r"/config{path:.*}", self._handler_config_query),
-                web.get(r"/config{path:.*}", self._handler_config_query),
-                web.delete(r"/config{path:.*}", self._handler_config_query),
+                web.post(r"/v1/config{path:.*}", self._handler_config_query),
+                web.put(r"/v1/config{path:.*}", self._handler_config_query),
+                web.patch(r"/v1/config{path:.*}", self._handler_config_query),
+                web.get(r"/v1/config{path:.*}", self._handler_config_query),
+                web.delete(r"/v1/config{path:.*}", self._handler_config_query),
                 web.post("/stop", self._handler_stop),
                 web.get("/schema", self._handler_schema),
                 web.get("/schema/ui", self._handle_view_schema),
diff --git a/manager/knot_resolver_manager/utils/modeling/parsing.py b/manager/knot_resolver_manager/utils/modeling/parsing.py
index ec14809ca98ca76e442a68bfeb758e854bbb483e..eb26c851ee60a3ade31213d70543bdc65fee3282 100644
--- a/manager/knot_resolver_manager/utils/modeling/parsing.py
+++ b/manager/knot_resolver_manager/utils/modeling/parsing.py
@@ -68,7 +68,7 @@ class ParsedTree:
 
     @property
     def etag(self) -> str:
-        m = blake2b(digest_size=9)
+        m = blake2b(digest_size=15)
         m.update(json.dumps(self._data, sort_keys=True).encode("utf8"))
         return base64.urlsafe_b64encode(m.digest()).decode("utf8")
 
diff --git a/manager/tests/packaging/interactive/etag.sh b/manager/tests/packaging/interactive/etag.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a4c49ed9d8a1588028d7174356c3015764b77cb2
--- /dev/null
+++ b/manager/tests/packaging/interactive/etag.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+socket_opt="--unix-socket /var/run/knot-resolver/manager.sock"
+
+etag="$(curl --silent $socket_opt --fail http://localhost:5000/v1/config -o /dev/null -v 2>&1 | grep ETag | sed 's/< ETag: //;s/\s//')"
+status=$(curl --silent $socket_opt --fail  http://localhost:5000/v1/config --header "If-None-Match: $etag" -w "%{http_code}" -o /dev/null)
+
+test "$status" -eq 304
diff --git a/manager/tests/packaging/interactive/metrics.sh b/manager/tests/packaging/interactive/metrics.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a3e8748f59d128b1d4a74b5716096cc9e30cf5a3
--- /dev/null
+++ b/manager/tests/packaging/interactive/metrics.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+curl --silent --fail --unix-socket /var/run/knot-resolver/manager.sock http://localhost/metrics > /dev/null
\ No newline at end of file
diff --git a/manager/tests/packaging/systemd_service.sh b/manager/tests/packaging/systemd_service.sh
index e942b7075be4e876e9eb500f315624bfca76690b..99835eedbeb3400733a54d6b6f07b11f464fd2ef 100755
--- a/manager/tests/packaging/systemd_service.sh
+++ b/manager/tests/packaging/systemd_service.sh
@@ -22,5 +22,11 @@ if ! systemctl start knot-resolver.service; then
 else
 	# check that the resolvers are actually running
 	kdig @127.0.0.1 nic.cz
+
+	echo "Running interactive tests..."
+	for test in "$(dirname $0)"/interactive/*; do
+		echo "[test] $test"
+		$test
+	done
 fi