diff mbox series

[1/1] support/scripts: added support for system and normal uid/gid.

Message ID 20191023211313.6758-1-stephan+buildroot@asklandd.dk
State Superseded
Headers show
Series [1/1] support/scripts: added support for system and normal uid/gid. | expand

Commit Message

Stephan Henningsen Oct. 23, 2019, 9:13 p.m. UTC
Signed-off-by: Stephan Henningsen <stephan+buildroot@asklandd.dk>
---
 docs/manual/makeusers-syntax.txt |  18 +--
 support/scripts/mkusers          | 196 ++++++++++++++++++++++++-------
 2 files changed, 162 insertions(+), 52 deletions(-)

Comments

Arnout Vandecappelle Oct. 23, 2019, 10:27 p.m. UTC | #1
Hi Stephan,

On 23/10/2019 23:13, Stephan Henningsen wrote:
> Signed-off-by: Stephan Henningsen <stephan+buildroot@asklandd.dk>

 What exactly is the difference between a system uid and a normal uid?

 I remember back in the days before dynamic uid assignment, it was customary to
make sure that the uids created by the sysadmin were in a high range, distinct
from the uids created by the package manager. I guess this was because the
package manager's uids are always local, while the sysadmin's uids could be used
over NFS. But I'm not a sysadmin :-)

 However, I don't see how this could ever be relevant in a Buildroot context.
Can you give an example of how this is supposed to be used?

 Regards,
 Arnout
Stephan Henningsen Oct. 23, 2019, 11:03 p.m. UTC | #2
Hi.

First of all: Sorry for the spam!  I'm...new here.

There's no technical difference between the two. It's purely
organizational. Also, it's standard on all the Linux dists I've used; all
except Buildroot.

Yet I made this change because I was annoyed with how my /etc/passwd and
/etc/group looked; my normal, real, login users were mixed up with system
uids and gids (sshd, sudo, kvm, etc).
At first glance user 'sshd' looks like a normal user; it has a uid of 1001
right above the normal login user at 1000 which I'd intentionally added.

I'm sorry, but I think it's messy, and I'd like to clean it up. I like that
uid/gid are within their usual system/normal regions; it makes it easier to
see what's going on, specifically which users and groups belong to the
system, and which groups that are main user groups.

Just because it's an embedded system, doesn't mean we have to be sloppy
about this ;)  I know it may sound subjective and based on personal
opinion, but it is actually conforming to standard, and I'm inclined to
think it's for a reason.

Also note, this change doesn't require any of the current packages to take
any action; they'll simply have their users and groups moved into the
normal system region from now on.  My guess is that this will fit perfectly
for the majority of packages. Of course, if any packages provide normal,
login users, these ought to be changed to -2 instead.  But nothing will
break.

So, that's it.

Some quick links on the topic to move you in the right direction ;)
- https://unix.stackexchange.com/a/80279/61306
-
https://geek-university.com/linux/uid-user-identifier-gid-group-identifier/

Yours,
Stephan

On Thu, Oct 24, 2019 at 12:27 AM Arnout Vandecappelle <arnout@mind.be>
wrote:

>  Hi Stephan,
>
> On 23/10/2019 23:13, Stephan Henningsen wrote:
> > Signed-off-by: Stephan Henningsen <stephan+buildroot@asklandd.dk>
>
>  What exactly is the difference between a system uid and a normal uid?
>
>  I remember back in the days before dynamic uid assignment, it was
> customary to
> make sure that the uids created by the sysadmin were in a high range,
> distinct
> from the uids created by the package manager. I guess this was because the
> package manager's uids are always local, while the sysadmin's uids could
> be used
> over NFS. But I'm not a sysadmin :-)
>
>  However, I don't see how this could ever be relevant in a Buildroot
> context.
> Can you give an example of how this is supposed to be used?
>
>  Regards,
>  Arnout
>
Yann E. MORIN Oct. 24, 2019, 7:11 a.m. UTC | #3
Stephan, All,

Please, don't top-post, that's bad etiquette; instead, intersperse your
replies in the text you are replying to (like I do below). Also, do not
post HTML mails, try to send only plain-text mails (html is ugly to read
in a terminal, even if there's a plain0text alternative).

On 2019-10-24 01:03 +0200, Stephan Henningsen spake thusly:
> There's no technical difference between the two. It's purely
> organizational. Also, it's standard on all the Linux dists I've used; all
> except Buildroot.

Buildroot is not a distribution; it is a buildsystem to generate
firmware images for embedded devices.

> Yet I made this change because I was annoyed with how my /etc/passwd and
> /etc/group looked; my normal, real, login users were mixed up with system
> uids and gids (sshd, sudo, kvm, etc).
> At first glance user 'sshd' looks like a normal user; it has a uid of 1001
> right above the normal login user at 1000 which I'd intentionally added.

As I already explained twice on IRC (to which you did not answer), if
you have specific requirements about uids/gids, you can already provide
your own /etc/passwd and /etc/group files, with the uids and gids you
need.

Also beware about automatic uid/gid assignent: they are not guaranteed
to be stable between two versions of Buildroot and/or two different
configurations.

For example, consider that you do a first build with sshd (or whatever
else, just an example) enabled, and it gets uid 27. You deploy this
build in production, and sshd created files on a remanent filesystem.
Then you decide to add new features to your device, and enable httpd
(another example). httpd sorts before sshd, so will be first to get
assigned a uid, and thus gets 27, while sshd now gets 42. Then you
deploy your new build in production.

Boom. The new sshd user can't read/write the files created by the
previous sshd user, because it is no longer the same uid.

To fix that, you really want to provide your own /etc/paswd and
/etc/group files when you have requirements on the uid and gid
assignemnts.

The auto-assignment by Buildroot is a best-effort, and exists just as a
starting point, but mostly as a check that no two-or-more usernames use
the same uid (ditto gid).

> I'm sorry, but I think it's messy, and I'd like to clean it up. I like that
> uid/gid are within their usual system/normal regions; it makes it easier to
> see what's going on, specifically which users and groups belong to the
> system, and which groups that are main user groups.

And again, as I said on IRC, the "usual system/normal regions" don't
mean anything. For my use-case, I have to generate all users in the
range 10000..10999. So your patches do not cover this use-case either.

An alternate solution I've been working on sometime ago, was to have two
new config entries:

    config BR2_UID_AUTO_MIN
        bool "lowest uid"
        int
        default 1000
        help
          The lowest UID assigned automatically by Buildroot.

    config BR2_UID_AUTO_MAX
        bool "highest uid"
        int
        default 1999
        help
          The gighest UID assigned automatically by Buildroot.

Which would be much more interesting to have (ditto gid).

Maybe I'll have time to finish it in the coming days...

> Just because it's an embedded system, doesn't mean we have to be sloppy
> about this ;)  I know it may sound subjective and based on personal

Sorry, but we're not beig sloppy. What are a "system user" or a
"non-system user"? The definition is not clear at all. On an embedded
device, I would argue that all users are system users, because there is
no human interacting with the device like they would interact with a
traditional distro.

> opinion, but it is actually conforming to standard, and I'm inclined to
> think it's for a reason.

What standard are you refering to?

> Also note, this change doesn't require any of the current packages to take
> any action; they'll simply have their users and groups moved into the
> normal system region from now on.  My guess is that this will fit perfectly
> for the majority of packages. Of course, if any packages provide normal,
> login users, these ought to be changed to -2 instead.  But nothing will
> break.

Still, I don't think that "system" vs. "non-system" users make sense,
and moreover, the stability of uids/gids is much more important, and
requires that packages either provide an explixit uid/gid, or that the
/etc/passwd and /etc/group be provided.

Regards,
Yann E. MORIN.
Stephan Henningsen Oct. 24, 2019, 11:30 p.m. UTC | #4
Hi,

Long mail, lots of reluctancy and personal feelings about my patch.  I
try to reply as good as I can.


On Thu, Oct 24, 2019 at 9:11 AM Yann E. MORIN <yann.morin.1998@free.fr> wrote:
>
> Please, don't top-post, that's bad etiquette; instead, intersperse your
> replies in the text you are replying to (like I do below).

Sorry, it was not my intention.


