diff --git a/docs/cni.md b/docs/cni.md
new file mode 100644
index 0000000000000000000000000000000000000000..e58c9e57081092d6ee5d7df596f08577dc9c9709
--- /dev/null
+++ b/docs/cni.md
@@ -0,0 +1,10 @@
+CNI
+==============
+
+This network plugin only unpacks CNI plugins version `cni_version` into `/opt/cni/bin` and instructs kubelet to use cni, that is adds following cli params:
+
+`KUBELET_NETWORK_PLUGIN="--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"`
+
+It's intended usage is for custom CNI configuration, e.g. manual routing tables + bridge + loopback CNI plugin outside kubespray scope. Furthermore, it's used for non-kubespray supported CNI plugins which you can install afterward.
+
+You are required to fill `/etc/cni/net.d` with valid CNI configuration after using kubespray.
\ No newline at end of file
diff --git a/inventory/sample/group_vars/k8s-cluster/k8s-cluster.yml b/inventory/sample/group_vars/k8s-cluster/k8s-cluster.yml
index 3d74b98f952fc452fa3836e4b69131581a608bfa..e24caef353d2aadbabe38745598e966f8ba842e7 100644
--- a/inventory/sample/group_vars/k8s-cluster/k8s-cluster.yml
+++ b/inventory/sample/group_vars/k8s-cluster/k8s-cluster.yml
@@ -70,7 +70,7 @@ kube_users:
 # kube_oidc_groups_prefix: oidc:
 
 
-# Choose network plugin (cilium, calico, contiv, weave or flannel)
+# Choose network plugin (cilium, calico, contiv, weave or flannel. Use cni for generic cni plugin)
 # Can also be set to 'cloud', which lets the cloud provider setup appropriate routing
 kube_network_plugin: calico
 
diff --git a/roles/kubernetes-apps/network_plugin/cni/tasks/main.yml b/roles/kubernetes-apps/network_plugin/cni/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..80c08dddf390d0332980c556c7070048be968d3a
--- /dev/null
+++ b/roles/kubernetes-apps/network_plugin/cni/tasks/main.yml
@@ -0,0 +1,13 @@
+- name: CNI | make sure /opt/cni/bin exists
+  file:
+    path: /opt/cni/bin
+    state: directory
+    mode: 0755
+    owner: root
+    group: root
+- name: CNI | Copy cni plugins
+  unarchive:
+    src: "{{ local_release_dir }}/cni-plugins-{{ image_arch }}-{{ cni_version }}.tgz"
+    dest: "/opt/cni/bin"
+    mode: 0755
+    remote_src: yes
diff --git a/roles/kubernetes-apps/network_plugin/meta/main.yml b/roles/kubernetes-apps/network_plugin/meta/main.yml
index 8d2a5be1b989a7e2cea5d3b285c3c37db0fa31f7..3d4ac3cc959eadab66cd95c80180994171df911d 100644
--- a/roles/kubernetes-apps/network_plugin/meta/main.yml
+++ b/roles/kubernetes-apps/network_plugin/meta/main.yml
@@ -25,6 +25,11 @@ dependencies:
     tags:
       - contiv
 
+  - role: kubernetes-apps/network_plugin/cni
+    when: kube_network_plugin == 'cni'
+    tags:
+      - cni
+
   - role: kubernetes-apps/network_plugin/weave
     when: kube_network_plugin == 'weave'
     tags:
diff --git a/roles/kubernetes/node/templates/kubelet.kubeadm.env.j2 b/roles/kubernetes/node/templates/kubelet.kubeadm.env.j2
index 2d40ac98bea12958c1acfee28474f917bdcf1792..b1b510d768d65eaf890115b032c5fdf845bd6840 100644
--- a/roles/kubernetes/node/templates/kubelet.kubeadm.env.j2
+++ b/roles/kubernetes/node/templates/kubelet.kubeadm.env.j2
@@ -117,7 +117,7 @@ KUBELET_HOSTNAME="--hostname-override={{ kube_override_hostname }}"
 {% endif %}
 
 KUBELET_ARGS="{{ kubelet_args_base }} {{ kubelet_args_dns }} {{ kube_reserved }} {% if node_taints|default([]) %}--register-with-taints={{ node_taints | join(',') }} {% endif %}--node-labels={{ all_node_labels | join(',') }} {% if kube_feature_gates %} --feature-gates={{ kube_feature_gates|join(',') }} {% endif %} {% if kubelet_custom_flags is string %} {{kubelet_custom_flags}} {% else %}{% for flag in kubelet_custom_flags %} {{flag}} {% endfor %}{% endif %}{% if inventory_hostname in groups['kube-node'] %}{% if kubelet_node_custom_flags is string %} {{kubelet_node_custom_flags}} {% else %}{% for flag in kubelet_node_custom_flags %} {{flag}} {% endfor %}{% endif %}{% endif %}"
-{% if kube_network_plugin is defined and kube_network_plugin in ["calico", "canal", "flannel", "weave", "contiv", "cilium", "kube-router"] %}
+{% if kube_network_plugin is defined and kube_network_plugin in ["calico", "canal", "cni", "flannel", "weave", "contiv", "cilium", "kube-router"] %}
 KUBELET_NETWORK_PLUGIN="--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"
 {% elif kube_network_plugin is defined and kube_network_plugin == "cloud" %}
 KUBELET_NETWORK_PLUGIN="--hairpin-mode=promiscuous-bridge --network-plugin=kubenet"
diff --git a/roles/kubernetes/preinstall/tasks/0020-verify-settings.yml b/roles/kubernetes/preinstall/tasks/0020-verify-settings.yml
index 4317f86f1db926c6b7ef82ecac7848b15a85a852..5994848789287f1074b24220d8a377c95ef9f06f 100644
--- a/roles/kubernetes/preinstall/tasks/0020-verify-settings.yml
+++ b/roles/kubernetes/preinstall/tasks/0020-verify-settings.yml
@@ -20,7 +20,7 @@
 
 - name: Stop if unknown network plugin
   assert:
-    that: kube_network_plugin in ['calico', 'canal', 'flannel', 'weave', 'cloud', 'cilium', 'contiv', 'kube-router']
+    that: kube_network_plugin in ['calico', 'canal', 'flannel', 'weave', 'cloud', 'cilium', 'cni', 'contiv', 'kube-router']
   when: kube_network_plugin is defined
   ignore_errors: "{{ ignore_assert_errors }}"
 
diff --git a/roles/network_plugin/cni/tasks/main.yml b/roles/network_plugin/cni/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..80c08dddf390d0332980c556c7070048be968d3a
--- /dev/null
+++ b/roles/network_plugin/cni/tasks/main.yml
@@ -0,0 +1,13 @@
+- name: CNI | make sure /opt/cni/bin exists
+  file:
+    path: /opt/cni/bin
+    state: directory
+    mode: 0755
+    owner: root
+    group: root
+- name: CNI | Copy cni plugins
+  unarchive:
+    src: "{{ local_release_dir }}/cni-plugins-{{ image_arch }}-{{ cni_version }}.tgz"
+    dest: "/opt/cni/bin"
+    mode: 0755
+    remote_src: yes