Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

argument to change uid/gid #927

Merged
merged 6 commits into from
Sep 22, 2024
Merged

Conversation

cathugger
Copy link
Contributor

different from #817 in that it can resolve user names, automatically use user's primary gid & allows specifying gid in the same argument, with : eg username:groupname.
feel free to criticize & suggest different argument name & description because i didn't put much of thought to that.

@@ -361,6 +364,13 @@ func run(args yggArgs, ctx context.Context, done chan struct{}) {
logger.Errorln("An error occurred starting TUN/TAP:", err)
}
n.tuntap.SetupAdminHandlers(n.admin)
// Change user if requested
if args.chuser != "" {
Copy link
Member

Choose a reason for hiding this comment

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

It may make sense to change to nobody:nobody by default, so things are a little less unsafe for people who just sudo yggdrasil -autoconf or similar. Thoughts @neilalexander?

Copy link
Contributor

@rany2 rany2 Aug 28, 2022

Choose a reason for hiding this comment

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

@Arceliar I think it should be noted that nobody:nobody is not standard across Linux distros. For example, Debian and friends use nobody:nogroup instead. We should check which group is available first or maybe use something like Dynamic Users in systemd (use a random UID/GID that's currently not in use by the system).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

could just use nobody and maybe it would just resolve primary group on its own i think?
also would need to check UID being 0 before doing chuser by default (as this can be started as own user if right caps are given).
and opting out may be kinda uglier - manually specifying empty user string, though i can't really find many reasons of opting out, other than some possible exotic distributions/installations without proper user databases - which i guess i could imagine in some sterilized docker environments or routers but i don't really know how common, and probably checking existence of nobody user before deciding to default to chuser'ing to it would make sense - and we could just check database before setting default to nobody, likely writing function in chuser_unix.go for that (& stub returning empty string in chuser_other.go).
Requesting opinions for this mindfuzz of mine i guess

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 thought about this a bit more and probably programming in this kind of logic into daemon itself is too much of a magical behavior imo, and this should be provided in distributions' .service files instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

well there isn't any need for this to be in any distro, Debian has a pretty hardened default that runs as yggdrasil user from the start. I think all you need to get it to work is CAP_NET_ADMIN, I think this is how Debian configured their OpenVPN services 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.

I see. This packaging is indeed fairly good. This has gotten a bit offtopic, but it would be cool if one could specify default socket / config path at compile time (eg using go build -ldflags="-X defaults.defaultAdminListen=unix:///var/run/yggdrasil/yggdrasil.sock") so that linux distros wouldn't need patches there to achieve saner packaging.
Either way this repo's systemd files suck at running non-root, and so consequentially do vendor (not official debian's) debs and archlinux package.

Copy link
Member

Choose a reason for hiding this comment

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

On the whole, I'm very pro-having separate flags for UID and GID, because then it is clearer that we can do one without the other, and it is easier for packagers to do the right thing for their distribution/platform.

I'm not entirely sure whether we want to do that by default — we could certainly do some testing to see how it feels. The main thing I can think of that might break is setting up new multicast sockets when an interface disappears and reappears later but we've already dropped privileges, but maybe it'll be OK?

Copy link
Member

Choose a reason for hiding this comment

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

Actually isn't that what #817 is doing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#817 can take only numeric UID/GIDs, which is unwieldy.

because then it is clearer that we can do one without the other

why would you want do one but not another? everywhere i looked around, one either wants to set uid and also gid to primary group of uid, or uid/gid separate, which is also possible. with this implementation, one can also set gid without uid (eg -user ":groupname" or -user ":gid"), though it may be a little bit unobvious, but why would you want to not drop root?

if you think that instead of using -user uid:gid syntax we should do -uid user -gid group, i can change to that too, looks like a minimal change to me.

The main thing I can think of that might break is setting up new multicast sockets when an interface disappears and reappears later but we've already dropped privileges, but maybe it'll be OK?

that's sorta fundamentally incompatible with privilege dropping tbh. on linux, we could probably do some magic with caps and reserve the right to deal with interfaces, but on other unixes we can't. restarting daemon when new interfaces to be managed pop up could probably make sense, but i don't quite feel that's what yggdrasil should do on its own either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unless multicast actually works without requiring root (i somehow confused it with managing interfaces, and using multicast aint that)

@cathugger
Copy link
Contributor Author

I looked around a bit, and git daemon implements this in a bit cleaner way, maybe, command line arguments wise:

   --user=<user>, --group=<group>
       Change daemon's uid and gid before entering the service loop. When
       only --user is given without --group, the primary group ID for the
       user is used. The values of the option are given to getpwnam(3) and
       getgrnam(3) and numeric IDs are not supported.

       Giving these options is an error when used with --inetd; use the
       facility of inet daemon to achieve the same before spawning git
       daemon if needed.

       Like many programs that switch user id, the daemon does not reset
       environment variables such as $HOME when it runs git programs, e.g.
       upload-pack and receive-pack. When using this option, you may also
       want to set and export HOME to point at the home directory of
       <user> before starting the daemon, and make sure any Git
       configuration files in that directory are readable by <user>.

Though nsd for example uses different syntax, using -u and taking in username, id, or id.gid, which is more similar to what I've implemented here.
I think that current implementation is kinda fine, but maybe documentation could be improved, but I can't really think of anything good atm.

@cathugger
Copy link
Contributor Author

rebased.

does this need any further work?
should I change argument format / documentation? current stuff is kinda unclear a bit imo but does anyone else find that to be an issue?
splitting to two arguments could make argument documentation easier too probably.
i think multicast stuff should work fine with this unless multicast port is picked to be <=1024.

@neilalexander
Copy link
Member

I haven't forgotten about this — I'd like to get this merged in some shape or other, but let's wait until the v0.5 branch is a bit cleaner before rebasing there.

@neilalexander neilalexander added the enhancement Enhances/improves functionality or UX label Apr 6, 2023
@neilalexander
Copy link
Member

If you're happy to rebase one last time on top of develop, let's get this merged for v0.5.

cmd/yggdrasil/chuser_unix.go Fixed Show fixed Hide fixed
cmd/yggdrasil/chuser_unix.go Fixed Show fixed Hide fixed
cmd/yggdrasil/chuser_unix.go Fixed Show fixed Hide fixed
@cathugger
Copy link
Contributor Author

@neilalexander done.
was away from this account while recovering from btrfs corruption.

cmd/yggdrasil/chuser_unix.go Dismissed Show dismissed Hide dismissed
cmd/yggdrasil/chuser_unix.go Fixed Show fixed Hide fixed
cmd/yggdrasil/chuser_unix.go Fixed Show fixed Hide fixed
cmd/yggdrasil/chuser_unix.go Outdated Show resolved Hide resolved
cmd/yggdrasil/chuser_unix.go Outdated Show resolved Hide resolved
cmd/yggdrasil/chuser_unix.go Fixed Show fixed Hide fixed
cmd/yggdrasil/chuser_unix.go Fixed Show fixed Hide fixed
@neilalexander neilalexander enabled auto-merge (squash) September 22, 2024 15:46
@neilalexander neilalexander merged commit 34f087d into yggdrasil-network:develop Sep 22, 2024
20 checks passed
group := ""
if i := strings.IndexByte(user, ':'); i >= 0 {
user, group = user[:i], user[i+1:]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just strings.Split(user, ':')?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IndexByte is probably speediest as Split allocates slice and uses string for splitting.
Cut would probably be more fitting here API niceness wise, but I'm not sure if really worth changing at this point.

Copy link
Contributor

Choose a reason for hiding this comment

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

IndexByte is probably speediest as Split allocates slice and uses string for splitting.

This code runs once, it is not a hot path.

Cut would probably be more fitting here API niceness wise, but I'm not sure if really worth changing at this point.

Sure, either way looks cleaner than manual indexing.

g := (*osuser.Group)(nil)

if user != "" {
if _, err := strconv.ParseUint(user, 10, 32); err == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why parse it yourself? You can try a name lookup, fallback to UID on error and return if that fails, too, just like getpwnam(3) behaves:

if u, err = osuser.Lookup(user); err != nil {
    if u, err = osuser.LookupId(user); if err != nil {
        return err
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this PR introduced as replacement for one that only used numeric IDs so I preserved ability to do chown/chgrp without passwords database file being present in system.
LookupId depends on /etc/passwd being present which is technically not a necessity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nvm we do LookupId after this anyway so it's a moot point.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

parsing before potentially IO hitting pw database lookup is probably what i thought back when i wrote this.

Copy link
Contributor

Choose a reason for hiding this comment

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

this PR introduced as replacement for one that only used numeric IDs so I preserved ability to do chown/chgrp without passwords database file being present in system.

Do these systems exist? Are they relevant?

LookupId depends on /etc/passwd being present which is technically not a necessity.

The next line uses that function, so this did not go as planned.

So you might as well clean it up, imho.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as I've mentioned in following comments, yes, this needs pw database anyways, but i prefer minimizing potentially latency introducing tasks (like opening/closing passwd file) even if they're done at init time, so it's guarded by fast parseuint check. it's a matter of taste, eliminating that goes into category of nitpicking IMO.

}

if g != nil {
gid, _ := strconv.ParseUint(g.Gid, 10, 32)
Copy link
Contributor

Choose a reason for hiding this comment

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

Questionable for this API to return the ID for a known-to-exist group as string...

if g != nil {
gid, _ := strconv.ParseUint(g.Gid, 10, 32)
var err error
if gid < math.MaxInt {
Copy link
Contributor

Choose a reason for hiding this comment

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

os/user returns ID strings that were "validated" with strconv.Atoi() which returns int, so this should always be true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

had to dig deep to confirm this but indeed.
also could probably be <=.
feel free to make a PR if you care for this as this looks like something code being saner internally not something that'll lead to bugs.

Copy link
Contributor

Choose a reason for hiding this comment

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

feel free to make a PR if you care for this as this looks like something code being saner internally not something that'll lead to bugs.

Either that or a clean(er) chuser_bsd.go implementation, should I ever start using this option.

Point being: Complexity hides bugs, so code ought to be as simple as possible.

To my eye, multiple Lookup*() and Parse*() are not as trivial to reason about as it could be.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

without digging deep into the source of osuser.LookupId one can't even know whether these ints were checked so I'm not sure complexity argument is even applicable here, it's more complex to verify whether the code is correct if there is no check.
my gut instinct when API doesn't tell stuff like returned ranges of ints to just check myself, because checking more usually catches more bugs than not checking stuff like returns.
IMO if check is eliminated, then strconv.Atoi() check behavior should be documented in comment, as otherwise it won't end up more simple.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also uint32 vs int is potentially hell on its own due to int being 64bit on 64bit systems and strconv.Atoi() ending up verifying different on 32bit vs 64bit systems but honestly systems just don't use ints as large as these for UIDs/GIDs anyways so i can't really care abt this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

87fa0a8 apparently was someone's else's change too but I can't argue one way or another abt it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to satisfy faulty Code scanning / CodeQL warnings out of all the things

Copy link
Contributor

Choose a reason for hiding this comment

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

to satisfy faulty Code scanning / CodeQL warnings out of all the things

This looks related over at #1202:
Screenshot 2024-11-04 at 04-05-21 Set groups when dropping privileges to not leak supplementary group access by klemensn · Pull Request #1202 · yggdrasil-network_yggdrasil-go

Copy link
Contributor

@klemensn klemensn Nov 4, 2024

Choose a reason for hiding this comment

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

feel free to make a PR if you care for this as this looks like something code being saner internally not something that'll lead to bugs.

  • -user '' silently ignored and the process continues without error or change in privilege.
  • -user ':' as well.
  • -user :group changes just the group and not the user, contrary to what the usage says:
  -user string
    	user (and, optionally, group) to set UID/GID to

I did fix these now in #1203.

panic(err)
}
}

Copy link
Contributor

@klemensn klemensn Nov 2, 2024

Choose a reason for hiding this comment

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

The main goroutine changing user/group to drop privileges is racing against others creating a tun(4) interface, raw socket for routing and UNIX socket as control interface, i.e. startup can fail now as yggdrasil may drop to an unprivileged user before having completed all privileged tasks.

# yggdrasil -autoconf -user nobody
2024/11/02 20:24:32 Build name: yggdrasil-go
2024/11/02 20:24:32 Build version: 0.5.9
2024/11/02 20:24:32 Your public key is 82ff9b15b30dc6e460f907cb3aecbe664c024c101efa773b3ecdccf1cb72d818
2024/11/02 20:24:32 Your IPv6 address is 200:fa00:c9d4:99e4:7237:3e0d:f069:8a26
2024/11/02 20:24:32 Your IPv6 subnet is 300:fa00:c9d4:99e4::/64
2024/11/02 20:24:32 Interface name: tun0
2024/11/02 20:24:32 Interface IPv6: 200:fa00:c9d4:99e4:7237:3e0d:f069:8a26/7
2024/11/02 20:24:32 Interface MTU: 16384
2024/11/02 20:24:32 Admin socket failed to listen: listen unix /var/run/yggdrasil.sock: bind: permission denied

Rerun this a few times; sometimes the control socket gets created as root, sometimes it fails to create it as nobody:

$  ls -ld /var/run/
drwxr-xr-x  7 root  wheel  512 Nov  2 20:31 /var/run

klemensn added a commit to klemensn/yggdrasil-go that referenced this pull request Nov 3, 2024
Otherwise the main goroutine races dropping privileges races against
those carrying out privileges tasks at startup.

Reproducible at least on OpenBSD/amd64 7.6-current, where it (expectedly)
fails to create a UNIX socket in `0755 root:wheel /var/run/` *after*
calling setuid(2) to `nobody`:
```
# yggdrasil -autoconf -user nobody
2024/11/03 21:15:27 Build name: yggdrasil-go
2024/11/03 21:15:27 Build version: 0.5.9
...
2024/11/03 21:15:27 Admin socket failed to listen: listen unix /var/run/yggdrasil.sock: bind: permission denied
```

Rerun, now the order is flipped:
```
# yggdrasil -autoconf -user nobody
2024/11/03 21:15:34 Build name: yggdrasil-go
2024/11/03 21:15:34 Build version: 0.5.9
[...]
2024/11/03 21:15:34 UNIX admin socket listening on /var/run/yggdrasil.sock
[...]
```

The `AdminSocket`s `done` channel is insufficient to sync here, waiting
for `n.admin.IsStarted()` does not guarantee socket creation, so export
a simple boolean instead.

This is a minimal fix to prevent startup failure;  TUN interface and
raw socket creation (to manage routes) might suffer the same problem,
but I have not seem them fail yet.

Fixes yggdrasil-network#927.
@klemensn
Copy link
Contributor

klemensn commented Nov 4, 2024

This code fails to unset supplementary groups, i.e. the process retains whatever access it had even after dropping to a different user and group:

# id                                                   
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)
# ./yggdrasil -autoconf -logto /dev/null -user nobody &
[1] 4337
# ps -o command,user,group,supgrp -U nobody
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   wheel,kmem,sys,tty,operator,staff,guest                         

Thus the process still has permissions to open, e.g. a 660 root:wheel file, which is bad.

klemensn added a commit to klemensn/yggdrasil-go that referenced this pull request Nov 4, 2024
…ccess

Changing the real and effective user/group IDs and the saved
set-user/group-ID is not enough to get rid of intial access permissions.

The list of groups must be cleared also, otherwise a process changing
from, e.g. `root:root` to `nobody:nobody` retains rights to access `:wheel`
files (assuming `root` is a member of the `wheel` group).

For example:
```
# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)
# ./yggdrasil -autoconf -logto /dev/null -user nobody &
[1] 4337
# ps -o command,user,group,supgrp -U nobody
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   wheel,kmem,sys,tty,operator,staff,guest
```

Fix that so the process runs as mere
```
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   nobody
```

Fixes yggdrasil-network#927.

# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# Дата:      Mon Nov 4 03:39:23 2024 +0300
#
# Текущая ветка: drop-supplementary-groups
# Изменения, которые будут включены в коммит:
#	изменено:      cmd/yggdrasil/chuser_unix.go
#
klemensn added a commit to klemensn/yggdrasil-go that referenced this pull request Nov 4, 2024
…ccess

Changing the real and effective user/group IDs and the saved
set-user/group-ID is not enough to get rid of intial access permissions.

The list of groups must be cleared also, otherwise a process changing
from, e.g. `root:root` to `nobody:nobody` retains rights to access `:wheel`
files (assuming `root` is a member of the `wheel` group).

For example:
```
# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)
# ./yggdrasil -autoconf -logto /dev/null -user nobody &
[1] 4337
# ps -o command,user,group,supgrp -U nobody
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   wheel,kmem,sys,tty,operator,staff,guest
```

Fix that so the process runs as mere
```
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   nobody
```

Fixes yggdrasil-network#927.
klemensn added a commit to klemensn/yggdrasil-go that referenced this pull request Nov 4, 2024
- Use unambiguous variable names (w/o package name conflict).
- Fail on invalid input such as the empty string or `:`.
- Do not change group without user, i.e. fail on `:group`.
- Parse input using mnemonic APIs.
- Do not juggle between integer types.
- Unset supplementary groups.
- Use setres[ug]id(2) to match the idiom of OpenBSD base programs.

Includes/Supersedes yggdrasil-network#1202.
Fixes yggdrasil-network#927.

I only tested on OpenBSD (so far), hence the split, but other systems
should just work.
neilalexander pushed a commit that referenced this pull request Nov 11, 2024
Creating UNIX sockets the listen() goroutine that races against the main
one dropping to an unprivileged user may cause startup failure when
privdrop happens before privileged filesystem access.

Setup or fail in New() and only do listen(2) in listen() to avoid this.

```
# yggdrasil -autoconf -user nobody
2024/11/03 21:15:27 Build name: yggdrasil-go
2024/11/03 21:15:27 Build version: 0.5.9
...
2024/11/03 21:15:27 Admin socket failed to listen: listen unix /var/run/yggdrasil.sock: bind: permission denied
```

Rerun, now the order is flipped:
```
# yggdrasil -autoconf -user nobody
2024/11/03 21:15:34 Build name: yggdrasil-go
2024/11/03 21:15:34 Build version: 0.5.9
[...]
2024/11/03 21:15:34 UNIX admin socket listening on /var/run/yggdrasil.sock
[...]
```

Fixes #927.
neilalexander pushed a commit that referenced this pull request Nov 11, 2024
…ccess (#1202)

Changing the real and effective user/group IDs and the saved
set-user/group-ID is not enough to get rid of intial access permissions.

The list of groups must be cleared also, otherwise a process changing
from, e.g. `root:root` to `nobody:nobody` retains rights to access
`:wheel` files (assuming `root` is a member of the `wheel` group).

For example:
```
# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)
# ./yggdrasil -autoconf -logto /dev/null -user nobody &
[1] 4337
# ps -o command,user,group,supgrp -U nobody
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   wheel,kmem,sys,tty,operator,staff,guest
```

Fix that so the process runs as mere
```
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   nobody
```

Fixes #927.
klemensn added a commit to klemensn/yggdrasil-go that referenced this pull request Nov 11, 2024
- Use unambiguous variable names (w/o package name conflict).
- Fail on invalid input such as the empty string or `:`.
- Do not change group without user, i.e. fail on `:group`.
- Parse input using mnemonic APIs.
- Do not juggle between integer types.
- Unset supplementary groups.
- Use setres[ug]id(2) to match the idiom of OpenBSD base programs.

Includes/Supersedes yggdrasil-network#1202.
Fixes yggdrasil-network#927.

I only tested on OpenBSD (so far), hence the split, but other systems
should just work.
neilalexander pushed a commit that referenced this pull request Nov 17, 2024
- Use unambiguous variable names (w/o package name conflict).
- Fail on invalid input such as the empty string or `:`.
- Do not change group without user, i.e. fail on `:group`.
- Parse input using mnemonic APIs.
- Do not juggle between integer types.
- Unset supplementary groups.
- Use set[ug]id(2) to follow the idiom of OpenBSD base programs.
  (cannot use setres[ug]id(2) as macOS does not have them.)

Includes/Supersedes #1202.
Fixes #927.

I only tested on OpenBSD (so far), but other systems should just work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Enhances/improves functionality or UX
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants