Skip to content

Conversation

@adombeck
Copy link
Contributor

@adombeck adombeck commented Sep 24, 2025

Support setting the UID of a user via authctl user set-uid <user> <uid>.

Also changes the owner and group of the user's home directory and all files in the home directory from the old UID and GID to the new UID and GID (if it is owned by the current user), same as usermod does when changing the UID of a user.

Closes #630
UDENG-7717
UDENG-8720

@adombeck adombeck force-pushed the 630-set-uid branch 3 times, most recently from b8f41fb to 07b3b86 Compare September 24, 2025 12:46
@codecov-commenter
Copy link

codecov-commenter commented Sep 24, 2025

Codecov Report

❌ Patch coverage is 64.08451% with 153 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.36%. Comparing base (ba5104c) to head (0702184).

Files with missing lines Patch % Lines
internal/users/proc/proc.go 60.41% 38 Missing ⚠️
cmd/authctl/internal/completion/completion.go 0.00% 29 Missing ⚠️
internal/users/db/update.go 69.87% 25 Missing ⚠️
cmd/authctl/user/set-uid.go 0.00% 19 Missing ⚠️
cmd/authctl/group/set-gid.go 0.00% 16 Missing ⚠️
internal/services/user/user.go 72.72% 6 Missing ⚠️
internal/testlog/testlog.go 66.66% 6 Missing ⚠️
internal/users/manager.go 94.33% 6 Missing ⚠️
cmd/authctl/internal/client/client.go 72.72% 3 Missing ⚠️
internal/fileutils/fileutils.go 83.33% 3 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1087      +/-   ##
==========================================
- Coverage   87.65%   86.36%   -1.30%     
==========================================
  Files          90       96       +6     
  Lines        6222     6615     +393     
  Branches      111      111              
==========================================
+ Hits         5454     5713     +259     
- Misses        712      846     +134     
  Partials       56       56              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@adombeck
Copy link
Contributor Author

@3v1n0 @denisonbarbosa The PR is still missing tests, but I would appreciate a first review nonetheless, especially regarding the decision to automatically change the owner of the home directory (see e68fd53) before I spend time adding tests for that.

