diff --git a/ironic/api/controllers/v1/ramdisk.py b/ironic/api/controllers/v1/ramdisk.py
index 47b320b24c..9ce0608f21 100644
--- a/ironic/api/controllers/v1/ramdisk.py
+++ b/ironic/api/controllers/v1/ramdisk.py
@@ -13,6 +13,7 @@
 #    under the License.
 
 from http import client as http_client
+from urllib import parse as urlparse
 
 from oslo_config import cfg
 from oslo_log import log
@@ -225,6 +226,20 @@ class HeartbeatController(rest.RestController):
         rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
         dii = rpc_node['driver_internal_info']
         agent_url = dii.get('agent_url')
+        try:
+            # NOTE(TheJulia): Use of urllib.urlparse is not a security
+            # guard, but detecting oddities and incorrect formatting
+            # https://docs.python.org/3/library/urllib.parse.html#url-parsing-security  # noqa
+            parsed_url = urlparse.urlparse(callback_url)
+            # Check if http (compatibility), or https (much newer agents)
+            if 'http' not in parsed_url.scheme:
+                raise ValueError
+            callback_url = parsed_url.geturl()
+        except ValueError:
+            raise exception.InvalidParameterValue(
+                _('An issue with the supplied "callback_url" has been '
+                  'detected.'))
+
         # If we have an agent_url on file, and we get something different
         # we should fail because this is unexpected behavior of the agent.
         if agent_url is not None and agent_url != callback_url:
diff --git a/ironic/tests/unit/api/controllers/v1/test_ramdisk.py b/ironic/tests/unit/api/controllers/v1/test_ramdisk.py
index 70a918fde6..0eb4fe426f 100644
--- a/ironic/tests/unit/api/controllers/v1/test_ramdisk.py
+++ b/ironic/tests/unit/api/controllers/v1/test_ramdisk.py
@@ -204,7 +204,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
     def test_old_api_version(self):
         response = self.post_json(
             '/heartbeat/%s' % uuidutils.generate_uuid(),
-            {'callback_url': 'url'},
+            {'callback_url': 'https://url'},
             headers={api_base.Version.string: str(api_v1.min_version())},
             expect_errors=True)
         self.assertEqual(http_client.NOT_FOUND, response.status_int)
@@ -212,7 +212,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
     def test_node_not_found(self):
         response = self.post_json(
             '/heartbeat/%s' % uuidutils.generate_uuid(),
-            {'callback_url': 'url'},
+            {'callback_url': 'https://url'},
             headers={api_base.Version.string: str(api_v1.max_version())},
             expect_errors=True)
         self.assertEqual(http_client.NOT_FOUND, response.status_int)
@@ -222,14 +222,14 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'x'},
             headers={api_base.Version.string: str(api_v1.max_version())})
         self.assertEqual(http_client.ACCEPTED, response.status_int)
         self.assertEqual(b'', response.body)
         mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
-                                               node.uuid, 'url', None, 'x',
-                                               None, None, None,
+                                               node.uuid, 'https://url', None,
+                                               'x', None, None, None,
                                                topic='test-topic')
 
     @mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
@@ -238,14 +238,14 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s.json' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'maybe some magic'},
             headers=headers)
         self.assertEqual(http_client.ACCEPTED, response.status_int)
         self.assertEqual(b'', response.body)
         mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
-                                               node.uuid, 'url', None,
-                                               'maybe some magic',
+                                               node.uuid, 'https://url',
+                                               None, 'maybe some magic',
                                                None, None, None,
                                                topic='test-topic')
 
@@ -254,13 +254,13 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context, name='test.1')
         response = self.post_json(
             '/heartbeat/%s' % node.name,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'token'},
             headers={api_base.Version.string: str(api_v1.max_version())})
         self.assertEqual(http_client.ACCEPTED, response.status_int)
         self.assertEqual(b'', response.body)
         mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
-                                               node.uuid, 'url', None,
+                                               node.uuid, 'https://url', None,
                                                'token', None, None, None,
                                                topic='test-topic')
 
@@ -269,14 +269,15 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_version': '1.4.1',
              'agent_token': 'meow'},
             headers={api_base.Version.string: str(api_v1.max_version())})
         self.assertEqual(http_client.ACCEPTED, response.status_int)
         self.assertEqual(b'', response.body)
         mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
