diff --git a/app/api/network.py b/app/api/network.py
index 2c8b3d4e2a58117dde385a51bcdad69b46603aec..99c3640de05efc88c707fcd32d45ec6f4b96397c 100644
--- a/app/api/network.py
+++ b/app/api/network.py
@@ -212,6 +212,7 @@ def create_host():
 
     :jsonparam name: hostname
     :jsonparam device_type: Physical|Virtual|...
+    :jsonparam is_ioc: True|False (optional)
     :jsonparam description: (optional) description
     :jsonparam items: (optional) list of items ICS id linked to the host
     :jsonparam ansible_vars: (optional) Ansible variables
diff --git a/app/defaults.py b/app/defaults.py
index ff6f2ab2928be8a5bd2f8cc90c5ab90998bbdef5..b140097add5b1995c9ac64b72f31344faf16464d 100644
--- a/app/defaults.py
+++ b/app/defaults.py
@@ -26,5 +26,4 @@ defaults = [
     models.DeviceType(name="MicroTCA"),
     models.DeviceType(name="VME"),
     models.DeviceType(name="PLC"),
-    models.Tag(name="IOC", admin_only=False),
 ]
diff --git a/app/factory.py b/app/factory.py
index cd6ab2f5cf3f3992f3436da8ea82819299c15ada..ce07c1c9dd0156eb03ea5bcb988a990ef7e53284 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -141,7 +141,6 @@ def create_app(config=None):
     admin.add_view(AdminModelView(models.Interface, db.session))
     admin.add_view(AdminModelView(models.Mac, db.session))
     admin.add_view(AdminModelView(models.Cname, db.session))
-    admin.add_view(AdminModelView(models.Tag, db.session))
     admin.add_view(TaskAdmin(models.Task, db.session, endpoint="tasks"))
 
     app.register_blueprint(main)
diff --git a/app/models.py b/app/models.py
index c766762e6cd140ca463ba0418f088cda2a933f61..e7d9156f93eaa6ecba854943db973318fa5ef06f 100644
--- a/app/models.py
+++ b/app/models.py
@@ -35,7 +35,6 @@ from .validators import (
     VLAN_NAME_RE,
     MAC_ADDRESS_RE,
     DEVICE_TYPE_RE,
-    TAG_RE,
 )
 from . import utils, search
 
@@ -905,28 +904,6 @@ class Network(CreatedMixin, db.Model):
         return d
 
 
-# Table required for Many-to-Many relationships between interfaces and tags
-interfacetags_table = db.Table(
-    "interfacetags",
-    db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
-    db.Column(
-        "interface_id", db.Integer, db.ForeignKey("interface.id"), primary_key=True
-    ),
-)
-
-
-class Tag(QRCodeMixin, db.Model):
-    admin_only = db.Column(db.Boolean, nullable=False, default=False)
-
-    @validates("name")
-    def validate_name(self, key, string):
-        """Ensure the name field matches the required format"""
-        if string is not None:
-            if TAG_RE.fullmatch(string) is None:
-                raise ValidationError(f"'{string}' is an invalid tag name")
-        return string
-
-
 class DeviceType(db.Model):
     __tablename__ = "device_type"
     id = db.Column(db.Integer, primary_key=True)
@@ -991,6 +968,7 @@ class AnsibleGroupType(Enum):
     NETWORK_SCOPE = "NETWORK_SCOPE"
     NETWORK = "NETWORK"
     DEVICE_TYPE = "DEVICE_TYPE"
+    IOC = "IOC"
 
     def __str__(self):
         return self.name
@@ -1060,6 +1038,8 @@ class AnsibleGroup(CreatedMixin, db.Model):
                 .order_by(Host.name)
                 .all()
             )
+        if self.type == AnsibleGroupType.IOC:
+            return Host.query.filter(Host.is_ioc.is_(True)).order_by(Host.name).all()
 
     @hosts.setter
     def hosts(self, value):
@@ -1110,7 +1090,6 @@ class Host(CreatedMixin, SearchableMixin, db.Model):
                 "host": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
                 "cnames": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
                 "domain": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
