diff --git a/api-ref/source/baremetal-api-v1-nodes-firmware.inc b/api-ref/source/baremetal-api-v1-nodes-firmware.inc
new file mode 100644
index 0000000000..ed17e0b7b2
--- /dev/null
+++ b/api-ref/source/baremetal-api-v1-nodes-firmware.inc
@@ -0,0 +1,48 @@
+.. -*- rst -*-
+
+=====================
+Node Firmware (nodes)
+=====================
+
+.. versionadded:: 1.84
+
+Given a Node identifier (``uuid`` or ``name``), the API exposes the list of
+all Firmware Components associated with that Node.
+
+These endpoints do not allow modification of the Firmware Components; that
+should be done by using ``clean steps``.
+
+List all Firmware Components by Node
+====================================
+
+.. rest_method:: GET /v1/nodes/{node_ident}/firmware
+
+Return a list of Firmware Components associated with ``node_ident``.
+
+Normal response code: 200
+
+Error codes: 404
+
+Request
+-------
+
+.. rest_parameters:: parameters.yaml
+
+    - node_ident: node_ident
+
+Response
+--------
+
+.. rest_parameters:: parameters.yaml
+
+    - firmware: firmware_components
+    - created_at: created_at
+    - updated_at: updated_at
+    - component: firmware_component
+    - initial_version: firmware_component_initial_version
+    - current_version: firmware_component_current_version
+    - last_version_flashed: firmware_component_last_version_flashed
+
+**Example list of a Node's Firmware Components:**
+
+.. literalinclude:: samples/node-firmware-components-list-response.json
diff --git a/api-ref/source/samples/node-firmware-components-list-response.json b/api-ref/source/samples/node-firmware-components-list-response.json
new file mode 100644
index 0000000000..2ed13018e6
--- /dev/null
+++ b/api-ref/source/samples/node-firmware-components-list-response.json
@@ -0,0 +1,20 @@
+{
+  "firmware": [
+    {
+      "created_at": "2016-08-18T22:28:49.653974+00:00",
+      "updated_at": "2016-08-18T22:28:49.653974+00:00",
+      "component": "BMC",
+      "initial_version": "v1.0.0",
+      "current_version": "v1.2.0",
+      "last_version_flashed": "v1.2.0"
+    },
+    {
+      "created_at": "2016-08-18T22:28:49.653974+00:00",
+      "updated_at": "2016-08-18T22:28:49.653974+00:00",
+      "component": "BIOS",
+      "initial_version": "v1.0.0",
+      "current_version": "v1.1.5",
+      "last_version_flashed": "v1.1.5"
+    }
+  ]
+}
diff --git a/ironic/api/controllers/v1/driver.py b/ironic/api/controllers/v1/driver.py
index f117c327f2..c8cd703876 100644
--- a/ironic/api/controllers/v1/driver.py
+++ b/ironic/api/controllers/v1/driver.py
@@ -80,6 +80,10 @@ def hide_fields_in_newer_versions(driver):
         driver.pop('default_bios_interface', None)
         driver.pop('enabled_bios_interfaces', None)
 
+    if not api_utils.allow_firmware_interface():
+        driver.pop('default_firmware_interface', None)
+        driver.pop('enabled_firmware_interfaces', None)
+
 
 def convert_with_links(name, hosts, detail=False, interface_info=None,
                        fields=None, sanitize=True):
diff --git a/ironic/api/controllers/v1/firmware.py b/ironic/api/controllers/v1/firmware.py
new file mode 100644
index 0000000000..5968edbb2a
--- /dev/null
+++ b/ironic/api/controllers/v1/firmware.py
@@ -0,0 +1,75 @@
+# Copyright 2023 Red Hat Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from ironic_lib import metrics_utils
+from pecan import rest
+
+from ironic import api
+from ironic.api.controllers.v1 import utils as api_utils
+from ironic.api import method
+from ironic.common import args
+from ironic import objects
+
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
+_DEFAULT_RETURN_FIELDS = ('component', 'initial_version', 'current_version',
+                          'last_version_flashed')
+
+
+# NOTE(iurygregory): Keeping same parameters just in case we decide
+# to support /v1/nodes/<node_uuid>/firmware/<component>
+def convert_with_links(rpc_firmware, node_uuid, detail=None, fields=None):
+    """Build a dict containing a firmware component."""
+
+    fw_component = api_utils.object_to_dict(
+        rpc_firmware,
+        include_uuid=False,
+        fields=fields,
+    )
+    return fw_component
+
+
+def collection_from_list(node_ident, firmware_components, detail=None,
+                         fields=None):
+    firmware_list = []
+    for fw_cmp in firmware_components:
+        firmware_list.append(convert_with_links(fw_cmp, node_ident,
+                             detail, fields))
+    return {'firmware': firmware_list}
+
+
+class NodeFirmwareController(rest.RestController):
+    """REST controller for Firmware."""
+
+    def __init__(self, node_ident=None):
+        super(NodeFirmwareController, self).__init__()
+        self.node_ident = node_ident
+
+    @METRICS.timer('NodeFirmwareController.get_all')
+    @method.expose()
+    @args.validate(fields=args.string_list, detail=args.boolean)
+    def get_all(self, detail=None, fields=None):
+        """List node firmware components."""
+        node = api_utils.check_node_policy_and_retrieve(
+            'baremetal:node:firmware:get', self.node_ident)
+
+        allow_query = api_utils.allow_firmware_interface
+        fields = api_utils.get_request_return_fields(fields, detail,
+                                                     _DEFAULT_RETURN_FIELDS,
+                                                     allow_query, allow_query)
+        components = objects.FirmwareComponentList.get_by_node_id(
+            api.request.context, node.id)
+        return collection_from_list(self.node_ident, components,
+                                    detail, fields)
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 94e1e5d844..ea6fcdca1b 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -32,6 +32,7 @@ from ironic.api.controllers import link
 from ironic.api.controllers.v1 import allocation
 from ironic.api.controllers.v1 import bios
 from ironic.api.controllers.v1 import collection
+from ironic.api.controllers.v1 import firmware
 from ironic.api.controllers.v1 import notification_utils as notify
 from ironic.api.controllers.v1 import port
 from ironic.api.controllers.v1 import portgroup
@@ -169,6 +170,7 @@ def node_schema():
             'driver': {'type': 'string'},
             'driver_info': {'type': ['object', 'null']},
             'extra': {'type': ['object', 'null']},
+            'firmware_interface': {'type': ['string', 'null']},
             'inspect_interface': {'type': ['string', 'null']},
             'instance_info': {'type': ['object', 'null']},
             'instance_uuid': {'type': ['string', 'null']},
@@ -283,7 +285,8 @@ PATCH_ALLOWED_FIELDS = [
     'shard',
     'storage_interface',
     'vendor_interface',
-    'parent_node'
+    'parent_node',
+    'firmware_interface'
 ]
 
 TRAITS_SCHEMA = {
@@ -1395,6 +1398,7 @@ def _get_fields_for_node_query(fields=None):
                     'driver_internal_info',
                     'extra',
                     'fault',
+                    'firmware_interface',
                     'inspection_finished_at',
                     'inspection_started_at',
                     'inspect_interface',
@@ -2114,6 +2118,7 @@ class NodesController(rest.RestController):
         'history': NodeHistoryController,
         'inventory': NodeInventoryController,
         'children': NodeChildrenController,
+        'firmware': firmware.NodeFirmwareController,
     }
 
     @pecan.expose()
@@ -2139,7 +2144,9 @@ class NodesController(rest.RestController):
             or (remainder[0] == 'history'
                 and not api_utils.allow_node_history())
             or (remainder[0] == 'inventory'
-                and not api_utils.allow_node_inventory())):
+                and not api_utils.allow_node_inventory())
+            or (remainder[0] == 'firmware'
+                and not api_utils.allow_firmware_interface())):
             pecan.abort(http_client.NOT_FOUND)
         if remainder[0] == 'traits' and not api_utils.allow_traits():
             # NOTE(mgoddard): Returning here will ensure we exhibit the
diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
index 23b9c24a26..269cfa3d26 100644
--- a/ironic/api/controllers/v1/utils.py
+++ b/ironic/api/controllers/v1/utils.py
@@ -807,7 +807,8 @@ VERSIONED_FIELDS = {
     'boot_mode': versions.MINOR_75_NODE_BOOT_MODE,
     'secure_boot': versions.MINOR_75_NODE_BOOT_MODE,
     'shard': versions.MINOR_82_NODE_SHARD,
-    'parent_node': versions.MINOR_83_PARENT_CHILD_NODES
+    'parent_node': versions.MINOR_83_PARENT_CHILD_NODES,
+    'firmware_interface': versions.MINOR_86_FIRMWARE_INTERFACE
 }
 
 for field in V31_FIELDS:
