diff --git a/src/etc/inc/easyrule.inc b/src/etc/inc/easyrule.inc
index 72bd0e3ad8..cb4e988317 100644
--- a/src/etc/inc/easyrule.inc
+++ b/src/etc/inc/easyrule.inc
@@ -116,7 +116,7 @@ function easyrule_block_rule_exists($int = 'wan', $ipproto = "inet") {
 }
 
 function easyrule_block_rule_create($int = 'wan', $ipproto = "inet") {
-	global $blockaliasname, $config;
+	global $blockaliasname;
 	/* If the alias doesn't exist, exit.
 	 * Can't create an empty alias, and we don't know a host */
 	if (easyrule_block_alias_getid($int) === false) {
@@ -144,20 +144,15 @@ function easyrule_block_rule_create($int = 'wan', $ipproto = "inet") {
 	$filterent['created'] = make_config_revision_entry(null, "EasyRule");
 	$filterent['tracker'] = (int)microtime(true);
 
-	// Refer to firewall_rules_edit.php separators updating code.
-	// Using same code, variables, and techniques here.
-	$after = -1;	// Place rule at top and move all separators.
-	array_splice($a_filter, $after+1, 0, array($filterent));
+	// place the rule on top
+	$ridx = get_interface_ruleindex($int);
+	array_splice($a_filter, $ridx['first'], 0, array($filterent));
 	config_set_path('filter/rule', $a_filter);
 
-	$tmpif = $int;
-
-	// Update the separators
-	$a_separators = config_get_path('filter/separator/' . strtolower($tmpif), []);
-	$ridx = ifridx($tmpif, $after);	// get rule index within interface
-	$mvnrows = +1;
-	move_separators($a_separators, $ridx, $mvnrows);
-	config_set_path('filter/separator/' . strtolower($tmpif), $a_separators);
+	// shift the separators
+	$a_separators = config_get_path('filter/separator/' . strtolower($int), []);
+	shift_separators($a_separators, -1);
+	config_set_path('filter/separator/' . strtolower($int), $a_separators);
 
 	return true;
 }
diff --git a/src/etc/inc/filter.inc b/src/etc/inc/filter.inc
index 255af0903c..6399c5a5e9 100644
--- a/src/etc/inc/filter.inc
+++ b/src/etc/inc/filter.inc
@@ -4736,23 +4736,61 @@ function filter_get_antilockout_ports($wantarray = false) {
 
 }
 
-/* get rule index within interface */
-function ifridx($if, $ridx) {
-	if ($ridx < 0) {
-		return $ridx;
-	}
-
-	$i = $ifridx = 0;
-	foreach (config_get_path('filter/rule',[]) as $filterent) {
-		if (($filterent['interface'] == $if && !isset($filterent['floating'])) || (isset($filterent['floating']) && "FloatingRules" == $if)) {
-			if ($i == $ridx) {
-				return $ifridx;
+/**
+ * Get the first and last rule's index for an interface, and optionally the
+ * relative index of a rule within the interface.
+ * 
+ * @param string $if The interface used to calculate the rule indices
+ * @param int $idx (optional) A rule index
+ * 
+ * @return array Contains the matching interface's first and last rule's index,
+ *               and the matching rule's relative index if found.
+ */
+function get_interface_ruleindex(string $interface, ?int $rule_index = null): array {
+	if (isset($rule_index) && ($rule_index < 0)) {
+		unset($rule_index);
+	}
+	$interface_rule_index = [
+		'index' => null,
+		'first' => null,
+		'last' => null
+	];
+	if (empty($interface)) {
+		return $interface_rule_index;
+	}
+
+	$interface = strtolower($interface);
+	$relative_index_count = -1; // a valid index count starts at 0
+	foreach (config_get_path('filter/rule',[]) as $idx => $rule) {
+		// skip rules on unrelated interfaces
+		if ((isset($rule['floating']) && ($interface != "floatingrules")) ||
+		    (!isset($rule['floating']) && ($interface != $rule['interface']))) {
+			if (isset($rule_index) && isset($interface_rule_index['first']) && ($idx >= $rule_index)) {
+				// the last rule on the matching interface has already been reached
+				break;
 			}
-			$ifridx++;
+			continue;
+		}
+		// this rule has a matching interface, increment the count
+		$relative_index_count++;
+
+		// save the interface's first rule's index
+		if ($relative_index_count == 0) {
+			$interface_rule_index['first'] = $idx;
+		}
+
+		// found the rule, save the relative index
+		if (isset($rule_index) && ($idx == $rule_index)) {
+			$interface_rule_index['index'] = $relative_index_count;
 		}
-		$i++;
 	}
-	return $ifridx;
+
+	// the interface was found, save its last rule's index
+	if (isset($interface_rule_index['first'])) {
+		$interface_rule_index['last'] = $interface_rule_index['first'] + $relative_index_count;
+	}
+	
+	return $interface_rule_index;
 }
 
 /* display rules separators */
@@ -4787,17 +4825,49 @@ function separator_rows($separators) {
 	return $seprows;
 }
 
-/* If the separator is located after the place ($ridx) where the rule was added or deleted, move the separator ($mvrows) */
-function move_separators(&$a_separators, $ridx, $mvnrows) {
-	if (is_array($a_separators)) {
-		foreach ($a_separators as $sepi => $separator) {
-			if (empty($separator)) {
-				continue;
-			}
-			$seprow = substr(array_get_path($separator, 'row/0'), 2);
-			if ($seprow > $ridx) {
-				array_set_path($a_separators, "{$sepi}/row/0", 'fr' . ($seprow + $mvnrows));
-			}
+/**
+ * Shift separators by 1 depending on the rule index.
+ * 
+ * @param array &$separators All separators for an interface's rules
+ * @param int $index Index of the rule relative to the section/interface.
+ *            To indicate a rule is being placed before all other rules
+ *            and separators, specify -1 (top).
+ * @param bool $shiftup Shift the separators up or down
+ * 
+ */
+function shift_separators(array &$separators, ?int $index = null, bool $shiftup = false): void {
+	foreach ($separators as $rowkey => $name) {
+		$row = substr(array_get_path($name, 'row/0'), 2);
+		$shift = ($shiftup) ? -1 : 1;
+		if ($index == -1) {
+			if ($shiftup) {
+				// Rule is being removed at the top; shift all up
+				$shift = -1;
+			} else {
+				// Rule is being added at the top; shift all down
+				$shift = 1;
+			}
+		} elseif (!isset($index) || empty($name)) {
+			// Skip undefined separators
+			$shift = 0;
+		} elseif ($row == 0) {
+			// Separators at the top cannot move up
+			$shift = 0;
+		} elseif ($row == $index) {
+			// Don't shift a separator with the same index as the rule
+			$shift = 0;
+		} elseif ($row < $index) {
+			// Don't shift the separator if it's above the rule
+			$shift = 0;
+		}
+
+		// shift the separators
+		if ($shift == 0) { 
+			continue;
+		} elseif ($shift < 0) {
+			array_set_path($separators, "{$rowkey}/row/0", 'fr' . --$row);
+		} else {
+			array_set_path($separators, "{$rowkey}/row/0", 'fr' . ++$row);
 		}
 	}
 }
diff --git a/src/etc/inc/itemid.inc b/src/etc/inc/itemid.inc
index e2c008b1e3..3de297fb0d 100644
--- a/src/etc/inc/itemid.inc
+++ b/src/etc/inc/itemid.inc
@@ -32,8 +32,6 @@
  *   boolean   - true if item was found and deleted
  ******/
 function delete_id($id) {
-	global $config;
-
 	$array = config_get_path('filter/rule', []);
 
 	// Search for the item in the array
@@ -47,13 +45,11 @@ function delete_id($id) {
 	$if = $array[$delete_index]['interface'];
 	unset($array[$delete_index]);
 
-	// Update the separators
-	init_config_arr(array('filter', 'separator', strtolower($if)));
-	$a_separators = &$config['filter']['separator'][strtolower($if)];
-	// get rule index within interface
-	$ridx = ifridx($if, $delete_index);
-	$mvnrows = -1;
-	move_separators($a_separators, $ridx, $mvnrows);
+	// shift the separators
+	$ridx = get_interface_ruleindex($if, $delete_index);
+	$a_separators = config_get_path('filter/separator/' . strtolower($if), []);
+	shift_separators($a_separators, $ridx['index'], true);
+	config_set_path('filter/separator/' . strtolower($if), $a_separators);
 
 	config_set_path('filter/rule', $array);
 	return true;
diff --git a/src/usr/local/pfSense/include/www/firewall_nat.inc b/src/usr/local/pfSense/include/www/firewall_nat.inc
index c37ecfb5d5..52cc0d7039 100644
--- a/src/usr/local/pfSense/include/www/firewall_nat.inc
+++ b/src/usr/local/pfSense/include/www/firewall_nat.inc
@@ -668,12 +668,19 @@ function saveNATrule($post, $id, $json = false) {
 		} else {
 			$natent['created'] = make_config_revision_entry();
 			if (is_numeric($after)) {
+				// place the rule on top
 				array_splice($a_nat, $after+1, 0, array($natent));
 
-				// Update the separators
-				$ridx = $after;
-				$mvnrows = +1;
-				move_separators($a_separators, $ridx, $mvnrows);
+				// shift the separators
+				$a_separators = config_get_path('nat/separator', []);
+				if ($after == -1) {
+					// rule is being placed on top
+					shift_separators($a_separators, -1);
+				} else {
+					// rule is being placed after another rule
+					shift_separators($a_separators, $after);
+				}
+				config_set_path('nat/separator', $a_separators);
 			} else {
 				$a_nat[] = $natent;
 			}
@@ -756,31 +763,27 @@ function deleteNATrule($post, $json = false) {
 		$post['rule'] = array( $post['id'] => $post['id'] );
 	}
 
+	/* When deleting rules using an unordered index array, $num_deleted may
+	   be inaccurate since removing a higher index rule (e.g. 5) should not
+	   decrement lower indecies (e.g. 2). This sorting is only necessary when
+	   the rule index is NOT recalculated on every loop. */
+	asort($post['rule'], SORT_NATURAL);
 	$num_deleted = 0;
-
+	$a_separators = config_get_path('nat/separator', []);
 	foreach ($post['rule'] as $rulei) {
-		$associated_rule_id = config_get_path("nat/rule/{$rulei}/associated-rule-id");
+		// separators must be updated before the rule is removed
+		shift_separators($a_separators, $rulei - $num_deleted, true);
 
+		// remove the rule
+		$associated_rule_id = config_get_path("nat/rule/{$rulei}/associated-rule-id");
 		if ($associated_rule_id != null) {
 			delete_id($associated_rule_id);
 			$want_dirty_filter = true;
 		}
-
 		config_del_path("/nat/rule/{$rulei}");
-
-		// Update the separators
-		// As rules are deleted, $ridx has to be decremented or separator position will break
-		if (count($post['rule']) == 1) { // Need special handling of single rule deletion
-			$ridx = $rulei;
-		} else {
-			$ridx = $rulei - $num_deleted;
-		}
-
-		$mvnrows = -1;
-		move_separators($a_separators, $ridx, $mvnrows);
-
 		$num_deleted++;
 	}
+	config_set_path('nat/separator', $a_separators);
 
 	if ($num_deleted) {
 		if ($num_deleted == 1) {
diff --git a/src/usr/local/www/firewall_rules.php b/src/usr/local/www/firewall_rules.php
index 2a02ba4274..3909dc8474 100644
--- a/src/usr/local/www/firewall_rules.php
+++ b/src/usr/local/www/firewall_rules.php
@@ -164,18 +164,18 @@ if ($_POST['apply']) {
 
 if ($_POST['act'] == "del") {
 	if ($a_filter[$_POST['id']]) {
+		// separators must be updated before the rule is removed
+		$ridx = get_interface_ruleindex($if, $_POST['id']);
+		$a_separators = config_get_path('filter/separator/' . strtolower($if), []);
+		shift_separators($a_separators, $ridx['index'], true);
+		config_set_path('filter/separator/' . strtolower($if), $a_separators);
+
+		// remove the rule
 		if (!empty($a_filter[$_POST['id']]['associated-rule-id'])) {
 			delete_nat_association($a_filter[$_POST['id']]['associated-rule-id']);
 		}
 		unset($a_filter[$_POST['id']]);
 
-		// Update the separators
-		init_config_arr(array('filter', 'separator', strtolower($if)));
-		$a_separators = &$config['filter']['separator'][strtolower($if)];
-		$ridx = ifridx($if, $_POST['id']);	// get rule index within interface
-		$mvnrows = -1;
-		move_separators($a_separators, $ridx, $mvnrows);
-
 		if (write_config(gettext("Firewall: Rules - deleted a firewall rule."))) {
 			mark_subsystem_dirty('filter');
 		}
@@ -200,30 +200,21 @@ if ($_REQUEST['savemsg']) {
 
 if (isset($_POST['del_x'])) {
 	if (is_array($_POST['rule']) && count($_POST['rule'])) {
-		init_config_arr(array('filter', 'separator', strtolower($if)));
-		$a_separators = &$config['filter']['separator'][strtolower($if)];
-
-		$num_deleted = 0;
-
+		$removed = false;
+		$a_separators = config_get_path('filter/separator/' . strtolower($if), []);
 		foreach ($_POST['rule'] as $rulei) {
+			// separators must be updated before the rule is removed
+			$ridx = get_interface_ruleindex($if, $rulei);
+			shift_separators($a_separators, $ridx['index'], true);
+
+			// remove the rule
 			delete_nat_association($a_filter[$rulei]['associated-rule-id']);
 			unset($a_filter[$rulei]);
-
-			// Update the separators
-			// As rules are deleted, $ridx has to be decremented or separator position will break
-			if (count($_POST['rule']) == 1) { // Need special handling of single rule deletion
-				$ridx = ifridx($if, $rulei);
-			} else {
-				$ridx = ifridx($if, $rulei) - $num_deleted + 1;
-			}
-
-			$mvnrows = -1;
-			move_separators($a_separators, $ridx, $mvnrows);
-
-			$num_deleted++;
+			$removed = true;
 		}
+		config_set_path('filter/separator/' . strtolower($if), $a_separators);
 
-		if ($num_deleted) {
+		if ($removed) {
 			if (write_config(gettext("Firewall: Rules - deleted selected firewall rules."))) {
 				mark_subsystem_dirty('filter');
 			}
@@ -994,8 +985,10 @@ foreach ($a_filter as $filteri => $filterent):
 endforeach;
 
 // There can be separator(s) after the last rule listed.
-if ($seprows[$nrules]) {
-	display_separator($separators, $nrules, $columns_in_table);
+foreach ($seprows as $idx => $sep) {
+	if ($idx >= $nrules) {
+		display_separator($separators, $idx, $columns_in_table);
+	}
 }
 ?>
 				</tbody>
diff --git a/src/usr/local/www/firewall_rules_edit.php b/src/usr/local/www/firewall_rules_edit.php
index 36d31101af..abce6edf91 100644
--- a/src/usr/local/www/firewall_rules_edit.php
+++ b/src/usr/local/www/firewall_rules_edit.php
@@ -1055,55 +1055,49 @@ if ($_POST['save']) {
 			if (($tmpif == $if) || (isset($pconfig['floating']))) {
 				$a_filter[$id] = $filterent;
 			} else {							// rule moved to different interface
-				// Update the separators of previous interface.
-				init_config_arr(array('filter', 'separator', strtolower($if)));
-				$a_separators = config_get_path('filter/separator/' . strtolower($if));
-				$ridx = ifridx($if, $id);		// get rule index within interface
-				$mvnrows = -1;
-				move_separators($a_separators, $ridx, $mvnrows);
+				// update the previous interface's separators
+				$a_separators = config_get_path('filter/separator/' . strtolower($if), []);
+				$ridx = get_interface_ruleindex($if, $id);
+				shift_separators($a_separators, $ridx['index'], true);
 				config_set_path('filter/separator/' . strtolower($if), $a_separators);
-				$a_filter[$id] = $filterent;	// save edited rule to new interface
-
-				// Update the separators of new interface.
-				init_config_arr(array('filter', 'separator', strtolower($tmpif)));
-				$a_separators = config_get_path('filter/separator/' . strtolower($tmpif));
-				$ridx = ifridx($tmpif, $id);	// get rule index within interface
-				if ($ridx == 0) {				// rule was placed at the top
-					$ridx = -1;					// move all separators
-				}
-				$mvnrows = +1;
-				move_separators($a_separators, $ridx, $mvnrows);
-				config_set_path('filter/separator/' . strtolower($tmpif), $a_separators);
+
+				// remove the rule from the previous interface and place it at the bottom of the new interface
+				unset($a_filter[$id]);
+				$ridx = get_interface_ruleindex($tmpif);
+				array_splice($a_filter, $ridx['last']+1, 0, array($filterent));
 			}
 
 		} else {
 			$filterent['created'] = make_config_revision_entry();
 			if (is_numeric($after)) {
-				array_splice($a_filter, $after+1, 0, array($filterent));
-
 				// For copy/dup the $if var is taken from the rule submission.
 				// In the case of floating rules that could be anything.  But never "FloatingRules" that is needed.
 				if (isset($pconfig['floating'])) {
 					$tmpif = 'FloatingRules';
 				} else if (isset($filterent['interface'])) {
 					$tmpif = $filterent['interface'];
-					if ($tmpif != $if) {					// rule copied to different interface
-						$ridx = ifridx($tmpif, $after+1);	// get rule index within interface
-						if ($ridx == 0) {					// rule was placed at the top
-							$after = -1;					// move all separators
-						}
-					}
 				} else {
 					$tmpif = $if;
 				}
 
-				// Update the separators
-				init_config_arr(array('filter', 'separator', strtolower($tmpif)));
-				$a_separators = config_get_path('filter/separator/' . strtolower($tmpif));
-				$ridx = ifridx($tmpif, $after);	// get rule index within interface
-				$mvnrows = +1;
-				move_separators($a_separators, $ridx, $mvnrows);
-				config_set_path('filter/separator/' . strtolower($tmpif), $a_separators);
+				$ridx = get_interface_ruleindex($tmpif, $after);
+				if ($tmpif == $if) {
+					// save the rule after the one being requested
+					array_splice($a_filter, $after+1, 0, array($filterent));
+					// shift the separators
+					$a_separators = config_get_path('filter/separator/' . strtolower($tmpif), []);
+					if ($after == -1) {
+						// rule is being placed on top
+						shift_separators($a_separators, -1);
+					} else {
+						// rule is being placed after another rule
+						shift_separators($a_separators, $ridx['index']);
+					}
+					config_set_path('filter/separator/' . strtolower($tmpif), $a_separators);
+				} else {
+					// rule copied to different interface; place it at the bottom
+					array_splice($a_filter, $ridx['last']+1, 0, array($filterent));
+				}
 			} else {
 				$a_filter[] = $filterent;
 			}