-                "tags": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
                 "device_type": {
                     "type": "text",
                     "fields": {"keyword": {"type": "keyword"}},
@@ -1129,6 +1108,7 @@ class Host(CreatedMixin, SearchableMixin, db.Model):
     device_type_id = db.Column(
         db.Integer, db.ForeignKey("device_type.id"), nullable=False
     )
+    is_ioc = db.Column(db.Boolean, nullable=False, default=False)
     ansible_vars = db.Column(postgresql.JSONB)
 
     # 1. Set cascade to all (to add delete) and delete-orphan to delete all interfaces
@@ -1174,13 +1154,6 @@ class Host(CreatedMixin, SearchableMixin, db.Model):
             ]
         super().__init__(**kwargs)
 
-    @property
-    def is_ioc(self):
-        for interface in self.interfaces:
-            if interface.is_ioc:
-                return True
-        return False
-
     @property
     def model(self):
         """Return the model of the first linked item"""
@@ -1295,12 +1268,6 @@ class Interface(CreatedMixin, db.Model):
         cascade="all, delete, delete-orphan",
         lazy="joined",
     )
-    tags = db.relationship(
-        "Tag",
-        secondary=interfacetags_table,
-        lazy="subquery",
-        backref=db.backref("interfaces", lazy=True),
-    )
 
     def __init__(self, **kwargs):
         # Always set self.host and not self.host_id to call validate_name
@@ -1382,10 +1349,7 @@ class Interface(CreatedMixin, db.Model):
 
     @property
     def is_ioc(self):
-        for tag in self.tags:
-            if tag.name == "IOC":
-                return True
-        return False
+        return self.is_main and self.host.is_ioc
 
     @property
     def is_main(self):
@@ -1410,7 +1374,6 @@ class Interface(CreatedMixin, db.Model):
                 "host": utils.format_field(self.host),
                 "cnames": [str(cname) for cname in self.cnames],
                 "domain": str(self.network.domain),
-                "tags": [str(tag) for tag in self.tags],
             }
         )
         if self.host:
diff --git a/app/network/forms.py b/app/network/forms.py
index fad2c3c44ccb1d1f3b3e46011ac10903ec667a50..644d83b964368e7a6c5c475115fc822ab0af1359 100644
--- a/app/network/forms.py
+++ b/app/network/forms.py
@@ -143,6 +143,7 @@ class HostForm(CSEntryForm):
     )
     description = TextAreaField("Description")
     device_type_id = SelectField("Device Type")
+    is_ioc = BooleanField("IOC", default=False)
     ansible_vars = YAMLField(
         "Ansible vars",
         description="Enter variables in YAML format. See https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html",
@@ -204,18 +205,10 @@ class InterfaceForm(CSEntryForm):
             UniqueAccrossModels([models.Host, models.Interface]),
         ],
     )
