diff --git a/app/api/network.py b/app/api/network.py
index 24be880c364144ccf8bc2717677b64204d8bd13c..e5f689df5ac4538a42f889456670d8d943a6bf8c 100644
--- a/app/api/network.py
+++ b/app/api/network.py
@@ -197,3 +197,38 @@ def create_domain():
     """
     return create_generic_model(models.Domain,
                                 mandatory_fields=('name',))
+
+
+@bp.route('/cnames')
+@jwt_required
+def get_cnames():
+    """Return cnames
+
+    .. :quickref: Network; Get cnames
+    """
+    domain = request.args.get('domain', None)
+    if domain is not None:
+        query = models.Cname.query
+        query = query.join(models.Cname.interface).join(
+            models.Interface.network).join(
+            models.Network.domain).filter(
+                models.Domain.name == domain)
+        query = query.order_by(models.Cname.name)
+        return get_generic_model(model=None, query=query)
+    return get_generic_model(models.Cname,
+                             order_by=models.Cname.name)
+
+
+@bp.route('/cnames', methods=['POST'])
+@jwt_required
+@jwt_groups_accepted('admin')
+def create_cname():
+    """Create a new cname
+
+    .. :quickref: Network; Create new cname
+
+    :jsonparam name: full cname
+    :jsonparam interface_id: primary key of the associated interface
+    """
+    return create_generic_model(models.Cname,
+                                mandatory_fields=('name', 'interface_id'))
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
index 75c6b356a6d5239d6784a6c967b15686d1a44025..d9166d2ef32146351f0f153e37550f296ca8f141 100644
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -30,6 +30,7 @@ register(factories.InterfaceFactory)
 register(factories.HostFactory)
 register(factories.MacFactory)
 register(factories.DomainFactory)
+register(factories.CnameFactory)
 
 
 @pytest.fixture(scope='session')
diff --git a/tests/functional/factories.py b/tests/functional/factories.py
index d0554a6ea93d1a735dec5b385deae072ccff06b9..523103ed31db5bf8ae409d27f96f89556d5a1c56 100644
--- a/tests/functional/factories.py
+++ b/tests/functional/factories.py
@@ -130,7 +130,10 @@ class NetworkFactory(factory.alchemy.SQLAlchemyModelFactory):
     def first_ip(self):
         net = ipaddress.ip_network(self.address)
         hosts = list(net.hosts())
-        return str(hosts[4])
+        try:
+            return str(hosts[4])
+        except IndexError:
+            return str(hosts[0])
 
     @factory.lazy_attribute
     def last_ip(self):
@@ -168,3 +171,14 @@ class MacFactory(factory.alchemy.SQLAlchemyModelFactory):
         sqlalchemy_session_persistence = 'commit'
 
     address = factory.Faker('mac_address')
+
+
+class CnameFactory(factory.alchemy.SQLAlchemyModelFactory):
+    class Meta:
+        model = models.Cname
+        sqlalchemy_session = common.Session
+        sqlalchemy_session_persistence = 'commit'
+
+    name = factory.Sequence(lambda n: f'host{n}.example.org')
+    interface = factory.SubFactory(InterfaceFactory)
+    user = factory.SubFactory(UserFactory)
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index c6ce317f621ef81696366a913b103b8b04b2716e..859f115de91139828aa89a4cbbed69fc210625f6 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -28,6 +28,7 @@ ENDPOINT_MODEL = {
     'network/hosts': models.Host,
     'network/macs': models.Mac,
     'network/domains': models.Domain,
+    'network/cnames': models.Cname,
 }
 GENERIC_GET_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys()
                          if key.startswith('inventory') and key != 'inventory/items']
@@ -827,3 +828,62 @@ def test_create_domain(client, admin_token):
     # Check that name shall be unique
     response = post(client, f'{API_URL}/network/domains', data=data, token=admin_token)
     check_response_message(response, '(psycopg2.IntegrityError) duplicate key value violates unique constraint', 422)
+
+
+def test_get_cnames(client, cname_factory, readonly_token):
+    # Create some cnames
+    cname1 = cname_factory()
+    cname2 = cname_factory()
+    response = get(client, f'{API_URL}/network/cnames', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 2
+    check_input_is_subset_of_response(response, (cname1.to_dict(), cname2.to_dict()))
+
+
+def test_get_cnames_by_domain(client, domain_factory, network_factory, interface_factory, cname_factory, readonly_token):
+    # Create some cnames
+    domain_a = domain_factory(name='a.esss.lu.se')
+    domain_b = domain_factory(name='b.esss.lu.se')
+    network_a = network_factory(domain=domain_a)
+    network_b = network_factory(domain=domain_b)
+    interface_a1 = interface_factory(network=network_a)
+    interface_a2 = interface_factory(network=network_a)
+    interface_b1 = interface_factory(network=network_b)
+    cname_a1 = cname_factory(interface=interface_a1)
+    cname_a2 = cname_factory(interface=interface_a1)
+    cname_a3 = cname_factory(interface=interface_a2)
+    cname_b1 = cname_factory(interface=interface_b1)
+    cname_b2 = cname_factory(interface=interface_b1)
+    response = get(client, f'{API_URL}/network/cnames', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 5
+    response = get(client, f'{API_URL}/network/cnames?domain=a.esss.lu.se', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 3
+    check_input_is_subset_of_response(response, (cname_a1.to_dict(), cname_a2.to_dict(), cname_a3.to_dict()))
+    response = get(client, f'{API_URL}/network/cnames?domain=b.esss.lu.se', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 2
+    check_input_is_subset_of_response(response, (cname_b1.to_dict(), cname_b2.to_dict()))
+
+
+def test_create_cname(client, interface, admin_token):
+    # check that name and interface_id are mandatory
+    response = post(client, f'{API_URL}/network/cnames', data={}, token=admin_token)
+    check_response_message(response, "Missing mandatory field 'name'", 422)
+    response = post(client, f'{API_URL}/network/cnames', data={'name': 'myhost.example.org'}, token=admin_token)
+    check_response_message(response, "Missing mandatory field 'interface_id'", 422)
+    response = post(client, f'{API_URL}/network/cnames', data={'interface_id': interface.id}, token=admin_token)
+    check_response_message(response, "Missing mandatory field 'name'", 422)
+
+    data = {'name': 'myhost.tn.esss.lu.se',
+            'interface_id': interface.id}
+    response = post(client, f'{API_URL}/network/cnames', data=data, token=admin_token)
+    assert response.status_code == 201
+    assert {'id', 'name', 'interface',
+            'created_at', 'updated_at', 'user'} == set(response.json.keys())
+    assert response.json['name'] == data['name']
+
+    # Check that name shall be unique
+    response = post(client, f'{API_URL}/network/cnames', data=data, token=admin_token)
+    check_response_message(response, '(psycopg2.IntegrityError) duplicate key value violates unique constraint', 422)