diff --git a/data/templates/node_exporter/node_exporter.service.j2 b/data/templates/node_exporter/node_exporter.service.j2
new file mode 100644
index 000000000..62e7e6774
--- /dev/null
+++ b/data/templates/node_exporter/node_exporter.service.j2
@@ -0,0 +1,20 @@
+{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' runuser -u node_exporter -- ' if vrf is vyos_defined else '' %}
+[Unit]
+Description=Node Exporter
+Documentation=https://github.com/prometheus/node_exporter
+After=network.target
+
+[Service]
+{% if vrf is not vyos_defined %}
+User=node_exporter
+{% endif %}
+ExecStart={{ vrf_command }}/usr/sbin/node_exporter \
+{% if listen_address is vyos_defined %}
+{%     for address in listen_address %}
+        --web.listen-address={{ address }}:{{ port }}
+{%     endfor %}
+{% else %}
+        --web.listen-address=:{{ port }}
+{% endif %}
+[Install]
+WantedBy=multi-user.target
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index dc8ada267..d83634cfc 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -1,264 +1,269 @@
 #!/bin/bash
 
 # Turn off Debian default for %sudo
 sed -i -e '/^%sudo/d' /etc/sudoers || true
 
 # Add minion user for salt-minion
 if ! grep -q '^minion' /etc/passwd; then
     adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg \
         --gecos "salt minion user" --shell /bin/vbash minion
     adduser --quiet minion frrvty
     adduser --quiet minion sudo
     adduser --quiet minion adm
     adduser --quiet minion dip
     adduser --quiet minion disk
     adduser --quiet minion users
     adduser --quiet minion frr
 fi
 
 # OpenVPN should get its own user
 if ! grep -q '^openvpn' /etc/passwd; then
     adduser --quiet --firstuid 100 --system --group --shell /usr/sbin/nologin openvpn
 fi
 
+# node_exporter should get its own user
+if ! grep -q '^node_exporter' /etc/passwd; then
+    adduser --quiet --firstuid 100 --system --group --shell /bin/false node_exporter
+fi
+
 # We need to have a group for RADIUS service users to use it inside PAM rules
 if ! grep -q '^radius' /etc/group; then
     addgroup --firstgid 1000 --quiet radius
 fi
 
 # Remove TACACS user added by base package - we use our own UID range and group
 # assignments - see below
 if grep -q '^tacacs' /etc/passwd; then
     if [ $(id -u tacacs0) -ge 1000 ]; then
         level=0
         vyos_group=vyattaop
         while [ $level -lt 16 ]; do
             userdel tacacs${level} || true
             rm -rf /home/tacacs${level} || true
             level=$(( level+1 ))
         done 2>&1
     fi
 fi
 
 # Remove TACACS+ PAM default profile
 if [[ -e /usr/share/pam-configs/tacplus ]]; then
     rm /usr/share/pam-configs/tacplus
 fi
 
 # Add TACACS system users required for TACACS based system authentication
 if ! grep -q '^tacacs' /etc/passwd; then
     # Add the tacacs group and all 16 possible tacacs privilege-level users to
     # the password file, home directories, etc. The accounts are not enabled
     # for local login, since they are only used to provide uid/gid/homedir for
     # the mapped TACACS+ logins (and lookups against them). The tacacs15 user
     # is also added to the sudo group, and vyattacfg group rather than vyattaop
     # (used for tacacs0-14).
     level=0
     vyos_group=vyattaop
     while [ $level -lt 16 ]; do
         adduser --quiet --system --firstuid 900 --disabled-login --ingroup tacacs \
             --no-create-home --gecos "TACACS+ mapped user at privilege level ${level}" \
             --shell /bin/vbash tacacs${level}
         adduser --quiet tacacs${level} frrvty
         adduser --quiet tacacs${level} adm
         adduser --quiet tacacs${level} dip
         adduser --quiet tacacs${level} users
         if [ $level -lt 15 ]; then
             adduser --quiet tacacs${level} vyattaop
             adduser --quiet tacacs${level} operator
         else
             adduser --quiet tacacs${level} vyattacfg
             adduser --quiet tacacs${level} sudo
             adduser --quiet tacacs${level} disk
             adduser --quiet tacacs${level} frr
             adduser --quiet tacacs${level} _kea
         fi
         level=$(( level+1 ))
     done 2>&1 | grep -v "User tacacs${level} already exists"
 fi
 
 # Add RADIUS operator user for RADIUS authenticated users to map to
 if ! grep -q '^radius_user' /etc/passwd; then
     adduser --quiet --firstuid 1000 --disabled-login --ingroup radius \
         --no-create-home --gecos "RADIUS mapped user at privilege level operator" \
         --shell /sbin/radius_shell radius_user
     adduser --quiet radius_user frrvty
     adduser --quiet radius_user vyattaop
     adduser --quiet radius_user operator
     adduser --quiet radius_user adm
     adduser --quiet radius_user dip
     adduser --quiet radius_user users
 fi
 
 # Add RADIUS admin user for RADIUS authenticated users to map to
 if ! grep -q '^radius_priv_user' /etc/passwd; then
     adduser --quiet --firstuid 1000 --disabled-login --ingroup radius \
         --no-create-home --gecos "RADIUS mapped user at privilege level admin" \
         --shell /sbin/radius_shell radius_priv_user
     adduser --quiet radius_priv_user frrvty
     adduser --quiet radius_priv_user vyattacfg
     adduser --quiet radius_priv_user sudo
     adduser --quiet radius_priv_user adm
     adduser --quiet radius_priv_user dip
     adduser --quiet radius_priv_user disk
     adduser --quiet radius_priv_user users
     adduser --quiet radius_priv_user frr
     adduser --quiet radius_priv_user _kea
 fi
 
 # add hostsd group for vyos-hostsd
 if ! grep -q '^hostsd' /etc/group; then
     addgroup --quiet --system hostsd
 fi
 
 # Add _kea user for kea-dhcp{4,6}-server to vyattacfg
 # The user should exist via kea-common installed as transitive dependency
 if grep -q '^_kea' /etc/passwd; then
     adduser --quiet _kea vyattacfg
 fi
 
 # ensure the proxy user has a proper shell
 chsh -s /bin/sh proxy
 
 # Set file capabilities
 setcap cap_net_admin=pe /sbin/ethtool
 setcap cap_net_admin=pe /sbin/tc
 setcap cap_net_admin=pe /bin/ip
 setcap cap_net_admin=pe /sbin/xtables-legacy-multi
 setcap cap_net_admin=pe /sbin/xtables-nft-multi
 setcap cap_net_admin=pe /usr/sbin/conntrack
 setcap cap_net_admin=pe /usr/sbin/arp
 setcap cap_net_raw=pe /usr/bin/tcpdump
 setcap cap_net_admin,cap_sys_admin=pe /sbin/sysctl
 setcap cap_sys_module=pe /bin/kmod
 setcap cap_sys_time=pe /bin/date
 
 # create needed directories
 mkdir -p /var/log/user
 mkdir -p /var/core
 mkdir -p /opt/vyatta/etc/config/auth
 mkdir -p /opt/vyatta/etc/config/scripts
 mkdir -p /opt/vyatta/etc/config/user-data
 mkdir -p /opt/vyatta/etc/config/support
 chown -R root:vyattacfg /opt/vyatta/etc/config
 chmod -R 775 /opt/vyatta/etc/config
 mkdir -p /opt/vyatta/etc/logrotate
 mkdir -p /opt/vyatta/etc/netdevice.d
 
 touch /etc/environment
 
 if [ ! -f /etc/bash_completion ]; then
   echo "source /etc/bash_completion.d/10vyatta-op" > /etc/bash_completion
   echo "source /etc/bash_completion.d/20vyatta-cfg" >> /etc/bash_completion
 fi
 
 sed -i 's/^set /builtin set /' /etc/bash_completion
 
 # Fix up PAM configuration for login so that invalid users are prompted
 # for password
 sed -i 's/requisite[ \t][ \t]*pam_securetty.so/required pam_securetty.so/' $rootfsdir/etc/pam.d/login
 
 # Change default shell for new accounts
 sed -i -e ':^DSHELL:s:/bin/bash:/bin/vbash:' /etc/adduser.conf
 
 # Do not allow users to change full name field (controlled by vyos-1x)
 sed -i -e 's/^CHFN_RESTRICT/#&/' /etc/login.defs
 
 # Only allow root to use passwd command
 if ! grep -q 'pam_succeed_if.so' /etc/pam.d/passwd ; then
     sed -i -e '/^@include/i \
 password    requisite pam_succeed_if.so user = root
 ' /etc/pam.d/passwd
 fi
 
 # remove unnecessary ddclient script in /etc/ppp/ip-up.d/
 # this logs unnecessary messages trying to start ddclient
 rm -f /etc/ppp/ip-up.d/ddclient
 
 # create /opt/vyatta/etc/config/scripts/vyos-preconfig-bootup.script
 PRECONFIG_SCRIPT=/opt/vyatta/etc/config/scripts/vyos-preconfig-bootup.script
 if [ ! -x $PRECONFIG_SCRIPT ]; then
     mkdir -p $(dirname $PRECONFIG_SCRIPT)
     touch $PRECONFIG_SCRIPT
     chmod 755 $PRECONFIG_SCRIPT
     cat <<EOF >>$PRECONFIG_SCRIPT
 #!/bin/sh
 # This script is executed at boot time before VyOS configuration is applied.
 # Any modifications required to work around unfixed bugs or use
 # services not available through the VyOS CLI system can be placed here.
 
 EOF
 fi
 
 # create /opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
 POSTCONFIG_SCRIPT=/opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
 if [ ! -x $POSTCONFIG_SCRIPT ]; then
     mkdir -p $(dirname $POSTCONFIG_SCRIPT)
     touch $POSTCONFIG_SCRIPT
     chmod 755 $POSTCONFIG_SCRIPT
     cat <<EOF >>$POSTCONFIG_SCRIPT
 #!/bin/sh
 # This script is executed at boot time after VyOS configuration is fully applied.
 # Any modifications required to work around unfixed bugs
 # or use services not available through the VyOS CLI system can be placed here.
 
 EOF
 fi
 
 # symlink destination is deleted during ISO assembly - this generates some noise
 # when the system boots: systemd-sysv-generator[1881]: stat() failed on
 # /etc/init.d/README, ignoring: No such file or directory. Thus we simply drop
 # the file.
 if [ -L /etc/init.d/README ]; then
     rm -f /etc/init.d/README
 fi
 
 # Remove unwanted daemon files from /etc
 # conntackd
 # pmacct
 # fastnetmon
 # ntp
 DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd
         /etc/default/pmacctd /etc/pmacct
         /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf
         /etc/ntp.conf /etc/default/ssh /etc/avahi/avahi-daemon.conf /etc/avahi/hosts
         /etc/powerdns /etc/default/pdns-recursor
         /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns"
 for tmp in $DELETE; do
     if [ -e ${tmp} ]; then
         rm -rf ${tmp}
     fi
 done
 
 # Remove logrotate items controlled via CLI and VyOS defaults
 sed -i '/^\/var\/log\/messages$/d' /etc/logrotate.d/rsyslog
 sed -i '/^\/var\/log\/auth.log$/d' /etc/logrotate.d/rsyslog
 
 # Fix FRR pam.d "vtysh_pam" vtysh_pam: Failed in account validation T5110
 if test -f /etc/pam.d/frr; then
     if grep -q 'pam_rootok.so' /etc/pam.d/frr; then
         sed -i -re 's/rootok/permit/' /etc/pam.d/frr
     fi
 fi
 
 # Enable Cloud-init pre-configuration service
 systemctl enable vyos-config-cloud-init.service
 
 # Enable Podman API
 systemctl enable podman.service
 
 # Generate API GraphQL schema
 /usr/libexec/vyos/services/api/graphql/generate/generate_schema.py
 
 # Update XML cache
 python3 /usr/lib/python3/dist-packages/vyos/xml_ref/update_cache.py
 
 # Generate hardlinks for systemd units for multi VRF support
 # as softlinks will fail in systemd:
 # symlink target name type "ssh.service" does not match source, rejecting.
 if [ ! -f /lib/systemd/system/ssh@.service ]; then
     ln /lib/systemd/system/ssh.service /lib/systemd/system/ssh@.service
 fi
 
 # T4287 - as we have a non-signed kernel use the upstream wireless reulatory database
 update-alternatives --set regulatory.db /lib/firmware/regulatory.db-upstream