-    tags = SelectMultipleField("Tags", coerce=utils.coerce_to_str_or_none)
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.host_id.choices = utils.get_model_choices(models.Host)
-        if current_user.is_admin:
-            tags_query = models.Tag.query
-        else:
-            tags_query = models.Tag.query.filter(models.Tag.admin_only.is_(False))
-        self.tags.choices = utils.get_model_choices(
-            models.Tag, attr="name", query=tags_query
-        )
         # Only return the networks the user has access to
         self.network_id.choices = [
             (str(network.id), network.vlan_name)
diff --git a/app/network/views.py b/app/network/views.py
index 11f0f2a5d5e661cf681bf449c9a084379c3b38dd..af320d307343e39ff95b5ba4f9ae05ff76c50264 100644
--- a/app/network/views.py
+++ b/app/network/views.py
@@ -85,22 +85,17 @@ def create_host():
             host = models.Host(
                 name=form.name.data,
                 device_type=models.DeviceType.query.get(form.device_type_id.data),
+                is_ioc=form.is_ioc.data,
                 description=form.description.data or None,
                 ansible_vars=form.ansible_vars.data or None,
                 ansible_groups=ansible_groups,
             )
-            # The total number of tags will always be quite small
-            # It's more efficient to retrieve all of them in one query
-            # and do the filtering here
-            all_tags = models.Tag.query.all()
-            tags = [tag for tag in all_tags if str(tag.id) in form.tags.data]
             interface = models.Interface(
                 host=host,
                 name=form.name.data,
                 ip=form.ip.data,
                 mac=form.mac.data,
                 network=network,
-                tags=tags,
             )
             interface.cnames = [
                 models.Cname(name=name) for name in form.cnames_string.data.split()
@@ -239,6 +234,7 @@ def edit_host(name):
         try:
             host.name = form.name.data
             host.device_type = models.DeviceType.query.get(form.device_type_id.data)
+            host.is_ioc = form.is_ioc.data
             host.description = form.description.data or None
             host.ansible_vars = form.ansible_vars.data or None
             host.ansible_groups = [
@@ -281,11 +277,6 @@ def create_interface(hostname):
         network = models.Network.query.get(form.network_id.data)
         if not current_user.has_access_to_network(network):
             abort(403)
-        # The total number of tags will always be quite small
-        # It's more efficient to retrieve all of them in one query
-        # and do the filtering here
-        all_tags = models.Tag.query.all()
-        tags = [tag for tag in all_tags if str(tag.id) in form.tags.data]
         try:
             interface = models.Interface(
                 host=host,
@@ -293,7 +284,6 @@ def create_interface(hostname):
                 ip=form.ip.data,
                 mac=form.mac.data,
                 network=network,
-                tags=tags,
             )
             interface.cnames = [
                 models.Cname(name=name) for name in form.cnames_string.data.split()
@@ -339,12 +329,6 @@ def edit_interface(name):
     ips = [interface.ip]
     ips.extend([str(address) for address in interface.network.available_ips()])
     form.ip.choices = utils.get_choices(ips)
-    # Passing tags as kwarg to the InterfaceForm doesn't work because
-    # obj takes precedence (but interface.tags contain Tag instances and not id)
-    # We need to update the default values. Calling process is required.
-    # See https://stackoverflow.com/questions/5519729/wtforms-how-to-select-options-in-selectmultiplefield
-    form.tags.default = [tag.id for tag in interface.tags]
-    form.tags.process(request.form)
     if form.validate_on_submit():
         network = models.Network.query.get(form.network_id.data)
         if not current_user.has_access_to_network(network):
@@ -378,9 +362,6 @@ def edit_interface(name):
             return render_template(
                 "network/edit_interface.html", form=form, hostname=interface.host.name
             )
-        all_tags = models.Tag.query.all()
-        tags = [tag for tag in all_tags if str(tag.id) in form.tags.data]
-        interface.tags = tags
         # Mark the host as "dirty" to add it to the session so that it will
         # be re-indexed
         sa.orm.attributes.flag_modified(interface.host, "interfaces")
diff --git a/app/static/js/hosts.js b/app/static/js/hosts.js
index b6c590b3912e615fe0ac7fe72cde3e752ed406df..75ac21571872027148100dc9d8c003b7666ec8fd 100644
--- a/app/static/js/hosts.js
+++ b/app/static/js/hosts.js
@@ -45,27 +45,7 @@ $(document).ready(function() {
     }
   }
 
-  // Select the IOC tag by default depending on the device type
-  function update_default_tags(device_type) {
-    var tags_selectize = $("#tags")[0].selectize;
-    var ioc_device_types = ["PhysicalMachine", "VirtualMachine", "MicroTCA", "VME"];
-    var is_ioc_selected = $.inArray(device_type, ioc_device_types) > -1;
-    var ioc_search = tags_selectize.search("IOC").items[0]
-    if ( ioc_search === undefined ) {
-      // IOC is already selected
-      var ioc_value = $("div .item:contains('IOC')").data("value");
-    } else {
-      var ioc_value = ioc_search.id;
-    }
-    if ( is_ioc_selected ) {
-      tags_selectize.addItem(ioc_value, false);
-    } else {
-      tags_selectize.removeItem(ioc_value, false);
-    }
-  }
-
   // And check / uncheck random_mac checkbox
-  // and update default tags
   function update_device_type_attributes() {
     var device_type = $("#device_type_id option:selected").text();
     if( device_type.startsWith("Virtual") ) {
@@ -73,7 +53,6 @@ $(document).ready(function() {
     } else {
       $("#random_mac").prop("checked", false).change();
     }
-    update_default_tags(device_type);
   }
 
   // If random_mac is checked, generate a random address
diff --git a/app/templates/network/create_host.html b/app/templates/network/create_host.html
index 15d844c8a87eb0e7fe7a5890aaa8c5df39e07b7d..f234cd8b29b2450ac81ee40b8d0ea510c62382ad 100644
--- a/app/templates/network/create_host.html
+++ b/app/templates/network/create_host.html
@@ -8,13 +8,13 @@
     {{ form.hidden_tag() }}
     {{ render_field(form.name, class_="text-lowercase") }}
     {{ render_field(form.device_type_id, class_="selectize-default") }}
+    {{ render_field(form.is_ioc) }}
     {{ render_field(form.description) }}
     {{ render_field(form.network_id, class_="selectize-default") }}
     {{ render_field(form.ip) }}
     {{ render_field(form.random_mac) }}
     {{ render_field(form.mac) }}
     {{ render_field(form.cnames_string) }}
-    {{ render_field(form.tags, class_="selectize-default") }}
     {{ render_field(form.ansible_vars) }}
     {{ render_field(form.ansible_groups, class_="selectize-default") }}
     <div class="form-group row">
diff --git a/app/templates/network/create_interface.html b/app/templates/network/create_interface.html
index a7ebe51ff2578f965a82be2554d1699a886f7af6..160668f19f91cc1a14241018ba6e99797c00bef5 100644
--- a/app/templates/network/create_interface.html
+++ b/app/templates/network/create_interface.html
@@ -26,7 +26,6 @@
     {{ render_field(form.random_mac) }}
     {{ render_field(form.mac) }}
     {{ render_field(form.cnames_string) }}
-    {{ render_field(form.tags, class_="selectize-default") }}
     <div class="form-group row">
       <div class="col-sm-10">
         <button type="submit" class="btn btn-primary">Submit</button>
diff --git a/app/templates/network/edit_host.html b/app/templates/network/edit_host.html
index 3875ffe96dd07db363bd7effdeba11f4b30f8e09..f6f0bc1f556d247b29bc8133e60e79da39fa1f0b 100644
--- a/app/templates/network/edit_host.html
+++ b/app/templates/network/edit_host.html
@@ -20,6 +20,7 @@
     {{ form.hidden_tag() }}
     {{ render_field(form.name, class_="text-lowercase") }}
     {{ render_field(form.device_type_id, class_="selectize-default") }}
+    {{ render_field(form.is_ioc) }}
     {{ render_field(form.description) }}
     {{ render_field(form.ansible_vars) }}
     {{ render_field(form.ansible_groups, class_="selectize-default") }}
diff --git a/app/templates/network/edit_interface.html b/app/templates/network/edit_interface.html
index 19ba2d8aed9fc28ee979a2230b36781911bbd4f3..cd751dfd0b561d88a393c8ef9e2eacb8e47c7191 100644
--- a/app/templates/network/edit_interface.html
+++ b/app/templates/network/edit_interface.html
@@ -28,7 +28,6 @@
     {{ render_field(form.ip) }}
     {{ render_field(form.mac) }}
     {{ render_field(form.cnames_string) }}
-    {{ render_field(form.tags, class_="selectize-default") }}
     <div class="form-group row">
       <div class="col-sm-10">
         <button type="submit" class="btn btn-primary">Submit</button>
diff --git a/app/templates/network/view_host.html b/app/templates/network/view_host.html
index a1dda629ba7cf27828dbaf15c6e86e6e65855f45..52705debf17257c27d2c55c1b9059b08a2d7cff7 100644
--- a/app/templates/network/view_host.html
+++ b/app/templates/network/view_host.html
@@ -40,6 +40,8 @@
         </dd>
         <dt class="col-sm-3">Device Type</dt>
         <dd class="col-sm-9">{{ host.device_type }}</dd>
+        <dt class="col-sm-3">IOC</dt>
+        <dd class="col-sm-9">{{ host.is_ioc }}</dd>
         {% if host.items %}
         <dt class="col-sm-3">Items</dt>
         <dd class="col-sm-9">{{ link_to_items(host.items) }}</dd>
@@ -101,7 +103,6 @@
         <th>IP</th>
         <th>MAC</th>
         <th>Network</th>
-        <th>Tags</th>
       </tr>
     </thead>
     <tbody>
@@ -122,7 +123,6 @@
         <td>{{ interface.ip }}</td>
         <td>{{ interface.mac }}</td>
         <td>{{ link_to_network(interface.network) }}</td>
-        <td>{{ interface.tags | join(' ') }}</td>
       </tr>
       {% endfor %}
     </tbody>
diff --git a/app/validators.py b/app/validators.py
index 575bc87e6e87f9a48881a9cf8f13c27fe1024593..2e7f8f48f1fe2915b40ba7b68248c769d0b5f028 100644
--- a/app/validators.py
+++ b/app/validators.py
@@ -19,7 +19,6 @@ HOST_NAME_RE = re.compile("^[a-z0-9\-]{2,20}$")
 VLAN_NAME_RE = re.compile("^[A-Za-z0-9\-]{3,25}$")
 MAC_ADDRESS_RE = re.compile("^(?:[0-9a-fA-F]{2}[:-]?){5}[0-9a-fA-F]{2}$")
 DEVICE_TYPE_RE = re.compile("^[A-Za-z0-9]{3,25}$")
-TAG_RE = DEVICE_TYPE_RE
 
 
 class NoValidateSelectField(SelectField):
diff --git a/docs/_static/create_vioc.png b/docs/_static/create_vioc.png
index c7878ba683e67765734f2ff4fa6e9de6c2891631..a0d25727887dff5098832992539ddd6bfdb5ea14 100644
Binary files a/docs/_static/create_vioc.png and b/docs/_static/create_vioc.png differ
diff --git a/docs/_static/create_vm.png b/docs/_static/create_vm.png
index 16033163b3bcb4054e3327e1cda5a1a78cb2e8e4..9eeefdf59d035977d1396411044bb7c8c9a2889a 100644
Binary files a/docs/_static/create_vm.png and b/docs/_static/create_vm.png differ
diff --git a/docs/_static/register_host.png b/docs/_static/register_host.png
index e7feab21ce44588dea4ec6b0b0f928229aa1b423..252d24cb84f6c7f50c774a3adad1b9caa3a092df 100644
Binary files a/docs/_static/register_host.png and b/docs/_static/register_host.png differ
diff --git a/docs/network.rst b/docs/network.rst
index 78cbb2c7b5cb7049abd92c6f76892114d631d082..656bd9f0af9bbe6a9516d5fb47a2bb59666eeb74 100644
--- a/docs/network.rst
+++ b/docs/network.rst
@@ -81,11 +81,11 @@ To register a new host, you should:
 
    1. Enter a hostname.
    2. Choose a Type. For physical machines, you can associate an **item** from the inventory (optional).
-   3. Enter a description (optional).
-   4. Choose the network for the primary interface. The first available IP address on that network is automatically filled.
+   3. If the host is an IOC, select the **IOC** checkbox.
+   4. Enter a description.
+   5. Choose the network for the primary interface. The first available IP address on that network is automatically filled.
       You should not modify it except if you have a specific reason.
-   5. For physical machines, you have to enter the MAC address. A random one is automatically generated for virtual machines.
-   6. If the host is an IOC, you should select the **IOC** tag. It should be selected by default depending on the device type. Unselect it otherwise.
+   6. For physical machines, you have to enter the MAC address. A random one is automatically generated for virtual machines.
    7. Click Submit.
 
 .. image:: _static/register_host.png
@@ -129,7 +129,7 @@ From the *View host* page, you can trigger the creation of a Virtual machine:
 
 .. image:: _static/create_vm.png
 
-If the host is an IOC (the interface should have the **IOC** tag), the form will be slightly different:
+If the host is an IOC, the form will be slightly different:
 
 .. image:: _static/create_vioc.png
 
diff --git a/migrations/versions/ac04850e5f68_add_is_ioc_field.py b/migrations/versions/ac04850e5f68_add_is_ioc_field.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9bc161ac26b822bd885654f1341a796bedc902a
--- /dev/null
+++ b/migrations/versions/ac04850e5f68_add_is_ioc_field.py
@@ -0,0 +1,76 @@
+"""Add is_ioc field
+
+Revision ID: ac04850e5f68
+Revises: f7d72e432f51
+Create Date: 2019-02-28 11:28:36.993953
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import citext
+
+
+# revision identifiers, used by Alembic.
+revision = "ac04850e5f68"
+down_revision = "f7d72e432f51"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.execute("COMMIT")
+    op.execute("ALTER TYPE ansible_group_type ADD VALUE 'IOC'")
+    op.add_column(
+        "host",
+        sa.Column("is_ioc", sa.Boolean(), nullable=False, server_default="False"),
+    )
+    op.add_column(
+        "host_version",
+        sa.Column("is_ioc", sa.Boolean(), autoincrement=False, nullable=True),
+    )
+    host = sa.sql.table("host", sa.sql.column("id"), sa.sql.column("is_ioc"))
+    conn = op.get_bind()
+    res = conn.execute("SELECT id FROM tag WHERE name = 'IOC';")
+    row = res.fetchone()
+    ioc_tag_id = row[0]
+    res.close()
+    res = conn.execute(
+        f"""SELECT interface.host_id FROM interface
+        INNER JOIN interfacetags ON interface.id = interfacetags.interface_id
+        WHERE interfacetags.tag_id = {ioc_tag_id};
+        """
+    )
+    results = res.fetchall()
+    for result in results:
+        op.execute(host.update().where(host.c.id == result[0]).values(is_ioc=True))
+    op.drop_table("interfacetags")
+    op.drop_table("tag")
+
+
+def downgrade():
+    # WARNING! The downgrade doesn't recreate the IOC tag
+    op.drop_column("host_version", "is_ioc")
+    op.drop_column("host", "is_ioc")
+    op.create_table(
+        "tag",
+        sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
+        sa.Column("name", citext.CIText(), autoincrement=False, nullable=False),
+        sa.Column("description", sa.TEXT(), autoincrement=False, nullable=True),
+        sa.Column("admin_only", sa.BOOLEAN(), autoincrement=False, nullable=False),
+        sa.PrimaryKeyConstraint("id", name="pk_tag"),
+        sa.UniqueConstraint("name", name="uq_tag_name"),
+    )
+    op.create_table(
+        "interfacetags",
+        sa.Column("tag_id", sa.INTEGER(), autoincrement=False, nullable=False),
+        sa.Column("interface_id", sa.INTEGER(), autoincrement=False, nullable=False),
+        sa.ForeignKeyConstraint(
+            ["interface_id"],
+            ["interface.id"],
+            name="fk_interfacetags_interface_id_interface",
+        ),
+        sa.ForeignKeyConstraint(
+            ["tag_id"], ["tag.id"], name="fk_interfacetags_tag_id_tag"
+        ),
+        sa.PrimaryKeyConstraint("tag_id", "interface_id", name="pk_interfacetags"),
+    )
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
index 048817eaaec6af5c9b160d878331bf7942b0083f..3c57475a19ec34d2911b1472eec0958902f800f1 100644
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -35,7 +35,6 @@ register(factories.HostFactory)
 register(factories.MacFactory)
 register(factories.DomainFactory)
 register(factories.CnameFactory)
-register(factories.TagFactory)
 register(factories.TaskFactory)
 
 
diff --git a/tests/functional/factories.py b/tests/functional/factories.py
index 72fa05b748b5ed4102da790cef1312422b007b0b..be0c0e14a4b915ed128c392230c592e3c26dcfec 100644
--- a/tests/functional/factories.py
+++ b/tests/functional/factories.py
@@ -217,15 +217,6 @@ class CnameFactory(factory.alchemy.SQLAlchemyModelFactory):
     user = factory.SubFactory(UserFactory)
 
 
-class TagFactory(factory.alchemy.SQLAlchemyModelFactory):
-    class Meta:
-        model = models.Tag
-        sqlalchemy_session = common.Session
-        sqlalchemy_session_persistence = "commit"
-
-    name = factory.Sequence(lambda n: f"Tag{n}")
-
-
 class TaskFactory(factory.alchemy.SQLAlchemyModelFactory):
     class Meta:
         model = models.Task
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index 294820165e8880b4f55fcad54a22d5f480dbb450..a6dfe5fab995f2af7af5c06a6a8ac437608c9ad1 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -74,7 +74,6 @@ INTERFACE_KEYS = {
     "device_type",
     "model",
     "cnames",
-    "tags",
     "created_at",
     "updated_at",
     "user",
diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py
index 18b55dd5ee89d76b63b40bcc1773451f0def405f..b57ed01279cbfbe5fb06dedfe6d185f7fe6658f8 100644
--- a/tests/functional/test_models.py
+++ b/tests/functional/test_models.py
@@ -195,14 +195,6 @@ def test_device_type_validation(device_type_factory):
     assert "'Physical Machine' is an invalid device type name" in str(excinfo.value)
 
 
-def test_tag_validation(tag_factory):
-    tag = tag_factory(name="IOC")
-    assert tag.name == "IOC"
-    with pytest.raises(ValidationError) as excinfo:
-        tag = tag_factory(name="My tag")
-    assert "'My tag' is an invalid tag name" in str(excinfo.value)
-
-
 def test_ansible_groups(ansible_group_factory, host_factory):
     group1 = ansible_group_factory()
     group2 = ansible_group_factory()
@@ -222,6 +214,8 @@ def test_ansible_group_is_dynamic(ansible_group_factory):
     assert group2.is_dynamic
     group3 = ansible_group_factory(type=models.AnsibleGroupType.NETWORK)
     assert group3.is_dynamic
+    group4 = ansible_group_factory(type=models.AnsibleGroupType.IOC)
+    assert group4.is_dynamic
 
 
 def test_ansible_groups_children(ansible_group_factory, host_factory):
@@ -327,6 +321,14 @@ def test_ansible_dynamic_network_scope_group(
     assert group3.hosts == []
 
 
+def test_ansible_dynamic_ioc_group(ansible_group_factory, host_factory):
+    host1 = host_factory(name="host1", is_ioc=True)
+    host2 = host_factory(name="host2", is_ioc=True)
+    host_factory(name="host3", is_ioc=False)
+    group = ansible_group_factory(name="iocs", type=models.AnsibleGroupType.IOC)
+    assert group.hosts == [host1, host2]
+
+
 @pytest.mark.parametrize("status", [None, "FINISHED", "FAILED", "STARTED"])
 def test_no_task_waiting(status, user, task_factory):
     if status is None:
@@ -488,16 +490,17 @@ def test_host_fqdn(host_factory, interface_factory):
     assert host1.fqdn == f"{host1.name}.{interface1.network.domain.name}"
 
 
-def test_host_is_ioc(host_factory, interface_factory, tag_factory):
-    ioc_tag = tag_factory(name="IOC")
-    another_tag = tag_factory(name="foo")
-    host1 = host_factory()
-    interface_factory(name=host1.name, host=host1, tags=[ioc_tag])
-    interface_factory(host=host1)
+def test_host_is_ioc(host_factory, interface_factory):
+    host1 = host_factory(is_ioc=True)
+    interface1 = interface_factory(name=host1.name, host=host1)
+    interface2 = interface_factory(host=host1)
     assert host1.is_ioc
+    assert interface1.is_ioc
+    assert not interface2.is_ioc
     host2 = host_factory()
-    interface_factory(name=host2.name, host=host2, tags=[another_tag])
+    interface3 = interface_factory(name=host2.name, host=host2)
     assert not host2.is_ioc
+    assert not interface3.is_ioc
 
 
 def test_cname_existing_host(db, host_factory, cname_factory):
diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py
index 820a66744fe412f53a6581501d97aceecc51c181..f37dfe4305957cb184f1f1d85cf6107ef5d701bf 100644
--- a/tests/functional/test_web.py
+++ b/tests/functional/test_web.py
@@ -228,7 +228,7 @@ def test_retrieve_hosts(logged_client, interface_factory, host_factory):
     hosts = response.get_json()["data"]
     assert {host1.name, host2.name} == set(host["name"] for host in hosts)
     assert len(hosts[0]) == 14
-    assert len(hosts[0]["interfaces"][0]) == 16
+    assert len(hosts[0]["interfaces"][0]) == 15
 
 
 def test_retrieve_hosts_by_ip(logged_client, interface_factory):
@@ -307,12 +307,12 @@ def test_create_host(client, domain_factory, network_factory, device_type):
         "network_id": network.id,
         "name": name,
         "device_type_id": device_type.id,
+        "is_ioc": False,
         "ip": ip,
         "mac": mac,
         "description": "test",
         "ansible_vars": "",
         "ansible_groups": [],
-        "tags": [],
         "random_mac": False,
         "cnames_string": "",
     }
@@ -356,12 +356,12 @@ def test_create_host_invalid_fields(
         "network_id": network.id,
         "name": name,
         "device_type_id": device_type.id,
+        "is_ioc": False,
         "ip": ip,
         "mac": mac,
         "description": "test",
         "ansible_vars": "",
         "ansible_groups": [],
-        "tags": [],
         "random_mac": False,
         "cnames_string": "",
     }
@@ -409,7 +409,6 @@ def test_create_interface(
         "ip": ip,
         "mac": mac,
         "cnames_string": "",
-        "tags": [],
     }
     # Permission denied
     # user_lab doesn't have permissions for the host domain: prod.example.org