nuageinit: implement chpasswd

Add support for chpasswd, with all possible syntaxes, including
deprecated one: chpasswd.list as a list or as a multiline string
as some providers are still only providing this deprecated form

Approved by:	 re (cperciva)
Sponsored by:	OVHCloud
MFC After:	1 week
Reviewed by:	kevans, jlduran
Differential Revision: 	https://reviews.freebsd.org/D50021

(cherry picked from commit c201a1198ad70e7d096ee32c364d539eed2dfec4)
(cherry picked from commit 6c912470030ba958f2e41a00b44f6430919b1389)
This commit is contained in:
Baptiste Daroussin 2025-04-25 17:16:22 +02:00
parent f30669ba97
commit b64d3fc884
3 changed files with 283 additions and 3 deletions

View file

@ -1,7 +1,7 @@
---
-- SPDX-License-Identifier: BSD-2-Clause
--
-- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
-- Copyright(c) 2022-2025 Baptiste Daroussin <bapt@FreeBSD.org>
local unistd = require("posix.unistd")
local sys_stat = require("posix.sys.stat")
@ -261,6 +261,106 @@ local function update_sshd_config(key, value)
os.rename(sshd_config .. ".nuageinit", sshd_config)
end
local function exec_change_password(user, password, type, expire)
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
local cmd = "pw "
if root then
cmd = cmd .. "-R " .. root .. " "
end
local postcmd = " -H 0"
local input = password
if type ~= nil and type == "text" then
postcmd = " -h 0"
else
if password == "RANDOM" then
input = nil
postcmd = " -w random"
end
end
cmd = cmd .. "usermod " .. user .. postcmd
if expire then
cmd = cmd .. " -p 1"
else
cmd = cmd .. " -p 0"
end
local f = io.popen(cmd .. " >/dev/null", "w")
if input then
f:write(input)
end
-- ignore stdout to avoid printing the password in case of random password
local r = f:close(cmd)
if not r then
warnmsg("fail to change user password ".. user)
warnmsg(cmd)
end
end
local function change_password_from_line(line, expire)
local user, password = line:match("%s*(%w+):(%S+)%s*")
local type = nil
if user and password then
if password == "R" then
password = "RANDOM"
end
if not password:match("^%$%d+%$%w+%$") then
if password ~= "RANDOM" then
type = "text"
end
end
exec_change_password(user, password, type, expire)
end
end
local function chpasswd(obj)
if type(obj) ~= "table" then
warnmsg("Invalid chpasswd entry, expecting an object")
return
end
local expire = false
if obj.expire ~= nil then
if type(obj.expire) == "boolean" then
expire = obj.expire
else
warnmsg("Invalid type for chpasswd.expire, expecting a boolean, got a ".. type(obj.expire))
end
end
if obj.users ~= nil then
if type(obj.users) ~= "table" then
warnmsg("Invalid type for chpasswd.users, expecting a list, got a ".. type(obj.users))
goto list
end
for _, u in ipairs(obj.users) do
if type(u) ~= "table" then
warnmsg("Invalid chpasswd.users entry, expecting an object, got a " .. type(u))
goto next
end
if not u.name then
warnmsg("Invalid entry for chpasswd.users: missing 'name'")
goto next
end
if not u.password then
warnmsg("Invalid entry for chpasswd.users: missing 'password'")
goto next
end
exec_change_password(u.name, u.password, u.type, expire)
::next::
end
end
::list::
if obj.list ~= nil then
warnmsg("chpasswd.list is deprecated consider using chpasswd.users")
if type(obj.list) == "string" then
for line in obj.list:gmatch("[^\n]+") do
change_password_from_line(line, expire)
end
elseif type(obj.list) == "table" then
for _, u in ipairs(obj.list) do
change_password_from_line(u, expire)
end
end
end
end
local n = {
warn = warnmsg,
err = errmsg,
@ -270,7 +370,8 @@ local n = {
adduser = adduser,
addgroup = addgroup,
addsshkey = addsshkey,
update_sshd_config = update_sshd_config
update_sshd_config = update_sshd_config,
chpasswd = chpasswd
}
return n

View file

@ -2,7 +2,7 @@
---
-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
--
-- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
-- Copyright(c) 2022-2025 Baptiste Daroussin <bapt@FreeBSD.org>
local nuage = require("nuage")
local ucl = require("ucl")
@ -359,6 +359,10 @@ if line == "#cloud-config" then
end
nuage.update_sshd_config("PasswordAuthentication", value)
end
if obj.chpasswd ~= nil then
nuage.chpasswd(obj.chpasswd)
end
else
local res, err = os.execute(path .. "/" .. ud)
if not res then

View file

@ -20,6 +20,9 @@ atf_test_case config2_network
atf_test_case config2_network_static_v4
atf_test_case config2_ssh_keys
atf_test_case nocloud_userdata_cloudconfig_ssh_pwauth
atf_test_case nocloud_userdata_cloudconfig_chpasswd
atf_test_case nocloud_userdata_cloudconfig_chpasswd_list_string
atf_test_case nocloud_userdata_cloudconfig_chpasswd_list_list
args_body()
{
@ -512,6 +515,175 @@ EOF
atf_check -o inline:"PasswordAuthentication no\n" cat etc/ssh/sshd_config
}
nocloud_userdata_cloudconfig_chpasswd_head()
{
atf_set "require.user" root
}
nocloud_userdata_cloudconfig_chpasswd_body()
{
mkdir -p etc
cat > etc/master.passwd << EOF
root:*:0:0::0:0:Charlie &:/root:/bin/sh
sys:*:1:0::0:0:Sys:/home/sys:/bin/sh
user:*:1:0::0:0:Sys:/home/sys:/bin/sh
EOF
pwd_mkdb -d etc "${PWD}"/etc/master.passwd
cat > etc/group << EOF
wheel:*:0:root
users:*:1:
EOF
mkdir -p media/nuageinit
printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: true
users:
- { user: "sys", password: RANDOM }
EOF
atf_check -o empty -e inline:"nuageinit: Invalid entry for chpasswd.users: missing 'name'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
# nothing modified
atf_check -o inline:"sys:*:1:0::0:0:Sys:/home/sys:/bin/sh\n" pw -R $(pwd) usershow sys
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: true
users:
- { name: "sys", pwd: RANDOM }
EOF
atf_check -o empty -e inline:"nuageinit: Invalid entry for chpasswd.users: missing 'password'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
# nothing modified
atf_check -o inline:"sys:*:1:0::0:0:Sys:/home/sys:/bin/sh\n" pw -R $(pwd) usershow sys
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: false
users:
- { name: "sys", password: RANDOM }
EOF
# not empty because the password is printed to stdout
atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
atf_check -o match:'sys:\$.*:1:0::0:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: true
users:
- { name: "sys", password: RANDOM }
EOF
# not empty because the password is printed to stdout
atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
atf_check -o match:'sys:\$.*:1:0::1:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: true
users:
- { name: "user", password: "$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/" }
EOF
# not empty because the password is printed to stdout
atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
atf_check -o inline:'user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1:0::1:0:Sys:/home/sys:/bin/sh\n' pw -R $(pwd) usershow user
}
nocloud_userdata_cloudconfig_chpasswd_list_string_head()
{
atf_set "require.user" root
}
nocloud_userdata_cloudconfig_chpasswd_list_string_body()
{
mkdir -p etc
cat > etc/master.passwd << EOF
root:*:0:0::0:0:Charlie &:/root:/bin/sh
sys:*:1:0::0:0:Sys:/home/sys:/bin/sh
user:*:1:0::0:0:Sys:/home/sys:/bin/sh
EOF
pwd_mkdb -d etc "${PWD}"/etc/master.passwd
cat > etc/group << EOF
wheel:*:0:root
users:*:1:
EOF
mkdir -p media/nuageinit
printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: true
list: |
sys:RANDOM
EOF
atf_check -o empty -e inline:"nuageinit: chpasswd.list is deprecated consider using chpasswd.users\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
atf_check -o match:'sys:\$.*:1:0::1:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: false
list: |
sys:plop
user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
root:R
EOF
atf_check -o empty -e ignore /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
atf_check -o match:'sys:\$.*:1:0::0:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys
atf_check -o inline:'user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1:0::0:0:Sys:/home/sys:/bin/sh\n' pw -R $(pwd) usershow user
atf_check -o match:'root:\$.*:0:0::0:0:Charlie &:/root:/bin/sh$' pw -R $(pwd) usershow root
}
nocloud_userdata_cloudconfig_chpasswd_list_list_head()
{
atf_set "require.user" root
}
nocloud_userdata_cloudconfig_chpasswd_list_list_body()
{
mkdir -p etc
cat > etc/master.passwd << EOF
root:*:0:0::0:0:Charlie &:/root:/bin/sh
sys:*:1:0::0:0:Sys:/home/sys:/bin/sh
user:*:1:0::0:0:Sys:/home/sys:/bin/sh
EOF
pwd_mkdb -d etc "${PWD}"/etc/master.passwd
cat > etc/group << EOF
wheel:*:0:root
users:*:1:
EOF
mkdir -p media/nuageinit
printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: true
list:
- sys:RANDOM
EOF
atf_check -o empty -e inline:"nuageinit: chpasswd.list is deprecated consider using chpasswd.users\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
atf_check -o match:'sys:\$.*:1:0::1:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys
cat > media/nuageinit/user-data << 'EOF'
#cloud-config
chpasswd:
expire: false
list:
- sys:plop
- user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
- root:R
EOF
atf_check -o empty -e ignore /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud
atf_check -o match:'sys:\$.*:1:0::0:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys
atf_check -o inline:'user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1:0::0:0:Sys:/home/sys:/bin/sh\n' pw -R $(pwd) usershow user
atf_check -o match:'root:\$.*:0:0::0:0:Charlie &:/root:/bin/sh$' pw -R $(pwd) usershow root
}
atf_init_test_cases()
{
atf_add_test_case args
@ -528,4 +700,7 @@ atf_init_test_cases()
atf_add_test_case config2_network_static_v4
atf_add_test_case config2_ssh_keys
atf_add_test_case nocloud_userdata_cloudconfig_ssh_pwauth
atf_add_test_case nocloud_userdata_cloudconfig_chpasswd
atf_add_test_case nocloud_userdata_cloudconfig_chpasswd_list_string
atf_add_test_case nocloud_userdata_cloudconfig_chpasswd_list_list
}