diff --git a/app/models.py b/app/models.py
index 3b5749b63cdc3ec28999375a2031f8f09628b3dd..4478aa16a31ba6dab6eb32c1aff8cb92b105af95 100644
--- a/app/models.py
+++ b/app/models.py
@@ -469,8 +469,10 @@ class Network(CreatedMixin, db.Model):
     def validate_interfaces(self, key, interface):
         """Ensure the interface IP is in the network range"""
         addr, net = self.ip_in_network(interface.ip, self.address)
-        if addr < self.first or addr > self.last:
-            raise ValidationError(f'IP address {interface.ip} is not in range {self.first} - {self.last}')
+        # Admin user can create IP outside the defined range
+        if not utils.cse_current_user().is_admin:
+            if addr < self.first or addr > self.last:
+                raise ValidationError(f'IP address {interface.ip} is not in range {self.first} - {self.last}')
         return interface
 
     @validates('vlan_name')
@@ -561,11 +563,15 @@ class Interface(CreatedMixin, db.Model):
                            backref=db.backref('interfaces', lazy=True))
 
     def __init__(self, **kwargs):
-        # Automatically convert network to an instance of Network if it was passed
-        # as a string
-        if 'network' in kwargs:
+        # Always set self.network and not self.network_id to call validate_interfaces
+        network_id = kwargs.pop('network_id', None)
+        if network_id is not None:
+            kwargs['network'] = Network.query.get(network_id)
+        elif 'network' in kwargs:
+            # Automatically convert network to an instance of Network if it was passed
+            # as a string
             kwargs['network'] = utils.convert_to_model(kwargs['network'], Network, 'vlan_name')
-        # WARNING! Setting self.network will call validates_interfaces in the Network class
+        # WARNING! Setting self.network will call validate_interfaces in the Network class
         # For the validation to work, self.ip must be set before!
         # Ensure that ip is passed before network
         try:
diff --git a/app/network/forms.py b/app/network/forms.py
index 755626abac6176bf3de2b5024192968448dffe21..268bbb3de3319205f1ecc061ac252b603bf961a1 100644
--- a/app/network/forms.py
+++ b/app/network/forms.py
@@ -9,6 +9,7 @@ This module defines the network blueprint forms.
 :license: BSD 2-Clause, see LICENSE for more details.
 
 """
+import ipaddress
 from flask_login import current_user
 from wtforms import (SelectField, StringField, TextAreaField, IntegerField,
                      SelectMultipleField, BooleanField, validators)
@@ -44,6 +45,19 @@ def validate_tags(form, field):
                 raise validators.ValidationError(f'A gateway is already defined for network {network}: {existing_gateway}')
 
 
+def ip_in_network(form, field):
+    """Check that the IP is in the network"""
+    network_id_field = form['network_id']
+    network = models.Network.query.get(network_id_field.data)
+    ip = ipaddress.ip_address(field.data)
+    if ip not in network.network_ip:
+        raise validators.ValidationError(f'IP address {ip} is not in network {network.address}')
+    # Admin user can create IP outside the defined range
+    if current_user.is_authenticated and not current_user.is_admin:
+        if ip < network.first or ip > network.last:
+            raise validators.ValidationError(f'IP address {ip} is not in range {network.first} - {network.last}')
+
+
 class NoValidateSelectField(SelectField):
     """SelectField with no choices validation
 
@@ -122,9 +136,14 @@ class HostForm(CSEntryForm):
 class InterfaceForm(CSEntryForm):
     host_id = SelectField('Host')
     network_id = SelectField('Network')
-    # The list of IPs is dynamically created on the browser side
-    # depending on the selected network
-    ip = NoValidateSelectField('IP', choices=[])
+    ip = StringField(
+        'IP address',
+        validators=[validators.InputRequired(),
+                    validators.IPAddress(),
+                    ip_in_network,
+                    Unique(models.Interface, column='ip'),
+                    ],
+    )
     interface_name = StringField(
         'Interface name',
         description='name must be 2-20 characters long and contain only letters, numbers and dash',
diff --git a/app/network/views.py b/app/network/views.py
index eefcadc79ac3fc566eadedf80f212125f9b7be56..258c680e67a0b252e375e177d61a986f47ec2aeb 100644
--- a/app/network/views.py
+++ b/app/network/views.py
@@ -290,16 +290,16 @@ def retrieve_hosts():
     return jsonify(data=data)
 
 
-@bp.route('/_retrieve_available_ips/<int:network_id>')
+@bp.route('/_retrieve_first_available_ip/<int:network_id>')
 @login_required
-def retrieve_available_ips(network_id):
+def retrieve_first_available_ip(network_id):
     try:
         network = models.Network.query.get(network_id)
     except sa.exc.DataError:
         current_app.logger.warning(f'Invalid network_id: {network_id}')
-        data = []
+        data = ''
     else:
-        data = [str(address) for address in network.available_ips()]
+        data = str(network.available_ips()[0])
     return jsonify(data=data)
 
 
diff --git a/app/static/js/hosts.js b/app/static/js/hosts.js
index 6a4ec41bd890a49467488aab9d9014e9b9f9f7e3..62f5a591365413f8841e9ad1616002523a9eacbc 100644
--- a/app/static/js/hosts.js
+++ b/app/static/js/hosts.js
@@ -1,17 +1,13 @@
 $(document).ready(function() {
 
-  function update_available_ips() {
-    // Retrieve available IPs for the selected network
-    // and update the IP select field
+  function set_default_ip() {
+    // Retrieve the first available IP for the selected network
+    // and update the IP field
     var network_id = $("#network_id").val();
     $.getJSON(
-      $SCRIPT_ROOT + "/network/_retrieve_available_ips/" + network_id,
+      $SCRIPT_ROOT + "/network/_retrieve_first_available_ip/" + network_id,
       function(json) {
-        var $ip = $("#ip");
-        $ip.empty();
-        $.map(json.data, function(option, index) {
-          $ip.append($("<option></option>").attr("value", option).text(option));
-        });
+        $("#ip").val(json.data);
       }
     );
   }
@@ -33,18 +29,18 @@ $(document).ready(function() {
     }
   }
 
-  // Populate IP select field on first page load for:
+  // Set the default IP on first page load for:
   // - register new host
   // - add interface
-  // Do NOT replace the IPs on edit interface page load!
+  // Do NOT replace the IP on edit interface page load!
   // (we have to keep the existing IP)
   if( $("#hostForm").length || $("#interfaceForm").length ) {
-    update_available_ips();
+    set_default_ip();
   }
 
-  // Update IP select field when changing network
+  // Set the default IP when changing network
   $("#network_id").on('change', function() {
-    update_available_ips();
+    set_default_ip();
   });
 
   // Enable / disable item_id field depending on type
diff --git a/app/utils.py b/app/utils.py
index 5c84920ee30722a282387d03fd6f60bdb6bdd869..b1913ab1487ec239e05bf63ff8e0114590809df4 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -21,13 +21,18 @@ from flask_login import current_user
 from flask_jwt_extended import get_current_user
 
 
+def cse_current_user():
+    """Return the current_user from flask_jwt_extended (API) or flask_login (web UI)"""
+    return get_current_user() or current_user
+
+
 def fetch_current_user_id():
     """Retrieve the user_id from flask_jwt_extended (API) or flask_login (web UI)"""
     # Return None if we are outside of request context.
     if _app_ctx_stack.top is None or _request_ctx_stack.top is None:
         return None
     # Try to get the user from both flask_jwt_extended and flask_login
-    user = get_current_user() or current_user
+    user = cse_current_user()
     try:
         return user.id
     except AttributeError:
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index 2f5fa75f8dee253b33ac88d0df72fb20e84311f2..e725def62f172971ea3acd202177111882e68d86 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -654,6 +654,26 @@ def test_create_interface_ip_not_in_network(client, network_factory, user_token)
     check_response_message(response, 'IP address 192.168.2.4 is not in network 192.168.1.0/24', 422)
 
 
+def test_create_interface_ip_not_in_range(client, network_factory, user_token):
+    network = network_factory(address='192.168.1.0/24', first_ip='192.168.1.10', last_ip='192.168.1.250')
+    # IP address not in range
+    data = {'network': network.vlan_name,
+            'ip': '192.168.1.4',
+            'name': 'hostname'}
+    response = post(client, f'{API_URL}/network/interfaces', data=data, token=user_token)
+    check_response_message(response, 'IP address 192.168.1.4 is not in range 192.168.1.10 - 192.168.1.250', 422)
+
+
+def test_create_interface_ip_not_in_range_as_admin(client, network_factory, admin_token):
+    network = network_factory(address='192.168.1.0/24', first_ip='192.168.1.10', last_ip='192.168.1.250')
+    # IP address not in range
+    data = {'network': network.vlan_name,
+            'ip': '192.168.1.4',
+            'name': 'hostname'}
+    response = post(client, f'{API_URL}/network/interfaces', data=data, token=admin_token)
+    assert response.status_code == 201
+
+
 def test_get_macs(client, mac_factory, readonly_token):
     # Create some macs
     mac1 = mac_factory()