diff --git a/.luacheckrc b/.luacheckrc
index 0f17ecc5659f818653be80cf48561201137abc20..963909728419ecd29a5e46331f63438fe1bfd36a 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -29,6 +29,7 @@ files["package/**/check_site.lua"] = {
 		"need",
 		"need_alphanumeric_key",
 		"need_array",
+		"need_array_elements_exclusive",
 		"need_array_of",
 		"need_boolean",
 		"need_chanlist",
diff --git a/package/gluon-core/check_site.lua b/package/gluon-core/check_site.lua
index 06b9ac59d0e32348184a197b4f193e195051d599..cf72f05fc8566f7a8258306e6c86643bf056764a 100644
--- a/package/gluon-core/check_site.lua
+++ b/package/gluon-core/check_site.lua
@@ -77,7 +77,11 @@ need_boolean(in_domain({'mesh', 'vxlan'}), false)
 
 local interfaces_roles = {'client', 'uplink', 'mesh'}
 for _, config in ipairs({'wan', 'lan', 'single'}) do
-	need_array_of(in_site({'interfaces', config, 'default_roles'}), interfaces_roles, false)
+	local default_roles = in_site({'interfaces', config, 'default_roles'})
+
+	need_array_of(default_roles, interfaces_roles, false)
+	need_array_elements_exclusive(default_roles, 'client', 'mesh', false)
+	need_array_elements_exclusive(default_roles, 'client', 'uplink', false)
 end
 
 obsolete({'mesh_on_wan'}, 'Use interfaces.wan.default_roles.')
diff --git a/package/gluon-core/luasrc/lib/gluon/check-site.lua b/package/gluon-core/luasrc/lib/gluon/check-site.lua
index 148f4968f74cd32b47585ba954c38bd5f6888d1c..cde55f442d66ff8dd294fb3b411b79760d79751f 100644
--- a/package/gluon-core/luasrc/lib/gluon/check-site.lua
+++ b/package/gluon-core/luasrc/lib/gluon/check-site.lua
@@ -55,6 +55,14 @@ local function merge(a, b)
 	return m
 end
 
+local function contains(table, val)
+	for i=1,#table do
+		if table[i] == val then
+			return true
+		end
+	end
+	return false
+end
 
 local function path_to_string(path)
 	if path.is_value then
@@ -370,6 +378,21 @@ function M.need_array_of(path, array, required)
 	return M.need_array(path, function(e) M.need_one_of(e, array) end, required)
 end
 
+function M.need_array_elements_exclusive(path, a, b, required)
+	local val = need_type(path, 'table', required, 'be an array')
+	if not val then
+		return nil
+	end
+
+	if contains(val, a) and contains(val, b) then
+		config_error(conf_src(path),
+			'expected %s to contain only one of the elements %s and %s, but not both.',
+			path_to_string(path), format(a), format(b))
+	end
+
+	return val
+end
+
 function M.need_chanlist(path, channels, required)
 	local valid_chanlist = check_chanlist(channels)
 	return M.need(path, valid_chanlist, required,