@@ -2006,3 +2007,11 @@ def allow_continue_inspection_endpoint():
     """
     return (new_continue_inspection_endpoint()
             or api.request.version.minor == versions.MINOR_1_INITIAL_VERSION)
+
+
+def allow_firmware_interface():
+    """Check if we should support firmware interface and endpoints.
+
+    Version 1.84 of the API added support for firmware interface.
+    """
+    return api.request.version.minor >= versions.MINOR_86_FIRMWARE_INTERFACE
diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py
index 797de17f7a..d0c81efa5b 100644
--- a/ironic/api/controllers/v1/versions.py
+++ b/ironic/api/controllers/v1/versions.py
@@ -209,6 +209,7 @@ MINOR_82_NODE_SHARD = 82
 MINOR_83_PARENT_CHILD_NODES = 83
 MINOR_84_CONTINUE_INSPECTION = 84
 MINOR_85_UNHOLD_VERB = 85
+MINOR_86_FIRMWARE_INTERFACE = 86
 
 # When adding another version, update:
 # - MINOR_MAX_VERSION
@@ -216,7 +217,7 @@ MINOR_85_UNHOLD_VERB = 85
 #   explanation of what changed in the new version
 # - common/release_mappings.py, RELEASE_MAPPING['master']['api']
 
-MINOR_MAX_VERSION = MINOR_85_UNHOLD_VERB
+MINOR_MAX_VERSION = MINOR_86_FIRMWARE_INTERFACE
 
 # String representations of the minor and maximum versions
 _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
diff --git a/ironic/common/policy.py b/ironic/common/policy.py
index ac25de2696..8343afef62 100644
--- a/ironic/common/policy.py
+++ b/ironic/common/policy.py
@@ -1009,6 +1009,15 @@ node_policies = [
                     'the API clients.',
         operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
     ),
+    policy.DocumentedRuleDefault(
+        name='baremetal:node:firmware:get',
+        check_str=SYSTEM_OR_PROJECT_READER,
+        scope_types=['system', 'project'],
+        description='Retrieve Node Firmware components information',
+        operations=[
+            {'path': '/nodes/{node_ident}/firmware', 'method': 'GET'}
+        ],
+    ),
 ]
 
 deprecated_port_reason = """
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index 8175dc3027..d76bd16f75 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -574,12 +574,12 @@ RELEASE_MAPPING = {
         }
     },
     'master': {
-        'api': '1.85',
+        'api': '1.86',
         'rpc': '1.56',
         'objects': {
             'Allocation': ['1.1'],
             'BIOSSetting': ['1.1'],
-            'Node': ['1.38', '1.37'],
+            'Node': ['1.39', '1.38', '1.37'],
             'NodeHistory': ['1.0'],
             'NodeInventory': ['1.0'],
             'Conductor': ['1.3'],
diff --git a/ironic/conf/default.py b/ironic/conf/default.py
index 5b40c1f311..2f6c0f1ce0 100644
--- a/ironic/conf/default.py
+++ b/ironic/conf/default.py
@@ -115,6 +115,11 @@ driver_opts = [
                 help=_ENABLED_IFACE_HELP.format('deploy')),
     cfg.StrOpt('default_deploy_interface',
                help=_DEFAULT_IFACE_HELP.format('deploy')),
+    cfg.ListOpt('enabled_firmware_interfaces',
+                default=['no-firmware'],
+                help=_ENABLED_IFACE_HELP.format('firmware')),
+    cfg.StrOpt('default_firmware_interface',
+               help=_DEFAULT_IFACE_HELP.format('firmware')),
     cfg.ListOpt('enabled_inspect_interfaces',
                 default=['no-inspect', 'redfish'],
                 help=_ENABLED_IFACE_HELP.format('inspect')),
diff --git a/ironic/conf/fake.py b/ironic/conf/fake.py
index 8f6d75ee3e..dd67a683c3 100644
--- a/ironic/conf/fake.py
+++ b/ironic/conf/fake.py
@@ -78,6 +78,12 @@ opts = [
                       'rescue driver. Two comma-delimited values will '
                       'result in a delay with a triangular random '
                       'distribution, weighted on the first value.')),
+    cfg.StrOpt('firmware_delay',
+               default='0',
+               help=_('Delay in seconds for operations with the fake '
+                      'firmware driver. Two comma-delimited values will '
+                      'result in a delay with a triangular random '
+                      'distribution, weighted on the first value.')),
 ]
 
 
diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py
index d89ce249ec..1e0cd49719 100644
--- a/ironic/drivers/base.py
+++ b/ironic/drivers/base.py
@@ -105,6 +105,12 @@ class BareDriver(object):
     A reference to an instance of :class:DeployInterface.
     """
 
+    firmware = None
+    """`Standard` attribute for inspection related features.
+
+    A reference to an instance of :class:FirmwareInterface.
+    """
+
     inspect = None
     """`Standard` attribute for inspection related features.
 
@@ -161,7 +167,8 @@ class BareDriver(object):
     @property
     def optional_interfaces(self):
         """Interfaces that can be no-op."""
-        return ['bios', 'console', 'inspect', 'raid', 'rescue', 'storage']
+        return ['bios', 'console', 'firmware', 'inspect', 'raid', 'rescue',
+                'storage']
 
     @property
     def all_interfaces(self):
@@ -1736,6 +1743,55 @@ class StorageInterface(BaseInterface, metaclass=abc.ABCMeta):
         """
 
 
+def cache_firmware_components(func):
+    """A decorator to cache firmware components after running the function.
+
+    :param func: Function or method to wrap.
+    """
+    @functools.wraps(func)
+    def wrapped(self, task, *args, **kwargs):
+        result = func(self, task, *args, **kwargs)
+        self.cache_firmware_components(task)
+        return result
+    return wrapped
+
+
+class FirmwareInterface(BaseInterface):
+    """Base class for firmware interface"""
+
+    interface_type = 'firmware'
+
+    @abc.abstractmethod
+    def update(self, task, settings):
+        """Update the Firmware on the given using the settings for components.
+
+        :param task: a TaskManager instance.
+        :param settings: a list of dictionaries, each dictionary contains the
+            component name and the url that will be used to update the
+            firmware.
+        :raises: UnsupportedDriverExtension, if the node's driver doesn't
+            support update via the interface.
+        :raises: InvalidParameterValue, if validation of the settings fails.
+        :raises: MissingParamterValue, if some required parameters are
+            missing.
+        :returns: states.CLEANWAIT if Firmware update with the settings is in
+            progress asynchronously of None if it is complete.
+        """
+
+    @abc.abstractmethod
+    def cache_firmware_components(self, task):
+        """Store or update Firmware Components on the given node.
+
+        This method stores Firmware Components to the firmware_information
+        table during 'cleaning' operation. It will also update the timestamp
+        of each Firmware Component.
+
+        :param task: a TaskManager instance.
+        :raises: UnsupportedDriverExtension, if the node's driver doesn't
+            support getting Firmware Components from bare metal.
+        """
+
+
 def _validate_argsinfo(argsinfo):
     """Validate args info.
 
diff --git a/ironic/drivers/fake_hardware.py b/ironic/drivers/fake_hardware.py
index 92fe8288c4..c370bf3e9c 100644
--- a/ironic/drivers/fake_hardware.py
+++ b/ironic/drivers/fake_hardware.py
@@ -86,3 +86,8 @@ class FakeHardware(generic.GenericHardware):
         return [
             fake.FakeVendorB, fake.FakeVendorA
         ] + super().supported_vendor_interfaces
+
+    @property
+    def supported_firmware_interfaces(self):
+        """List of classes of supported bios interfaces."""
+        return [fake.FakeFirmware] + super().supported_firmware_interfaces
diff --git a/ironic/drivers/generic.py b/ironic/drivers/generic.py
index 787915e088..0136ca86b2 100644
--- a/ironic/drivers/generic.py
+++ b/ironic/drivers/generic.py
@@ -86,6 +86,11 @@ class GenericHardware(hardware_type.AbstractHardwareType):
         return [noop_storage.NoopStorage, cinder.CinderStorage,
                 external_storage.ExternalStorage]
 
+    @property
+    def supported_firmware_interfaces(self):
+        """List of supported firmware interfaces."""
+        return [noop.NoFirmware]
+
 
 class ManualManagementHardware(GenericHardware):
     """Hardware type that uses manual power and boot management.
diff --git a/ironic/drivers/hardware_type.py b/ironic/drivers/hardware_type.py
index df5f437825..df1b064c49 100644
--- a/ironic/drivers/hardware_type.py
+++ b/ironic/drivers/hardware_type.py
@@ -103,6 +103,11 @@ class AbstractHardwareType(object, metaclass=abc.ABCMeta):
         """List of supported vendor interfaces."""
         return [noop.NoVendor]
 
+    @property
+    def supported_firmware_interfaces(self):
+        """List of supported firmware interfaces."""
+        return [noop.NoFirmware]
+
     def get_properties(self):
         """Get the properties of the hardware type.
 
diff --git a/ironic/drivers/modules/fake.py b/ironic/drivers/modules/fake.py
index 0a26efb4ce..5823fb3796 100644
--- a/ironic/drivers/modules/fake.py
+++ b/ironic/drivers/modules/fake.py
@@ -443,3 +443,24 @@ class FakeRescue(base.RescueInterface):
     def unrescue(self, task):
         sleep(CONF.fake.rescue_delay)
         return states.ACTIVE
+
+
+class FakeFirmware(base.FirmwareInterface):
+    """Example implementation of a simple firmware interface."""
+
+    def get_properties(self):
+        return {}
+
+    def validate(self, task):
+        pass
+
+    @base.clean_step(priority=0, argsinfo={
+        'settings': {'description': ('List of Firmware components, each item '
+                     'needs to contain a dictionary with name/value pairs'),
+                     'required': True}})
+    def update(self, task, settings):
+        sleep(CONF.fake.firmware_delay)
+
+    def cache_firmware_components(self, task):
+        sleep(CONF.fake.firmware_delay)
+        pass
diff --git a/ironic/drivers/modules/noop.py b/ironic/drivers/modules/noop.py
index 3c8cb97038..491e1db619 100644
--- a/ironic/drivers/modules/noop.py
+++ b/ironic/drivers/modules/noop.py
@@ -81,3 +81,13 @@ class NoBIOS(FailMixin, base.BIOSInterface):
 
     def cache_bios_settings(self, task):
         pass
+
+
+class NoFirmware(FailMixin, base.FirmwareInterface):
+    """Firmware interface implementation that raises errors on all requests"""
+
+    def update(self, task, settings):
+        _fail(self, task, settings)
+
+    def cache_firmware_components(self, task):
+        pass
diff --git a/ironic/objects/node.py b/ironic/objects/node.py
index 1567baa9a5..f86b9f78a5 100644
--- a/ironic/objects/node.py
+++ b/ironic/objects/node.py
@@ -80,7 +80,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
     # Version 1.36: Add boot_mode and secure_boot fields
     # Version 1.37: Add shard field
     # Version 1.38: Add parent_node field
-    VERSION = '1.38'
+    # Version 1.39: Add firmware_interface field
+    VERSION = '1.39'
 
     dbapi = db_api.get_instance()
 
@@ -155,6 +156,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
         'boot_interface': object_fields.StringField(nullable=True),
         'console_interface': object_fields.StringField(nullable=True),
         'deploy_interface': object_fields.StringField(nullable=True),
+        'firmware_interface': object_fields.StringField(nullable=True),
         'inspect_interface': object_fields.StringField(nullable=True),
         'management_interface': object_fields.StringField(nullable=True),
         'network_interface': object_fields.StringField(nullable=True),
@@ -662,6 +664,9 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
             For versions prior to this, it should be set to None or removed.
         Version 1.37: shard was added. Default is None. For versions prior to
             this, it should be set to None or removed.
+        Version 1.39: firmware_interface field was added. Its default value is
+            None. For versions prior to this, it should be set to None (or
+            removed).
 
         :param target_version: the desired version of the object
         :param remove_unavailable_fields: True to remove fields that are
@@ -677,7 +682,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
                   ('automated_clean', 28), ('protected_reason', 29),
                   ('owner', 30), ('allocation_id', 31), ('description', 32),
                   ('retired_reason', 33), ('lessee', 34), ('boot_mode', 36),
