diff --git a/Knot.files b/Knot.files
index b60215cb63c396049aa8bf6c6305cb345be984c0..66d882fb000232b9068d4f214098be48a010093e 100644
--- a/Knot.files
+++ b/Knot.files
@@ -127,8 +127,8 @@ src/knot/nameserver/query_module.c
 src/knot/nameserver/query_module.h
 src/knot/nameserver/requestor.c
 src/knot/nameserver/requestor.h
-src/knot/nameserver/requestor_tsig.c
-src/knot/nameserver/requestor_tsig.h
+src/knot/nameserver/tsig_ctx.c
+src/knot/nameserver/tsig_ctx.h
 src/knot/nameserver/update.c
 src/knot/nameserver/update.h
 src/knot/other/debug.h
diff --git a/src/Makefile.am b/src/Makefile.am
index d3ce53a3bede5e07458910cbb5ef98dca26cad14..e61a2e18048f9fb18bd873a57d4288d04afec34a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -240,14 +240,14 @@ libknotd_la_SOURCES =				\
 	knot/nameserver/process_answer.h		\
 	knot/nameserver/requestor.c		\
 	knot/nameserver/requestor.h		\
-	knot/nameserver/requestor_tsig.c	\
-	knot/nameserver/requestor_tsig.h	\
 	knot/nameserver/query_module.c		\
 	knot/nameserver/query_module.h		\
 	knot/nameserver/update.c		\
 	knot/nameserver/update.h		\
 	knot/nameserver/notify.c			\
 	knot/nameserver/notify.h			\
+	knot/nameserver/tsig_ctx.c		\
+	knot/nameserver/tsig_ctx.h		\
 	knot/modules/synth_record.c		\
 	knot/modules/synth_record.h		\
 	knot/other/debug.h			\
diff --git a/src/knot/nameserver/axfr.c b/src/knot/nameserver/axfr.c
index da336d6cb14239bdb6077d8f1bdc766aad3f3cf9..57d4a25199e7d44274ce76c98c7eadbbf1c46163 100644
--- a/src/knot/nameserver/axfr.c
+++ b/src/knot/nameserver/axfr.c
@@ -318,10 +318,14 @@ int axfr_process_answer(knot_pkt_t *pkt, struct answer_data *data)
 	/* Initialize processing with first packet. */
 	int ret = KNOT_EOK;
 	if (data->ext == NULL) {
+		NS_NEED_TSIG_SIGNED(&data->param->tsig_ctx, 0);
+
 		ret = axfr_answer_init(data);
 		if (ret != KNOT_EOK) {
 			return NS_PROC_FAIL;
 		}
+	} else {
+		NS_NEED_TSIG_SIGNED(&data->param->tsig_ctx, 100);
 	}
 
 	/* Process answer packet. */
diff --git a/src/knot/nameserver/internet.c b/src/knot/nameserver/internet.c
index e27274618d0e36d6fb7225023fbc5f547a577114..d1f2c1c66dc3d070cbb6c007d00f805265a63e3f 100644
--- a/src/knot/nameserver/internet.c
+++ b/src/knot/nameserver/internet.c
@@ -899,6 +899,8 @@ int internet_process_answer(knot_pkt_t *pkt, struct answer_data *data)
 		return NS_PROC_FAIL;
 	}
 
+	NS_NEED_TSIG_SIGNED(&data->param->tsig_ctx, 0);
+
 	/* As of now, server can only issue SOA queries. */
 	switch(knot_pkt_qtype(pkt)) {
 	case KNOT_RRTYPE_SOA:
diff --git a/src/knot/nameserver/internet.h b/src/knot/nameserver/internet.h
index 6a74d124cdd8e1f5d070347e73c881b19367d79c..fe785a9e9dbb8c6e1b2c3710f4d14ff58bd96492 100644
--- a/src/knot/nameserver/internet.h
+++ b/src/knot/nameserver/internet.h
@@ -127,6 +127,11 @@ int ns_put_rr(knot_pkt_t *pkt, const knot_rrset_t *rr,
 		} \
 	}
 
