* [PATCH] image-account-extension: configure adduser UID/GID pools @ 2026-05-21 16:21 'Cedric Hombourger' via isar-users 2026-05-21 16:44 ` 'Jan Kiszka' via isar-users 0 siblings, 1 reply; 4+ messages in thread From: 'Cedric Hombourger' via isar-users @ 2026-05-21 16:21 UTC (permalink / raw) To: isar-users; +Cc: Cedric Hombourger For users and groups with an explicit uid/gid set, generate adduser pool files so that maintainer scripts calling adduser/addgroup during package installation will reserve the expected IDs. A new 'reserve-only' flag allows entries to exist solely for pool reservation without being explicitly created during image postprocessing. Work-around: /etc/adduser.conf is pre-created with UID_POOL/GID_POOL directives and --force-confold is passed to dpkg so that our version is kept when the adduser package is installed. This is needed because adduser does not support loading configuration fragments from a .d directory or from environment variables. We want to discuss this! Do we want to create images from a template richer than bootstrap so adduser could be pre-installed and possibly its configuration already patched to use UID_POOL / GID_POOL? Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com> --- doc/user_manual.md | 44 +++++-- .../image-account-extension.bbclass | 113 +++++++++++++++++- 2 files changed, 145 insertions(+), 12 deletions(-) diff --git a/doc/user_manual.md b/doc/user_manual.md index 69e8dfef..3bd2e767 100644 --- a/doc/user_manual.md +++ b/doc/user_manual.md @@ -737,7 +737,8 @@ The `GROUP_<groupname>` variable contains the settings of a group named `groupna - `gid` - The numeric group id. - `flags` - A list of additional flags of the group. Those are the currently recognized flags: - - `system` - The group is created using the `--system` parameter. + - `system` - The group is created using the `--system` parameter. + - `reserve-only` - The group is not explicitly created during image postprocessing. Instead, its `gid` is reserved in the adduser GID pool so that packages creating this group via maintainer scripts will use the specified ID. The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GROUP:<groupname>` variable. The difference are the accepted flags of the `USER:<username>` variable. It accepts the following flags: @@ -750,13 +751,14 @@ The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GR - `home` - This changes the default home directory of the user with `usermod --move-home`. Only takes effect when used together with the `create-home` flag. - `shell` - This users login shell - `groups` - A space separated list of groups this user is a member of. - - `flags` - A list of additional flags of the user: - - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. - - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. - - `system` - `useradd` will be called with `--system`. - - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. - - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. - - `force-passwd-change` - Force the user to change to password on first login. + - `flags` - A list of additional flags of the user: + - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. + - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. + - `system` - `useradd` will be called with `--system`. + - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. + - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. + - `force-passwd-change` - Force the user to change to password on first login. + - `reserve-only` - The user is not explicitly created during image postprocessing. Instead, its `uid` is reserved in the adduser UID pool so that packages creating this user via maintainer scripts will use the specified ID. #### Example @@ -779,6 +781,32 @@ USER_root[flags] = "create-home system force-passwd-change" Some examples can be also found in `meta-isar/conf/local.conf.sample`. +#### UID/GID pool reservation + +When a user or group entry has an explicit `uid` or `gid` set, it is added to +the adduser UID/GID pool. This ensures that packages creating users or groups +via their maintainer scripts (e.g. `adduser` or `addgroup`) will allocate the +specified IDs. Combined with the `reserve-only` flag, this allows reserving IDs +without explicitly creating the accounts: + +``` +USERS += "tss" +USER_tss[uid] = "666" +USER_tss[flags] = "reserve-only" + +GROUPS += "tss" +GROUP_tss[gid] = "666" +GROUP_tss[flags] = "reserve-only" + +GROUPS += "docker" +GROUP_docker[gid] = "1234" +GROUP_docker[flags] = "reserve-only" +``` + +In this example, when `tpm2-abrmd` or `docker.io` are installed, their +maintainer scripts will create the `tss` and `docker` accounts using the +reserved IDs rather than dynamically allocated ones. + #### Home directory contents prefilling To cover all users simply use `/etc/skel`. Files in there will be available in every home directory under correct permissions. diff --git a/meta/classes-recipe/image-account-extension.bbclass b/meta/classes-recipe/image-account-extension.bbclass index e874f3c7..7dfcd8e0 100644 --- a/meta/classes-recipe/image-account-extension.bbclass +++ b/meta/classes-recipe/image-account-extension.bbclass @@ -14,16 +14,18 @@ python() { for entry in (d.getVar("GROUPS") or "").split(): group_entry = "GROUP_{}".format(entry) d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(group_entry)) + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(group_entry)) d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(group_entry)) for entry in (d.getVar("USERS") or "").split(): user_entry = "USER_{}".format(entry) d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(user_entry)) + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(user_entry)) d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(user_entry)) } do_rootfs_install[vardeps] += "GROUPS USERS" -def image_create_groups(d: "DataSmart") -> None: +def image_create_groups(d): """Creates the groups defined in the ``GROUPS`` bitbake variable. Args: @@ -40,6 +42,10 @@ def image_create_groups(d: "DataSmart") -> None: args = [] group_entry = "GROUP_{}".format(entry) + flags = (d.getVarFlag(group_entry, "flags") or "").split() + if "reserve-only" in flags: + continue + with open("{}/etc/group".format(rootfsdir), "r") as group_file: exists = any(line.startswith("{}:".format(entry)) for line in group_file) @@ -59,7 +65,7 @@ def image_create_groups(d: "DataSmart") -> None: bb.process.run([*chroot, "/usr/sbin/groupadd", *args, entry]) -def image_create_users(d: "DataSmart") -> None: +def image_create_users(d): """Creates the users defined in the ``USERS`` bitbake variable. Args: @@ -78,6 +84,10 @@ def image_create_users(d: "DataSmart") -> None: args = [] user_entry = "USER_{}".format(entry) + flags = (d.getVarFlag(user_entry, "flags") or "").split() + if "reserve-only" in flags: + continue + with open("{}/etc/passwd".format(rootfsdir), "r") as passwd_file: exists = any(line.startswith("{}:".format(entry)) for line in passwd_file) @@ -99,8 +109,6 @@ def image_create_users(d: "DataSmart") -> None: args.append("--groups") args.append(','.join(groups)) - flags = (d.getVarFlag(user_entry, "flags") or "").split() - if exists: add_user_option("--home", "home") if d.getVarFlag(user_entry, "home") or "": @@ -143,6 +151,103 @@ def image_create_users(d: "DataSmart") -> None: bb.process.run([*chroot, "/usr/bin/passwd", "--expire", entry]) +def configure_adduser_pools(d): + """Configures adduser UID/GID pools for users and groups with explicit IDs. + + Creates pool files and a minimal /etc/adduser.conf with UID_POOL/GID_POOL + directives before package installation. + + Args: + d (DataSmart): The bitbake datastore. + + Returns: + None + """ + import os + import tempfile + + rootfsdir = d.getVar("ROOTFSDIR") + adduser_conf = "{}/etc/adduser.conf".format(rootfsdir) + uid_pool_path = "/etc/adduser-uid.pool" + gid_pool_path = "/etc/adduser-gid.pool" + + uid_pool_entries = [] + seen_users = set() + for entry in (d.getVar("USERS") or "").split(): + if entry in seen_users: + continue + seen_users.add(entry) + user_entry = "USER_{}".format(entry) + uid = d.getVarFlag(user_entry, "uid") or "" + if uid: + uid_pool_entries.append("{}:{}".format(entry, uid)) + + gid_pool_entries = [] + seen_groups = set() + for entry in (d.getVar("GROUPS") or "").split(): + if entry in seen_groups: + continue + seen_groups.add(entry) + group_entry = "GROUP_{}".format(entry) + gid = d.getVarFlag(group_entry, "gid") or "" + if gid: + gid_pool_entries.append("{}:{}".format(entry, gid)) + + if not uid_pool_entries and not gid_pool_entries: + return + + if uid_pool_entries: + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("\n".join(uid_pool_entries) + "\n") + tmp = f.name + bb.process.run( + ["sudo", "cp", tmp, "{}{}".format(rootfsdir, uid_pool_path)]) + bb.process.run( + ["sudo", "chmod", "644", "{}{}".format(rootfsdir, uid_pool_path)]) + os.unlink(tmp) + + if gid_pool_entries: + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("\n".join(gid_pool_entries) + "\n") + tmp = f.name + bb.process.run( + ["sudo", "cp", tmp, "{}{}".format(rootfsdir, gid_pool_path)]) + bb.process.run( + ["sudo", "chmod", "644", "{}{}".format(rootfsdir, gid_pool_path)]) + os.unlink(tmp) + + # Create /etc/adduser.conf with the upstream default content plus pool + # directives. We use --force-confold during package installation so that + # dpkg keeps this version when the adduser package is installed. + conf_lines = [] + conf_lines.append("# /etc/adduser.conf: `adduser' configuration.") + conf_lines.append("# See adduser(8) and adduser.conf(5) for full documentation.") + conf_lines.append("") + if uid_pool_entries: + conf_lines.append("UID_POOL={}".format(uid_pool_path)) + if gid_pool_entries: + conf_lines.append("GID_POOL={}".format(gid_pool_path)) + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("\n".join(conf_lines) + "\n") + tmp = f.name + bb.process.run(["sudo", "cp", tmp, adduser_conf]) + bb.process.run(["sudo", "chmod", "644", adduser_conf]) + os.unlink(tmp) + + +# Work-around: pre-create /etc/adduser.conf with pool directives and use +# --force-confold so dpkg keeps our version when the adduser package is +# installed. This is needed because adduser does not support loading +# configuration from /etc/adduser.conf.d/ or from environment variables. +ROOTFS_APT_ARGS += "-o DPkg::Options::=--force-confold" + +ROOTFS_CONFIGURE_COMMAND += "image_configure_adduser_pools" +image_configure_adduser_pools[vardeps] += "USERS GROUPS" +python image_configure_adduser_pools() { + configure_adduser_pools(d) +} + ROOTFS_POSTPROCESS_COMMAND += "image_postprocess_accounts" image_postprocess_accounts[vardeps] += "USERS GROUPS" python image_postprocess_accounts() { -- 2.47.3 -- You received this message because you are subscribed to the Google Groups "isar-users" group. To unsubscribe from this group and stop receiving emails from it, send an email to isar-users+unsubscribe@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/isar-users/20260521162215.1348898-1-cedric.hombourger%40siemens.com. ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] image-account-extension: configure adduser UID/GID pools 2026-05-21 16:21 [PATCH] image-account-extension: configure adduser UID/GID pools 'Cedric Hombourger' via isar-users @ 2026-05-21 16:44 ` 'Jan Kiszka' via isar-users 2026-05-21 18:48 ` [RFC PATCH v2] " 'Cedric Hombourger' via isar-users 0 siblings, 1 reply; 4+ messages in thread From: 'Jan Kiszka' via isar-users @ 2026-05-21 16:44 UTC (permalink / raw) To: Cedric Hombourger, isar-users; +Cc: Quirin Gylstorff, Christian Storm On 21.05.26 18:21, 'Cedric Hombourger' via isar-users wrote: > For users and groups with an explicit uid/gid set, generate adduser pool > files so that maintainer scripts calling adduser/addgroup during package > installation will reserve the expected IDs. > > A new 'reserve-only' flag allows entries to exist solely for pool > reservation without being explicitly created during image postprocessing. > > Work-around: /etc/adduser.conf is pre-created with UID_POOL/GID_POOL > directives and --force-confold is passed to dpkg so that our version is > kept when the adduser package is installed. This is needed because > adduser does not support loading configuration fragments from a .d > directory or from environment variables. We want to discuss this! > Do we want to create images from a template richer than bootstrap > so adduser could be pre-installed and possibly its configuration > already patched to use UID_POOL / GID_POOL? > This is a valuable starting point for (public) discussions, thanks! Internally, we already thought about this, also considering to add a way for deriving this preceeding of the UIDs/GIDs from a "version 1" run of a build so that you do not have to collect and encode all the data manually. This would also emulate a normal Debian system lifecycle: Install a blank version, then add or upgrade packages, thus, users/groups while maintaining the assignments of the initial installation. Jan > Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com> > --- > doc/user_manual.md | 44 +++++-- > .../image-account-extension.bbclass | 113 +++++++++++++++++- > 2 files changed, 145 insertions(+), 12 deletions(-) > > diff --git a/doc/user_manual.md b/doc/user_manual.md > index 69e8dfef..3bd2e767 100644 > --- a/doc/user_manual.md > +++ b/doc/user_manual.md > @@ -737,7 +737,8 @@ The `GROUP_<groupname>` variable contains the settings of a group named `groupna > > - `gid` - The numeric group id. > - `flags` - A list of additional flags of the group. Those are the currently recognized flags: > - - `system` - The group is created using the `--system` parameter. > + - `system` - The group is created using the `--system` parameter. > + - `reserve-only` - The group is not explicitly created during image postprocessing. Instead, its `gid` is reserved in the adduser GID pool so that packages creating this group via maintainer scripts will use the specified ID. > > The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GROUP:<groupname>` variable. The difference are the accepted flags of the `USER:<username>` variable. It accepts the following flags: > > @@ -750,13 +751,14 @@ The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GR > - `home` - This changes the default home directory of the user with `usermod --move-home`. Only takes effect when used together with the `create-home` flag. > - `shell` - This users login shell > - `groups` - A space separated list of groups this user is a member of. > - - `flags` - A list of additional flags of the user: > - - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. > - - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. > - - `system` - `useradd` will be called with `--system`. > - - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. > - - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. > - - `force-passwd-change` - Force the user to change to password on first login. > + - `flags` - A list of additional flags of the user: > + - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. > + - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. > + - `system` - `useradd` will be called with `--system`. > + - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. > + - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. > + - `force-passwd-change` - Force the user to change to password on first login. > + - `reserve-only` - The user is not explicitly created during image postprocessing. Instead, its `uid` is reserved in the adduser UID pool so that packages creating this user via maintainer scripts will use the specified ID. > > #### Example > > @@ -779,6 +781,32 @@ USER_root[flags] = "create-home system force-passwd-change" > > Some examples can be also found in `meta-isar/conf/local.conf.sample`. > > +#### UID/GID pool reservation > + > +When a user or group entry has an explicit `uid` or `gid` set, it is added to > +the adduser UID/GID pool. This ensures that packages creating users or groups > +via their maintainer scripts (e.g. `adduser` or `addgroup`) will allocate the > +specified IDs. Combined with the `reserve-only` flag, this allows reserving IDs > +without explicitly creating the accounts: > + > +``` > +USERS += "tss" > +USER_tss[uid] = "666" > +USER_tss[flags] = "reserve-only" > + > +GROUPS += "tss" > +GROUP_tss[gid] = "666" > +GROUP_tss[flags] = "reserve-only" > + > +GROUPS += "docker" > +GROUP_docker[gid] = "1234" > +GROUP_docker[flags] = "reserve-only" > +``` > + > +In this example, when `tpm2-abrmd` or `docker.io` are installed, their > +maintainer scripts will create the `tss` and `docker` accounts using the > +reserved IDs rather than dynamically allocated ones. > + > #### Home directory contents prefilling > > To cover all users simply use `/etc/skel`. Files in there will be available in every home directory under correct permissions. > diff --git a/meta/classes-recipe/image-account-extension.bbclass b/meta/classes-recipe/image-account-extension.bbclass > index e874f3c7..7dfcd8e0 100644 > --- a/meta/classes-recipe/image-account-extension.bbclass > +++ b/meta/classes-recipe/image-account-extension.bbclass > @@ -14,16 +14,18 @@ python() { > for entry in (d.getVar("GROUPS") or "").split(): > group_entry = "GROUP_{}".format(entry) > d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(group_entry)) > + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(group_entry)) > d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(group_entry)) > > for entry in (d.getVar("USERS") or "").split(): > user_entry = "USER_{}".format(entry) > d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(user_entry)) > + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(user_entry)) > d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(user_entry)) > } > do_rootfs_install[vardeps] += "GROUPS USERS" > > -def image_create_groups(d: "DataSmart") -> None: > +def image_create_groups(d): > """Creates the groups defined in the ``GROUPS`` bitbake variable. > > Args: > @@ -40,6 +42,10 @@ def image_create_groups(d: "DataSmart") -> None: > args = [] > group_entry = "GROUP_{}".format(entry) > > + flags = (d.getVarFlag(group_entry, "flags") or "").split() > + if "reserve-only" in flags: > + continue > + > with open("{}/etc/group".format(rootfsdir), "r") as group_file: > exists = any(line.startswith("{}:".format(entry)) for line in group_file) > > @@ -59,7 +65,7 @@ def image_create_groups(d: "DataSmart") -> None: > bb.process.run([*chroot, "/usr/sbin/groupadd", *args, entry]) > > > -def image_create_users(d: "DataSmart") -> None: > +def image_create_users(d): > """Creates the users defined in the ``USERS`` bitbake variable. > > Args: > @@ -78,6 +84,10 @@ def image_create_users(d: "DataSmart") -> None: > args = [] > user_entry = "USER_{}".format(entry) > > + flags = (d.getVarFlag(user_entry, "flags") or "").split() > + if "reserve-only" in flags: > + continue > + > with open("{}/etc/passwd".format(rootfsdir), "r") as passwd_file: > exists = any(line.startswith("{}:".format(entry)) for line in passwd_file) > > @@ -99,8 +109,6 @@ def image_create_users(d: "DataSmart") -> None: > args.append("--groups") > args.append(','.join(groups)) > > - flags = (d.getVarFlag(user_entry, "flags") or "").split() > - > if exists: > add_user_option("--home", "home") > if d.getVarFlag(user_entry, "home") or "": > @@ -143,6 +151,103 @@ def image_create_users(d: "DataSmart") -> None: > bb.process.run([*chroot, "/usr/bin/passwd", "--expire", entry]) > > > +def configure_adduser_pools(d): > + """Configures adduser UID/GID pools for users and groups with explicit IDs. > + > + Creates pool files and a minimal /etc/adduser.conf with UID_POOL/GID_POOL > + directives before package installation. > + > + Args: > + d (DataSmart): The bitbake datastore. > + > + Returns: > + None > + """ > + import os > + import tempfile > + > + rootfsdir = d.getVar("ROOTFSDIR") > + adduser_conf = "{}/etc/adduser.conf".format(rootfsdir) > + uid_pool_path = "/etc/adduser-uid.pool" > + gid_pool_path = "/etc/adduser-gid.pool" > + > + uid_pool_entries = [] > + seen_users = set() > + for entry in (d.getVar("USERS") or "").split(): > + if entry in seen_users: > + continue > + seen_users.add(entry) > + user_entry = "USER_{}".format(entry) > + uid = d.getVarFlag(user_entry, "uid") or "" > + if uid: > + uid_pool_entries.append("{}:{}".format(entry, uid)) > + > + gid_pool_entries = [] > + seen_groups = set() > + for entry in (d.getVar("GROUPS") or "").split(): > + if entry in seen_groups: > + continue > + seen_groups.add(entry) > + group_entry = "GROUP_{}".format(entry) > + gid = d.getVarFlag(group_entry, "gid") or "" > + if gid: > + gid_pool_entries.append("{}:{}".format(entry, gid)) > + > + if not uid_pool_entries and not gid_pool_entries: > + return > + > + if uid_pool_entries: > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.write("\n".join(uid_pool_entries) + "\n") > + tmp = f.name > + bb.process.run( > + ["sudo", "cp", tmp, "{}{}".format(rootfsdir, uid_pool_path)]) > + bb.process.run( > + ["sudo", "chmod", "644", "{}{}".format(rootfsdir, uid_pool_path)]) > + os.unlink(tmp) > + > + if gid_pool_entries: > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.write("\n".join(gid_pool_entries) + "\n") > + tmp = f.name > + bb.process.run( > + ["sudo", "cp", tmp, "{}{}".format(rootfsdir, gid_pool_path)]) > + bb.process.run( > + ["sudo", "chmod", "644", "{}{}".format(rootfsdir, gid_pool_path)]) > + os.unlink(tmp) > + > + # Create /etc/adduser.conf with the upstream default content plus pool > + # directives. We use --force-confold during package installation so that > + # dpkg keeps this version when the adduser package is installed. > + conf_lines = [] > + conf_lines.append("# /etc/adduser.conf: `adduser' configuration.") > + conf_lines.append("# See adduser(8) and adduser.conf(5) for full documentation.") > + conf_lines.append("") > + if uid_pool_entries: > + conf_lines.append("UID_POOL={}".format(uid_pool_path)) > + if gid_pool_entries: > + conf_lines.append("GID_POOL={}".format(gid_pool_path)) > + > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.write("\n".join(conf_lines) + "\n") > + tmp = f.name > + bb.process.run(["sudo", "cp", tmp, adduser_conf]) > + bb.process.run(["sudo", "chmod", "644", adduser_conf]) > + os.unlink(tmp) > + > + > +# Work-around: pre-create /etc/adduser.conf with pool directives and use > +# --force-confold so dpkg keeps our version when the adduser package is > +# installed. This is needed because adduser does not support loading > +# configuration from /etc/adduser.conf.d/ or from environment variables. > +ROOTFS_APT_ARGS += "-o DPkg::Options::=--force-confold" > + > +ROOTFS_CONFIGURE_COMMAND += "image_configure_adduser_pools" > +image_configure_adduser_pools[vardeps] += "USERS GROUPS" > +python image_configure_adduser_pools() { > + configure_adduser_pools(d) > +} > + > ROOTFS_POSTPROCESS_COMMAND += "image_postprocess_accounts" > image_postprocess_accounts[vardeps] += "USERS GROUPS" > python image_postprocess_accounts() { -- Siemens AG, Foundational Technologies Linux Expert Center -- You received this message because you are subscribed to the Google Groups "isar-users" group. To unsubscribe from this group and stop receiving emails from it, send an email to isar-users+unsubscribe@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/isar-users/bc1f5efb-40ab-4740-b659-327709d4079b%40siemens.com. ^ permalink raw reply [flat|nested] 4+ messages in thread
* [RFC PATCH v2] image-account-extension: configure adduser UID/GID pools 2026-05-21 16:44 ` 'Jan Kiszka' via isar-users @ 2026-05-21 18:48 ` 'Cedric Hombourger' via isar-users 2026-05-22 9:41 ` 'Jan Kiszka' via isar-users 0 siblings, 1 reply; 4+ messages in thread From: 'Cedric Hombourger' via isar-users @ 2026-05-21 18:48 UTC (permalink / raw) To: isar-users; +Cc: Cedric Hombourger For users and groups with an explicit uid/gid set, generate adduser pool files so that maintainer scripts calling adduser/addgroup during package installation will reserve the expected IDs. Pool directories (/etc/adduser-uid.pool.d/ and /etc/adduser-gid.pool.d/) are used, with a numbered fragment (00-image-accounts.conf) generated from USERS/GROUPS entries. Additional .uid/.gid files from SRC_URI are installed as numbered fragments, following the same pattern as .list files for apt sources. Duplicates across fragments are filtered out (USERS/GROUPS wins) with build warnings for traceability. A new 'reserve-only' flag allows entries to exist solely for pool reservation without being explicitly created during image postprocessing. After postprocessing, ${IMAGE_FULLNAME}.uid and ${IMAGE_FULLNAME}.gid are deployed to DEPLOY_DIR_IMAGE with all users/groups from the final rootfs in adduser pool format. Work-around: /etc/adduser.conf is pre-created with UID_POOL/GID_POOL directives and --force-confold is passed to dpkg so that our version is kept when the adduser package is installed. This is needed because adduser does not support loading configuration fragments from a .d directory or from environment variables. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com> --- doc/user_manual.md | 73 ++++- .../image-account-extension.bbclass | 282 +++++++++++++++++- 2 files changed, 343 insertions(+), 12 deletions(-) diff --git a/doc/user_manual.md b/doc/user_manual.md index 69e8dfef..b5b54f64 100644 --- a/doc/user_manual.md +++ b/doc/user_manual.md @@ -737,7 +737,8 @@ The `GROUP_<groupname>` variable contains the settings of a group named `groupna - `gid` - The numeric group id. - `flags` - A list of additional flags of the group. Those are the currently recognized flags: - - `system` - The group is created using the `--system` parameter. + - `system` - The group is created using the `--system` parameter. + - `reserve-only` - The group is not explicitly created during image postprocessing. Instead, its `gid` is reserved in the adduser GID pool so that packages creating this group via maintainer scripts will use the specified ID. The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GROUP:<groupname>` variable. The difference are the accepted flags of the `USER:<username>` variable. It accepts the following flags: @@ -750,13 +751,14 @@ The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GR - `home` - This changes the default home directory of the user with `usermod --move-home`. Only takes effect when used together with the `create-home` flag. - `shell` - This users login shell - `groups` - A space separated list of groups this user is a member of. - - `flags` - A list of additional flags of the user: - - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. - - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. - - `system` - `useradd` will be called with `--system`. - - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. - - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. - - `force-passwd-change` - Force the user to change to password on first login. + - `flags` - A list of additional flags of the user: + - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. + - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. + - `system` - `useradd` will be called with `--system`. + - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. + - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. + - `force-passwd-change` - Force the user to change to password on first login. + - `reserve-only` - The user is not explicitly created during image postprocessing. Instead, its `uid` is reserved in the adduser UID pool so that packages creating this user via maintainer scripts will use the specified ID. #### Example @@ -779,6 +781,61 @@ USER_root[flags] = "create-home system force-passwd-change" Some examples can be also found in `meta-isar/conf/local.conf.sample`. +#### UID/GID pool reservation + +When a user or group entry has an explicit `uid` or `gid` set, it is added to +the adduser UID/GID pool. This ensures that packages creating users or groups +via their maintainer scripts (e.g. `adduser` or `addgroup`) will allocate the +specified IDs. Combined with the `reserve-only` flag, this allows reserving IDs +without explicitly creating the accounts: + +``` +USERS += "tss" +USER_tss[uid] = "666" +USER_tss[flags] = "reserve-only" + +GROUPS += "tss" +GROUP_tss[gid] = "666" +GROUP_tss[flags] = "reserve-only" + +GROUPS += "docker" +GROUP_docker[gid] = "1234" +GROUP_docker[flags] = "reserve-only" +``` + +In this example, when `tpm2-abrmd` or `docker.io` are installed, their +maintainer scripts will create the `tss` and `docker` accounts using the +reserved IDs rather than dynamically allocated ones. + +#### UID/GID pool files from SRC_URI + +Pool entries can also be provided via `.uid` and `.gid` files in `SRC_URI`. +These files use the adduser pool format (`name:id`, one per line) and are +installed as numbered fragments in `/etc/adduser-uid.pool.d/` and +`/etc/adduser-gid.pool.d/` respectively. + +``` +SRC_URI += "file://my-accounts.uid file://my-accounts.gid" +``` + +Where `my-accounts.uid` might contain: + +``` +# Reserve UIDs for package-created users +tss:666 +sshd:800 +``` + +Entries from `USERS`/`GROUPS` (placed in `00-image-accounts.conf`) take +priority over SRC_URI pool files. Duplicates are automatically filtered +with a build warning indicating which entries were dropped and from which +file. + +After image postprocessing, `${IMAGE_FULLNAME}.uid` and +`${IMAGE_FULLNAME}.gid` files are deployed to `DEPLOY_DIR_IMAGE` containing +all users and groups from the final rootfs. These files can be used as pool +inputs for other images to maintain consistent UID/GID allocation. + #### Home directory contents prefilling To cover all users simply use `/etc/skel`. Files in there will be available in every home directory under correct permissions. diff --git a/meta/classes-recipe/image-account-extension.bbclass b/meta/classes-recipe/image-account-extension.bbclass index e874f3c7..52eeec1b 100644 --- a/meta/classes-recipe/image-account-extension.bbclass +++ b/meta/classes-recipe/image-account-extension.bbclass @@ -14,16 +14,18 @@ python() { for entry in (d.getVar("GROUPS") or "").split(): group_entry = "GROUP_{}".format(entry) d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(group_entry)) + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(group_entry)) d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(group_entry)) for entry in (d.getVar("USERS") or "").split(): user_entry = "USER_{}".format(entry) d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(user_entry)) + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(user_entry)) d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(user_entry)) } do_rootfs_install[vardeps] += "GROUPS USERS" -def image_create_groups(d: "DataSmart") -> None: +def image_create_groups(d): """Creates the groups defined in the ``GROUPS`` bitbake variable. Args: @@ -40,6 +42,10 @@ def image_create_groups(d: "DataSmart") -> None: args = [] group_entry = "GROUP_{}".format(entry) + flags = (d.getVarFlag(group_entry, "flags") or "").split() + if "reserve-only" in flags: + continue + with open("{}/etc/group".format(rootfsdir), "r") as group_file: exists = any(line.startswith("{}:".format(entry)) for line in group_file) @@ -59,7 +65,7 @@ def image_create_groups(d: "DataSmart") -> None: bb.process.run([*chroot, "/usr/sbin/groupadd", *args, entry]) -def image_create_users(d: "DataSmart") -> None: +def image_create_users(d): """Creates the users defined in the ``USERS`` bitbake variable. Args: @@ -78,6 +84,10 @@ def image_create_users(d: "DataSmart") -> None: args = [] user_entry = "USER_{}".format(entry) + flags = (d.getVarFlag(user_entry, "flags") or "").split() + if "reserve-only" in flags: + continue + with open("{}/etc/passwd".format(rootfsdir), "r") as passwd_file: exists = any(line.startswith("{}:".format(entry)) for line in passwd_file) @@ -99,8 +109,6 @@ def image_create_users(d: "DataSmart") -> None: args.append("--groups") args.append(','.join(groups)) - flags = (d.getVarFlag(user_entry, "flags") or "").split() - if exists: add_user_option("--home", "home") if d.getVarFlag(user_entry, "home") or "": @@ -143,9 +151,275 @@ def image_create_users(d: "DataSmart") -> None: bb.process.run([*chroot, "/usr/bin/passwd", "--expire", entry]) +def account_pool_files(d): + """Returns lists of .uid and .gid files found in SRC_URI.""" + uid_files = [] + gid_files = [] + sources = d.getVar("SRC_URI").split() + for s in sources: + _, _, local, _, _, _ = bb.fetch.decodeurl(s) + base, ext = os.path.splitext(os.path.basename(local)) + if ext == ".uid": + uid_files.append(local) + elif ext == ".gid": + gid_files.append(local) + return uid_files, gid_files + + +def configure_adduser_pools(d): + """Configures adduser UID/GID pools for users and groups with explicit IDs. + + Creates pool directories (/etc/adduser-uid.pool.d/ and + /etc/adduser-gid.pool.d/) containing: + - A numbered fragment (00-image-accounts.conf) generated from + USERS/GROUPS entries with explicit uid/gid. + - Additional .uid/.gid files from SRC_URI copied as numbered fragments. + + A minimal /etc/adduser.conf is pre-created pointing UID_POOL and GID_POOL + at the respective directories. + + Args: + d (DataSmart): The bitbake datastore. + + Returns: + None + """ + import os + import tempfile + + rootfsdir = d.getVar("ROOTFSDIR") + workdir = d.getVar("WORKDIR") + adduser_conf = "{}/etc/adduser.conf".format(rootfsdir) + uid_pool_dir = "/etc/adduser-uid.pool.d" + gid_pool_dir = "/etc/adduser-gid.pool.d" + + uid_pool_entries = [] + seen_users = set() + for entry in (d.getVar("USERS") or "").split(): + if entry in seen_users: + continue + seen_users.add(entry) + user_entry = "USER_{}".format(entry) + uid = d.getVarFlag(user_entry, "uid") or "" + if uid: + uid_pool_entries.append("{}:{}".format(entry, uid)) + + gid_pool_entries = [] + seen_groups = set() + for entry in (d.getVar("GROUPS") or "").split(): + if entry in seen_groups: + continue + seen_groups.add(entry) + group_entry = "GROUP_{}".format(entry) + gid = d.getVarFlag(group_entry, "gid") or "" + if gid: + gid_pool_entries.append("{}:{}".format(entry, gid)) + + # Collect .uid/.gid files from SRC_URI + src_uid_files, src_gid_files = account_pool_files(d) + + has_uid_pool = uid_pool_entries or src_uid_files + has_gid_pool = gid_pool_entries or src_gid_files + + if not has_uid_pool and not has_gid_pool: + return + + # Create pool directories + if has_uid_pool: + bb.process.run( + ["sudo", "mkdir", "-p", "{}{}".format(rootfsdir, uid_pool_dir)]) + if has_gid_pool: + bb.process.run( + ["sudo", "mkdir", "-p", "{}{}".format(rootfsdir, gid_pool_dir)]) + + # Track seen names and IDs to detect duplicates across fragments. + # 00-image-accounts.conf (from USERS/GROUPS) has highest priority. + uid_seen_names = set() + uid_seen_ids = set() + gid_seen_names = set() + gid_seen_ids = set() + + # Install fragment from USERS/GROUPS as 00-image-accounts.conf + if uid_pool_entries: + for e in uid_pool_entries: + name, uid = e.split(":") + uid_seen_names.add(name) + uid_seen_ids.add(uid) + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("\n".join(uid_pool_entries) + "\n") + tmp = f.name + bb.process.run( + ["sudo", "cp", tmp, + "{}{}/00-image-accounts.conf".format(rootfsdir, uid_pool_dir)]) + os.unlink(tmp) + + if gid_pool_entries: + for e in gid_pool_entries: + name, gid = e.split(":") + gid_seen_names.add(name) + gid_seen_ids.add(gid) + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("\n".join(gid_pool_entries) + "\n") + tmp = f.name + bb.process.run( + ["sudo", "cp", tmp, + "{}{}/00-image-accounts.conf".format(rootfsdir, gid_pool_dir)]) + os.unlink(tmp) + + # Install .uid files from SRC_URI as numbered fragments, filtering + # duplicates. Keeping original filenames provides traceability. + for idx, uid_file in enumerate(src_uid_files, start=1): + src = os.path.join(workdir, uid_file) + filtered_lines = [] + with open(src, "r") as f: + for line in f: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + filtered_lines.append(line) + continue + parts = stripped.split(":") + if len(parts) < 2: + filtered_lines.append(line) + continue + name, uid = parts[0], parts[1] + if name in uid_seen_names: + bb.warn("{}: dropping '{}' (name already in pool)" + .format(uid_file, stripped)) + continue + if uid in uid_seen_ids: + bb.warn("{}: dropping '{}' (UID {} already in pool)" + .format(uid_file, stripped, uid)) + continue + uid_seen_names.add(name) + uid_seen_ids.add(uid) + filtered_lines.append(line) + + dst_name = "{:02d}-{}.conf".format(idx, os.path.splitext(uid_file)[0]) + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.writelines(filtered_lines) + tmp = f.name + bb.process.run( + ["sudo", "cp", tmp, "{}{}/{}".format(rootfsdir, uid_pool_dir, dst_name)]) + os.unlink(tmp) + + # Install .gid files from SRC_URI as numbered fragments, filtering + # duplicates. + for idx, gid_file in enumerate(src_gid_files, start=1): + src = os.path.join(workdir, gid_file) + filtered_lines = [] + with open(src, "r") as f: + for line in f: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + filtered_lines.append(line) + continue + parts = stripped.split(":") + if len(parts) < 2: + filtered_lines.append(line) + continue + name, gid = parts[0], parts[1] + if name in gid_seen_names: + bb.warn("{}: dropping '{}' (name already in pool)" + .format(gid_file, stripped)) + continue + if gid in gid_seen_ids: + bb.warn("{}: dropping '{}' (GID {} already in pool)" + .format(gid_file, stripped, gid)) + continue + gid_seen_names.add(name) + gid_seen_ids.add(gid) + filtered_lines.append(line) + + dst_name = "{:02d}-{}.conf".format(idx, os.path.splitext(gid_file)[0]) + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.writelines(filtered_lines) + tmp = f.name + bb.process.run( + ["sudo", "cp", tmp, "{}{}/{}".format(rootfsdir, gid_pool_dir, dst_name)]) + os.unlink(tmp) + + # Ensure pool directories are world-readable + if has_uid_pool: + bb.process.run( + ["sudo", "chmod", "-R", "a+rX", "{}{}".format(rootfsdir, uid_pool_dir)]) + if has_gid_pool: + bb.process.run( + ["sudo", "chmod", "-R", "a+rX", "{}{}".format(rootfsdir, gid_pool_dir)]) + + # Work-around: pre-create /etc/adduser.conf with pool directives and use + # --force-confold so dpkg keeps our version when the adduser package is + # installed. This is needed because adduser does not support loading + # configuration from /etc/adduser.conf.d/ or from environment variables. + conf_lines = [] + conf_lines.append("# /etc/adduser.conf: `adduser' configuration.") + conf_lines.append("# See adduser(8) and adduser.conf(5) for full documentation.") + conf_lines.append("") + if has_uid_pool: + conf_lines.append("UID_POOL={}".format(uid_pool_dir)) + if has_gid_pool: + conf_lines.append("GID_POOL={}".format(gid_pool_dir)) + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("\n".join(conf_lines) + "\n") + tmp = f.name + bb.process.run(["sudo", "cp", tmp, adduser_conf]) + bb.process.run(["sudo", "chmod", "644", adduser_conf]) + os.unlink(tmp) + + +# Work-around: use --force-confold so dpkg keeps our pre-created +# /etc/adduser.conf when the adduser package is installed. +ROOTFS_APT_ARGS += "-o DPkg::Options::=--force-confold" + +ROOTFS_CONFIGURE_COMMAND += "image_configure_adduser_pools" +image_configure_adduser_pools[vardeps] += "USERS GROUPS" +python image_configure_adduser_pools() { + configure_adduser_pools(d) +} + ROOTFS_POSTPROCESS_COMMAND += "image_postprocess_accounts" image_postprocess_accounts[vardeps] += "USERS GROUPS" python image_postprocess_accounts() { image_create_groups(d) image_create_users(d) + image_deploy_id_pools(d) } + + +def image_deploy_id_pools(d): + """Deploys UID/GID pool files from the final rootfs to DEPLOY_DIR_IMAGE. + + Generates ${IMAGE_FULLNAME}.uid and ${IMAGE_FULLNAME}.gid files in + adduser pool format (name:id) from /etc/passwd and /etc/group. + + Args: + d (DataSmart): The bitbake datastore. + + Returns: + None + """ + import os + + rootfsdir = d.getVar("ROOTFSDIR") + deploy_dir = d.getVar("DEPLOY_DIR_IMAGE") + image_fullname = d.getVar("IMAGE_FULLNAME") + + os.makedirs(deploy_dir, exist_ok=True) + + # Generate .uid from /etc/passwd + uid_file = os.path.join(deploy_dir, "{}.uid".format(image_fullname)) + with open("{}/etc/passwd".format(rootfsdir), "r") as f: + with open(uid_file, "w") as out: + for line in f: + fields = line.strip().split(":") + if len(fields) >= 3: + out.write("{}:{}\n".format(fields[0], fields[2])) + + # Generate .gid from /etc/group + gid_file = os.path.join(deploy_dir, "{}.gid".format(image_fullname)) + with open("{}/etc/group".format(rootfsdir), "r") as f: + with open(gid_file, "w") as out: + for line in f: + fields = line.strip().split(":") + if len(fields) >= 3: + out.write("{}:{}\n".format(fields[0], fields[2])) -- 2.47.3 -- You received this message because you are subscribed to the Google Groups "isar-users" group. To unsubscribe from this group and stop receiving emails from it, send an email to isar-users+unsubscribe@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/isar-users/20260521184852.1455458-1-cedric.hombourger%40siemens.com. ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [RFC PATCH v2] image-account-extension: configure adduser UID/GID pools 2026-05-21 18:48 ` [RFC PATCH v2] " 'Cedric Hombourger' via isar-users @ 2026-05-22 9:41 ` 'Jan Kiszka' via isar-users 0 siblings, 0 replies; 4+ messages in thread From: 'Jan Kiszka' via isar-users @ 2026-05-22 9:41 UTC (permalink / raw) To: Cedric Hombourger, isar-users On 21.05.26 20:48, 'Cedric Hombourger' via isar-users wrote: > For users and groups with an explicit uid/gid set, generate adduser pool > files so that maintainer scripts calling adduser/addgroup during package > installation will reserve the expected IDs. > > Pool directories (/etc/adduser-uid.pool.d/ and /etc/adduser-gid.pool.d/) > are used, with a numbered fragment (00-image-accounts.conf) generated > from USERS/GROUPS entries. Additional .uid/.gid files from SRC_URI are > installed as numbered fragments, following the same pattern as .list > files for apt sources. Duplicates across fragments are filtered out > (USERS/GROUPS wins) with build warnings for traceability. > Didn't study all details yet, but this looks fairly appealing. > A new 'reserve-only' flag allows entries to exist solely for pool > reservation without being explicitly created during image postprocessing. > > After postprocessing, ${IMAGE_FULLNAME}.uid and ${IMAGE_FULLNAME}.gid > are deployed to DEPLOY_DIR_IMAGE with all users/groups from the final > rootfs in adduser pool format. > > Work-around: /etc/adduser.conf is pre-created with UID_POOL/GID_POOL > directives and --force-confold is passed to dpkg so that our version is > kept when the adduser package is installed. This is needed because > adduser does not support loading configuration fragments from a .d > directory or from environment variables. > > Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com> > --- > doc/user_manual.md | 73 ++++- > .../image-account-extension.bbclass | 282 +++++++++++++++++- > 2 files changed, 343 insertions(+), 12 deletions(-) > > diff --git a/doc/user_manual.md b/doc/user_manual.md > index 69e8dfef..b5b54f64 100644 > --- a/doc/user_manual.md > +++ b/doc/user_manual.md > @@ -737,7 +737,8 @@ The `GROUP_<groupname>` variable contains the settings of a group named `groupna > > - `gid` - The numeric group id. > - `flags` - A list of additional flags of the group. Those are the currently recognized flags: > - - `system` - The group is created using the `--system` parameter. > + - `system` - The group is created using the `--system` parameter. > + - `reserve-only` - The group is not explicitly created during image postprocessing. Instead, its `gid` is reserved in the adduser GID pool so that packages creating this group via maintainer scripts will use the specified ID. > > The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GROUP:<groupname>` variable. The difference are the accepted flags of the `USER:<username>` variable. It accepts the following flags: > > @@ -750,13 +751,14 @@ The `USERS` and `USER:<username>` variable works similar to the `GROUPS` and `GR > - `home` - This changes the default home directory of the user with `usermod --move-home`. Only takes effect when used together with the `create-home` flag. > - `shell` - This users login shell > - `groups` - A space separated list of groups this user is a member of. > - - `flags` - A list of additional flags of the user: > - - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. > - - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. > - - `system` - `useradd` will be called with `--system`. > - - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. > - - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. > - - `force-passwd-change` - Force the user to change to password on first login. > + - `flags` - A list of additional flags of the user: > + - `no-create-home` - `useradd` will be called with `-M` to prevent creation of the users home directory. > + - `create-home` - `useradd` will be called with `-m` to force creation of the users home directory. > + - `system` - `useradd` will be called with `--system`. > + - `allow-empty-password` - Even if the `password` flag is empty, it will still be set. This results in a login without password. > + - `clear-text-password` - The `password` flag of the given user contains a clear-text password and not an encrypted version of it. > + - `force-passwd-change` - Force the user to change to password on first login. > + - `reserve-only` - The user is not explicitly created during image postprocessing. Instead, its `uid` is reserved in the adduser UID pool so that packages creating this user via maintainer scripts will use the specified ID. > > #### Example > > @@ -779,6 +781,61 @@ USER_root[flags] = "create-home system force-passwd-change" > > Some examples can be also found in `meta-isar/conf/local.conf.sample`. > > +#### UID/GID pool reservation > + > +When a user or group entry has an explicit `uid` or `gid` set, it is added to > +the adduser UID/GID pool. This ensures that packages creating users or groups > +via their maintainer scripts (e.g. `adduser` or `addgroup`) will allocate the > +specified IDs. Combined with the `reserve-only` flag, this allows reserving IDs > +without explicitly creating the accounts: > + > +``` > +USERS += "tss" > +USER_tss[uid] = "666" > +USER_tss[flags] = "reserve-only" > + > +GROUPS += "tss" > +GROUP_tss[gid] = "666" > +GROUP_tss[flags] = "reserve-only" > + > +GROUPS += "docker" > +GROUP_docker[gid] = "1234" > +GROUP_docker[flags] = "reserve-only" > +``` > + > +In this example, when `tpm2-abrmd` or `docker.io` are installed, their > +maintainer scripts will create the `tss` and `docker` accounts using the > +reserved IDs rather than dynamically allocated ones. > + > +#### UID/GID pool files from SRC_URI > + > +Pool entries can also be provided via `.uid` and `.gid` files in `SRC_URI`. > +These files use the adduser pool format (`name:id`, one per line) and are > +installed as numbered fragments in `/etc/adduser-uid.pool.d/` and > +`/etc/adduser-gid.pool.d/` respectively. > + > +``` > +SRC_URI += "file://my-accounts.uid file://my-accounts.gid" > +``` > + > +Where `my-accounts.uid` might contain: > + > +``` > +# Reserve UIDs for package-created users > +tss:666 > +sshd:800 > +``` > + > +Entries from `USERS`/`GROUPS` (placed in `00-image-accounts.conf`) take > +priority over SRC_URI pool files. Duplicates are automatically filtered > +with a build warning indicating which entries were dropped and from which > +file. > + > +After image postprocessing, `${IMAGE_FULLNAME}.uid` and > +`${IMAGE_FULLNAME}.gid` files are deployed to `DEPLOY_DIR_IMAGE` containing > +all users and groups from the final rootfs. These files can be used as pool > +inputs for other images to maintain consistent UID/GID allocation. > + > #### Home directory contents prefilling > > To cover all users simply use `/etc/skel`. Files in there will be available in every home directory under correct permissions. > diff --git a/meta/classes-recipe/image-account-extension.bbclass b/meta/classes-recipe/image-account-extension.bbclass > index e874f3c7..52eeec1b 100644 > --- a/meta/classes-recipe/image-account-extension.bbclass > +++ b/meta/classes-recipe/image-account-extension.bbclass > @@ -14,16 +14,18 @@ python() { > for entry in (d.getVar("GROUPS") or "").split(): > group_entry = "GROUP_{}".format(entry) > d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(group_entry)) > + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(group_entry)) > d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(group_entry)) > > for entry in (d.getVar("USERS") or "").split(): > user_entry = "USER_{}".format(entry) > d.appendVarFlag("image_postprocess_accounts", "vardeps", " {}".format(user_entry)) > + d.appendVarFlag("image_configure_adduser_pools", "vardeps", " {}".format(user_entry)) > d.appendVarFlag("do_rootfs_install", "vardeps", " {}".format(user_entry)) > } > do_rootfs_install[vardeps] += "GROUPS USERS" > > -def image_create_groups(d: "DataSmart") -> None: > +def image_create_groups(d): This signature change/alignment should be factored out and argued about separately. > """Creates the groups defined in the ``GROUPS`` bitbake variable. > > Args: > @@ -40,6 +42,10 @@ def image_create_groups(d: "DataSmart") -> None: > args = [] > group_entry = "GROUP_{}".format(entry) > > + flags = (d.getVarFlag(group_entry, "flags") or "").split() > + if "reserve-only" in flags: > + continue > + > with open("{}/etc/group".format(rootfsdir), "r") as group_file: > exists = any(line.startswith("{}:".format(entry)) for line in group_file) > > @@ -59,7 +65,7 @@ def image_create_groups(d: "DataSmart") -> None: > bb.process.run([*chroot, "/usr/sbin/groupadd", *args, entry]) > > > -def image_create_users(d: "DataSmart") -> None: > +def image_create_users(d): > """Creates the users defined in the ``USERS`` bitbake variable. > > Args: > @@ -78,6 +84,10 @@ def image_create_users(d: "DataSmart") -> None: > args = [] > user_entry = "USER_{}".format(entry) > > + flags = (d.getVarFlag(user_entry, "flags") or "").split() > + if "reserve-only" in flags: > + continue > + > with open("{}/etc/passwd".format(rootfsdir), "r") as passwd_file: > exists = any(line.startswith("{}:".format(entry)) for line in passwd_file) > > @@ -99,8 +109,6 @@ def image_create_users(d: "DataSmart") -> None: > args.append("--groups") > args.append(','.join(groups)) > > - flags = (d.getVarFlag(user_entry, "flags") or "").split() > - > if exists: > add_user_option("--home", "home") > if d.getVarFlag(user_entry, "home") or "": > @@ -143,9 +151,275 @@ def image_create_users(d: "DataSmart") -> None: > bb.process.run([*chroot, "/usr/bin/passwd", "--expire", entry]) > > > +def account_pool_files(d): > + """Returns lists of .uid and .gid files found in SRC_URI.""" > + uid_files = [] > + gid_files = [] > + sources = d.getVar("SRC_URI").split() > + for s in sources: > + _, _, local, _, _, _ = bb.fetch.decodeurl(s) > + base, ext = os.path.splitext(os.path.basename(local)) > + if ext == ".uid": > + uid_files.append(local) > + elif ext == ".gid": > + gid_files.append(local) > + return uid_files, gid_files > + > + > +def configure_adduser_pools(d): > + """Configures adduser UID/GID pools for users and groups with explicit IDs. > + > + Creates pool directories (/etc/adduser-uid.pool.d/ and > + /etc/adduser-gid.pool.d/) containing: > + - A numbered fragment (00-image-accounts.conf) generated from > + USERS/GROUPS entries with explicit uid/gid. > + - Additional .uid/.gid files from SRC_URI copied as numbered fragments. > + > + A minimal /etc/adduser.conf is pre-created pointing UID_POOL and GID_POOL > + at the respective directories. > + > + Args: > + d (DataSmart): The bitbake datastore. > + > + Returns: > + None > + """ > + import os > + import tempfile > + > + rootfsdir = d.getVar("ROOTFSDIR") > + workdir = d.getVar("WORKDIR") > + adduser_conf = "{}/etc/adduser.conf".format(rootfsdir) > + uid_pool_dir = "/etc/adduser-uid.pool.d" > + gid_pool_dir = "/etc/adduser-gid.pool.d" > + > + uid_pool_entries = [] > + seen_users = set() > + for entry in (d.getVar("USERS") or "").split(): > + if entry in seen_users: > + continue > + seen_users.add(entry) > + user_entry = "USER_{}".format(entry) > + uid = d.getVarFlag(user_entry, "uid") or "" > + if uid: > + uid_pool_entries.append("{}:{}".format(entry, uid)) > + > + gid_pool_entries = [] > + seen_groups = set() > + for entry in (d.getVar("GROUPS") or "").split(): > + if entry in seen_groups: > + continue > + seen_groups.add(entry) > + group_entry = "GROUP_{}".format(entry) > + gid = d.getVarFlag(group_entry, "gid") or "" > + if gid: > + gid_pool_entries.append("{}:{}".format(entry, gid)) > + > + # Collect .uid/.gid files from SRC_URI > + src_uid_files, src_gid_files = account_pool_files(d) > + > + has_uid_pool = uid_pool_entries or src_uid_files > + has_gid_pool = gid_pool_entries or src_gid_files > + > + if not has_uid_pool and not has_gid_pool: > + return > + > + # Create pool directories > + if has_uid_pool: > + bb.process.run( > + ["sudo", "mkdir", "-p", "{}{}".format(rootfsdir, uid_pool_dir)]) > + if has_gid_pool: > + bb.process.run( > + ["sudo", "mkdir", "-p", "{}{}".format(rootfsdir, gid_pool_dir)]) > + > + # Track seen names and IDs to detect duplicates across fragments. > + # 00-image-accounts.conf (from USERS/GROUPS) has highest priority. > + uid_seen_names = set() > + uid_seen_ids = set() > + gid_seen_names = set() > + gid_seen_ids = set() > + > + # Install fragment from USERS/GROUPS as 00-image-accounts.conf > + if uid_pool_entries: > + for e in uid_pool_entries: > + name, uid = e.split(":") > + uid_seen_names.add(name) > + uid_seen_ids.add(uid) > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.write("\n".join(uid_pool_entries) + "\n") > + tmp = f.name > + bb.process.run( > + ["sudo", "cp", tmp, > + "{}{}/00-image-accounts.conf".format(rootfsdir, uid_pool_dir)]) > + os.unlink(tmp) > + > + if gid_pool_entries: > + for e in gid_pool_entries: > + name, gid = e.split(":") > + gid_seen_names.add(name) > + gid_seen_ids.add(gid) > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.write("\n".join(gid_pool_entries) + "\n") > + tmp = f.name > + bb.process.run( > + ["sudo", "cp", tmp, > + "{}{}/00-image-accounts.conf".format(rootfsdir, gid_pool_dir)]) > + os.unlink(tmp) > + > + # Install .uid files from SRC_URI as numbered fragments, filtering > + # duplicates. Keeping original filenames provides traceability. > + for idx, uid_file in enumerate(src_uid_files, start=1): > + src = os.path.join(workdir, uid_file) > + filtered_lines = [] > + with open(src, "r") as f: > + for line in f: > + stripped = line.strip() > + if not stripped or stripped.startswith("#"): > + filtered_lines.append(line) > + continue > + parts = stripped.split(":") > + if len(parts) < 2: > + filtered_lines.append(line) > + continue > + name, uid = parts[0], parts[1] > + if name in uid_seen_names: > + bb.warn("{}: dropping '{}' (name already in pool)" > + .format(uid_file, stripped)) > + continue > + if uid in uid_seen_ids: > + bb.warn("{}: dropping '{}' (UID {} already in pool)" > + .format(uid_file, stripped, uid)) > + continue > + uid_seen_names.add(name) > + uid_seen_ids.add(uid) > + filtered_lines.append(line) > + > + dst_name = "{:02d}-{}.conf".format(idx, os.path.splitext(uid_file)[0]) > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.writelines(filtered_lines) > + tmp = f.name > + bb.process.run( > + ["sudo", "cp", tmp, "{}{}/{}".format(rootfsdir, uid_pool_dir, dst_name)]) > + os.unlink(tmp) > + > + # Install .gid files from SRC_URI as numbered fragments, filtering > + # duplicates. > + for idx, gid_file in enumerate(src_gid_files, start=1): > + src = os.path.join(workdir, gid_file) > + filtered_lines = [] > + with open(src, "r") as f: > + for line in f: > + stripped = line.strip() > + if not stripped or stripped.startswith("#"): > + filtered_lines.append(line) > + continue > + parts = stripped.split(":") > + if len(parts) < 2: > + filtered_lines.append(line) > + continue > + name, gid = parts[0], parts[1] > + if name in gid_seen_names: > + bb.warn("{}: dropping '{}' (name already in pool)" > + .format(gid_file, stripped)) > + continue > + if gid in gid_seen_ids: > + bb.warn("{}: dropping '{}' (GID {} already in pool)" > + .format(gid_file, stripped, gid)) > + continue > + gid_seen_names.add(name) > + gid_seen_ids.add(gid) > + filtered_lines.append(line) > + > + dst_name = "{:02d}-{}.conf".format(idx, os.path.splitext(gid_file)[0]) > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.writelines(filtered_lines) > + tmp = f.name > + bb.process.run( > + ["sudo", "cp", tmp, "{}{}/{}".format(rootfsdir, gid_pool_dir, dst_name)]) > + os.unlink(tmp) > + > + # Ensure pool directories are world-readable > + if has_uid_pool: > + bb.process.run( > + ["sudo", "chmod", "-R", "a+rX", "{}{}".format(rootfsdir, uid_pool_dir)]) > + if has_gid_pool: > + bb.process.run( > + ["sudo", "chmod", "-R", "a+rX", "{}{}".format(rootfsdir, gid_pool_dir)]) > + > + # Work-around: pre-create /etc/adduser.conf with pool directives and use > + # --force-confold so dpkg keeps our version when the adduser package is > + # installed. This is needed because adduser does not support loading > + # configuration from /etc/adduser.conf.d/ or from environment variables. > + conf_lines = [] > + conf_lines.append("# /etc/adduser.conf: `adduser' configuration.") > + conf_lines.append("# See adduser(8) and adduser.conf(5) for full documentation.") > + conf_lines.append("") > + if has_uid_pool: > + conf_lines.append("UID_POOL={}".format(uid_pool_dir)) > + if has_gid_pool: > + conf_lines.append("GID_POOL={}".format(gid_pool_dir)) > + > + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: > + f.write("\n".join(conf_lines) + "\n") > + tmp = f.name > + bb.process.run(["sudo", "cp", tmp, adduser_conf]) > + bb.process.run(["sudo", "chmod", "644", adduser_conf]) > + os.unlink(tmp) > + > + > +# Work-around: use --force-confold so dpkg keeps our pre-created > +# /etc/adduser.conf when the adduser package is installed. > +ROOTFS_APT_ARGS += "-o DPkg::Options::=--force-confold" > + Would be a valuable follow-up task then to work with upstream adduser to overcome this. Specifically as we cannot confine this workaround to the package or even files that need it. > +ROOTFS_CONFIGURE_COMMAND += "image_configure_adduser_pools" > +image_configure_adduser_pools[vardeps] += "USERS GROUPS" > +python image_configure_adduser_pools() { > + configure_adduser_pools(d) > +} > + > ROOTFS_POSTPROCESS_COMMAND += "image_postprocess_accounts" > image_postprocess_accounts[vardeps] += "USERS GROUPS" > python image_postprocess_accounts() { > image_create_groups(d) > image_create_users(d) > + image_deploy_id_pools(d) > } > + > + > +def image_deploy_id_pools(d): This is not C, but I think the pattern was so far defining functions before they are used. > + """Deploys UID/GID pool files from the final rootfs to DEPLOY_DIR_IMAGE. > + > + Generates ${IMAGE_FULLNAME}.uid and ${IMAGE_FULLNAME}.gid files in > + adduser pool format (name:id) from /etc/passwd and /etc/group. > + > + Args: > + d (DataSmart): The bitbake datastore. > + > + Returns: > + None > + """ > + import os > + > + rootfsdir = d.getVar("ROOTFSDIR") > + deploy_dir = d.getVar("DEPLOY_DIR_IMAGE") > + image_fullname = d.getVar("IMAGE_FULLNAME") > + > + os.makedirs(deploy_dir, exist_ok=True) > + > + # Generate .uid from /etc/passwd > + uid_file = os.path.join(deploy_dir, "{}.uid".format(image_fullname)) > + with open("{}/etc/passwd".format(rootfsdir), "r") as f: > + with open(uid_file, "w") as out: > + for line in f: > + fields = line.strip().split(":") > + if len(fields) >= 3: > + out.write("{}:{}\n".format(fields[0], fields[2])) > + > + # Generate .gid from /etc/group > + gid_file = os.path.join(deploy_dir, "{}.gid".format(image_fullname)) > + with open("{}/etc/group".format(rootfsdir), "r") as f: > + with open(gid_file, "w") as out: > + for line in f: > + fields = line.strip().split(":") > + if len(fields) >= 3: > + out.write("{}:{}\n".format(fields[0], fields[2])) Hmm, should we filter out from those file the users/groups that are coming from Isar itself? Our primary concern is about the package-generated accounts. Jan -- Siemens AG, Foundational Technologies Linux Expert Center -- You received this message because you are subscribed to the Google Groups "isar-users" group. To unsubscribe from this group and stop receiving emails from it, send an email to isar-users+unsubscribe@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/isar-users/b4fa45b6-456f-49f9-9813-9cada242d883%40siemens.com. ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-05-22 9:41 UTC | newest] Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-05-21 16:21 [PATCH] image-account-extension: configure adduser UID/GID pools 'Cedric Hombourger' via isar-users 2026-05-21 16:44 ` 'Jan Kiszka' via isar-users 2026-05-21 18:48 ` [RFC PATCH v2] " 'Cedric Hombourger' via isar-users 2026-05-22 9:41 ` 'Jan Kiszka' via isar-users
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox