diff --git a/.gitignore b/.gitignore
index d5c14f297646180b61e8e01ac9b20448228bdbe7..7579e289674a903ab64b6ef5c118cbf4f8c63377 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,7 +23,6 @@
 .libs
 .deps
 _obj
-tmp*
 /autom4te.cache/*
 /config.log
 /config.h
diff --git a/daemon/main.c b/daemon/main.c
index de1c9605dba90c58c513f838777e1eff7ea61620..5bf56c726f28dba5fd09aaa85145c76a36619c81 100644
--- a/daemon/main.c
+++ b/daemon/main.c
@@ -321,7 +321,7 @@ static struct worker_ctx *init_worker(struct engine *engine, knot_mm_t *pool, in
 	return worker;
 }
 
-static int run_worker(uv_loop_t *loop, struct engine *engine, fd_array_t *ipc_set, bool leader)
+static int run_worker(uv_loop_t *loop, struct engine *engine, fd_array_t *ipc_set, bool leader, int control_fd)
 {
 	/* Control sockets or TTY */
 	auto_free char *sock_file = NULL;
@@ -335,12 +335,18 @@ static int run_worker(uv_loop_t *loop, struct engine *engine, fd_array_t *ipc_se
 		uv_pipe_open(&pipe, 0);
 		uv_read_start((uv_stream_t*) &pipe, tty_alloc, tty_read);
 	} else {
-		(void) mkdir("tty", S_IRWXU|S_IRWXG);
-		sock_file = afmt("tty/%ld", getpid());
-		if (sock_file) {
-			uv_pipe_bind(&pipe, sock_file);
-			uv_listen((uv_stream_t *) &pipe, 16, tty_accept);
+		int pipe_ret = -1;
+		if (control_fd != -1) {
+			pipe_ret = uv_pipe_open(&pipe, control_fd);
+		} else {
+			(void) mkdir("tty", S_IRWXU|S_IRWXG);
+			sock_file = afmt("tty/%ld", getpid());
+			if (sock_file) {
+				pipe_ret = uv_pipe_bind(&pipe, sock_file);
+			}
 		}
+		if (!pipe_ret)
+			uv_listen((uv_stream_t *) &pipe, 16, tty_accept);
 	}
 	/* Watch IPC pipes (or just assign them if leading the pgroup). */
 	if (!leader) {
@@ -364,6 +370,14 @@ static int run_worker(uv_loop_t *loop, struct engine *engine, fd_array_t *ipc_se
 	return kr_ok();
 }
 
+void free_sd_socket_names(char **socket_names, int count)
+{
+	for (int i = 0; i < count; i++) {
+		free(socket_names[i]);
+	}
+	free(socket_names);
+}
+
 int main(int argc, char **argv)
 {
 	int forks = 1;
@@ -374,6 +388,7 @@ int main(int argc, char **argv)
 	char *keyfile = NULL;
 	const char *config = NULL;
 	char *keyfile_buf = NULL;
+	int control_fd = -1;
 
 	/* Long options. */
 	int c = 0, li = 0, ret = 0;
@@ -457,11 +472,25 @@ int main(int argc, char **argv)
 
 #ifdef HAS_SYSTEMD
 	/* Accept passed sockets from systemd supervisor. */
-	int sd_nsocks = sd_listen_fds(0);
+	char **socket_names = NULL;
+	int sd_nsocks = sd_listen_fds_with_names(0, &socket_names);
 	for (int i = 0; i < sd_nsocks; ++i) {
 		int fd = SD_LISTEN_FDS_START + i;
-		array_push(fd_set, fd);
+		/* when run under systemd supervision, do not use interactive mode */
+		g_interactive = false;
+		if (forks != 1) {
+			kr_log_error("[system] when run under systemd-style supervision, "
+				     "use single-process only (bad: --fork=%d).\n", forks);
+			free_sd_socket_names(socket_names, sd_nsocks);
+			return EXIT_FAILURE;
+		}
+		if (!strcasecmp("control",socket_names[i])) {
+			control_fd = fd;
+		} else {
+			array_push(fd_set, fd);
+		}
 	}
+	free_sd_socket_names(socket_names, sd_nsocks);
 #endif
 
 	/* Switch to rundir. */
@@ -563,7 +592,7 @@ int main(int argc, char **argv)
 				lua_settop(engine.L, 0);
 			}
 			/* Run the event loop */
-			ret = run_worker(loop, &engine, &ipc_set, fork_id == 0);
+			ret = run_worker(loop, &engine, &ipc_set, fork_id == 0, control_fd);
 		}
 	}
 	if (ret != 0) {
diff --git a/doc/kresd.8.in b/doc/kresd.8.in
index bd9e27afe5674f692f21dc1a844f38d865babe27..2fac891904918de28c2b9081e053d6b47b87c3ff 100644
--- a/doc/kresd.8.in
+++ b/doc/kresd.8.in
@@ -111,6 +111,10 @@ With this option, the daemon is started in non-interactive mode and instead crea
 UNIX socket in \fIrundir\fR that the operator can connect to for interactive session.
 A number greater than 1 forks the daemon N times, all forks will bind to same addresses
 and the kernel will load-balance between them on Linux with \fISO_REUSEPORT\fR support.
+
+When socket-activated and supervised by systemd or the equivalent, kresd defaults to
+--forks=1, and must not be set to any other value.  If you want multiple concurrent
+processes supervised in this way, they should be supervised independently.
 .TP
 .B \-q\fR, \fB\-\-quiet
 Daemon will refrain from printing any informative messages, not even a prompt.
diff --git a/scripts/kresd.service b/scripts/kresd.service
deleted file mode 100644
index c28adaa76dea953e153ceb8d7c28e0274268a942..0000000000000000000000000000000000000000
--- a/scripts/kresd.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=Knot DNS Resolver daemon
-After=network.target
-
-[Service]
-Type=simple
-EnvironmentFile=-/etc/sysconfig/kresd
-ExecStart=/usr/sbin/kresd -c /etc/kresd/config -f $KRESD_WORKERS $KRESD_OPTIONS /var/lib/kresd/
-Restart=on-abort
-
-[Install]
-WantedBy=multi-user.target
diff --git a/scripts/kresd.sysconfig b/scripts/kresd.sysconfig
deleted file mode 100644
index ceb5e3068a44d38161979ab78e4e8ff2f26d7aa4..0000000000000000000000000000000000000000
--- a/scripts/kresd.sysconfig
+++ /dev/null
@@ -1,14 +0,0 @@
-## Path:        System/DNS
-## Description: Number of worker processes to spawn
-## Type:        integer
-## Default:     1
-## ServiceRestart:      kresd
-##
-#
-# Number of workers to spawn for kresd.
-# If you get start up failures with "already in use" your libuv is too
-# old and you have to stick to 1.
-#
-KRESD_WORKERS=1
-# Additional options
-KRESD_OPTIONS=
diff --git a/systemd/README.md b/systemd/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8ddbdc17d41fb38ca88383f75c006ac5768bd740
--- /dev/null
+++ b/systemd/README.md
@@ -0,0 +1,23 @@
+Running Knot Resolver under systemd (or equivalent) socket activation
+=====================================================================
+
+You can use the files in this directory to run kresd under supervision
+by systemd (or any supervisor that provides equivalent file descriptor
+initialization via the interface supported by
+sd_listen_fds_with_names(3)).
+
+When run in this configuration:
+
+ * it will be run under a non-privileged user, which means it will not
+   be able to open any new non-privileged ports.
+
+ * it will use a single process (implicitly uses --forks=1, and will
+   fail if that configuration variable is set to a different value).
+   If you want multiple daemons to listen on these ports publicly
+   concurrently, you'll need the supervisor to manage them
+   differently, for example via a systemd generator:
+
+     https://www.freedesktop.org/software/systemd/man/systemd.generator.html
+
+   If you have a useful systemd generator for multiple concurrent
+   processes, please contribute it upstream!
diff --git a/systemd/knot-resolver-control.socket b/systemd/knot-resolver-control.socket
new file mode 100644
index 0000000000000000000000000000000000000000..9f1976e8e77c2b3d59b9703e96af7d7454ad3e65
--- /dev/null
+++ b/systemd/knot-resolver-control.socket
@@ -0,0 +1,13 @@
+[Unit]
+Description=Knot DNS Resolver control socket
+Documentation=man:kresd(8)
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/knot-resolver/control
+FileDescriptorName=control
+Service=knot-resolver.service
+SocketMode=0660
+
+[Install]
+WantedBy=sockets.target
diff --git a/systemd/knot-resolver.service b/systemd/knot-resolver.service
new file mode 100644
index 0000000000000000000000000000000000000000..927b06a6ea1991370c1c623d43e2ac96377b9866
--- /dev/null
+++ b/systemd/knot-resolver.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Knot DNS Resolver daemon
+## This is a socket-activated service:
+RefuseManualStart=true
+
+[Service]
+Type=notify
+WorkingDirectory=/run/knot-resolver/cache
+ExecStart=/usr/sbin/kresd
+User=knot-resolver
+Restart=on-failure
+
+[Install]
+WantedBy=sockets.target
diff --git a/systemd/knot-resolver.socket b/systemd/knot-resolver.socket
new file mode 100644
index 0000000000000000000000000000000000000000..7d8953c233416474d5a082d78f9541a53cdefa4b
--- /dev/null
+++ b/systemd/knot-resolver.socket
@@ -0,0 +1,13 @@
+[Unit]
+Description=Knot DNS Resolver network listeners
+Documentation=man:kresd(8)
+Before=sockets.target
+
+[Socket]
+ListenStream=[::1]:53
+ListenDatagram=[::1]:53
+ListenStream=127.0.0.1:53
+ListenDatagram=127.0.0.1:53
+
+[Install]
+WantedBy=sockets.target
diff --git a/systemd/tmpfiles/knot-resolver.conf b/systemd/tmpfiles/knot-resolver.conf
new file mode 100644
index 0000000000000000000000000000000000000000..6006f030e17fdb465904c997a549203485794c11
--- /dev/null
+++ b/systemd/tmpfiles/knot-resolver.conf
@@ -0,0 +1,7 @@
+# tmpfiles.d(5) runtime directory for knot-resolver (kresd)
+#Type Path                            Mode UID           GID          Age Argument
+    d /run/knot-resolver              0750 root          root          -   -
+    d /run/knot-resolver/cache        0750 knot-resolver knot-resolver -   -
+    L /run/knot-resolver/cache/config 0750 knot-resolver knot-resolver -   /etc/knot-resolver/kresd.conf
+
+