-                                               node.uuid, 'url', '1.4.1',
+                                               node.uuid, 'https://url',
+                                               '1.4.1',
                                                'meow',
                                                None, None, None,
                                                topic='test-topic')
@@ -286,20 +287,31 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_version': '1.4.1'},
             headers={api_base.Version.string: '1.35'},
             expect_errors=True)
         self.assertEqual(http_client.BAD_REQUEST, response.status_int)
 
+    @mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
+    def test_heartbeat_rejects_file_url(self, mock_heartbeat):
+        node = obj_utils.create_test_node(
+            self.context)
+        response = self.post_json(
+            '/heartbeat/%s' % node.uuid,
+            {'callback_url': 'file:///path/to/the/wizzard'},
+            headers={api_base.Version.string: str(api_v1.max_version())},
+            expect_errors=True)
+        self.assertEqual(http_client.BAD_REQUEST, response.status_int)
+
     @mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
     def test_heartbeat_rejects_different_callback_url(self, mock_heartbeat):
         node = obj_utils.create_test_node(
             self.context,
-            driver_internal_info={'agent_url': 'url'})
+            driver_internal_info={'agent_url': 'https://url'})
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url2'},
+            {'callback_url': 'https://url2'},
             headers={api_base.Version.string: str(api_v1.max_version())},
             expect_errors=True)
         self.assertEqual(http_client.BAD_REQUEST, response.status_int)
@@ -309,13 +321,13 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'http://url',
              'agent_token': 'abcdef1'},
             headers={api_base.Version.string: str(api_v1.max_version())})
         self.assertEqual(http_client.ACCEPTED, response.status_int)
         self.assertEqual(b'', response.body)
         mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
-                                               node.uuid, 'url', None,
+                                               node.uuid, 'http://url', None,
                                                'abcdef1', None, None, None,
                                                topic='test-topic')
 
@@ -324,14 +336,14 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'meow',
              'agent_verify_ca': 'abcdef1'},
             headers={api_base.Version.string: str(api_v1.max_version())})
         self.assertEqual(http_client.ACCEPTED, response.status_int)
         self.assertEqual(b'', response.body)
         mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
-                                               node.uuid, 'url', None,
+                                               node.uuid, 'https://url', None,
                                                'meow', 'abcdef1', None, None,
                                                topic='test-topic')
 
@@ -340,7 +352,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'meow',
              'agent_status': 'start',
              'agent_status_message': 'woof',
@@ -349,7 +361,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         self.assertEqual(http_client.ACCEPTED, response.status_int)
         self.assertEqual(b'', response.body)
         mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
-                                               node.uuid, 'url', None,
+                                               node.uuid, 'https://url', None,
                                                'meow', 'abcdef1', 'start',
                                                'woof', topic='test-topic')
 
@@ -358,7 +370,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'meow',
              'agent_status': 'invalid_state',
              'agent_status_message': 'woof',
@@ -372,7 +384,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'meow',
              'agent_verify_ca': 'abcd'},
             headers={api_base.Version.string: '1.67'},
@@ -384,7 +396,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
         node = obj_utils.create_test_node(self.context)
         response = self.post_json(
             '/heartbeat/%s' % node.uuid,
-            {'callback_url': 'url',
+            {'callback_url': 'https://url',
              'agent_token': 'meow',
              'agent_verify_ca': 'abcd',
              'agent_status': 'wow',
diff --git a/releasenotes/notes/additional-agent-url-validation-97271ce72b0b1a9d.yaml b/releasenotes/notes/additional-agent-url-validation-97271ce72b0b1a9d.yaml
new file mode 100644
index 0000000000..2dbcb21cdf
--- /dev/null
+++ b/releasenotes/notes/additional-agent-url-validation-97271ce72b0b1a9d.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    Adds additional validation to the agent ``callback_url``.
+security:
+  - |
+    Additional validation of the ``callback_url`` which is supplied to Ironic
+    by the agent has been added. In addition to any standardized formatting
+    checks included in Python urllib, we will also reject requests which
+    have an invalid URL schema formatting.