diff --git a/src/etc/inc/priv/user.priv.inc b/src/etc/inc/priv/user.priv.inc
index 729f483b15..25a87f1058 100644
--- a/src/etc/inc/priv/user.priv.inc
+++ b/src/etc/inc/priv/user.priv.inc
@@ -166,4 +166,14 @@ $priv_list['page-openvpn-client-advanced']['name'] = gettext("WebCfg - OpenVPN:
 $priv_list['page-openvpn-client-advanced']['descr'] = gettext("Allow edit access to the 'OpenVPN: Servers' Advanced settings field.");
 $priv_list['page-openvpn-client-advanced']['warn'] = "standard-warning-root";
 
+$priv_list['page-services-dhcpserver-custom'] = array();
+$priv_list['page-services-dhcpserver-custom']['name'] = gettext("WebCfg - Services: DHCP Server: Edit Custom Configuration");
+$priv_list['page-services-dhcpserver-custom']['descr'] = gettext("Allow edit access to the 'Services: DHCP Server' Custom Configuration fields.");
+$priv_list['page-services-dhcpserver-custom']['warn'] = "standard-warning-root";
+
+$priv_list['page-services-dhcpv6server-custom'] = array();
+$priv_list['page-services-dhcpv6server-custom']['name'] = gettext("WebCfg - Services: DHCPv6 Server: Edit Custom Configuration");
+$priv_list['page-services-dhcpv6server-custom']['descr'] = gettext("Allow edit access to the 'Services: DHCPv6 Server' Custom Configuration fields.");
+$priv_list['page-services-dhcpv6server-custom']['warn'] = "standard-warning-root";
+
 ?>
diff --git a/src/usr/local/pfSense/include/www/services_dhcp.inc b/src/usr/local/pfSense/include/www/services_dhcp.inc
index dbb439ddf9..9fa8fdcf3d 100644
--- a/src/usr/local/pfSense/include/www/services_dhcp.inc
+++ b/src/usr/local/pfSense/include/www/services_dhcp.inc
@@ -24,6 +24,18 @@ require_once('util.inc');
 require_once('interfaces.inc');
 require_once('pfsense-utils.inc');
 
+/* custom config can run commands as root, so it has its own privilege */
+function dhcp_can_edit_custom_config(bool $v6 = false): bool {
+	$user_entry = getUserEntry($_SESSION['Username']);
+	$user_entry = $user_entry['item'];
+	$priv = $v6 ? 'page-services-dhcpv6server-custom'
+		    : 'page-services-dhcpserver-custom';
+
+	return (isAdminUID($_SESSION['Username']) ||
+	    userHasPrivilege($user_entry, $priv) ||
+	    userHasPrivilege($user_entry, 'page-all'));
+}
+
 function kea_defaults(string $key): int|string {
 	/* throws an exception if $key isn't matched */
 	return (match($key) {
@@ -442,10 +454,24 @@ function dhcp_do_settings_post(): array {
 		}
 	}
 
-	if (!empty($_POST['custom_kea_config'])) {
-		array_set_path($pconfig, 'custom_kea_config', base64_encode($_POST['custom_kea_config']));
+	$custom_priv_error = false;
+	if (dhcp_can_edit_custom_config()) {
+		if (!empty($_POST['custom_kea_config'])) {
+			array_set_path($pconfig, 'custom_kea_config', base64_encode($_POST['custom_kea_config']));
+		} else {
+			array_del_path($pconfig, 'custom_kea_config');
+		}
 	} else {
-		array_del_path($pconfig, 'custom_kea_config');
+		$old_custom = base64_decode(array_get_path($old_config, 'custom_kea_config', ''));
+		if (isset($_POST['custom_kea_config']) && ($_POST['custom_kea_config'] !== $old_custom)) {
+			$custom_priv_error = true;
+		}
+		/* preserve stored value */
+		if (!empty($old_custom)) {
+			array_set_path($pconfig, 'custom_kea_config', base64_encode($old_custom));
+		} else {
+			array_del_path($pconfig, 'custom_kea_config');
+		}
 	}
 
 	/* don't leave empty ha config section */
@@ -454,6 +480,9 @@ function dhcp_do_settings_post(): array {
 	}
 
 	$input_errors = dhcp_validate_settings_post($pconfig);
+	if ($custom_priv_error) {
+		$input_errors[] = gettext('This user does not have sufficient privileges to edit the Custom Configuration field.');
+	}
 	if (!$input_errors && ($pconfig !== $old_config)) {
 		config_set_path('kea', $pconfig);
 		write_config(gettext('DHCP Server - Settings changed'));
@@ -550,10 +579,24 @@ function dhcp6_do_settings_post(): array {
 		}
 	}
 
-	if (!empty($_POST['custom_kea_config'])) {
-		array_set_path($pconfig, 'custom_kea_config', base64_encode($_POST['custom_kea_config']));
+	$custom_priv_error = false;
+	if (dhcp_can_edit_custom_config(true)) {
+		if (!empty($_POST['custom_kea_config'])) {
+			array_set_path($pconfig, 'custom_kea_config', base64_encode($_POST['custom_kea_config']));
+		} else {
+			array_del_path($pconfig, 'custom_kea_config');
+		}
 	} else {
-		array_del_path($pconfig, 'custom_kea_config');
+		$old_custom = base64_decode(array_get_path($old_config, 'custom_kea_config', ''));
+		if (isset($_POST['custom_kea_config']) && ($_POST['custom_kea_config'] !== $old_custom)) {
+			$custom_priv_error = true;
+		}
+		/* preserve stored value */
+		if (!empty($old_custom)) {
+			array_set_path($pconfig, 'custom_kea_config', base64_encode($old_custom));
+		} else {
+			array_del_path($pconfig, 'custom_kea_config');
+		}
 	}
 
 	/* don't leave empty ha config section */
@@ -562,6 +605,9 @@ function dhcp6_do_settings_post(): array {
 	}
 
 	$input_errors = dhcp_validate_settings_post($pconfig);
+	if ($custom_priv_error) {
+		$input_errors[] = gettext('This user does not have sufficient privileges to edit the Custom Configuration field.');
+	}
 	if (!$input_errors && ($pconfig !== $old_config)) {
 		config_set_path('kea6', $pconfig);
 		write_config(gettext('DHCPv6 Server - Settings changed'));
diff --git a/src/usr/local/www/services_dhcp.php b/src/usr/local/www/services_dhcp.php
index 9b1a201198..f50bf69a4f 100644
--- a/src/usr/local/www/services_dhcp.php
+++ b/src/usr/local/www/services_dhcp.php
@@ -625,11 +625,20 @@ if ($if && isset($_POST['save'])) {
 
 	/* validate custom config */
 	if (dhcp_is_backend('kea')) {
-		if (!empty($_POST['custom_kea_config'])) {
-			$json = json_decode($_POST['custom_kea_config'], true);
-			if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
-				$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+		if (dhcp_can_edit_custom_config()) {
+			if (!empty($_POST['custom_kea_config'])) {
+				$json = json_decode($_POST['custom_kea_config'], true);
+				if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
+					$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+				}
+			}
+		} else {
+			$stored_custom = base64_decode(array_get_path($dhcpdconf, 'custom_kea_config', ''));
+			if (isset($_POST['custom_kea_config']) && ($_POST['custom_kea_config'] !== $stored_custom)) {
+				$input_errors[] = gettext('This user does not have sufficient privileges to edit the Custom Configuration field.');
 			}
+			/* show the stored value, not the submitted one */
+			$pconfig['custom_kea_config'] = $stored_custom;
 		}
 	}
 
@@ -817,7 +826,7 @@ if ($if && isset($_POST['save'])) {
 			$dhcpdconf['omapi_key_algorithm'] = $_POST['omapi_key_algorithm'];
 		}
 
-		if (dhcp_is_backend('kea')) {
+		if (dhcp_is_backend('kea') && dhcp_can_edit_custom_config()) {
 			$dhcpdconf['custom_kea_config'] = base64_encode($_POST['custom_kea_config']);
 		}
 
@@ -1862,11 +1871,15 @@ $form->add($section);
 
 if (dhcp_is_backend('kea')):
 $section = new Form_Section(gettext('Custom Configuration'));
-$section->addInput(new Form_Textarea(
+$custom_config = new Form_Textarea(
 	'custom_kea_config',
 	gettext('JSON Configuration'),
 	array_get_path($pconfig, 'custom_kea_config')
-))->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv4 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), $kea_section, '<br/>');
+);
+if (!dhcp_can_edit_custom_config()) {
+	$custom_config->setDisabled();
+}
+$section->addInput($custom_config)->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv4 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), $kea_section, '<br/>');
 $form->add($section);
 endif;
 
diff --git a/src/usr/local/www/services_dhcp_edit.php b/src/usr/local/www/services_dhcp_edit.php
index 902d989808..0821f90904 100644
--- a/src/usr/local/www/services_dhcp_edit.php
+++ b/src/usr/local/www/services_dhcp_edit.php
@@ -423,11 +423,20 @@ if ($_POST['save']) {
 
 	/* validate custom config */
 	if (dhcp_is_backend('kea')) {
-		if (!empty($_POST['custom_kea_config'])) {
-			$json = json_decode($_POST['custom_kea_config'], true);
-			if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
-				$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+		if (dhcp_can_edit_custom_config()) {
+			if (!empty($_POST['custom_kea_config'])) {
+				$json = json_decode($_POST['custom_kea_config'], true);
+				if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
+					$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+				}
 			}
+		} else {
+			$stored_custom = is_array($this_map_config) ? base64_decode(array_get_path($this_map_config, 'custom_kea_config', '')) : '';
+			if (isset($_POST['custom_kea_config']) && ($_POST['custom_kea_config'] !== $stored_custom)) {
+				$input_errors[] = gettext('This user does not have sufficient privileges to edit the Custom Configuration field.');
+			}
+			/* show the stored value, not the submitted one */
+			$pconfig['custom_kea_config'] = $stored_custom;
 		}
 	}
 
@@ -503,7 +512,12 @@ if ($_POST['save']) {
 		$mapent['numberoptions'] = $pconfig['numberoptions'];
 
 		if (dhcp_is_backend('kea')) {
-			$mapent['custom_kea_config'] = base64_encode($_POST['custom_kea_config']);
+			if (dhcp_can_edit_custom_config()) {
+				$mapent['custom_kea_config'] = base64_encode($_POST['custom_kea_config']);
+			} elseif (!empty($this_map_config['custom_kea_config'])) {
+				/* preserve stored value */
+				$mapent['custom_kea_config'] = $this_map_config['custom_kea_config'];
+			}
 		}
 
 		if ($this_map_config) {
@@ -1112,11 +1126,15 @@ $form->add($section);
 
 if (dhcp_is_backend('kea')):
 $section = new Form_Section(gettext('Custom Configuration'));
-$section->addInput(new Form_Textarea(
+$custom_config = new Form_Textarea(
 	'custom_kea_config',
 	gettext('JSON Configuration'),
 	array_get_path($pconfig, 'custom_kea_config')
-))->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv4 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'reservation', '<br/>');
+);
+if (!dhcp_can_edit_custom_config()) {
+	$custom_config->setDisabled();
+}
+$section->addInput($custom_config)->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv4 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'reservation', '<br/>');
 $form->add($section);
 endif;
 
diff --git a/src/usr/local/www/services_dhcp_settings.php b/src/usr/local/www/services_dhcp_settings.php
index 6b318f2418..a62fabba63 100644
--- a/src/usr/local/www/services_dhcp_settings.php
+++ b/src/usr/local/www/services_dhcp_settings.php
@@ -359,11 +359,15 @@ $section->addInput(new Form_Select(
 $form->add($section);
 
 $section = new Form_Section(gettext('Custom Configuration'));
-$section->addInput(new Form_Textarea(
+$custom_config = new Form_Textarea(
 	'custom_kea_config',
 	gettext('JSON Configuration'),
 	array_get_path($pconfig, 'custom_kea_config')
-))->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv4 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'Dhcp4', '<br/>');
+);
+if (!dhcp_can_edit_custom_config()) {
+	$custom_config->setDisabled();
+}
+$section->addInput($custom_config)->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv4 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'Dhcp4', '<br/>');
 $form->add($section);
 
 $form->addGlobal(new Form_Input(
diff --git a/src/usr/local/www/services_dhcpv6.php b/src/usr/local/www/services_dhcpv6.php
index e5cb3f2df4..2276f64737 100644
--- a/src/usr/local/www/services_dhcpv6.php
+++ b/src/usr/local/www/services_dhcpv6.php
@@ -453,11 +453,20 @@ if (isset($_POST['apply'])) {
 
 	/* validate custom config */
 	if (dhcp_is_backend('kea')) {
-		if (!empty($_POST['custom_kea_config'])) {
-			$json = json_decode($_POST['custom_kea_config'], true);
-			if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
-				$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+		if (dhcp_can_edit_custom_config(true)) {
+			if (!empty($_POST['custom_kea_config'])) {
+				$json = json_decode($_POST['custom_kea_config'], true);
+				if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
+					$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+				}
+			}
+		} else {
+			$stored_custom = base64_decode(array_get_path($dhcpdconf, 'custom_kea_config', ''));
+			if (isset($_POST['custom_kea_config']) && ($_POST['custom_kea_config'] !== $stored_custom)) {
+				$input_errors[] = gettext('This user does not have sufficient privileges to edit the Custom Configuration field.');
 			}
+			/* show the stored value, not the submitted one */
+			$pconfig['custom_kea_config'] = $stored_custom;
 		}
 	}
 
@@ -580,7 +589,7 @@ if (isset($_POST['apply'])) {
 
 		$dhcpdconf['numberoptions'] = $numberoptions;
 
-		if (dhcp_is_backend('kea')) {
+		if (dhcp_is_backend('kea') && dhcp_can_edit_custom_config(true)) {
 			$dhcpdconf['custom_kea_config'] = base64_encode($_POST['custom_kea_config']);
 		}
 
@@ -1434,11 +1443,15 @@ endif; /* dhcp_is_backend('kea') */
 
 if (dhcp_is_backend('kea')):
 $section = new Form_Section(gettext('Custom Configuration'));
-$section->addInput(new Form_Textarea(
+$custom_config = new Form_Textarea(
 	'custom_kea_config',
 	gettext('JSON Configuration'),
 	array_get_path($pconfig, 'custom_kea_config')
-))->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv6 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), $kea_section, '<br/>');
+);
+if (!dhcp_can_edit_custom_config(true)) {
+	$custom_config->setDisabled();
+}
+$section->addInput($custom_config)->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv6 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), $kea_section, '<br/>');
 $form->add($section);
 endif;
 
diff --git a/src/usr/local/www/services_dhcpv6_edit.php b/src/usr/local/www/services_dhcpv6_edit.php
index ea43fc33d1..8e02c5bf7a 100644
--- a/src/usr/local/www/services_dhcpv6_edit.php
+++ b/src/usr/local/www/services_dhcpv6_edit.php
@@ -176,11 +176,20 @@ if ($_POST['save']) {
 
 	/* validate custom config */
 	if (dhcp_is_backend('kea')) {
-		if (!empty($_POST['custom_kea_config'])) {
-			$json = json_decode($_POST['custom_kea_config'], true);
-			if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
-				$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+		if (dhcp_can_edit_custom_config(true)) {
+			if (!empty($_POST['custom_kea_config'])) {
+				$json = json_decode($_POST['custom_kea_config'], true);
+				if (!is_array($json) || (json_last_error() !== JSON_ERROR_NONE)) {
+					$input_errors[] = gettext('Custom configuration is not a well formed JSON object.');
+				}
 			}
+		} else {
+			$stored_custom = is_array($this_map_config) ? base64_decode(array_get_path($this_map_config, 'custom_kea_config', '')) : '';
+			if (isset($_POST['custom_kea_config']) && ($_POST['custom_kea_config'] !== $stored_custom)) {
+				$input_errors[] = gettext('This user does not have sufficient privileges to edit the Custom Configuration field.');
+			}
+			/* show the stored value, not the submitted one */
+			$pconfig['custom_kea_config'] = $stored_custom;
 		}
 	}
 
@@ -198,7 +207,12 @@ if ($_POST['save']) {
 			if (!empty($_POST['pdprefix'])) {
 				$mapent['pdprefix'] = text_to_compressed_ip6($_POST['pdprefix']);
 			}
-			$mapent['custom_kea_config'] = base64_encode($_POST['custom_kea_config']);
+			if (dhcp_can_edit_custom_config(true)) {
+				$mapent['custom_kea_config'] = base64_encode($_POST['custom_kea_config']);
+			} elseif (!empty($this_map_config['custom_kea_config'])) {
+				/* preserve stored value */
+				$mapent['custom_kea_config'] = $this_map_config['custom_kea_config'];
+			}
 		}
 
 		if ($this_map_config) {
@@ -354,11 +368,15 @@ $form->add($section);
 
 if (dhcp_is_backend('kea')):
 $section = new Form_Section(gettext('Custom Configuration'));
-$section->addInput(new Form_Textarea(
+$custom_config = new Form_Textarea(
 	'custom_kea_config',
 	gettext('JSON Configuration'),
 	array_get_path($pconfig, 'custom_kea_config')
-))->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv6 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'reservation', '<br/>');
+);
+if (!dhcp_can_edit_custom_config(true)) {
+	$custom_config->setDisabled();
+}
+$section->addInput($custom_config)->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv6 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'reservation', '<br/>');
 $form->add($section);
 endif;
 
diff --git a/src/usr/local/www/services_dhcpv6_settings.php b/src/usr/local/www/services_dhcpv6_settings.php
index 596b6c6821..b306680c8a 100644
--- a/src/usr/local/www/services_dhcpv6_settings.php
+++ b/src/usr/local/www/services_dhcpv6_settings.php
@@ -373,11 +373,15 @@ $section->addInput(new Form_Select(
 $form->add($section);
 
 $section = new Form_Section(gettext('Custom Configuration'));
-$section->addInput(new Form_Textarea(
+$custom_config = new Form_Textarea(
 	'custom_kea_config',
 	gettext('JSON Configuration'),
 	array_get_path($pconfig, 'custom_kea_config')
-))->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv6 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'Dhcp6', '<br/>');
+);
+if (!dhcp_can_edit_custom_config(true)) {
+	$custom_config->setDisabled();
+}
+$section->addInput($custom_config)->setWidth(8)->setHelp(gettext('JSON to be merged into the "%1$s" section of the generated Kea DHCPv6 configuration.%2$sThe input must be a well formed JSON object and should not include the "%1$s" key itself.'), 'Dhcp6', '<br/>');
 $form->add($section);
 
 $form->addGlobal(new Form_Input(