diff --git a/interface-definitions/service_monitoring_node_exporter.xml.in b/interface-definitions/service_monitoring_node_exporter.xml.in
new file mode 100644
index 000000000..a11d2304f
--- /dev/null
+++ b/interface-definitions/service_monitoring_node_exporter.xml.in
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="service">
+    <children>
+      <node name="monitoring">
+        <children>
+          <node name="node-exporter" owner="${vyos_conf_scripts_dir}/service_monitoring_node-exporter.py">
+            <properties>
+              <help>Prometheus exporter for hardware and operating system metrics</help>
+              <priority>1280</priority>
+            </properties>
+            <children>
+              #include <include/listen-address.xml.i>
+              #include <include/port-number.xml.i>
+              <leafNode name="port">
+                <defaultValue>9100</defaultValue>
+              </leafNode>
+              #include <include/interface/vrf.xml.i>
+            </children>
+          </node>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/smoketest/scripts/cli/test_service_monitoring_node-exporter.py b/smoketest/scripts/cli/test_service_monitoring_node-exporter.py
new file mode 100755
index 000000000..e18a3f7a2
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_node-exporter.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'node_exporter'
+base_path = ['service', 'monitoring', 'node-exporter']
+service_file = '/etc/systemd/system/node_exporter.service'
+listen_if = 'dum3421'
+listen_ip = '192.0.2.1'
+
+
+class TestMonitoringNodeExporter(VyOSUnitTestSHIM.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        # call base-classes classmethod
+        super(TestMonitoringNodeExporter, cls).setUpClass()
+        # create a test interfaces
+        cls.cli_set(
+            cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32']
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, ['interfaces', 'dummy', listen_if])
+        super(TestMonitoringNodeExporter, cls).tearDownClass()
+
+    def tearDown(self):
+        self.cli_delete(base_path)
+        self.cli_commit()
+        self.assertFalse(process_named_running(PROCESS_NAME))
+
+    def test_01_basic_config(self):
+        self.cli_set(base_path + ['listen-address', listen_ip])
+
+        # commit changes
+        self.cli_commit()
+
+        file_content = read_file(service_file)
+        self.assertIn(f'{listen_ip}:9100', file_content)
+
+        # Check for running process
+        self.assertTrue(process_named_running(PROCESS_NAME))
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/src/conf_mode/service_monitoring_node-exporter.py b/src/conf_mode/service_monitoring_node-exporter.py
new file mode 100755
index 000000000..db34bb5d0
--- /dev/null
+++ b/src/conf_mode/service_monitoring_node-exporter.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+
+
+airbag.enable()
+
+service_file = '/etc/systemd/system/node_exporter.service'
+systemd_service = 'node_exporter.service'
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'monitoring', 'node-exporter']
+    if not conf.exists(base):
+        return None
+
+    config_data = conf.get_config_dict(
+        base, key_mangling=('-', '_'), get_first_key=True
+    )
+    config_data = conf.merge_defaults(config_data, recursive=True)
+
+    tmp = is_node_changed(conf, base + ['vrf'])
+    if tmp:
+        config_data.update({'restart_required': {}})
+
+    return config_data
+
+
+def verify(config_data):
+    # bail out early - looks like removal from running config
+    if not config_data:
+        return None
+
+    verify_vrf(config_data)
+    return None
+
+
+def generate(config_data):
+    if not config_data:
+        # Delete systemd files
+        if os.path.isfile(service_file):
+            os.unlink(service_file)
+        return None
+
+    # Render node_exporter service_file
+    render(service_file, 'node_exporter/node_exporter.service.j2', config_data)
+    return None
+
+
+def apply(config_data):
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+    if not config_data:
+        call(f'systemctl stop {systemd_service}')
+        return
+
+    # we need to restart the service if e.g. the VRF name changed
+    systemd_action = 'reload-or-restart'
+    if 'restart_required' in config_data:
+        systemd_action = 'restart'
+
+    call(f'systemctl {systemd_action} {systemd_service}')
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)