diff --git a/package/gluon-core/files/lib/gluon/functions/users.sh b/package/gluon-core/files/lib/gluon/functions/users.sh
deleted file mode 100644
index d05798588922f188df0ba7a2b1230ce4377ccd45..0000000000000000000000000000000000000000
--- a/package/gluon-core/files/lib/gluon/functions/users.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-add_user() {
-	local username="$1"
-	local id="$2"
-
-	[ "$username" -a "$id" ] || return 1
-
-	sed -i "/^$username:/d" /etc/passwd
-	sed -i "/^$username:/d" /etc/shadow
-
-	echo "$username:*:$id:100:$username:/var:/bin/false" >> /etc/passwd
-	echo "$username:*:0:0:99999:7:::" >> /etc/shadow
-}
diff --git a/package/gluon-core/files/usr/lib/lua/gluon/users.lua b/package/gluon-core/files/usr/lib/lua/gluon/users.lua
new file mode 100644
index 0000000000000000000000000000000000000000..8e618d88c74edf1862c30022b56ee3177d892b18
--- /dev/null
+++ b/package/gluon-core/files/usr/lib/lua/gluon/users.lua
@@ -0,0 +1,33 @@
+local util = require 'gluon.util'
+
+local os = os
+local string = string
+
+
+module 'gluon.users'
+
+function add_user(username, uid, gid)
+	util.lock('/var/lock/passwd')
+	util.replace_prefix('/etc/passwd', username .. ':', string.format('%s:*:%u:%u::/var:/bin/false\n', username, uid, gid))
+	util.replace_prefix('/etc/shadow', username .. ':', string.format('%s:*:0:0:99999:7:::\n', username))
+	util.unlock('/var/lock/passwd')
+end
+
+function remove_user(username)
+	util.lock('/var/lock/passwd')
+	util.replace_prefix('/etc/passwd', username .. ':')
+	util.replace_prefix('/etc/shadow', username .. ':')
+	util.unlock('/var/lock/passwd')
+end
+
+function add_group(groupname, gid)
+	util.lock('/var/lock/group')
+	util.replace_prefix('/etc/group', groupname .. ':', string.format('%s:x:%u:\n', groupname, gid))
+	util.unlock('/var/lock/group')
+end
+
+function remove_group(groupname)
+	util.lock('/var/lock/group')
+	util.replace_prefix('/etc/group', groupname .. ':')
+	util.unlock('/var/lock/group')
+end
diff --git a/package/gluon-core/files/usr/lib/lua/gluon/util.lua b/package/gluon-core/files/usr/lib/lua/gluon/util.lua
new file mode 100644
index 0000000000000000000000000000000000000000..8bfc8cdf3314c64b7da46ef7b12db43b5fd2640d
--- /dev/null
+++ b/package/gluon-core/files/usr/lib/lua/gluon/util.lua
@@ -0,0 +1,52 @@
+-- Writes all lines from the file input to the file output except those starting with prefix
+-- Doesn't close the output file, but returns the file object
+local function do_filter_prefix(input, output, prefix)
+	local f = io.open(output, 'w+')
+	local l = prefix:len()
+
+	for line in io.lines(input) do
+		if line:sub(1, l) ~= prefix then
+			f:write(line, '\n')
+		end
+	end
+
+	return f
+end
+
+
+local function escape_args(ret, arg0, ...)
+	if not arg0 then
+		return ret
+	end
+
+	return escape_args(ret .. "'" .. string.gsub(arg0, "'", "'\\''") .. "' ", ...)
+end
+
+
+local os = os
+local string = string
+
+module 'gluon.util'
+
+function exec(...)
+	return os.execute(escape_args('', ...))
+end
+
+-- Removes all lines starting with a prefix from a file, optionally adding a new one
+function replace_prefix(file, prefix, add)
+	local tmp = file .. '.tmp'
+	local f = do_filter_prefix(file, tmp, prefix)
+	if add then
+		f:write(add)
+	end
+	f:close()
+	os.rename(tmp, file)
+end
+
+function lock(file)
+	exec('lock', file)
+end
+
+function unlock(file)
+	exec('lock', '-u', file)
+end
diff --git a/package/gluon-mesh-vpn-fastd/files/lib/gluon/upgrade/mesh-vpn-fastd/invariant/010-mesh-vpn-fastd b/package/gluon-mesh-vpn-fastd/files/lib/gluon/upgrade/mesh-vpn-fastd/invariant/010-mesh-vpn-fastd
index 1d6d581125c15eeff61880f46fb6923f2033e1cd..ebce54f6d859fd8bfd5a6a3fe8e9651b6b25b8b6 100755
--- a/package/gluon-mesh-vpn-fastd/files/lib/gluon/upgrade/mesh-vpn-fastd/invariant/010-mesh-vpn-fastd
+++ b/package/gluon-mesh-vpn-fastd/files/lib/gluon/upgrade/mesh-vpn-fastd/invariant/010-mesh-vpn-fastd
@@ -2,13 +2,15 @@
 
 local site = require 'gluon.site_config'
 local sysconfig = require 'gluon.sysconfig'
+local users = require 'gluon.users'
+
 local nixio = require 'nixio'
 local uci = require 'luci.model.uci'
 
 local c = uci.cursor()
 
 
-os.execute('. /lib/gluon/functions/users.sh && add_user gluon-fastd 800')
+users.add_user('gluon-fastd', 800, 100)
 
 
 c:section('fastd', 'fastd', 'mesh_vpn',
diff --git a/package/gluon-radvd/files/lib/gluon/upgrade/radvd/invariant/10-radvd-user b/package/gluon-radvd/files/lib/gluon/upgrade/radvd/invariant/10-radvd-user
index baa0c9d2eaaab47b3224dbdad1c26122d5125a2b..d2be86aecb48f69e9ba41d6f19eb20dc29d7095d 100755
--- a/package/gluon-radvd/files/lib/gluon/upgrade/radvd/invariant/10-radvd-user
+++ b/package/gluon-radvd/files/lib/gluon/upgrade/radvd/invariant/10-radvd-user
@@ -1,5 +1,5 @@
-#!/bin/sh
+#!/usr/bin/lua
 
-. /lib/gluon/functions/users.sh
+local users = require 'gluon.users'
 
-add_user gluon-radvd 801
+users.add_user('gluon-radvd', 801, 100)