-                  ('secure_boot', 36), ('shard', 37)]
+                  ('secure_boot', 36), ('shard', 37),
+                  ('firmware_interface', 39)]
 
         for name, minor in fields:
             self._adjust_field_to_version(name, None, target_version,
diff --git a/ironic/tests/unit/api/base.py b/ironic/tests/unit/api/base.py
index 6670be14e3..5f53e30735 100644
--- a/ironic/tests/unit/api/base.py
+++ b/ironic/tests/unit/api/base.py
@@ -72,8 +72,8 @@ class BaseApiTest(db_base.DbTestCase):
 
     def _make_app(self):
         # Determine where we are so we can set up paths in the config
-        root_dir = self.path_get()
 
+        root_dir = self.path_get()
         self.app_config = {
             'app': {
                 'root': self.root_controller,
diff --git a/ironic/tests/unit/api/controllers/v1/test_driver.py b/ironic/tests/unit/api/controllers/v1/test_driver.py
index 6bf04297ff..645f1ce3ba 100644
--- a/ironic/tests/unit/api/controllers/v1/test_driver.py
+++ b/ironic/tests/unit/api/controllers/v1/test_driver.py
@@ -219,7 +219,7 @@ class TestListDrivers(base.BaseApiTest):
 
         for iface in driver_base.ALL_INTERFACES:
             if iface != 'bios':
-                if latest_if or iface not in ['rescue', 'storage']:
+                if latest_if or iface not in ['rescue', 'storage', 'firmware']:
                     self.assertIn('default_%s_interface' % iface, data)
                     self.assertIn('enabled_%s_interfaces' % iface, data)
 
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index 083bab870b..ce2c069300 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -8478,3 +8478,40 @@ class TestNodeParentNodePatch(test_api_base.BaseApiTest):
             '/nodes/%s' % self.child_node.uuid, body, headers=headers)
         self.assertEqual(http_client.OK, response.status_code)
         self.mock_update_node.assert_called_once()
+
+
+class TestNodeFirmwareComponent(test_api_base.BaseApiTest):
+
+    def setUp(self):
+        super(TestNodeFirmwareComponent, self).setUp()
+        self.version = "1.86"
+        self.node = obj_utils.create_test_node(
+            self.context, id=1)
+
+        self.fw_cmp = obj_utils.create_test_firmware_component(
+            self.context, node_id=self.node.id)
+        self.fw_cmp2 = obj_utils.create_test_firmware_component(
+            self.context, node_id=self.node.id, component='BIOS')
+
+    def test_get_all_firmware_components(self):
+        ret = self.get_json('/nodes/%s/firmware' % self.node.uuid,
+                            headers={api_base.Version.string: self.version})
+        expected_components = [
+            {'created_at': ret['firmware'][0]['created_at'],
+             'updated_at': ret['firmware'][0]['updated_at'],
+             'component': 'BIOS',
+             'initial_version': 'v1.0.0', 'current_version': 'v1.0.0',
+             'last_version_flashed': None},
+            {'created_at': ret['firmware'][1]['created_at'],
+             'updated_at': ret['firmware'][1]['updated_at'],
+             'component': 'bmc',
+             'initial_version': 'v1.0.0', 'current_version': 'v1.0.0',
+             'last_version_flashed': None}]
+        self.assertEqual({'firmware': expected_components}, ret)
+
+    def test_wrong_version_get_all_firmware_components_old_version(self):
+        ret = self.get_json('/nodes/%s/firmware' % self.node.uuid,
+                            headers={api_base.Version.string: "1.81"},
+                            expect_errors=True)
+
+        self.assertEqual(http_client.NOT_FOUND, ret.status_int)
diff --git a/ironic/tests/unit/api/test_acl.py b/ironic/tests/unit/api/test_acl.py
index 4ac76eef96..0481843bd7 100644
--- a/ironic/tests/unit/api/test_acl.py
+++ b/ironic/tests/unit/api/test_acl.py
@@ -285,6 +285,10 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
             value=fake_setting)
         db_utils.create_test_node_trait(
             node_id=fake_db_node['id'])
+        # Create a Fake Firmware Component BMC
+        db_utils.create_test_firmware_component(
+            node_id=fake_db_node['id'],
+        )
         fake_history = db_utils.create_test_history(node_id=fake_db_node.id)
         fake_inventory = db_utils.create_test_inventory(
             node_id=fake_db_node.id)
diff --git a/ironic/tests/unit/api/test_rbac_project_scoped.yaml b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
index f4a48df512..e52fe6a19c 100644
--- a/ironic/tests/unit/api/test_rbac_project_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
@@ -3946,3 +3946,35 @@ lessee_cannot_get_a_nodes_children:
   method: get
   headers: *lessee_reader_headers
   assert_status: 404
+
+# Node Firmware
+
+owner_reader_can_get_firmware_components:
+  path: '/v1/nodes/{owner_node_ident}/firmware'
+  method: get
+  headers: *owner_reader_headers
+  assert_status: 200
+
+lessee_reader_can_get_firmware_components:
+  path: '/v1/nodes/{lessee_node_ident}/firmware'
+  method: get
+  headers: *lessee_reader_headers
+  assert_status: 200
+
+third_party_admin_cannot_get_firmware_components:
+  path: '/v1/nodes/{owner_node_ident}/firmware'
+  method: get
+  headers: *third_party_admin_headers
+  assert_status: 404
+
+service_can_get_firmware_components_owner_project:
+  path: '/v1/nodes/{owner_node_ident}/firmware'
+  method: get
+  headers: *service_headers_owner_project
+  assert_status: 200
+
+service_cannot_get_firmware_components:
+  path: '/v1/nodes/{owner_node_ident}/firmware'
+  method: get
+  headers: *service_headers
+  assert_status: 404
diff --git a/ironic/tests/unit/api/test_rbac_system_scoped.yaml b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
index 8a56d31695..919c7c1cbe 100644
--- a/ironic/tests/unit/api/test_rbac_system_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
@@ -2340,3 +2340,23 @@ parent_node_patch_by_reader:
   headers: *reader_headers
   body: *patch_parent_node
   assert_status: 403
+
+# Node Firmware  - baremetal:node:firmware:get
+
+nodes_firmware_component_get_admin:
+  path: '/v1/nodes/{node_ident}/firmware'
+  method: get
+  headers: *admin_headers
+  assert_status: 200
+
+nodes_firmware_component_get_member:
+  path: '/v1/nodes/{node_ident}/firmware'
+  method: get
+  headers: *scoped_member_headers
+  assert_status: 200
+
+nodes_firmware_component_get_reader:
+  path: '/v1/nodes/{node_ident}/firmware'
+  method: get
+  headers: *reader_headers
+  assert_status: 200
diff --git a/ironic/tests/unit/common/test_driver_factory.py b/ironic/tests/unit/common/test_driver_factory.py
index c4857d21ce..dd569fda7c 100644
--- a/ironic/tests/unit/common/test_driver_factory.py
+++ b/ironic/tests/unit/common/test_driver_factory.py
@@ -378,6 +378,11 @@ class TestFakeHardware(hardware_type.AbstractHardwareType):
         """List of supported deploy interfaces."""
         return [fake.FakeDeploy]
 
+    @property
+    def supported_firmware_interfaces(self):
+        """List of supported firmware interfaces."""
+        return [fake.FakeFirmware]
+
     @property
     def supported_inspect_interfaces(self):
         """List of supported inspect interfaces."""
@@ -586,6 +591,7 @@ class HardwareTypeLoadTestCase(db_base.DbTestCase):
             'boot': set(['fake']),
             'console': set(['fake', 'no-console']),
             'deploy': set(['fake']),
+            'firmware': set(['fake', 'no-firmware']),
             'inspect': set(['fake', 'no-inspect']),
             'management': set(['fake']),
             'network': set(['noop']),
diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py
index b8d4ccebd5..582c87d80c 100644
--- a/ironic/tests/unit/conductor/test_manager.py
+++ b/ironic/tests/unit/conductor/test_manager.py
@@ -3578,7 +3578,8 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
                     'network': {'result': True},
                     'storage': {'result': True},
                     'rescue': {'result': True},
-                    'bios': {'result': True}}
+                    'bios': {'result': True},
+                    'firmware': {'result': True}}
         self.assertEqual(expected, ret)
         mock_iwdi.assert_called_once_with(self.context, expected_info)
 
