diff --git a/roles/kubernetes/preinstall/defaults/main.yml b/roles/kubernetes/preinstall/defaults/main.yml
index 4e6fba915c7c481035e4bdd7f8e63f1aab86e558..3c4d8a40d527130659211c2ae6944245ff1112d2 100644
--- a/roles/kubernetes/preinstall/defaults/main.yml
+++ b/roles/kubernetes/preinstall/defaults/main.yml
@@ -6,18 +6,6 @@ epel_enabled: false
 # Kubespray sets this to true after clusterDNS is running to apply changes to the host resolv.conf
 dns_late: false
 
-common_required_pkgs:
-  - "{{ (ansible_distribution == 'openSUSE Tumbleweed') | ternary('openssl-1_1', 'openssl') }}"
-  - curl
-  - rsync
-  - socat
-  - unzip
-  - e2fsprogs
-  - xfsprogs
-  - ebtables
-  - bash-completion
-  - tar
-
 # Set to true if your network does not support IPv6
 # This may be necessary for pulling Docker images from
 # GCE docker repository
diff --git a/roles/kubernetes/preinstall/files/pkgs-schema.json b/roles/kubernetes/preinstall/files/pkgs-schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..1fb9e28de2097ae599af89dd931d77a63eaed9d0
--- /dev/null
+++ b/roles/kubernetes/preinstall/files/pkgs-schema.json
@@ -0,0 +1,80 @@
+{
+    "$schema": "https://json-schema.org/draft/2020-12/schema",
+    "$id": "https://kubespray.io/internal/os_packages.schema.json",
+    "title": "Os packages",
+    "description": "Criteria for selecting packages to install on Kubernetes nodes during installation by Kubespray",
+    "type": "object",
+    "patternProperties": {
+        ".*": {
+            "type": "object",
+            "additionalProperties": false,
+            "properties": {
+                "enabled": {
+                    "description": "Escape hatch to filter packages. The value is expected to be pre-resolved to a boolean by Jinja",
+                    "type": "boolean",
+                    "default": true
+                },
+                "groups": {
+                    "description": "Match if the host is in one of these groups. If not specified match any host.",
+                    "type": "array",
+                    "minItems": 1,
+                    "items":{
+                        "type": "string",
+                        "pattern": "^[0-9A-Za-z_]*$"
+                    }
+                },
+                "os": {
+                    "type": "object",
+                    "description": "If not specified match any OS. Otherwise, must match by 'families' or 'distributions' to be included.",
+                    "additionalProperties": false,
+                    "minProperties": 1,
+                    "properties": {
+                        "families": {
+                            "description": "Match if ansible_os_family is part of the list.",
+                            "type": "array",
+                            "minItems": 1,
+                            "items": {
+                                "type": "string"
+                            }
+                        },
+                        "distributions": {
+                            "type": "object",
+                            "description": "Match if ansible_distribution match one of defined keys.",
+                            "minProperties": 1,
+                            "patternProperties": {
+                                ".*": {
+                                    "description": "Match if either the value is the empty hash, or one major_versions/versions/releases contains the corresponding variable ('ansible_distrbution_*')",
+                                    "type": "object",
+                                    "additionalProperties": false,
+                                    "properties": {
+                                        "major_versions": {
+                                            "type": "array",
+                                            "minItems": 1,
+                                            "items": {
+                                                "type": "string"
+                                            }
+                                        },
+                                        "versions": {
+                                            "type": "array",
+                                            "minItems": 1,
+                                            "items": {
+                                                "type": "string"
+                                            }
+                                        },
+                                        "releases": {
+                                            "type": "array",
+                                            "minItems": 1,
+                                            "items": {
+                                                "type": "string"
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/roles/kubernetes/preinstall/tasks/0020-set_facts.yml b/roles/kubernetes/preinstall/tasks/0020-set_facts.yml
index fa7fba11381291d85c72ea881eacfc22c40f0480..4541c14c5d77705171ce13e35603997ef95d54a0 100644
--- a/roles/kubernetes/preinstall/tasks/0020-set_facts.yml
+++ b/roles/kubernetes/preinstall/tasks/0020-set_facts.yml
@@ -199,20 +199,6 @@
       supersede domain-name-servers {{ (nameservers | d([]) + cloud_resolver | d([])) | unique | join(', ') }};
   when: dns_early and not dns_late
 
-- name: Gather os specific variables
-  include_vars: "{{ item }}"
-  with_first_found:
-    - files:
-        - "{{ ansible_distribution | lower }}-{{ ansible_distribution_version | lower | replace('/', '_') }}.yml"
-        - "{{ ansible_distribution | lower }}-{{ ansible_distribution_release }}.yml"
-        - "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version | lower | replace('/', '_') }}.yml"
-        - "{{ ansible_distribution | lower }}.yml"
-        - "{{ ansible_os_family | lower }}.yml"
-        - defaults.yml
-      paths:
-        - ../vars
-      skip: true
-
 - name: Set etcd vars if using kubeadm mode
   set_fact:
     etcd_cert_dir: "{{ kube_cert_dir }}"
diff --git a/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml b/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml
index f2d40e99525e0181ccea80857dda9abd5ebd987c..91b78b75f614bdcfad4222e94c9f8d6d320c98bd 100644
--- a/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml
+++ b/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml
@@ -316,3 +316,15 @@
   when:
     - kube_apiserver_enable_admission_plugins is defined
     - kube_apiserver_enable_admission_plugins | length > 0
+
+- name: Verify that the packages list structure is valid
+  ansible.utils.validate:
+    criteria: "{{ lookup('file', 'pkgs-schema.json') }}"
+    data: "{{ pkgs }}"
+
+- name: Verify that the packages list is sorted
+  vars:
+    pkgs_lists: "{{ pkgs.keys() | list }}"
+  assert:
+    that: "pkgs_lists | sort == pkgs_lists"
+    fail_msg: "pkgs is not sorted: {{ pkgs_lists | ansible.utils.fact_diff(pkgs_lists | sort) }}"
diff --git a/roles/kubernetes/preinstall/tasks/0070-system-packages.yml b/roles/kubernetes/preinstall/tasks/0070-system-packages.yml
index 8d02a85756d0bbdf51814d7cab17e55943c8a00d..7085ffb0c4966166e4c6eaf526db6b84fe3dce6a 100644
--- a/roles/kubernetes/preinstall/tasks/0070-system-packages.yml
+++ b/roles/kubernetes/preinstall/tasks/0070-system-packages.yml
@@ -59,19 +59,28 @@
   tags:
     - bootstrap-os
 
-- name: Update common_required_pkgs with ipvsadm when kube_proxy_mode is ipvs
-  set_fact:
-    common_required_pkgs: "{{ common_required_pkgs | default([]) + ['ipvsadm', 'ipset'] }}"
-  when: kube_proxy_mode == 'ipvs'
-
 - name: Install packages requirements
+  vars:
+    # The json_query for selecting packages name is split for readability
+    # see files/pkgs-schema.json for the structure of `pkgs`
+    # and the matching semantics
+    full_query: "[? value | (enabled == null || enabled) && ( {{ filters_os }} ) && ( {{ filters_groups }} ) ].key"
+    filters_groups: "groups | @ == null || [? contains(`{{ group_names }}`, @)]"
+    filters_os: "os == null || (os | ( {{ filters_family }} ) || ( {{ filters_distro }} ))"
+    dquote: !unsafe '"'
+    # necessary to workaround Ansible escaping
+    filters_distro: "distributions.{{ dquote }}{{ ansible_distribution  }}{{ dquote }} |
+                          @ == `{}` ||
+                          contains(not_null(major_versions, `[]`), '{{ ansible_distribution_major_version }}') ||
+                          contains(not_null(versions, `[]`), '{{ ansible_distribution_version }}') ||
+                          contains(not_null(releases, `[]`), '{{ ansible_distribution_release }}')"
+    filters_family: "families && contains(families, '{{ ansible_os_family }}')"
   package:
-    name: "{{ required_pkgs | default([]) | union(common_required_pkgs | default([])) }}"
+    name: "{{ pkgs | dict2items | to_json|from_json | community.general.json_query(full_query) }}"
     state: present
   register: pkgs_task_result
   until: pkgs_task_result is succeeded
   retries: "{{ pkg_install_retries }}"
   delay: "{{ retry_stagger | random + 3 }}"
-  when: not (ansible_os_family in ["Flatcar", "Flatcar Container Linux by Kinvolk"] or is_fedora_coreos)
   tags:
     - bootstrap-os
diff --git a/roles/kubernetes/preinstall/vars/amazon.yml b/roles/kubernetes/preinstall/vars/amazon.yml
deleted file mode 100644
index 09c645f510d020eae49e208d906627aba09a5731..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/amazon.yml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-required_pkgs:
-  - libselinux-python
-  - device-mapper-libs
-  - nss
-  - conntrack-tools
-  - libseccomp
diff --git a/roles/kubernetes/preinstall/vars/centos.yml b/roles/kubernetes/preinstall/vars/centos.yml
deleted file mode 100644
index 9b1a8749e62f36cc818c02afa7b876d139c73c9b..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/centos.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-required_pkgs:
-  - "{{ ((ansible_distribution_major_version | int) < 8) | ternary('libselinux-python', 'python3-libselinux') }}"
-  - device-mapper-libs
-  - nss
-  - conntrack
-  - container-selinux
-  - libseccomp
diff --git a/roles/kubernetes/preinstall/vars/debian-11.yml b/roles/kubernetes/preinstall/vars/debian-11.yml
deleted file mode 100644
index 59cbc5a370ec7088eaedcf0798e9b7d10e60af42..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/debian-11.yml
+++ /dev/null
@@ -1,10 +0,0 @@
----
-required_pkgs:
-  - python3-apt
-  - gnupg
-  - apt-transport-https
-  - software-properties-common
-  - conntrack
-  - iptables
-  - apparmor
-  - libseccomp2
diff --git a/roles/kubernetes/preinstall/vars/debian-12.yml b/roles/kubernetes/preinstall/vars/debian-12.yml
deleted file mode 100644
index e0dca4dcd858ca3b39a90121dfc0b94ea2df0f27..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/debian-12.yml
+++ /dev/null
@@ -1,11 +0,0 @@
----
-required_pkgs:
-  - python3-apt
-  - gnupg
-  - apt-transport-https
-  - software-properties-common
-  - conntrack
-  - iptables
-  - apparmor
-  - libseccomp2
-  - mergerfs
diff --git a/roles/kubernetes/preinstall/vars/debian.yml b/roles/kubernetes/preinstall/vars/debian.yml
deleted file mode 100644
index 51a280237811e0d8bc1b30efd77bc2fef0b12ccc..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/debian.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-required_pkgs:
-  - python-apt
-  - aufs-tools
-  - apt-transport-https
-  - software-properties-common
-  - conntrack
-  - apparmor
-  - libseccomp2
diff --git a/roles/kubernetes/preinstall/vars/fedora.yml b/roles/kubernetes/preinstall/vars/fedora.yml
deleted file mode 100644
index d69b111b6d483f75c0a1fe7041910df826b7361b..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/fedora.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-required_pkgs:
-  - iptables
-  - libselinux-python3
-  - device-mapper-libs
-  - conntrack
-  - container-selinux
-  - libseccomp
diff --git a/roles/kubernetes/preinstall/vars/main.yml b/roles/kubernetes/preinstall/vars/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..28ee56a278635f246bf8de7b9d472a3ead6d1b38
--- /dev/null
+++ b/roles/kubernetes/preinstall/vars/main.yml
@@ -0,0 +1,106 @@
+---
+pkgs:
+  apparmor: &debian_family_base
+    os:
+      families:
+      - Debian
+  apt-transport-https: *debian_family_base
+  aufs-tools: &deb_10
+    groups:
+    - k8s_cluster
+    os:
+      distributions:
+        Debian:
+          major_versions:
+          - "10"
+  bash-completion: {}
+  conntrack: &deb_redhat
+    groups:
+    - k8s_cluster
+    os:
+      families:
+      - Debian
+      - RedHat
+  conntrack-tools:
+    groups:
+    - k8s_cluster
+    os:
+      families:
+      - Suse
+      distributions:
+        Amazon: {}
+  container-selinux: &redhat_family
+    groups:
+    - k8s_cluster
+    os:
+      families:
+      - RedHat
+  curl: {}
+  device-mapper:
+    groups:
+    - k8s_cluster
+    os:
+      families:
+      - Suse
+  device-mapper-libs: *redhat_family
+  e2fsprogs: {}
+  ebtables: {}
+  gnupg: &debian
+    groups:
+    - k8s_cluster
+    os:
+      distributions:
+        Debian:
+          major_versions:
+          - "11"
+          - "12"
+  ipset:
+    enabled: "{{ kube_proxy_mode != 'ipvs' }}"
+    groups:
+    - k8s_cluster
+  iptables: *deb_redhat
+  ipvsadm:
+    enabled: "{{ kube_proxy_mode == 'ipvs' }}"
+    groups:
+    - k8s_cluster
+  libseccomp: *redhat_family
+  libseccomp2:
+    groups:
+    - k8s_cluster
+    os:
+      families:
+      - Suse
+      - Debian
+  libselinux-python:  # TODO: Handle rehat_family + major < 8
+    os:
+      distributions:
+        Amazon: {}
+  libselinux-python3:
+    os:
+      distributions:
+        Fedora: {}
+  mergerfs:
+    os:
+      distributions:
+        Debian:
+          major_versions:
+          - "12"
+  nss: *redhat_family
+  openssl: {}
+  python-apt: *deb_10
+  # TODO: not for debian 10
+  python3-apt: *debian_family_base
+  python3-libselinux:
+    os:
+      distributions:
+        RedHat: &major_redhat_like
+          major_versions:
+          - "8"
+          - "9"
+        Centos: *major_redhat_like
+  rsync: {}
+  socat: {}
+  software-properties-common: *debian_family_base
+  tar: {}
+  unzip: {}
+  xfsprogs: {}
diff --git a/roles/kubernetes/preinstall/vars/redhat.yml b/roles/kubernetes/preinstall/vars/redhat.yml
deleted file mode 100644
index 9b1a8749e62f36cc818c02afa7b876d139c73c9b..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/redhat.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-required_pkgs:
-  - "{{ ((ansible_distribution_major_version | int) < 8) | ternary('libselinux-python', 'python3-libselinux') }}"
-  - device-mapper-libs
-  - nss
-  - conntrack
-  - container-selinux
-  - libseccomp
diff --git a/roles/kubernetes/preinstall/vars/suse.yml b/roles/kubernetes/preinstall/vars/suse.yml
deleted file mode 100644
index d089ac1501292bcb8be47607981e6fc069f3c9b9..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/suse.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-required_pkgs:
-  - device-mapper
-  - conntrack-tools
-  - libseccomp2
diff --git a/roles/kubernetes/preinstall/vars/ubuntu.yml b/roles/kubernetes/preinstall/vars/ubuntu.yml
deleted file mode 100644
index 85b3f255a20ab58ad5c449a0922f65ea1b8dc832..0000000000000000000000000000000000000000
--- a/roles/kubernetes/preinstall/vars/ubuntu.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-required_pkgs:
-  - python3-apt
-  - apt-transport-https
-  - software-properties-common
-  - conntrack
-  - apparmor
-  - libseccomp2