diff --git a/distro/pkg/deb/libknot12.symbols b/distro/pkg/deb/libknot12.symbols
index 44859b3de830b8ae09157b455e77e6f1b3ba495f..55ed8c1dcfdddaf3ba0f19052ae68db8ebbf6ec8 100644
--- a/distro/pkg/deb/libknot12.symbols
+++ b/distro/pkg/deb/libknot12.symbols
@@ -116,6 +116,7 @@ libknot.so.12 libknot12 #MINVER#
  knot_rdataset_copy@Base 3.1.0
  knot_rdataset_eq@Base 3.1.0
  knot_rdataset_intersect@Base 3.1.0
+ knot_rdataset_intersect2@Base 3.2.0
  knot_rdataset_member@Base 3.1.0
  knot_rdataset_merge@Base 3.1.0
  knot_rdataset_subset@Base 3.2.0
diff --git a/doc/man/knot.conf.5in b/doc/man/knot.conf.5in
index 4a08769aeeb31f4944c452826e8e7bdfab1ef8e6..be80d3e53f35669f5a6d982e6039737ba97c72bc 100644
--- a/doc/man/knot.conf.5in
+++ b/doc/man/knot.conf.5in
@@ -1335,6 +1335,7 @@ policy:
     ds\-push: remote_id | remotes_id ...
     cds\-cdnskey\-publish: none | delete\-dnssec | rollover | always | double\-ds
     cds\-digest\-type: sha256 | sha384
+    dnskey\-management: full | incremental
     offline\-ksk: BOOL
     unsafe\-operation: none | no\-check\-keyset | no\-update\-dnskey | no\-update\-nsec | no\-update\-expired ...
 .ft P
@@ -1673,6 +1674,41 @@ more records depending on the keys available.
 Specify digest type for published CDS records.
 .sp
 \fIDefault:\fP sha256