> Also, do not
> post HTML mails, try to send only plain-text mails (html is ugly to read
> in a terminal, even if there's a plain0text alternative).

I'll try.


> On 2019-10-24 01:03 +0200, Stephan Henningsen spake thusly:
> > There's no technical difference between the two. It's purely
> > organizational. Also, it's standard on all the Linux dists I've used; all
> > except Buildroot.
>
> Buildroot is not a distribution; it is a buildsystem to generate
> firmware images for embedded devices.

It does build a Linux system, and there are best practices, conventions,
and even standard for Linux systems.  This is what I meant when I said
"standards".
I'll use the term "well-known convention", from now on; it sounds less
bossy, and gives you more room for doubt =).

It's far from everyday I study uid/gids. But when I do, it's typically
because I need to fix some permission issue,
And then a well-known convention is gold!  I know exactly who uid 1000
is, I have a good idea of who uid 100 is,
and an even better feeling of who 3 is.


> > Yet I made this change because I was annoyed with how my /etc/passwd and
> > /etc/group looked; my normal, real, login users were mixed up with system
> > uids and gids (sshd, sudo, kvm, etc).
> > At first glance user 'sshd' looks like a normal user; it has a uid of 1001
> > right above the normal login user at 1000 which I'd intentionally added.
>
> As I already explained twice on IRC (to which you did not answer),

I must have overlooked it, don't take it personal.


> if you have specific requirements about uids/gids, you can already provide
> your own /etc/passwd and /etc/group files, with the uids and gids you
> need.

Oh, that. I'm not discussing special requirements. I simply argue that
well-known conventions should apply to Buildroot too.

Why shouldn't it?

It's quite simple to correct too. And believe my patch just did that.
I still hope we can discuss it at some point.


> Also beware about automatic uid/gid assignent: they are not guaranteed
> to be stable between two versions of Buildroot and/or two different
> configurations.
>
> For example, consider that you do a first build with sshd (or whatever
> else, just an example) enabled, and it gets uid 27. You deploy this
> build in production, and sshd created files on a remanent filesystem.
> Then you decide to add new features to your device, and enable httpd
> (another example). httpd sorts before sshd, so will be first to get
> assigned a uid, and thus gets 27, while sshd now gets 42. Then you
> deploy your new build in production.
>
> Boom. The new sshd user can't read/write the files created by the
> previous sshd user, because it is no longer the same uid.
>
> To fix that, you really want to provide your own /etc/paswd and
> /etc/group files when you have requirements on the uid and gid
> assignemnts.
>
> The auto-assignment by Buildroot is a best-effort, and exists just as a
> starting point, but mostly as a check that no two-or-more usernames use
> the same uid (ditto gid).

How does offsetting the uid/gids from 1000 to 100 in Buildroot's mkusers script
change any of this?


> > I'm sorry, but I think it's messy, and I'd like to clean it up. I like that
> > uid/gid are within their usual system/normal regions; it makes it easier to
> > see what's going on, specifically which users and groups belong to the
> > system, and which groups that are main user groups.
>
> And again, as I said on IRC, the "usual system/normal regions" don't
> mean anything.

But that's where you're wrong; they do mean something. I already
explained it. I gave examples. Both here and on IRC.

I'm aware that from the system's perspective, their numerical region
is of no importance, only their value is. However, don't care about
the system's perspective; I'm speaking from the user's perspective.
That's why we both keep referring to /etc/passwd and /etc/group; their
purpose is to map system's ids to names meaningful to the user (the
admin).  And here regions make sense.  And if there are users that
don't care about regions, I don't see there should a problem with my
patch.

> For my use-case, I have to generate all users in the
> range 10000..10999. So your patches do not cover this use-case either.

So the regions do have meaning for you too!  I'm guessing that the
reason you blow your user region off-chart like that, is because
Buildroot has already occupied the standard user region with its
system users. And you want that numerical space so you can clearly
differ system users from your normal users.

And you're right, my patch doesn't support your special requirements.
But it doesn't interfere with them either.  I could argue that it
actually helps you, as you would no longer have to add fixed uids but
could use -2 to have them auto generated into user region (if that's
an option).


> An alternate solution I've been working on sometime ago, was to have two
> new config entries:
>
>     config BR2_UID_AUTO_MIN
>         bool "lowest uid"
>         int
>         default 1000
>         help
>           The lowest UID assigned automatically by Buildroot.
>
>     config BR2_UID_AUTO_MAX
>         bool "highest uid"
>         int
>         default 1999
>         help
>           The gighest UID assigned automatically by Buildroot.
>
> Which would be much more interesting to have (ditto gid).