Comment on lines 260 to 264
// Update the users table. We update both the UID and the GID of the user private group,
// because UID == GID for user private groups (see https://wiki.debian.org/UserPrivateGroups#UPGs)
if _, err := tx.Exec(`UPDATE users SET uid = ?, gid = ? WHERE name = ?`, newUID, newUID, username); err != nil {
return err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably update the group ID of the private group itself as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you're right. Now I'm not sure anymore if we should actually change the GID of the user private group automatically as well. usermod doesn't do that and there is no Debian-specific alternative to usermod which handles user private groups (like adduser is for useradd). Thinking about it, I see some potential problems if we change the GID as well:

  • Less flexibility for admins
  • More complexity in the implementation of SetUserID
    • Need to handle the case that the UID is an existing GID
  • What if a user private group's ID is changed via authctl group set-gid? Should we disallow that?

I'm leaning towards letting the admin use authctl set-gid to also change the GID of the user private group if they want to make use of user private groups, same as they have to do with usermod / groupmod. What do you think? @3v1n0, also interested in your opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards letting the admin use authctl set-gid to also change the GID of the user private group if they want to make use of user private groups

I just realized that would mean we don't only have to add a authctl group set-gid command (which we should do anyway) but also a authctl user set-gid, to change the GID of a user (analogous to usermod --gid). That also doesn't feel like a great solution 😕

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I experimented some more with usermod and groupmod. Some of my findings:

  1. Changing a user's UID (usermod --uid <new-uid> <user>) does not update the user's primary group (as already noted above).

  2. Changing a group's GID (groupmod --gid <new-gid> <group>) does also update the ID of the user's primary group in /etc/passwd

    • For example if I have this entry in /etc/passwd:

      test:x:1000:1000:,,,:/home/test:/bin/bash
      

      and this entry in /etc/group:

      test:x:1000
      

      then after executing sudo groupmod --gid=1234 test the entries are changed to

      test:x:1000:1234:,,,:/home/test:/bin/bash
      

      and

      test:x:1234
      
  3. A user's primary group can only be changed to the GID of a group that actually exists.

    • So sudo usermod --gid 1234 test fails with usermod: group '1234' does not exist if there is no group with GID 1234

If we replicate the behavior of 1. with authctl user set-uid and of 2. with authctl group set-gid, I think we could get away without adding a authctl user set-gid command. For example, given the user entry

[email protected]:x:10000:10000:,,,:/home/[email protected]:/bin/bash

and the group entry

To change the UID, the admin would use:

authctl user set-uid 12345 [email protected]

and to then also change the user private group's ID:

authctl group set-gid 12345 [email protected]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authctl user set-uid now only changes the user's UID. I will add a authctl group set-gid command in a follow-up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we replicate the behavior of 1. with authctl user set-uid and of 2. with authctl group set-gid, I think we could get away without adding a authctl user set-gid command.

While groupmod --gid <gid> <group> does change the primary group ID of users who have <group> as their primary group, it does not change the group ownership of these user's home directories. usermod --gid <gid> <user> does change the group ownership of the user's home directory (for files which are owned by the previous primary group).

I'm considering to deviate from that and make authctl group set-gid also change the group of the home directory of user's whose primary group is updated that way. Otherwise users will have to run sudo chown -R --from :<old-gid> :<new-gid> $HOME manually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering to deviate from that and make authctl group set-gid also change the group of the home directory of user's whose primary group is updated that way

Although the approach you suggested makes more sense and is more usable, I'm a bit worried about mimicking the behavior of a well known tool but doing it quietly differently. Although, I think we shouldn't have any issues in this case, since we are adding a behavior rather than removing one.

I do prefer the way you suggested, though. I think it's a good one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried about mimicking the behavior of a well known tool but doing it quietly differently

We should pay attention that we:

  • Don't state that it behaves the same as usermod / groupmod
  • Make the behavior clear in the documentation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the behavior clear in the documentation

I added long descriptions for set-uid:

$ authctl user set-uid --help
Set the UID of a user managed by authd to the specified value.

The new UID value must be unique and non-negative.

The user's home directory and any files within it owned by the user will
automatically have their ownership updated to the new UID. If the home directory
ownership cannot be changed, a warning will be displayed but the command will
still exit successfully.

Files outside the home directory are not updated and must be changed manually 
if needed. To update ownership of all files on the system, use:
  sudo chown -R --from OLD_UID NEW_UID /

This command requires root privileges.

Examples:
  authctl user set-uid john 15000
  authctl user set-uid alice 20000

Usage:
  authctl user set-uid <name> <uid> [flags]

Flags:
  -h, --help   help for set-uid

and set-gid:

$ authctl group set-gid --help
Set the GID of a group managed by authd to the specified value.

The new GID value must be unique and non-negative.

When a group's GID is changed, any users whose primary group is set to this group
will have their primary group GID updated. The home directories of these users and
files within them owned by the group will be updated to the new GID. If changing
ownership fails, a warning will be displayed but the command will still succeed.

Files outside users' home directories are not updated and must be changed manually.
To update group ownership of all files on the system, use:
  sudo chown -R --from :OLD_GID :NEW_GID /

This command requires root privileges.

Examples:
  authctl group set-gid staff 30000
  authctl group set-gid developers 40000

Usage:
  authctl group set-gid <name> <gid> [flags]

Flags:
  -h, --help   help for set-gid

@adombeck adombeck force-pushed the 630-set-uid branch 3 times, most recently from 09f3d78 to 8ddbc86 Compare November 21, 2025 11:02
@adombeck adombeck marked this pull request as ready for review November 21, 2025 15:56
@adombeck adombeck requested a review from a team as a code owner November 21, 2025 15:56
Copy link
Member

@denisonbarbosa denisonbarbosa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work. Some small comments, but we should be able to address them quite quickly.

@adombeck
Copy link
Contributor Author

adombeck commented Dec 2, 2025

Will add the missing tests and also update TestChownRecursiveFrom to use bubblewrap :)

Done and I also added authctl group set-gid. Should be ready for another round of review.

@adombeck adombeck force-pushed the 630-set-uid branch 3 times, most recently from d863174 to 8b9dadb Compare December 3, 2025 13:41
Comment on lines +388 to +404
err = userslocking.WriteLock()
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, userslocking.WriteUnlock()) }()