+.SS dnskey\-management
+.sp
+Specify how the DNSKEY, CDNSKEY, and CDS RRSets at the zone apex are handled
+when (re\-)signing the zone.
+.sp
+Possible values:
+.INDENT 0.0
+.IP \(bu 2
+\fBfull\fP – Upon every zone (re\-)sign, delete all unknown DNSKEY, CDNSKEY, and CDS
+records and keep just those that are related to the zone keys stored in the KASP database.
+.IP \(bu 2
+\fBincremental\fP – Keep unknown DNSKEY, CDNSKEY, and CDS records in the zone, and
+modify server\-managed records incrementally by employing changes in the KASP database.
+.UNINDENT
+.sp
+\fBNOTE:\fP
+.INDENT 0.0
+.INDENT 3.5
+Prerequisites for \fIincremental\fP:
+.INDENT 0.0
+.IP \(bu 2
+The Offline KSK isn\(aqt supported.
+.IP \(bu 2
+The \fI\%delete\-delay\fP is long enough to cover possible daemon
+shutdown (e.g. due to server maintenance).
+.IP \(bu 2
+Avoided manual deletion of keys with keymgr\&.
+.UNINDENT
+.sp
+Otherwise there might remain some DNSKEY records in the zone, belonging to
+deleted keys.
+.UNINDENT
+.UNINDENT
+.sp
+\fIDefault:\fP full
 .SS offline\-ksk
 .sp
 Specifies if Offline KSK feature is enabled.
diff --git a/doc/reference.rst b/doc/reference.rst
index 4a110fa051a0915b0960188e7447158d641c8678..dbfa45e89e161a25515dc55cb8a977a5adfba807 100644
--- a/doc/reference.rst
+++ b/doc/reference.rst
@@ -1463,6 +1463,7 @@ DNSSEC policy configuration.
      ds-push: remote_id | remotes_id ...
      cds-cdnskey-publish: none | delete-dnssec | rollover | always | double-ds
      cds-digest-type: sha256 | sha384
+     dnskey-management: full | incremental
      offline-ksk: BOOL
      unsafe-operation: none | no-check-keyset | no-update-dnskey | no-update-nsec | no-update-expired ...
 
@@ -1835,6 +1836,34 @@ Specify digest type for published CDS records.
 
 *Default:* sha256
 
+.. _policy_dnskey-management:
+
+dnskey-management
+-----------------
+
+Specify how the DNSKEY, CDNSKEY, and CDS RRSets at the zone apex are handled
+when (re-)signing the zone.
+
+Possible values:
+
+- ``full`` – Upon every zone (re-)sign, delete all unknown DNSKEY, CDNSKEY, and CDS
+  records and keep just those that are related to the zone keys stored in the KASP database.
+- ``incremental`` – Keep unknown DNSKEY, CDNSKEY, and CDS records in the zone, and
+  modify server-managed records incrementally by employing changes in the KASP database.
+
+.. NOTE::
+   Prerequisites for *incremental*:
+
+   - The :ref:`Offline KSK <DNSSEC Offline KSK>` isn't supported.
+   - The :ref:`policy_delete-delay` is long enough to cover possible daemon
+     shutdown (e.g. due to server maintenance).
+   - Avoided manual deletion of keys with :doc:`keymgr<man_keymgr>`.
+
+   Otherwise there might remain some DNSKEY records in the zone, belonging to
+   deleted keys.
+
+*Default:* full
+
 .. _policy_offline-ksk:
 
 offline-ksk
diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c
index 7409abf5907effd99eb463fd21c57b120f2d8c78..7559301624ab188a8dd76f4c8c33098838ab419d 100644
--- a/src/knot/conf/schema.c
+++ b/src/knot/conf/schema.c
@@ -90,6 +90,12 @@ static const knot_lookup_t cds_cdnskey[] = {
 	{ 0, NULL }
 };
 
+static const knot_lookup_t dnskey_mgmt[] = {
+	{ DNSKEY_MGMT_FULL,        "full" },
+	{ DNSKEY_MGMT_INCREMENTAL, "incremental" },
+	{ 0, NULL }
+};
+
 static const knot_lookup_t cds_digesttype[] = {
 	{ DNSSEC_KEY_DIGEST_SHA256,   "sha256" },
 	{ DNSSEC_KEY_DIGEST_SHA384,   "sha384" },
@@ -414,6 +420,8 @@ static const yp_item_t desc_policy[] = {
 	                                   CONF_IO_FRLD_ZONES },
 	{ C_CDS_DIGESTTYPE,      YP_TOPT,  YP_VOPT = { cds_digesttype, DNSSEC_KEY_DIGEST_SHA256 },
 	                                   CONF_IO_FRLD_ZONES },
+	{ C_DNSKEY_MGMT,         YP_TOPT,  YP_VOPT = { dnskey_mgmt, DNSKEY_MGMT_FULL },
+	                                   CONF_IO_FRLD_ZONES },
 	{ C_OFFLINE_KSK,         YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES },
 	{ C_UNSAFE_OPERATION,    YP_TOPT,  YP_VOPT = { unsafe_operation, UNSAFE_NONE }, YP_FMULTI },
 	{ C_COMMENT,             YP_TSTR,  YP_VNONE },
diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h
index 43ac1f9b243a116b252efacaa8afa57e5ba92475..9617c00c7b8775e18b531254bfd711f84d68ffc2 100644
--- a/src/knot/conf/schema.h
+++ b/src/knot/conf/schema.h
@@ -47,6 +47,7 @@
 #define C_DBUS_EVENT		"\x0A""dbus-event"
 #define C_DDNS_MASTER		"\x0B""ddns-master"
 #define C_DENY			"\x04""deny"
+#define C_DNSKEY_MGMT		"\x11""dnskey-management"
 #define C_DNSKEY_TTL		"\x0A""dnskey-ttl"
 #define C_DNSSEC_POLICY		"\x0D""dnssec-policy"
 #define C_DNSSEC_SIGNING	"\x0E""dnssec-signing"
@@ -202,6 +203,11 @@ enum {
 	CDS_CDNSKEY_DOUBLE_DS = 4,
 };
 
+enum {
+	DNSKEY_MGMT_FULL        = 0,
+	DNSKEY_MGMT_INCREMENTAL = 1,
+};
+
 enum {
 	SERIAL_POLICY_INCREMENT  = 1,
 	SERIAL_POLICY_UNIXTIME   = 2,
diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c
index 4390ffaf9f40cccf636fc3e00b91b0f62ef6647c..0b423e28115dccd126fc570426bb4f9b7814f3ac 100644
--- a/src/knot/conf/tools.c
+++ b/src/knot/conf/tools.c
@@ -655,6 +655,23 @@ int check_policy(
 		}
 	}
 
+	conf_val_t dnskey_mgmt = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY,
+	                                            C_DNSKEY_MGMT, args->id, args->id_len);
+	conf_val_t offline_ksk = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY,
+	                                            C_OFFLINE_KSK, args->id, args->id_len);
+	conf_val_t delete_dely = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY,
+	                                            C_DELETE_DELAY, args->id, args->id_len);
+	if (conf_opt(&dnskey_mgmt) != DNSKEY_MGMT_FULL) {
+		if (conf_bool(&offline_ksk)) {
+			args->err_str = "incremental DNSKEY management can't be used with offline-ksk";
+			return KNOT_EINVAL;
+		}
+		if (conf_int(&delete_dely) <= 0) {
+			args->err_str = "incremental DNSKEY management requires configured delete-delay";
+			return KNOT_EINVAL;
+		}
+	}
+
 	return KNOT_EOK;
 }
 
diff --git a/src/knot/dnssec/context.c b/src/knot/dnssec/context.c
index 132948fe339001130513b90e6f0626ee78bb7297..1e045b3760b81ee9ea22a76b8991f9b73c422de0 100644
--- a/src/knot/dnssec/context.c
+++ b/src/knot/dnssec/context.c
@@ -112,6 +112,9 @@ static void policy_load(knot_kasp_policy_t *policy, conf_t *conf, conf_val_t *id
 	val = conf_id_get(conf, C_POLICY, C_CDS_DIGESTTYPE, id);
 	policy->cds_dt = conf_opt(&val);
 
+	val = conf_id_get(conf, C_POLICY, C_DNSKEY_MGMT, id);
+	policy->incremental = (conf_opt(&val) == DNSKEY_MGMT_INCREMENTAL);
+
 	conf_val_t ksk_sbm = conf_id_get(conf, C_POLICY, C_KSK_SBM, id);
 	if (ksk_sbm.code == KNOT_EOK) {
 		val = conf_id_get(conf, C_SBM, C_CHK_INTERVAL, &ksk_sbm);
diff --git a/src/knot/dnssec/kasp/policy.h b/src/knot/dnssec/kasp/policy.h
index 5e6d110f39a2f3ee72598296c5f87b8d6e7b124e..a56bb8b0aeaeeb2b8f053727de902143ff626caf 100644
--- a/src/knot/dnssec/kasp/policy.h
+++ b/src/knot/dnssec/kasp/policy.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -128,6 +128,7 @@ typedef struct {
 	uint16_t signing_threads;
 	bool ds_push;
 	bool offline_ksk;
+	bool incremental;
 	unsigned unsafe;
 } knot_kasp_policy_t;
 // TODO make the time parameters knot_timediff_t ??
diff --git a/src/knot/dnssec/key_records.c b/src/knot/dnssec/key_records.c
index 88b102bd294219e2fbb750398bc35cb5cb8fc60b..14788c41dd6deea7bc59c4cdf8faaa80ac465f9a 100644
--- a/src/knot/dnssec/key_records.c
+++ b/src/knot/dnssec/key_records.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -108,6 +108,34 @@ int key_records_to_changeset(const key_records_t *r, changeset_t *ch,
 	return ret;
 }
 
+static int subtract_one(knot_rrset_t *from, const knot_rrset_t *what,
+                        int (*fcn)(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *),
+                        int ret)
+{
+	if (ret == KNOT_EOK && !knot_rrset_empty(from)) {
+		ret = fcn(&from->rrs, &what->rrs, NULL);
+	}
+	return ret;
+}
+
+int key_records_subtract(key_records_t *r, const key_records_t *against)
+{
+	int ret = KNOT_EOK;
+	ret = subtract_one(&r->dnskey,  &against->dnskey,  knot_rdataset_subtract, ret);
+	ret = subtract_one(&r->cdnskey, &against->cdnskey, knot_rdataset_subtract, ret);
+	ret = subtract_one(&r->cds,     &against->cds,     knot_rdataset_subtract, ret);
+	return ret;
+}
+
+int key_records_intersect(key_records_t *r, const key_records_t *against)
+{
+	int ret = KNOT_EOK;
+	ret = subtract_one(&r->dnskey,  &against->dnskey,  knot_rdataset_intersect2, ret);
+	ret = subtract_one(&r->cdnskey, &against->cdnskey, knot_rdataset_intersect2, ret);
+	ret = subtract_one(&r->cds,     &against->cds,     knot_rdataset_intersect2, ret);
+	return ret;
+}
+
 int key_records_dump(char **buf, size_t *buf_size, const key_records_t *r, bool verbose)
 {
 	if (*buf == NULL) {
diff --git a/src/knot/dnssec/key_records.h b/src/knot/dnssec/key_records.h
index 0d1c3516973db1cfd4346c7843bee23359c1a249..1f43467b057d267aa85406527353e54e9c870529 100644
--- a/src/knot/dnssec/key_records.h
+++ b/src/knot/dnssec/key_records.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -33,6 +33,10 @@ void key_records_clear_rdatasets(key_records_t *r);
 int key_records_to_changeset(const key_records_t *r, changeset_t *ch,
                              bool rem, changeset_flag_t chfl);
 
+int key_records_subtract(key_records_t *r, const key_records_t *against);
+
+int key_records_intersect(key_records_t *r, const key_records_t *against);
+
 int key_records_dump(char **buf, size_t *buf_size, const key_records_t *r, bool verbose);
 
 int key_records_sign(const zone_key_t *key, key_records_t *r, const kdnssec_ctx_t *kctx, knot_time_t *expires);
diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c
index 2822c4578eaddfe5fd735adfee6828f2d4c25177..e2cc164a5307ef13dd07b85f1c35bbd46de878be 100644
--- a/src/knot/dnssec/zone-sign.c
+++ b/src/knot/dnssec/zone-sign.c
@@ -775,26 +775,52 @@ keyptr_dynarray_t knot_zone_sign_get_cdnskeys(const kdnssec_ctx_t *ctx,
 }
 
 int knot_zone_sign_add_dnskeys(zone_keyset_t *zone_keys, const kdnssec_ctx_t *dnssec_ctx,
-			       key_records_t *add_r)
+                               key_records_t *add_r, key_records_t *rem_r, key_records_t *orig_r)
 {
-	if (add_r == NULL) {
+	if (add_r == NULL || (rem_r != NULL && orig_r == NULL)) {
 		return KNOT_EINVAL;
 	}
+
+	bool incremental = (dnssec_ctx->policy->incremental && rem_r != NULL);
+	dnssec_key_digest_t cds_dt = dnssec_ctx->policy->cds_dt;
 	int ret = KNOT_EOK;
+
 	for (int i = 0; i < zone_keys->count; i++) {
 		zone_key_t *key = &zone_keys->keys[i];
 		if (key->is_public) {
 			ret = rrset_add_zone_key(&add_r->dnskey, key);
-			if (ret != KNOT_EOK) {
-				return ret;
-			}
+		} else if (incremental) {
+			ret = rrset_add_zone_key(&rem_r->dnskey, key);
+		}
+
+		// add all possible known CDNSKEYs and CDSs to removals. Sort it out later
+		if (incremental && ret == KNOT_EOK) {
+			ret = rrset_add_zone_key(&rem_r->cdnskey, key);
+		}
+		if (incremental && ret == KNOT_EOK) {
+			ret = rrset_add_zone_ds(&rem_r->cds, key, cds_dt);
+		}
+
+		if (ret != KNOT_EOK) {
+			return ret;
 		}
 	}
+
 	keyptr_dynarray_t kcdnskeys = knot_zone_sign_get_cdnskeys(dnssec_ctx, zone_keys);
 	knot_dynarray_foreach(keyptr, zone_key_t *, ksk_for_cds, kcdnskeys) {
 		ret = rrset_add_zone_key(&add_r->cdnskey, *ksk_for_cds);
 		if (ret == KNOT_EOK) {
-			ret = rrset_add_zone_ds(&add_r->cds, *ksk_for_cds, dnssec_ctx->policy->cds_dt);
+			ret = rrset_add_zone_ds(&add_r->cds, *ksk_for_cds, cds_dt);
+		}
+	}
+
+	if (incremental && ret == KNOT_EOK) { // else rem_r is empty
+		ret = key_records_subtract(rem_r, add_r);
+		if (ret == KNOT_EOK) {
+			ret = key_records_intersect(rem_r, orig_r);
+		}
+		if (ret == KNOT_EOK) {
+			ret = key_records_subtract(add_r, orig_r);
 		}
 	}
 
@@ -825,8 +851,9 @@ int knot_zone_sign_update_dnskeys(zone_update_t *update,
 	}
 
 	const zone_node_t *apex = update->new_cont->apex;
-	key_records_t add_r, orig_r;
+	key_records_t add_r, rem_r, orig_r;
 	memset(&add_r, 0, sizeof(add_r));
+	memset(&rem_r, 0, sizeof(rem_r));
 	key_records_from_apex(apex, &orig_r);
 	knot_rrset_t soa = node_rrset(apex, KNOT_RRTYPE_SOA);
 	if (knot_rrset_empty(&soa)) {
@@ -841,12 +868,14 @@ int knot_zone_sign_update_dnskeys(zone_update_t *update,
 
 #define CHECK_RET if (ret != KNOT_EOK) goto cleanup
 
-	// remove all. This will cancel out with additions later
-	ret = key_records_to_changeset(&orig_r, &ch, true, 0);
-	CHECK_RET;
+	if (!dnssec_ctx->policy->incremental) {
+		// remove all. This will cancel out with additions later
+		ret = key_records_to_changeset(&orig_r, &ch, true, 0);
+		CHECK_RET;
+	}
 
-	// add DNSKEYs, CDNSKEYs and CDSs
 	key_records_init(dnssec_ctx, &add_r);
+	key_records_init(dnssec_ctx, &rem_r);
 
 	if (dnssec_ctx->policy->offline_ksk) {
 		ret = kasp_db_load_offline_records(dnssec_ctx->kasp_db, apex->owner, dnssec_ctx->now, next_resign, &add_r);
@@ -859,10 +888,12 @@ int knot_zone_sign_update_dnskeys(zone_update_t *update,
 			                 knot_strerror(ret));
 		}
 	} else {
-		ret = knot_zone_sign_add_dnskeys(zone_keys, dnssec_ctx, &add_r);
+		ret = knot_zone_sign_add_dnskeys(zone_keys, dnssec_ctx, &add_r, &rem_r, &orig_r);
 	}
 	CHECK_RET;
 
+	ret = key_records_to_changeset(&rem_r, &ch, true, CHANGESET_CHECK);
+	CHECK_RET;
 	ret = key_records_to_changeset(&add_r, &ch, false, CHANGESET_CHECK);
 	CHECK_RET;
 	if (dnssec_ctx->policy->ds_push && node_rrtype_exists(ch.add->apex, KNOT_RRTYPE_CDS)) {
@@ -871,6 +902,7 @@ int knot_zone_sign_update_dnskeys(zone_update_t *update,
 		zone_events_schedule_at(update->zone, ZONE_EVENT_DS_PUSH, update->zone->timers.next_ds_push);
 	}
 
+	assert(knot_rrset_empty(&rem_r.rrsig));
 	if (!knot_rrset_empty(&add_r.rrsig)) {
 		dnssec_ctx->offline_rrsig = knot_rrset_copy(&add_r.rrsig, NULL);
 	}
@@ -881,6 +913,7 @@ int knot_zone_sign_update_dnskeys(zone_update_t *update,
 
 cleanup:
 	key_records_clear(&add_r);
+	key_records_clear(&rem_r);
 	changeset_clear(&ch);
 	return ret;
 }
diff --git a/src/knot/dnssec/zone-sign.h b/src/knot/dnssec/zone-sign.h
index 2f75d7a33473bc62fde29adbcb65169767244daa..c3990a52928a3051d389349d4cc1f190bf8a03b7 100644
--- a/src/knot/dnssec/zone-sign.h
+++ b/src/knot/dnssec/zone-sign.h
@@ -32,11 +32,13 @@ bool rrsig_covers_type(const knot_rrset_t *rrsig, uint16_t type);
  * \param zone_keys     Zone keyset.
  * \param dnssec_ctx    KASP context.
  * \param add_r         RRSets to be added.
+ * \param rem_r         RRSets to be removed (only for incremental policy).
+ * \param orig_r        RRSets that was originally in zone (only for incremental policy).
  *
  * \return KNOT_E*
  */
 int knot_zone_sign_add_dnskeys(zone_keyset_t *zone_keys, const kdnssec_ctx_t *dnssec_ctx,
-			       key_records_t *add_r);
+                               key_records_t *add_r, key_records_t *rem_r, key_records_t *orig_r);
 
 /*!
  * \brief Adds/removes DNSKEY (and CDNSKEY, CDS) records to zone according to zone keyset.
diff --git a/src/libknot/rdataset.c b/src/libknot/rdataset.c
index 0b9b4fd8c611358a74ad1f62e3733cf7965966b8..03e078486f27a6bb4a2ba6882f0df82d25d306b5 100644
--- a/src/libknot/rdataset.c
+++ b/src/libknot/rdataset.c
@@ -309,6 +309,38 @@ int knot_rdataset_intersect(const knot_rdataset_t *rrs1, const knot_rdataset_t *
 	return KNOT_EOK;
 }
 
+_public_
+int knot_rdataset_intersect2(knot_rdataset_t *from, const knot_rdataset_t *what,
+                             knot_mm_t *mm)
+{
+	if (from == NULL || what == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	if (from->rdata == what->rdata) {
+		return KNOT_EOK;
+	}
+
+	knot_rdata_t *rr1 = from->rdata;
+	for (uint16_t i = 0; i < from->count; ) {
+		if (!knot_rdataset_member(what, rr1)) {
+			int ret = remove_rr_at(from, i, mm);
+			if (ret != KNOT_EOK) {
+				return ret;
+			}
+			if (i < from->count) {
+				// Just to make sure rr1 remains valid if re-allocated.
+				rr1 = rr_seek(from, i);
+			}
+		} else {
+			i++;
+			rr1 = knot_rdataset_next(rr1);
+		}
+	}
+
+	return KNOT_EOK;
+}
+
 _public_
 int knot_rdataset_subtract(knot_rdataset_t *from, const knot_rdataset_t *what,
                            knot_mm_t *mm)
diff --git a/src/libknot/rdataset.h b/src/libknot/rdataset.h
index f3e65a86e63b49277c1af3a6ddd1d4e9d49eeb0b..6d9808663e93a85e84a492e4835e5a976c83523b 100644
--- a/src/libknot/rdataset.h
+++ b/src/libknot/rdataset.h
@@ -171,6 +171,18 @@ int knot_rdataset_merge(knot_rdataset_t *rrs1, const knot_rdataset_t *rrs2,
 int knot_rdataset_intersect(const knot_rdataset_t *rrs1, const knot_rdataset_t *rrs2,
                             knot_rdataset_t *out, knot_mm_t *mm);
 
+/*!
+ * \brief Does set-like RRS intersection. \a from RRS is changed.
+ *
+ * \param from  RRS to be modified by intersection.
+ * \param what  RRS to intersect.
+ * \param mm    Memory context use to reallocated \a from data.
+ *
+ * \return KNOT_E*
+ */
+int knot_rdataset_intersect2(knot_rdataset_t *from, const knot_rdataset_t *what,
+                             knot_mm_t *mm);
+
 /*!
  * \brief Does set-like RRS subtraction. \a from RRS is changed.
  *
diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c
index 7eeca12a434dde65fb8b470fbd1f69c8a40b0b77..438230babbbffd973847fc70deee44e93db4cec3 100644
--- a/src/utils/keymgr/offline_ksk.c
+++ b/src/utils/keymgr/offline_ksk.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -297,7 +297,7 @@ static int ksr_sign_dnskey(kdnssec_ctx_t *ctx, knot_rrset_t *zsk, knot_time_t no
 	key_records_t r;
 	key_records_init(ctx, &r);
 
-	ret = knot_zone_sign_add_dnskeys(&keyset, ctx, &r);
+	ret = knot_zone_sign_add_dnskeys(&keyset, ctx, &r, NULL, NULL);
 	if (ret != KNOT_EOK) {
 		goto done;
 	}
diff --git a/tests-extra/tests/dnssec/key_rollovers/test.py b/tests-extra/tests/dnssec/key_rollovers/test.py
index a102a3b40b621490dcc99e0a86a110f5c6b83f3f..51f4b2f23be8a825516b7eddf881cc62029a8f4d 100644
--- a/tests-extra/tests/dnssec/key_rollovers/test.py
+++ b/tests-extra/tests/dnssec/key_rollovers/test.py
@@ -16,16 +16,17 @@ from dnstest.utils import *
 from dnstest.keys import Keymgr
 from dnstest.test import Test
 
-PUB_ONLY_SCENARIO = random.choice([0, 1, 2])
+PUB_ONLY_SCENARIO = random.choice([0, 0, 1, 2]) # Higher probability of zero to give INCREMENTAL a chance
 PUB_ONLY_KEYS = 1 if PUB_ONLY_SCENARIO > 0 else 0
 PUB_ONLY_CDS = 1 if PUB_ONLY_SCENARIO > 1 else 0
 PUB_ONLY_KEYID = ""
 DELETE_DELAY = random.choice([0, 2, 7, 17, 117])
+INCREMENTAL = random.choice([True, False]) if DELETE_DELAY > 0 and PUB_ONLY_KEYS == 0 else False
 
 DOUBLE_DS = random.choice([True, False])
 CDS_DT = random.choice(["sha256", "sha384"])
-check_log("DOUBLE DS %s, cds dt %s, PUB_ONLY_KEYS %d, PUB_ONLY_CDS %d DELETE_DELAY %d" % \
-          (str(DOUBLE_DS), CDS_DT, PUB_ONLY_KEYS, PUB_ONLY_CDS, DELETE_DELAY))
+check_log("DOUBLE DS %s, cds dt %s, PUB_ONLY_KEYS %d, PUB_ONLY_CDS %d, DELETE_DELAY %d, INCREMENTAL %s" % \
+          (str(DOUBLE_DS), CDS_DT, PUB_ONLY_KEYS, PUB_ONLY_CDS, DELETE_DELAY, str(INCREMENTAL)))
 
 def generate_public_only(server, zone, alg):
     global PUB_ONLY_KEYID
@@ -274,6 +275,7 @@ child.dnssec(child_zone).dnskey_ttl = 2
 child.dnssec(child_zone).zsk_lifetime = 99999
 child.dnssec(child_zone).ksk_lifetime = 300 # this can be possibly left also infinity
 child.dnssec(child_zone).delete_delay = DELETE_DELAY
+child.dnssec(child_zone).dnskey_mgmt = "incremental" if INCREMENTAL else "full"
 child.dnssec(child_zone).propagation_delay = 11
 child.dnssec(child_zone).ksk_sbm_check = [ parent ]
 child.dnssec(child_zone).ksk_sbm_check_interval = 2
diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py
index 4f3a9b7cc9982a3b21f197044dbce69ffd6912ef..fae1f3e6cf3a92929900b8e6d8425148664cd754 100644
--- a/tests-extra/tools/dnstest/server.py
+++ b/tests-extra/tools/dnstest/server.py
@@ -70,6 +70,7 @@ class ZoneDnssec(object):
         self.shared_policy_with = None
         self.cds_publish = None
         self.cds_digesttype = None
+        self.dnskey_mgmt = None
         self.offline_ksk = None
 
 class Zone(object):
@@ -1447,6 +1448,7 @@ class Knot(Server):
             self._str(s, "cds-cdnskey-publish", z.dnssec.cds_publish)
             if z.dnssec.cds_digesttype:
                 self._str(s, "cds-digest-type", z.dnssec.cds_digesttype)
+            self._str(s, "dnskey-management", z.dnssec.dnskey_mgmt)
             self._bool(s, "offline-ksk", z.dnssec.offline_ksk)
             self._str(s, "signing-threads", str(random.randint(1,4)))
         if have_policy:
diff --git a/tests/libknot/test_rdataset.c b/tests/libknot/test_rdataset.c
index 572cc49a1ecc71e60b29da7790f40c6ca20efa99..d364be3c3ca0a32f41c9439d4b8890d82de0a5c6 100644
--- a/tests/libknot/test_rdataset.c
+++ b/tests/libknot/test_rdataset.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -165,6 +165,28 @@ int main(int argc, char *argv[])
 	ok(intersect_ok, "rdataset: intersect normal.");
 	knot_rdataset_clear(&intersection, NULL);
 
+	// Test intersect2
+	ok(knot_rdataset_intersect2(NULL, NULL, NULL) == KNOT_EINVAL,
+	   "rdataset: intersect2 NULL.");
+
+	ret = knot_rdataset_intersect2(&rdataset, &rdataset, NULL);
+	intersect_ok = ret == KNOT_EOK && knot_rdataset_eq(&rdataset, &rdataset);
+	ok(intersect_ok, "rdataset: intersect2 self.");
+	knot_rdataset_clear(&intersection, NULL);
+
+	RDATASET_INIT_WITH(rdataset_lo, rdata_lo);
+	RDATASET_INIT_WITH(rdataset_gt, rdata_gt);
+	ret = knot_rdataset_intersect2(&rdataset_lo, &rdataset_gt, NULL);
+	intersect_ok = ret == KNOT_EOK && rdataset_lo.count == 0;
+	ok(intersect_ok, "rdataset: intersect2 no common.");
+
+	ret = knot_rdataset_copy(&copy, &rdataset, NULL);
+	assert(ret == KNOT_EOK);
+	ret = knot_rdataset_intersect2(&copy, &rdataset_lo, NULL);
+	intersect_ok = ret == KNOT_EOK && knot_rdataset_eq(&copy, &rdataset_lo);
+	ok(intersect_ok, "rdataset: intersect2 normal.");
+	knot_rdataset_clear(&copy, NULL);
+
 	// Test subtract
 	ok(knot_rdataset_subtract(NULL, NULL, NULL) == KNOT_EINVAL,
 	   "rdataset: subtract NULL.");