So not 2 but 4 new configuration entries.


> Maybe I'll have time to finish it in the coming days...

I'd advise you correct your defaults to standard.  Still I think it's
a lot of configuration options just to allow someone to break the
standard and configure for special requirements.  This should be fixed
by adding custom /etc/passwd and /etc/group files.  You said so
yourself.


> > Just because it's an embedded system, doesn't mean we have to be sloppy
> > about this ;)  I know it may sound subjective and based on personal
>
> Sorry, but we're not beig sloppy. What are a "system user" or a
> "non-system user"? The definition is not clear at all.

Oh yes you do, mr. +10000 uid.


> On an embedded
> device, I would argue that all users are system users, because there is
> no human interacting with the device like they would interact with a
> traditional distro.

Then it would be sane to move the default users into the system uid/gid region.

On a side note: I'm currently working on creating a light-weight Linux
that boots of a USB,
and it will require a normal user login.  And guessing by your
10000..10999 uid range I suspect you're
familiar with user interaction.  In any case, people use Buildroot to
create all sorts of systems.


> > opinion, but it is actually conforming to standard, and I'm inclined to
> > think it's for a reason.
>
> What standard are you refering to?

Yes, well I was mostly referring to my observations of a "best
practice" or "common approach" on
various Linux dists I've come across.  But it appears Debian has a standard:

https://www.debian.org/doc/debian-policy/ch-opersys.html#uid-and-gid-classes

There's a nice write-up here: https://unix.stackexchange.com/a/319751/61306


> > Also note, this change doesn't require any of the current packages to take
> > any action; they'll simply have their users and groups moved into the
> > normal system region from now on.  My guess is that this will fit perfectly
> > for the majority of packages. Of course, if any packages provide normal,
> > login users, these ought to be changed to -2 instead.  But nothing will
> > break.
>
> Still, I don't think that "system" vs. "non-system" users make sense,

Still?

Another option could also be to just offset the autogenerated uid/gids
from 1000 into 100,
and don't add that -2 fix.  I still think it would be a nice addition
to help users create normal users.
I even updated the documentation about it.

Oh this reminds me, that I or we should probably have a look that
/etc/adduser.conf or its equivalent on Buildroot.
It should match the mkusers script I think, but I don't have a running
system, so I'm just speculating.


> and moreover, the stability of uids/gids is much more important, and
> requires that packages either provide an explixit uid/gid, or that the
> /etc/passwd and /etc/group be provided.

Sorry I'm misunderstanding you here and not answering within context:
But all my script does, it offset the autogenerated uid/gid into the
right region;
I'm not touching packages' explicit uid/gid.  I haven't touched any
packages at all.

I'm aware that you don't agree to the well-known notion - or even the
Debian standard
- of system and normal user id ranges.

But how is it you find that my patch *breaks* anything?


--
Yours,
Stephan
diff mbox series

Patch

diff --git a/docs/manual/makeusers-syntax.txt b/docs/manual/makeusers-syntax.txt
index 467e596230..2c57df1a13 100644
--- a/docs/manual/makeusers-syntax.txt
+++ b/docs/manual/makeusers-syntax.txt
@@ -20,13 +20,17 @@  Where:
   It can not be +root+, and must be unique. If set to +-+, then just a
   group will be created.
 - +uid+ is the desired UID for the user. It must be unique, and not
-  +0+. If set to +-1+, then a unique UID will be computed by Buildroot
-  in the range [1000...1999]
+  +0+. If set to +-1+, then a unique systen UID will be computed by
+  Buildroot in the range [100...499].  If set to +-2+, then a unique
+  normal UID will be computed by Buildroot in the range [1000...1999]
 - +group+ is the desired name for the user's main group. It can not
   be +root+. If the group does not exist, it will be created.
 - +gid+ is the desired GID for the user's main group. It must be unique,
   and not +0+. If set to +-1+, and the group does not already exist, then
-  a unique GID will be computed by Buildroot in the range [1000..1999]
+  a unique system GID will be computed by Buildroot in the range
+  [100..499].  If set to +-2+, and the group does not already exist,
+  then a unique normal GID will be computed by Buildroot in the range
+  [1000..1999].
 - +password+ is the crypt(3)-encoded password. If prefixed with +!+,
   then login is disabled. If prefixed with +=+, then it is interpreted
   as clear-text, and will be crypt-encoded (using MD5). If prefixed with
@@ -57,16 +61,16 @@  will belong to the user and its main group.
 Examples:
 
 ----
-foo -1 bar -1 !=blabla /home/foo /bin/sh alpha,bravo Foo user
+foo -1 bar -1 !=secret123 /home/foo /bin/sh alpha,bravo Foo user
 ----
 
 This will create this user:
 
 - +username+ (aka login name) is: +foo+
-- +uid+ is computed by Buildroot
+- +uid+ is a system uid computed by Buildroot
 - main +group+ is: +bar+
-- main group +gid+ is computed by Buildroot
-- clear-text +password+ is: +blabla+, will be crypt(3)-encoded, and login is disabled.
+- main group +gid+ is a system uid computed by Buildroot
+- clear-text +password+ is: +secret123+, will be crypt(3)-encoded, and login is disabled.
 - +home+ is: +/home/foo+
 - +shell+ is: +/bin/sh+
 - +foo+ is also a member of +groups+: +alpha+ and +bravo+
diff --git a/support/scripts/mkusers b/support/scripts/mkusers
index d00ba33823..ff13e81160 100755
--- a/support/scripts/mkusers
+++ b/support/scripts/mkusers
@@ -4,10 +4,25 @@  myname="${0##*/}"
 
 #----------------------------------------------------------------------------
 # Configurable items
-MIN_UID=1000
-MAX_UID=1999
-MIN_GID=1000
-MAX_GID=1999
+NEW_SYSTEM_UID=-1
+NEW_SYSTEM_GID=-1
+
+MIN_SYSTEM_UID=100
+MAX_SYSTEM_UID=499
+MIN_SYSTEM_GID=100
+MAX_SYSTEM_GID=499
+
+NEW_NORMAL_UID=-2
+NEW_NORMAL_GID=-2
+
+MIN_NORMAL_UID=1000
+MAX_NORMAL_UID=1999
+MIN_NORMAL_GID=1000
+MAX_NORMAL_GID=1999
+
+MIN_UID=-2
+MIN_GID=-2
+
 # No more is configurable below this point
 #----------------------------------------------------------------------------
 
@@ -19,11 +34,19 @@  error() {
     printf "%s: " "${myname}" >&2
     printf "${fmt}" "${@}" >&2
 }
+
 fail() {
     error "$@"
     exit 1
 }
 
