diff --git a/app/api/network.py b/app/api/network.py
index a07fbd871555cfcf7e75d59f3c53fc1026e7268a..ef2c28c888ea5405bda4910d958ea0dec85260af 100644
--- a/app/api/network.py
+++ b/app/api/network.py
@@ -66,3 +66,18 @@ def get_interfaces():
 def create_interface():
     """Create a new interface"""
     return create_generic_model(models.Interface, mandatory_fields=('network', 'ip', 'name'))
+
+
+@bp.route('/macs')
+@jwt_required
+def get_macs():
+    return get_generic_model(models.Mac, request.args,
+                             order_by=models.Mac.address)
+
+
+@bp.route('/macs', methods=['POST'])
+@jwt_required
+@jwt_groups_accepted('admin', 'create')
+def create_macs():
+    return create_generic_model(models.Mac,
+                                mandatory_fields=('address',))
diff --git a/app/models.py b/app/models.py
index 2570421c0f0e054868264bbfb1ce93b322bd7ce1..6c333c9ab757453e6ccbfe86ef5dcb9579688d33 100644
--- a/app/models.py
+++ b/app/models.py
@@ -24,7 +24,7 @@ from flask_login import UserMixin
 from wtforms import ValidationError
 from .extensions import db, login_manager, ldap_manager, cache
 from .plugins import FlaskUserPlugin
-from .validators import ICS_ID_RE, HOST_NAME_RE, VLAN_NAME_RE
+from .validators import ICS_ID_RE, HOST_NAME_RE, VLAN_NAME_RE, MAC_ADDRESS_RE
 from . import utils
 
 
@@ -638,6 +638,15 @@ class Mac(db.Model):
     def __str__(self):
         return str(self.address)
 
+    @validates('address')
+    def validate_address(self, key, string):
+        """Ensure the address is a valid MAC address"""
+        if string is None:
+            return None
+        if MAC_ADDRESS_RE.fullmatch(string) is None:
+            raise ValidationError(f"'{string}' does not appear to be a MAC address")
+        return string
+
     def to_dict(self):
         return {
             'id': self.id,
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
index f976c8bc91a054d2c484683722d3d081cee3986c..492442fb8ca6904bf8ffdc06339131675d5d521f 100644
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -28,6 +28,7 @@ register(factories.NetworkScopeFactory)
 register(factories.NetworkFactory)
 register(factories.InterfaceFactory)
 register(factories.HostFactory)
+register(factories.MacFactory)
 
 
 @pytest.fixture(scope='session')
diff --git a/tests/functional/factories.py b/tests/functional/factories.py
index f71df8a0aede7ddfed2318cd7f722659157b0acb..09ad58be608c2fd37bdac2060d565519bb7453af 100644
--- a/tests/functional/factories.py
+++ b/tests/functional/factories.py
@@ -147,3 +147,12 @@ class HostFactory(factory.alchemy.SQLAlchemyModelFactory):
 
     name = factory.Sequence(lambda n: f'host{n}')
     user = factory.SubFactory(UserFactory)
+
+
+class MacFactory(factory.alchemy.SQLAlchemyModelFactory):
+    class Meta:
+        model = models.Mac
+        sqlalchemy_session = common.Session
+        sqlalchemy_session_persistence = 'commit'
+
+    address = factory.Faker('mac_address')
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index ee9d0f2706d5df12978486603201db6414629a6d..81a16f8fe3d72e1d55f898a9a59755620dc16f4a 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -25,11 +25,12 @@ ENDPOINT_MODEL = {
     'inventory/items': models.Item,
     'network/networks': models.Network,
     'network/interfaces': models.Interface,
+    'network/macs': models.Mac,
 }
 GENERIC_GET_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys()
-                         if key not in ('inventory/items', 'network/networks', 'network/interfaces')]
+                         if key not in ('inventory/items', 'network/macs', 'network/networks', 'network/interfaces')]
 GENERIC_CREATE_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys()
-                            if key not in ('inventory/items', 'inventory/actions', 'network/networks', 'network/interfaces')]
+                            if key not in ('inventory/items', 'inventory/actions', 'network/macs', 'network/networks', 'network/interfaces')]
 CREATE_AUTH_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys() if key != 'inventory/actions']
 
 
@@ -650,3 +651,47 @@ def test_create_interface_ip_not_in_network(client, network_factory, user_token)
             'name': 'hostname'}
     response = post(client, f'{API_URL}/network/interfaces', data=data, token=user_token)
     check_response_message(response, 'IP address 192.168.2.4 is not in network 192.168.1.0/24', 422)
+
+
+def test_get_macs(client, mac_factory, readonly_token):
+    # Create some macs
+    mac1 = mac_factory()
+    mac2 = mac_factory()
+
+    response = get(client, f'{API_URL}/network/macs', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 2
+    check_input_is_subset_of_response(response, (mac1.to_dict(), mac2.to_dict()))
+
+
+def test_create_mac(client, item_factory, user_token):
+    item = item_factory()
+    # check that address is mandatory
+    response = post(client, f'{API_URL}/network/macs', data={}, token=user_token)
+    check_response_message(response, "Missing mandatory field 'address'", 422)
+
+    data = {'address': 'b5:4b:7d:a4:23:43'}
+    response = post(client, f'{API_URL}/network/macs', data=data, token=user_token)
+    assert response.status_code == 201
+    assert {'id', 'address', 'item', 'interfaces'} == set(response.json.keys())
+    assert response.json['address'] == data['address']
+
+    # Check that address shall be unique
+    response = post(client, f'{API_URL}/network/macs', data=data, token=user_token)
+    check_response_message(response, '(psycopg2.IntegrityError) duplicate key value violates unique constraint', 422)
+
+    # Check that all parameters can be passed
+    data2 = {'address': 'b5:4b:7d:a4:23:44',
+             'item_id': item.id}
+    response = post(client, f'{API_URL}/network/macs', data=data2, token=user_token)
+    assert response.status_code == 201
+
+    # check that all items were created
+    assert models.Mac.query.count() == 2
+
+
+@pytest.mark.parametrize('address', ('', 'foo', 'b5:4b:7d:a4:23'))
+def test_create_mac_invalid_address(address, client, user_token):
+    data = {'address': address}
+    response = post(client, f'{API_URL}/network/macs', data=data, token=user_token)
+    check_response_message(response, f"'{address}' does not appear to be a MAC address", 422)
diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py
index 83a2fc8e63b8549c4b4dad8d50be91cff74d2535..4ec1ea5744dadf51c87420aee507237ce3663407 100644
--- a/tests/functional/test_models.py
+++ b/tests/functional/test_models.py
@@ -10,6 +10,8 @@ This module defines models tests.
 
 """
 import ipaddress
+import pytest
+from wtforms import ValidationError
 
 
 def test_network_ip_properties(network_factory):
@@ -62,3 +64,15 @@ def test_network_available_and_used_ips(network_factory, interface_factory):
     interface_factory(network=network2, ip='172.16.20.12')
     assert network2.used_ips() == [ipaddress.ip_address(f'172.16.20.{i}') for i in range(11, 15)]
     assert list(network2.available_ips()) == []
+
+
+def test_mac_address_validation(mac_factory):
+    mac = mac_factory(address='F4:A7:39:15:DA:01')
+    assert mac.address == 'f4:a7:39:15:da:01'
+    mac = mac_factory(address='F4-A7-39-15-DA-02')
+    assert mac.address == 'f4:a7:39:15:da:02'
+    mac = mac_factory(address='F4A73915DA06')
+    assert mac.address == 'f4:a7:39:15:da:06'
+    with pytest.raises(ValidationError) as excinfo:
+        mac = mac_factory(address='F4A73915DA')
+    assert "'F4A73915DA' does not appear to be a MAC address" in str(excinfo.value)