+#define NS_NEED_TSIG_SIGNED(tsig_ctx, max_unsigned) \
+	if (tsig_unsigned_count(tsig_ctx) > max_unsigned) { \
+		return NS_PROC_FAIL; \
+	}
+
 #endif /* _KNOT_INTERNET_H_ */
 
 /*! @} */
diff --git a/src/knot/nameserver/notify.c b/src/knot/nameserver/notify.c
index bd51cb23fc3c42ce90ceabae03f3a779334e002a..acddf4f7cf11cdd857541bf609694f3a2d685bf1 100644
--- a/src/knot/nameserver/notify.c
+++ b/src/knot/nameserver/notify.c
@@ -33,6 +33,8 @@
 #include "knot/nameserver/internet.h"
 #include "common/debug.h"
 #include "knot/nameserver/process_query.h"
+#include "knot/nameserver/tsig_ctx.h"
+#include "knot/nameserver/process_answer.h"
 #include "libknot/dnssec/random.h"
 #include "libknot/rdata/soa.h"
 
@@ -84,5 +86,7 @@ int notify_query(knot_pkt_t *pkt, struct query_data *qdata)
 
 int notify_process_answer(knot_pkt_t *pkt, struct answer_data *data)
 {
+	NS_NEED_TSIG_SIGNED(&data->param->tsig_ctx, 0);
+
 	return NS_PROC_DONE; /* No processing. */
 }
diff --git a/src/knot/nameserver/process_answer.c b/src/knot/nameserver/process_answer.c
index 93be7568201f17fb8e5630c9712390d59b87b90d..692b3d30b854d54614e4325fd71768e4ad50e8d6 100644
--- a/src/knot/nameserver/process_answer.c
+++ b/src/knot/nameserver/process_answer.c
@@ -103,6 +103,12 @@ static int process_answer(knot_pkt_t *pkt, knot_process_t *ctx)
 	/* Class specific answer processing. */
 	ANSWER_REQUIRES(knot_pkt_qclass(pkt) == KNOT_CLASS_IN, NS_PROC_NOOP);
 
+	/* Verify incoming packet. */
+	int ret = tsig_verify_packet(&data->param->tsig_ctx, pkt);
+	if (ret != KNOT_EOK) {
+		return NS_PROC_FAIL;
+	}
+
 	/* Call appropriate processing handler. */
 	int next_state = NS_PROC_NOOP;
 	switch(knot_pkt_type(pkt)) {
diff --git a/src/knot/nameserver/process_answer.h b/src/knot/nameserver/process_answer.h
index 4d751dacb6ef7160f7bf7773c8d3fc93954ce10e..ec4495d15f48dffbbb60adcec9d956fb8c9aca62 100644
--- a/src/knot/nameserver/process_answer.h
+++ b/src/knot/nameserver/process_answer.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include "knot/nameserver/process_query.h"
+#include "knot/nameserver/tsig_ctx.h"
 
 /* Query processing module implementation. */
 const knot_process_module_t *process_answer_get_module(void);
@@ -35,6 +36,7 @@ struct process_answer_param {
 	zone_t   *zone;
 	const knot_pkt_t *query;
 	const struct sockaddr_storage *remote;
+	tsig_ctx_t tsig_ctx;
 };
 
 struct answer_data
diff --git a/src/knot/nameserver/requestor.c b/src/knot/nameserver/requestor.c
index fc6eae93d4def610abe558818d0b21524ac03d69..453e8d30ec1c6dd74cd11fd374d773a7825cf82f 100644
--- a/src/knot/nameserver/requestor.c
+++ b/src/knot/nameserver/requestor.c
@@ -55,7 +55,6 @@ static void request_close(mm_ctx_t *mm, struct request *request)
 	knot_process_finish(&request->process);
 
 	rem_node(&request->data.node);
-	requestor_tsig_cleanup(&request->data.tsig_ctx);
 	close(request->data.fd);
 	knot_pkt_free(&request->data.query);
 	mm_free(mm, request->pkt_buf);
@@ -155,11 +154,12 @@ struct request *requestor_make(struct requestor *requestor,
 		return NULL;
 	}
 
+	memcpy(&request->data.origin, &remote->via, sizeof(remote->via));
+	memcpy(&request->data.remote, &remote->addr, sizeof(remote->addr));
+
 	request->state = NS_PROC_DONE;
-	request->data.remote = remote;
 	request->data.fd = -1;
 	request->data.query = query;
-	requestor_tsig_init(&request->data.tsig_ctx, remote->key);
 	return request;
 }
 
@@ -170,8 +170,8 @@ int requestor_enqueue(struct requestor *requestor, struct request * request, voi
 	}
 
 	/* Fetch a bound socket. */
-	int fd = net_connected_socket(SOCK_STREAM, &request->data.remote->addr,
-	                              &request->data.remote->via, O_NONBLOCK);
+	int fd = net_connected_socket(SOCK_STREAM, &request->data.remote,
+	                              &request->data.origin, O_NONBLOCK);
 	if (fd < 0) {
 		return KNOT_ECONN;
 	}
@@ -199,39 +199,12 @@ int requestor_dequeue(struct requestor *requestor)
 	return KNOT_EOK;
 }
 
-/*!
- * \brief Sign outbound packet using TSIG.
- */
-static int request_sign(struct request *request)
-{
-	assert(request);
-
-	return requestor_tsig_sign_packet(&request->data.tsig_ctx,
-	                                  request->data.query);
-}
-
-/*!
- * \brief Check inbound packet TSIG signature.
- */
-static int request_verify(struct request *request)
-{
-	assert(request);
-
-	return requestor_tsig_verify_packet(&request->data.tsig_ctx,
-	                                    request->data.query);
-}
-
 static int exec_request(struct request *last, struct timeval *timeout)
 {
 	int ret = KNOT_EOK;
 
 	/* Process any pending data. */
 	if (last->state == NS_PROC_FULL) {
-		ret = request_sign(last);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-
 		ret = request_send(last, timeout);
 		if (ret != KNOT_EOK) {
 			return ret;
@@ -246,11 +219,6 @@ static int exec_request(struct request *last, struct timeval *timeout)
 			return rcvd;
 		}
 
-		ret = request_verify(last);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-
 		last->state = knot_process_in(last->pkt_buf, rcvd, &last->process);
 		if (last->state == NS_PROC_FAIL) {
 			return KNOT_EMALF;
diff --git a/src/knot/nameserver/requestor.h b/src/knot/nameserver/requestor.h
index 1f1ba2d5ef7d298f988633db430f723c70a9b82e..e849a3d03454a0fe5488e569f503230d0094137e 100644
--- a/src/knot/nameserver/requestor.h
+++ b/src/knot/nameserver/requestor.h
@@ -18,7 +18,6 @@
 
 #include "knot/nameserver/process_query.h"
 #include "knot/nameserver/process_answer.h"
-#include "knot/nameserver/requestor_tsig.h"
 #include "common/lists.h"
 
 struct request;
@@ -41,9 +40,8 @@ struct requestor {
 struct request_data {
 	node_t node;
 	int fd;
-	const conf_iface_t *remote;
+	struct sockaddr_storage remote, origin;
 	knot_pkt_t *query;
-	requestor_tsig_ctx_t tsig_ctx;
 };
 
 /*!
diff --git a/src/knot/nameserver/requestor_tsig.c b/src/knot/nameserver/tsig_ctx.c
similarity index 57%
rename from src/knot/nameserver/requestor_tsig.c
rename to src/knot/nameserver/tsig_ctx.c
index 5ad8f2eabf56b3dbbb966e45d900a58de9765b5d..9f2c6dae45109b7f9591589c545c0e930d4ccb62 100644
--- a/src/knot/nameserver/requestor_tsig.c
+++ b/src/knot/nameserver/tsig_ctx.c
@@ -21,9 +21,9 @@
 #include "common/errcode.h"
 #include "libknot/rdata/tsig.h"
 #include "libknot/tsig-op.h"
-#include "knot/nameserver/requestor_tsig.h"
+#include "knot/nameserver/tsig_ctx.h"
 
-void requestor_tsig_init(requestor_tsig_ctx_t *ctx, const knot_tsig_key_t *key)
+void tsig_init(tsig_ctx_t *ctx, const knot_tsig_key_t *key)
 {
 	if (!ctx) {
 		return;
@@ -31,19 +31,12 @@ void requestor_tsig_init(requestor_tsig_ctx_t *ctx, const knot_tsig_key_t *key)
 
 	memset(ctx, 0, sizeof(*ctx));
 	ctx->key = key;
-}
 
-void requestor_tsig_cleanup(requestor_tsig_ctx_t *ctx)
-{
-	if (!ctx) {
-		return;
-	}
 
-	free(ctx->digest);
-	memset(ctx, 0, sizeof(*ctx));
+
 }
 
-int requestor_tsig_sign_packet(requestor_tsig_ctx_t *ctx, knot_pkt_t *packet)
+int tsig_sign_packet(tsig_ctx_t *ctx, knot_pkt_t *packet)
 {
 	if (!ctx || !packet) {
 		return KNOT_EINVAL;
@@ -54,14 +47,8 @@ int requestor_tsig_sign_packet(requestor_tsig_ctx_t *ctx, knot_pkt_t *packet)
 	}
 
 	int ret = KNOT_ERROR;
-
 	if (ctx->digest_size == 0) {
 		ctx->digest_size = knot_tsig_digest_length(ctx->key->algorithm);
-		ctx->digest = malloc(ctx->digest_size);
-		if (!ctx->digest) {
-			return KNOT_ENOMEM;
-		}
-
 		ret = knot_tsig_sign(packet->wire, &packet->size, packet->max_size,
 		                     NULL, 0,
 		                     ctx->digest, &ctx->digest_size,
@@ -79,7 +66,23 @@ int requestor_tsig_sign_packet(requestor_tsig_ctx_t *ctx, knot_pkt_t *packet)
 	return ret;
 }
 
-int requestor_tsig_verify_packet(requestor_tsig_ctx_t *ctx, knot_pkt_t *packet)
+static int update_ctx_after_verify(tsig_ctx_t *ctx, knot_rrset_t *tsig_rr)
+{
+	assert(ctx);
+	assert(tsig_rr);
+
+	if (ctx->digest_size != tsig_rdata_mac_length(tsig_rr)) {
+		return KNOT_EMALF;
+	}
+
+	memcpy(ctx->digest, tsig_rdata_mac(tsig_rr), ctx->digest_size);
+	ctx->last_signed = tsig_rdata_time_signed(tsig_rr);
+	ctx->unsigned_count = 0;
+
+	return KNOT_EOK;
+}
+
+int tsig_verify_packet(tsig_ctx_t *ctx, knot_pkt_t *packet)
 {
 	if (!ctx || !packet) {
 		return KNOT_EINVAL;
@@ -89,7 +92,40 @@ int requestor_tsig_verify_packet(requestor_tsig_ctx_t *ctx, knot_pkt_t *packet)
 		return KNOT_EOK;
 	}
 
-	#warning "TODO: TSIG verify invocation."
-	//return KNOT_ENOTSUP;
+	if (packet->tsig_rr == NULL) {
+		ctx->unsigned_count += 1;
+		return KNOT_EOK;
+	}
+
+	int ret = KNOT_ERROR;
+	if (ctx->last_signed == 0) {
+		ret = knot_tsig_client_check(packet->tsig_rr, packet->wire,
+		                             packet->size, ctx->digest,
+		                             ctx->digest_size, ctx->key, 0);
+	} else {
+		ret = knot_tsig_client_check_next(packet->tsig_rr, packet->wire,
+		                                  packet->size, ctx->digest,
+		                                  ctx->digest_size, ctx->key,
+		                                  ctx->last_signed);
+	}
+
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	ret = update_ctx_after_verify(ctx, packet->tsig_rr);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
 	return KNOT_EOK;
 }
+
+unsigned tsig_unsigned_count(tsig_ctx_t *ctx)
+{
+	if (!ctx) {
+		return -1;
+	}
+
+	return ctx->unsigned_count;
+}
diff --git a/src/knot/nameserver/requestor_tsig.h b/src/knot/nameserver/tsig_ctx.h
similarity index 64%
rename from src/knot/nameserver/requestor_tsig.h
rename to src/knot/nameserver/tsig_ctx.h
index cb2ea25c934bfa6bc3b6e05e0bcb60b7678371cd..cba3693c9317388fe1e52ac1764ecc2617c894f1 100644
--- a/src/knot/nameserver/requestor_tsig.h
+++ b/src/knot/nameserver/tsig_ctx.h
@@ -21,17 +21,26 @@
 
 #include "libknot/packet/pkt.h"
 #include "libknot/rdata/tsig.h"
+#include "libknot/tsig-op.h"
 
-typedef struct requestor_tsig_ctx {
+#define TSIG_MAX_DIGEST_SIZE 64
+
+typedef struct tsig_ctx {
 	const knot_tsig_key_t *key;
-	uint8_t *digest;
+	uint8_t digest[TSIG_MAX_DIGEST_SIZE];
 	size_t digest_size;
-} requestor_tsig_ctx_t;
 
-void requestor_tsig_init(requestor_tsig_ctx_t *ctx, const knot_tsig_key_t *key);
+	uint64_t last_signed;
+	unsigned unsigned_count;
+} tsig_ctx_t;
+
+void tsig_init(tsig_ctx_t *ctx, const knot_tsig_key_t *key);
 
-void requestor_tsig_cleanup(requestor_tsig_ctx_t *ctx);
+int tsig_sign_packet(tsig_ctx_t *ctx, knot_pkt_t *packet);
 
-int requestor_tsig_sign_packet(requestor_tsig_ctx_t *ctx, knot_pkt_t *packet);
+int tsig_verify_packet(tsig_ctx_t *ctx, knot_pkt_t *packet);
 
-int requestor_tsig_verify_packet(requestor_tsig_ctx_t *ctx, knot_pkt_t *packet);
+/*!
+ * \brief Get number of unsigned packets since the last signed one.
+ */
+unsigned tsig_unsigned_count(tsig_ctx_t *ctx);
diff --git a/src/knot/zone/events.c b/src/knot/zone/events.c
index 6aea40834e199109085d82e620913f8d7d30f4d1..2e0e35d38e772f9a82ed383c4c9c1c9bf80ec7d0 100644
--- a/src/knot/zone/events.c
+++ b/src/knot/zone/events.c
@@ -34,6 +34,7 @@
 #include "knot/nameserver/update.h"
 #include "knot/nameserver/notify.h"
 #include "knot/nameserver/requestor.h"
+#include "knot/nameserver/tsig_ctx.h"
 #include "knot/nameserver/process_answer.h"
 
 /* ------------------------- bootstrap timer logic -------------------------- */
@@ -108,6 +109,19 @@ static int zone_query_execute(zone_t *zone, uint16_t pkt_type, const conf_iface_
 		knot_pkt_put(query, COMPR_HINT_QNAME, &soa_rr, 0);
 	}
 
+	/* Answer processing parameters. */
+	struct process_answer_param param = { 0 };
+	param.zone = zone;
+	param.query = query;
+	param.remote = &remote->addr;
+	tsig_init(&param.tsig_ctx, remote->key);
+
+	ret = tsig_sign_packet(&param.tsig_ctx, query);
+	if (ret != KNOT_EOK) {
+		mp_delete(mm.ctx);
+		return ret;
+	}
+
 	/* Create requestor instance. */
 	struct requestor re;
 	requestor_init(&re, NS_PROC_ANSWER, &mm);
@@ -115,13 +129,10 @@ static int zone_query_execute(zone_t *zone, uint16_t pkt_type, const conf_iface_
 	/* Create a request. */
 	struct request *req = requestor_make(&re, remote, query);
 	if (req == NULL) {
+		mp_delete(mm.ctx);
 		return KNOT_ENOMEM;
 	}
 
-	struct process_answer_param param;
-	param.zone = zone;
-	param.query = query;
-	param.remote = &remote->addr;
 	requestor_enqueue(&re, req, &param);
 
 	/* Send the queries and process responses. */
@@ -330,7 +341,7 @@ static int event_update(zone_t *zone)
 
 	/* Create minimal query data context. */
 	struct process_query_param param = {0};
-	param.remote = &update->remote->addr;
+	param.remote = &update->remote;
 	struct query_data qdata = {0};
 	qdata.param = &param;
 	qdata.query = update->query;