// Check if the user exists
oldUser, err := m.db.UserByName(name)
if err != nil {
return nil, err
}
// Check if the user already has the given UID
if oldUser.UID == uid {
warning := fmt.Sprintf("User %q already has UID %d", name, uid)
log.Info(context.Background(), warning)
return []string{warning}, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a per-process lock though, so... If another user is being added with the same ID while we're modifying one we could have racy behavior here.

Unless we have some kind of protection at DB commit phase (as we have in UpdateUser).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said... I'd likely lock the whole thing now when manipulating users through these APIs (including UpdateUser).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userslocking.WriteLock is a global lock

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a global per-process lock but it would ErrLock in case of concurrent requests rather than blocking, so, not sure if we want that.

I would also use lockedentries instead, as I assume we want to check if the new UID/GIDs are not used locally, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also use lockedentries instead, as I assume we want to check if the new UID/GIDs are not used locally, no?

We already check that via user.LookupId. We can't use UserDBLocked.IsUniqueUID because that also returns false if the ID is used as a GID.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I thought it would behave exactly like lckpwdf, i.e. block for 15 seconds before returning an error. Is there a reason we made it behave differently?

No, that's the default behavior, since it is meant to give unique access to a process to the locked resources, so if we're owning the lock, we're good to go basically, but we can't lock again (thus the error).

Other processes instead will behave that way.

We already check that via user.LookupId. We can't use UserDBLocked.IsUniqueUID because that also returns false if the ID is used as a GID.

Well, can't it split it out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a global per-process lock but it would ErrLock in case of concurrent requests rather than blocking

If that's the behavior of lckpwdf then the man page should be updated to mention that IMO. Currently it reads like it would always block for 15 seconds:

   The  lckpwdf() function is intended to protect against multiple simultaneous accesses of the shadow password database.
   It tries to acquire a lock, and returns 0 on success, or -1 on failure (lock not obtained within 15 seconds).  The ul‐
   ckpwdf() function releases the lock again.  Note that there is no protection against direct access of the shadow pass‐
   word file.  Only programs that use lckpwdf() will notice the lock.

Well, can't it split it out?

I don't see why we should change UserDBLocked. We don't need it here AFAICT, userslocking.WriteLock and user.LookupId give us everything we need.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userslocking.WriteLock and user.LookupId give us everything we need.

That's fine, but we then need to just not rely on userslocking.WriteLock as global lock, or add a version with a mutex on top.

IIRC these were things I somewhat had drafted in the past but then we went for a simpler approach.

If that's the behavior of lckpwdf then the man page should be updated to mention that IMO

Well, feel free to update libc, but we also are testing this case (in fact we do not hang):

go test -C internal/users/locking -v -run TestLockAndLockAgainGroupFile

And it was one of the reasons why I initially added the reflock too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, but we then need to just not rely on userslocking.WriteLock as global lock, or add a version with a mutex on top.

yeah I did that now

Well, feel free to update libc

not today, maybe I'll create an issue / PR tomorrow :)

but we also are testing this case (in fact we do not hang)

👍

Comment on lines +86 to +161
// Since 25.10 Ubuntu ships the AppArmor profile /etc/apparmor.d/bwrap-userns-restrict
// which restricts bwrap and causes chown to fail with "Operation not permitted".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this reported upstream? Is it something that should not be done?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it's on purpose to better confine processes run in bwrap. We don't care about that because we don't use bwrap for security.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well not here, I'm speaking at wider level in the distro.

It doesn't test anything that's not already covered by other tests and
it's annoying to have to manually update the golden files of the SSH
integration tests whenever the authctl usage message changes.
userslocking.WriteLock() immediately returns ErrLock if the lock is
already taken *by the current process*. lckpwdf behaves similarly (even
though the man page doesn't mention it).

To avoid that issue, we now take another lock which blocks concurrent
goroutines.
We broke the bubblewrap tests in the CI without noticing it (at first)
because the tests were skipped. The only case where we really want to
skip the tests is on Launchpad builders. To detect that, we check if the
DEB_BUILD_ARCH environment variable is set and we're *not* in GitHub CI.
When executing `unshare --map-user` via exec.Command and connecting the
process's stdout or stderr, the command hangs forever if unprivileged
user namespaces are disabled.

We avoid that by checking via `unshare --user` if unprivileged user
namespaces are enabled.
The "Run autopkgtests" CI job runs the tests in an LXD container which
doesn't allow using bubblewrap. It fails with:

    bwrap: Failed to make / slave: Permission denied

To avoid that these jobs fail, we allow them to skip the bubblewrap
tests. We still run the tests in the "Go Tests" CI jobs.
Running our tests with -v produces so much output that it makes it
harder to inspect test failures, for example when viewing the logs of
the "Run autopkgtests" CI job in GitHub.

Running the tests without -v still prints the logs of the failed tests
which should include all the information we need to debug test failures.
@codecov
Copy link

codecov bot commented Jan 26, 2026

Codecov Report

❌ Patch coverage is 64.43418% with 154 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.33%. Comparing base (bbd1cb6) to head (d2585ba).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/users/proc/proc.go 62.13% 39 Missing ⚠️
cmd/authctl/internal/completion/completion.go 0.00% 29 Missing ⚠️
internal/users/db/update.go 69.87% 25 Missing ⚠️
cmd/authctl/user/set-uid.go 0.00% 19 Missing ⚠️
cmd/authctl/group/set-gid.go 0.00% 16 Missing ⚠️
internal/services/user/user.go 72.72% 6 Missing ⚠️
internal/testlog/testlog.go 66.66% 6 Missing ⚠️
internal/users/manager.go 94.33% 6 Missing ⚠️
cmd/authctl/internal/client/client.go 72.72% 3 Missing ⚠️
internal/fileutils/fileutils.go 83.33% 3 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1087      +/-   ##
==========================================
- Coverage   87.56%   86.33%   -1.23%     
==========================================
  Files          91       97       +6     
  Lines        6231     6631     +400     
  Branches      111      111              
==========================================
+ Hits         5456     5725     +269     
- Misses        719      850     +131     
  Partials       56       56              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

As suggested by reviewer. It's not implemented for now, warnings are
always returned in English.
We don't need to load the bwrap-userns-restrict AppArmor profile for the
bubblewrap tests to work. In fact, we even have to circumvent the
AppArmor profile (if it's loaded) for the tests to work.

This reverts commit 7b926c0.
Copy link

Copilot AI commented Jan 27, 2026

@3v1n0 I've opened a new pull request, #1222, to work on those changes. Once the pull request is ready, I'll request review from you.

@aleasto
Copy link
Member

aleasto commented Jan 27, 2026

Beware that AccountsService does not currently handle well the case where an account changes UID. We found out because GDM does the same thing when managing its own temporary users.

accounts-daemon will keep the old user id in its database, which means that querying the user's uid returns the wrong id. It kinda works for users managed by /etc/passwd because accounts-daemon has a file-monitor on it and will rebuild its database on changes, but there's no way to universally monitor other NSS providers.

I have submitted a patch upstream which "fixes" this by updating the user record on the next query. It is now included in Ubuntu 26.04.

It might be possible for authd to instruct AccountsService to drop or refresh the stale user record when calling authctl user set-shell, e.g. by calling org.freedesktop.Accounts.UncacheUser(name), but it doesn't seem to be working either.

Refs:
https://bugs.launchpad.net/ubuntu/+source/gdm3/+bug/2134405
https://gitlab.gnome.org/GNOME/gdm/-/issues/1047
https://gitlab.freedesktop.org/accountsservice/accountsservice/-/merge_requests/168
https://salsa.debian.org/freedesktop-team/accountsservice/-/commit/5b2471cd22c0b8740d7f5732c205d43ca6d2d3d5

@adombeck
Copy link
Contributor Author

@aleasto thanks for the heads up, Alessandro. I quickly tested what happens with the user in AccountsService when I use authctl set-uid to change the UID. The Uid property of the org.freedesktop.Accounts.User object is indeed not updated. However, the user is still listed in GDM and I can successfully log in as it, and after successful login, the Uid property in the AccountsService shows the correct UID. So GDM doesn't seem to have a problem with the fact that the UID is outdated in AccountsService. Is there anything else we need to worry about?

@aleasto
Copy link
Member

aleasto commented Jan 28, 2026

So GDM doesn't seem to have a problem with the fact that the UID is outdated in AccountsService. Is there anything else we need to worry about?

The implications may vary depending on which clients are using AccountsService.
In https://bugs.launchpad.net/ubuntu/+source/gdm3/+bug/2134405, gsd-xsettings calling act_user_set_input_sources was incorrectly triggering an authentication prompt because it thought the requesting user did not match the target user (uid didn't match).

If authctl user set-uid is only to be called while logged-out, and you say that logging-in resolves the discrepancy in AccountsService, then there might be no visible problem.

@adombeck
Copy link
Contributor Author

If authctl user set-uid is only to be called while logged-out

Yes, it also checks that no processes of that user are running and bails out if it finds any.

and you say that logging-in resolves the discrepancy in AccountsService, then there might be no visible problem.

Okay, then I think we're good for now 🤞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support setting UID and GID via authctl

6 participants