diff --git a/ironic/tests/unit/drivers/test_base.py b/ironic/tests/unit/drivers/test_base.py
index 2d41174b1a..6d63e51f3a 100644
--- a/ironic/tests/unit/drivers/test_base.py
+++ b/ironic/tests/unit/drivers/test_base.py
@@ -839,12 +839,43 @@ class TestManagementInterface(base.TestCase):
                           management.get_mac_addresses, task_mock)
 
 
+class MyFirmwareInterface(driver_base.FirmwareInterface):
+
+    def get_properties(self):
+        pass
+
+    def validate(self, task):
+        pass
+
+    @driver_base.cache_firmware_components
+    def update(self, task, settings):
+        return "return_update"
+
+    def cache_firmware_components(self, task):
+        pass
+
+
+class TestFirmwareInterface(base.TestCase):
+
+    @mock.patch.object(MyFirmwareInterface, 'cache_firmware_components',
+                       autospec=True)
+    def test_update_with_wrapper(self, cache_firmware_components_mock):
+        firmware = MyFirmwareInterface()
+        task_mock = mock.MagicMock()
+
+        actual = firmware.update(task_mock, "")
+        cache_firmware_components_mock.assert_called_once_with(
+            firmware, task_mock)
+        self.assertEqual(actual, "return_update")
+
+
 class TestBareDriver(base.TestCase):
 
     def test_class_variables(self):
         self.assertEqual(['boot', 'deploy', 'management', 'network', 'power'],
                          driver_base.BareDriver().core_interfaces)
         self.assertEqual(
-            ['bios', 'console', 'inspect', 'raid', 'rescue', 'storage'],
+            ['bios', 'console', 'firmware', 'inspect', 'raid',
+             'rescue', 'storage'],
             driver_base.BareDriver().optional_interfaces
         )
diff --git a/ironic/tests/unit/objects/test_node.py b/ironic/tests/unit/objects/test_node.py
index 05793e3a61..2b607cdf06 100644
--- a/ironic/tests/unit/objects/test_node.py
+++ b/ironic/tests/unit/objects/test_node.py
@@ -1377,6 +1377,68 @@ class TestConvertToVersion(db_base.DbTestCase):
         self.assertIsNone(node.secure_boot)
         self.assertEqual({}, node.obj_get_changes())
 
+    def test_firmware_supported_missing(self):
+        # firmware_interface not set, should be set to default.
+        node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
+        delattr(node, 'firmware_interface')
+        node.obj_reset_changes()
+
+        node._convert_to_version("1.39")
+
+        self.assertIsNone(node.firmware_interface)
+        self.assertEqual({'firmware_interface': None},
+                         node.obj_get_changes())
+
+    def test_firmware_supported_set(self):
+        # firmware_interface set, no change required.
+        node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
+
+        node.firmware_interface = 'fake'
+        node.obj_reset_changes()
+        node._convert_to_version("1.39")
+        self.assertEqual('fake', node.firmware_interface)
+        self.assertEqual({}, node.obj_get_changes())
+
+    def test_firmware_unsupported_missing(self):
+        # firmware_interface not set, no change required.
+        node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
+
+        delattr(node, 'firmware_interface')
+        node.obj_reset_changes()
+        node._convert_to_version("1.38")
+        self.assertNotIn('firmware_interface', node)
+        self.assertEqual({}, node.obj_get_changes())
+
+    def test_firmware_unsupported_set_remove(self):
+        # firmware_interface set, should be removed.
+        node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
+
+        node.firmware_interface = 'fake'
+        node.obj_reset_changes()
+        node._convert_to_version("1.38")
+        self.assertNotIn('firmware_interface', node)
+        self.assertEqual({}, node.obj_get_changes())
+
+    def test_firmware_unsupported_set_no_remove_non_default(self):
+        # firmware_interface set, should be set to default.
+        node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
+
+        node.firmware_interface = 'fake'
+        node.obj_reset_changes()
+        node._convert_to_version("1.38", False)
+        self.assertIsNone(node.firmware_interface)
+        self.assertEqual({'firmware_interface': None}, node.obj_get_changes())
+
+    def test_firmware_unsupported_set_no_remove_default(self):
+        # firmware_interface set, no change required.
+        node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
+
+        node.firmware_interface = None
+        node.obj_reset_changes()
+        node._convert_to_version("1.38", False)
+        self.assertIsNone(node.firmware_interface)
+        self.assertEqual({}, node.obj_get_changes())
+
 
 class TestNodePayloads(db_base.DbTestCase):
 
diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py
index d17739e276..ab9f64693c 100644
--- a/ironic/tests/unit/objects/test_objects.py
+++ b/ironic/tests/unit/objects/test_objects.py
@@ -676,7 +676,7 @@ class TestObject(_LocalTest, _TestObject):
 # version bump. It is an MD5 hash of the object fields and remotable methods.
 # The fingerprint values should only be changed if there is a version bump.
 expected_object_fingerprints = {
-    'Node': '1.38-7e7fdaa2c2bb01153ad567c9f1081cb7',
+    'Node': '1.39-ee3f5ff28b79f9fabf84a50e34a71684',
     'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
     'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
     'Port': '1.11-97bf15b61224f26c65e90f007d78bfd2',
diff --git a/setup.cfg b/setup.cfg
index 915d50ccce..2fd0e89814 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -93,6 +93,10 @@ ironic.hardware.interfaces.deploy =
     fake = ironic.drivers.modules.fake:FakeDeploy
     ramdisk = ironic.drivers.modules.ramdisk:RamdiskDeploy
 
+ironic.hardware.interfaces.firmware =
+    fake = ironic.drivers.modules.fake:FakeFirmware
+    no-firmware = ironic.drivers.modules.noop:NoFirmware
+
 ironic.hardware.interfaces.inspect =
     fake = ironic.drivers.modules.fake:FakeInspect
     idrac = ironic.drivers.modules.drac.inspect:DracInspect