diff --git a/lib/dnssec/nsec.c b/lib/dnssec/nsec.c
index 52bfb94300580cb8cae3ca31a964d3e640e4dc12..0b9193f076c9814f86f7c70e6f72c678b9171e8f 100644
--- a/lib/dnssec/nsec.c
+++ b/lib/dnssec/nsec.c
@@ -30,6 +30,7 @@
 bool kr_nsec_bitmap_contains_type(const uint8_t *bm, uint16_t bm_size, uint16_t type)
 {
 	if (!bm || bm_size == 0) {
+		assert(bm);
 		return false;
 	}
 
@@ -59,29 +60,52 @@ bool kr_nsec_bitmap_contains_type(const uint8_t *bm, uint16_t bm_size, uint16_t
 	return false;
 }
 
+int kr_nsec_children_in_zone_check(const uint8_t *bm, uint16_t bm_size)
+{
+	if (!bm) {
+		return kr_error(EINVAL);
+	}
+	const bool parent_side =
+		kr_nsec_bitmap_contains_type(bm, bm_size, KNOT_RRTYPE_DNAME)
+		|| (kr_nsec_bitmap_contains_type(bm, bm_size, KNOT_RRTYPE_NS)
+		    && !kr_nsec_bitmap_contains_type(bm, bm_size, KNOT_RRTYPE_SOA)
+		);
+	return parent_side ? abs(ENOENT) : kr_ok();
+	/* LATER: after refactoring, probably also check if signer name equals owner,
+	 * but even without that it's not possible to attack *correctly* signed zones.
+	 */
+}
+
 /**
  * Check whether the NSEC RR proves that there is no closer match for <SNAME, SCLASS>.
  * @param nsec  NSEC RRSet.
  * @param sname Searched name.
- * @return      0 or error code.
+ * @return      0 if proves, >0 if not (abs(ENOENT)), or error code (<0).
  */
-static int nsec_nonamematch(const knot_rrset_t *nsec, const knot_dname_t *sname)
+static int nsec_covers(const knot_rrset_t *nsec, const knot_dname_t *sname)
 {
 	assert(nsec && sname);
 	const knot_dname_t *next = knot_nsec_next(&nsec->rrs);
+	if (knot_dname_cmp(sname, nsec->owner) <= 0) {
+		return abs(ENOENT); /* 'sname' before 'owner', so can't be covered */
+	}
 	/* If NSEC 'owner' >= 'next', it means that there is nothing after 'owner' */
-	const bool is_last_nsec = (knot_dname_cmp(nsec->owner, next) >= 0);
-	if (is_last_nsec) { /* SNAME is after owner => provably doesn't exist */
-		if (knot_dname_cmp(nsec->owner, sname) < 0) {
-			return kr_ok();
-		}
-	} else {
-		/* Prove that SNAME is between 'owner' and 'next' */
-		if ((knot_dname_cmp(nsec->owner, sname) < 0) && (knot_dname_cmp(sname, next) < 0)) {
-			return kr_ok();
-		}
+	const bool is_last_nsec = knot_dname_cmp(nsec->owner, next) >= 0;
+	const bool in_range = is_last_nsec || knot_dname_cmp(sname, next) < 0;
+	if (!in_range) {
+		return abs(ENOENT);
 	}
-	return kr_error(EINVAL);
+	/* Before returning kr_ok(), we have to check a special case:
+	 * sname might be under delegation from owner and thus
+	 * not in the zone of this NSEC at all.
+	 */
+	if (!knot_dname_is_sub(sname, nsec->owner)) {
+		return kr_ok();
+	}
+	uint8_t *bm = NULL;
+	uint16_t bm_size = 0;
+	knot_nsec_bitmap(&nsec->rrs, &bm, &bm_size);
+	return kr_nsec_children_in_zone_check(bm, bm_size);
 }
 
 #define FLG_NOEXIST_RRTYPE (1 << 0) /**< <SNAME, SCLASS> exists, <SNAME, SCLASS, STYPE> does not exist. */
@@ -128,7 +152,7 @@ static int name_error_response_check_rr(int *flags, const knot_rrset_t *nsec,
 {
 	assert(flags && nsec && name);
 
-	if (nsec_nonamematch(nsec, name) == 0) {
+	if (nsec_covers(nsec, name) == 0) {
 		*flags |= FLG_NOEXIST_RRSET;
 	}
 
@@ -144,7 +168,7 @@ static int name_error_response_check_rr(int *flags, const knot_rrset_t *nsec,
 		*(--ptr) = '*';
 		*(--ptr) = 1;
 		/* True if this wildcard provably doesn't exist. */
-		if (nsec_nonamematch(nsec, ptr) == 0) {
+		if (nsec_covers(nsec, ptr) == 0) {
 			*flags |= FLG_NOEXIST_WILDCARD;
 			break;
 		}
@@ -391,7 +415,7 @@ int kr_nsec_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t
 		if (rrset->type != KNOT_RRTYPE_NSEC) {
 			continue;
 		}
-		if (nsec_nonamematch(rrset, sname) == 0) {
+		if (nsec_covers(rrset, sname) == 0) {
 			return kr_ok();
 		}
 	}
diff --git a/lib/dnssec/nsec.h b/lib/dnssec/nsec.h
index c86a9a980d4fba59367859475918985fd8c6b2ac..7d78a8cfb918c752e5e7ce9e069b461f4bfcc64c 100644
--- a/lib/dnssec/nsec.h
+++ b/lib/dnssec/nsec.h
@@ -20,13 +20,22 @@
 
 /**
  * Check whether bitmap contains given type.
- * @param bm      Bitmap.
+ * @param bm      Bitmap from NSEC or NSEC3.
  * @param bm_size Bitmap size.
  * @param type    RR type to search for.
  * @return        True if bitmap contains type.
  */
 bool kr_nsec_bitmap_contains_type(const uint8_t *bm, uint16_t bm_size, uint16_t type);
 
+/**
+ * Check bitmap that child names are contained in the same zone.
+ * @note see RFC6840 4.1.
+ * @param bm      Bitmap from NSEC or NSEC3.
+ * @param bm_size Bitmap size.
+ * @return 0 if they are, >0 if not (abs(ENOENT)), <0 on error.
+ */
+int kr_nsec_children_in_zone_check(const uint8_t *bm, uint16_t bm_size);
+
 /**
  * Check an NSEC or NSEC3 bitmap for NODATA for a type.
  * @param bm      Bitmap.
diff --git a/lib/dnssec/nsec3.c b/lib/dnssec/nsec3.c
index 88f6f119bc4f8a789fbdd5e9ae41ee0d76fb6226..da5539cfcd260c044fd76672a3eb04594b286779 100644
--- a/lib/dnssec/nsec3.c
+++ b/lib/dnssec/nsec3.c
@@ -391,6 +391,18 @@ static int closest_encloser_proof(const knot_pkt_t *pkt,
 		if (rrset->type != KNOT_RRTYPE_NSEC3) {
 			continue;
 		}
+		/* Also skip the NSEC3-to-match an ancestor of sname if it's
+		 * a parent-side delegation, as that would mean the owner
+		 * does not really exist (authoritatively in this zone,
+		 * even in case of opt-out).
+		 */
+		uint8_t *bm = NULL;
+		uint16_t bm_size;
+		knot_nsec3_bitmap(&rrset->rrs, 0, &bm, &bm_size);
+		if (kr_nsec_children_in_zone_check(bm, bm_size) != 0) {
+			continue; /* no fatal errors from bad RRs */
+		}
+		/* Match the NSEC3 to sname or one of its ancestors. */
 		unsigned skipped = 0;
 		flags = 0;
 		int ret = closest_encloser_match(&flags, rrset, sname, &skipped);
@@ -401,6 +413,7 @@ static int closest_encloser_proof(const knot_pkt_t *pkt,
 			continue;
 		}
 		matching = rrset;
+		/* Construct the next closer name and try to cover it. */
 		--skipped;
 		next_closer = sname;
 		for (unsigned j = 0; j < skipped; ++j) {