+# Verbose debugging, enable with e.g.: V=1 make
+v() {
+    if [ -n "${V}" ] && [ ${V} -ge 1 ]; then
+        printf "${@}\\n" >&2
+    fi
+}
+
 #----------------------------------------------------------------------------
 if [ ${#} -ne 2 ]; then
     fail "usage: %s USERS_TABLE TARGET_DIR\n"
@@ -125,6 +148,8 @@  check_user_validity() {
     local gid="${4}"
     local _uid _ugid _gid _username _group _ugroup
 
+    v "check_user_validity: username=$username, uid=$uid, group=$group, gid=$gid"
+
     _group="$( get_group "${gid}" )"
     _gid="$( get_gid "${group}" )"
     _ugid="$( get_ugid "${username}" )"
@@ -132,13 +157,15 @@  check_user_validity() {
     _uid="$( get_uid "${username}" )"
     _ugroup="$( get_ugroup "${username}" )"
 
+    v "... _group=$_group, _gid=$_gid, _ugid=$_ugid, _uid=$_uid, _username=$_username, _ugroup=$_ugroup"
+
     if [ "${username}" = "root" ]; then
         fail "invalid username '%s\n'" "${username}"
     fi
 
-    if [ ${gid} -lt -1 -o ${gid} -eq 0 ]; then
+    if [ ${gid} -lt ${MIN_GID} -o ${gid} -eq 0 ]; then
         fail "invalid gid '%d' for '%s'\n" ${gid} "${username}"
-    elif [ ${gid} -ne -1 ]; then
+    elif [ ${gid} -ne ${NEW_NORMAL_GID} -a ${gid} -ne ${NEW_SYSTEM_GID} ]; then
         # check the gid is not already used for another group
         if [ -n "${_group}" -a "${_group}" != "${group}" ]; then
             fail "gid '%d' for '%s' is already used by group '%s'\n" \
@@ -149,7 +176,7 @@  check_user_validity() {
         # Need to split the check in two, otherwise '[' complains it
         # is missing arguments when _gid is empty
         if [ -n "${_gid}" ] && [ ${_gid} -ne ${gid} ]; then
-            fail "group '%s' for '%s' already exists with gid '%d' (wants '%d')\n" \
+            fail "group '%s' for user '%s' already exists with gid '%d' (wants '%d')\n" \
                  "${group}" "${username}" ${_gid} ${gid}
         fi
 
@@ -162,9 +189,9 @@  check_user_validity() {
         fi
     fi
 
-    if [ ${uid} -lt -1 -o ${uid} -eq 0 ]; then
+    if [ ${uid} -lt ${MIN_UID} -o ${uid} -eq 0 ]; then
         fail "invalid uid '%d' for '%s'\n" ${uid} "${username}"
-    elif [ ${uid} -ne -1 ]; then
+    elif [ ${uid} -ne ${NEW_NORMAL_UID} -o ${uid} -ne ${NEW_SYSTEM_UID} ]; then
         # check the uid is not already used for another user
         if [ -n "${_username}" -a "${_username}" != "${username}" ]; then
             fail "uid '%d' for '%s' already used by user '%s'\n" \
@@ -190,30 +217,56 @@  check_user_validity() {
 }
 
 #----------------------------------------------------------------------------
-# Generate a unique GID for given group. If the group already exists,
-# then simply report its current GID. Otherwise, generate the lowest GID
-# that is:
+# Generate a unique *normal* GID for given group. If the group already
+# exists, then simply report its current GID. Otherwise, generate the lowest
+# GID that is:
+#   - not 0
+#   - comprised in [MIN_NORMAL_GID..MAX_NORMAL_GID]
+#   - not already used by a group
+generate_normal_gid() {
+    local group="${1}"
+    local gid
+
+    gid="$( get_gid "${group}" )"
+    if [ -z "${gid}" ]; then
+        for(( gid=MIN_NORMAL_GID; gid<=MAX_NORMAL_GID; gid++ )); do
+            if [ -z "$( get_group "${gid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${gid} -gt ${MAX_NORMAL_GID} ]; then
+            fail "can not allocate a GID for normal group '%s'\n" "${group}"
+        fi
+    fi
+    printf "%d\n" "${gid}"
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique *system* GID for given group. If the group already
+# exists, then simply report its current GID. Otherwise, generate the lowest
+# GID that is:
 #   - not 0
-#   - comprised in [MIN_GID..MAX_GID]
+#   - comprised in [MIN_SYSTEM_GID..MAX_SYSTEM_GID]
 #   - not already used by a group
-generate_gid() {
+generate_system_gid() {
     local group="${1}"
     local gid
 
     gid="$( get_gid "${group}" )"
     if [ -z "${gid}" ]; then
-        for(( gid=MIN_GID; gid<=MAX_GID; gid++ )); do
+        for(( gid=MIN_SYSTEM_GID; gid<=MAX_SYSTEM_GID; gid++ )); do
             if [ -z "$( get_group "${gid}" )" ]; then
                 break
             fi
         done
-        if [ ${gid} -gt ${MAX_GID} ]; then
-            fail "can not allocate a GID for group '%s'\n" "${group}"
+        if [ ${gid} -gt ${MAX_SYSTEM_GID} ]; then
+            fail "can not allocate a GID for system group '%s'\n" "${group}"
         fi
     fi
     printf "%d\n" "${gid}"
 }
 
+
 #----------------------------------------------------------------------------
 # Add a group; if it does already exist, remove it first
 add_one_group() {
@@ -221,11 +274,17 @@  add_one_group() {
     local gid="${2}"
     local members
 
+    v "add_one_group: group=$group, gid=$gid"
+
     # Generate a new GID if needed
-    if [ ${gid} -eq -1 ]; then
-        gid="$( generate_gid "${group}" )"
+    if [ ${gid} -eq ${NEW_NORMAL_GID} ]; then
+        gid="$( generate_normal_gid "${group}" )"
+    elif [ ${gid} -eq ${NEW_SYSTEM_GID} ]; then
+        gid="$( generate_system_gid "${group}" )"
     fi
 
+    v "...gid=$gid"
+
     members=$(get_members "$group")
     # Remove any previous instance of this group, and re-add the new one
     sed -i --follow-symlinks -e '/^'"${group}"':.*/d;' "${GROUP}"
@@ -239,25 +298,50 @@  add_one_group() {
 }
 
 #----------------------------------------------------------------------------
-# Generate a unique UID for given username. If the username already exists,
-# then simply report its current UID. Otherwise, generate the lowest UID
-# that is:
+# Generate a unique *normal* UID for given username. If the username already
+# exists then simply report its current UID. Otherwise, generate the lowest
+# UID that is:
+#   - not 0
+#   - comprised in [MIN_NORMAL_UID..MAX_NORMAL_UID]
+#   - not already used by a user
+generate_normal_uid() {
+    local username="${1}"
+    local uid
+
+    uid="$( get_uid "${username}" )"
+    if [ -z "${uid}" ]; then
+        for(( uid=MIN_NORMAL_UID; uid<=MAX_NORMAL_UID; uid++ )); do
+            if [ -z "$( get_username "${uid}" )" ]; then
+                break
+            fi
+        done
+        if [ ${uid} -gt ${MAX_NORMAL_UID} ]; then
+            fail "can not allocate a UID for normal user '%s'\n" "${username}"
+        fi
+    fi
+    printf "%d\n" "${uid}"
+}
+
+#----------------------------------------------------------------------------
+# Generate a unique *system* UID for given username. If the username already
+# exists then simply report its current UID. Otherwise, generate the lowest
+# UID that is:
 #   - not 0
-#   - comprised in [MIN_UID..MAX_UID]
+#   - comprised in [MIN_SYSTEM_UID..MAX_SYSTEM_UID]
 #   - not already used by a user
-generate_uid() {
+generate_system_uid() {
     local username="${1}"
     local uid
 
     uid="$( get_uid "${username}" )"
     if [ -z "${uid}" ]; then
-        for(( uid=MIN_UID; uid<=MAX_UID; uid++ )); do
+        for(( uid=MIN_SYSTEM_UID; uid<=MAX_SYSTEM_UID; uid++ )); do
             if [ -z "$( get_username "${uid}" )" ]; then
                 break
             fi
         done
-        if [ ${uid} -gt ${MAX_UID} ]; then
-            fail "can not allocate a UID for user '%s'\n" "${username}"
+        if [ ${uid} -gt ${MAX_SYSTEM_UID} ]; then
+            fail "can not allocate a UID for system user '%s'\n" "${username}"
         fi
     fi
     printf "%d\n" "${uid}"
@@ -303,12 +387,16 @@  add_one_user() {
     local comment="${9}"
     local _f _group _home _shell _gid _passwd
 
+    v "add_one_user: username=$username, uid=$uid, group=$group, gid=$gid, groups=$groups"
+
     # First, sanity-check the user
     check_user_validity "${username}" "${uid}" "${group}" "${gid}"
 
     # Generate a new UID if needed
-    if [ ${uid} -eq -1 ]; then
-        uid="$( generate_uid "${username}" )"
+    if [ ${uid} -eq ${NEW_NORMAL_UID} ]; then
+        uid="$( generate_normal_uid "${username}" )"
+    elif [ ${uid} -eq ${NEW_SYSTEM_UID} ]; then
+        uid="$( generate_system_uid "${username}" )"
     fi
 
     # Remove any previous instance of this user
@@ -372,11 +460,18 @@  main() {
     local -a ENTRIES
 
     # Some sanity checks
-    if [ ${MIN_UID} -le 0 ]; then
-        fail "MIN_UID must be >0 (currently %d)\n" ${MIN_UID}
+    if [ ${MIN_NORMAL_UID} -le 0 ]; then
+        fail "MIN_NORMAL_UID must be >0 (currently %d)\n" ${MIN_NORMAL_UID}
+    fi
+    if [ ${MIN_NORMAL_GID} -le 0 ]; then
+        fail "MIN_NORMAL_GID must be >0 (currently %d)\n" ${MIN_NORMAL_GID}
+    fi
+
+    if [ ${MIN_SYSTEM_UID} -le 0 ]; then
+        fail "MIN_SYSTEM_UID must be >0 (currently %d)\n" ${MIN_SYSTEM_UID}
     fi
-    if [ ${MIN_GID} -le 0 ]; then
-        fail "MIN_GID must be >0 (currently %d)\n" ${MIN_GID}
+    if [ ${MIN_SYSTEM_GID} -le 0 ]; then
+        fail "MIN_SYSTEM_GID must be >0 (currently %d)\n" ${MIN_SYSTEM_GID}
     fi
 
     # Read in all the file in memory, exclude empty lines and comments
@@ -384,33 +479,44 @@  main() {
         ENTRIES+=( "${line}" )
     done < <( sed -r -e 's/#.*//; /^[[:space:]]*$/d;' "${USERS_TABLE}" )
 
-    # We first create groups whose gid is not -1, and then we create groups
-    # whose gid is -1 (automatic), so that, if a group is defined both with
-    # a specified gid and an automatic gid, we ensure the specified gid is
-    # used, rather than a different automatic gid is computed.
+    # We first create groups whose gid is specific (not ANY_*_GID),
+    # and then we create groups whose gid is automatic (is ANY_*_GID,
+    # so that, if a group is defined both with a specified gid and an
+    # automatic gid, we ensure the specified gid is used, rather than
+    # a different automatic gid is computed.
 
-    # First, create all the main groups which gid is *not* automatic
+    # First, create all the main groups where gid is *specific*.
     for line in "${ENTRIES[@]}"; do
         read username uid group gid passwd home shell groups comment <<<"${line}"
-        [ ${gid} -ge 0 ] || continue    # Automatic gid
+        [ ${gid} -ge 0 ] || continue # Automatic gid
         add_one_group "${group}" "${gid}"
     done
 
-    # Then, create all the main groups which gid *is* automatic
+    # Then, create all the main groups where gid is *automatic*.
     for line in "${ENTRIES[@]}"; do
         read username uid group gid passwd home shell groups comment <<<"${line}"
-        [ ${gid} -eq -1 ] || continue    # Non-automatic gid
+        [ ${gid} -eq ${NEW_NORMAL_GID} -o ${gid} -eq ${NEW_SYSTEM_GID} ] || continue # Non-automatic gid
         add_one_group "${group}" "${gid}"
     done
 
-    # Then, create all the additional groups
+    # Then, create all the additional groups.
     # If any additional group is already a main group, we should use
-    # the gid of that main group; otherwise, we can use any gid
+    # the gid of that main group; otherwise, we create a normal or system gid, see below.
     for line in "${ENTRIES[@]}"; do
         read username uid group gid passwd home shell groups comment <<<"${line}"
         if [ "${groups}" != "-" ]; then
             for g in ${groups//,/ }; do
-                add_one_group "${g}" -1
+                # If the uid or gid is already a system uid or gid, then create a system gid too;
+                # otherwise create normal gid
+                if [ ${uid} -eq ${NEW_SYSTEM_GID} -o ${gid} -eq ${NEW_SYSTEM_GID} ]; then
+                    v "creating system group, uid=$uid, gid=$gid"
+                    gid=${NEW_SYSTEM_GID}
+                else
+                    v "creating normal group, uid=$uid, gid=$gid"
+                    gid=${NEW_NORMAL_GID}
+                fi
+
+                add_one_group "${g}" ${gid}
             done
         fi
     done
@@ -433,7 +539,7 @@  main() {
     for line in "${ENTRIES[@]}"; do
         read username uid group gid passwd home shell groups comment <<<"${line}"
         [ "${username}" != "-" ] || continue # Magic string to skip user creation
-        [ ${uid} -eq -1        ] || continue # Non-automatic uid
+        [ ${uid} -eq ${NEW_NORMAL_UID} -o ${uid} -eq ${NEW_SYSTEM_UID} ] || continue # Non-automatic gid
         add_one_user "${username}" "${uid}" "${group}" "${gid}" "${passwd}" \
                      "${home}" "${shell}" "${groups}" "${comment}"
     done