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

[Security] do not run as root. #816

Closed
wants to merge 4 commits into from

Conversation

zander
Copy link
Contributor

@zander zander commented Jul 24, 2021

An Internet accessible service should aim to have as little as
possible attack surface, which is much easier to do when running
with the absolute minimum number of priviledges at all times.

This makes the systemd setup run the service as a user 'yggdrasil'
and uses the systemd feature RuntimeDirectory to auto-create
the /var/run/yggdrasil dir so our non-elevated client can still
create the socket. (see #802)

The sysusers file will cause the user be created on first install
using the sysusers subsystem, this can be used by packagers as well.

Copy link
Contributor

@cofob cofob left a comment

Choose a reason for hiding this comment

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

Looks fine

@@ -7,7 +7,7 @@ package defaults
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
DefaultAdminListen: "unix:///var/run/yggdrasil/yggdrasil.sock",
Copy link
Member

Choose a reason for hiding this comment

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

This breaks running if the /var/run/yggdrasil folder doesn't exist, so the default depends on the systemd package (or otherwise creating the folder). The binary produced by ./build needs to be able to run with default settings (i.e. -autoconf) with no further changes to the OS/environment.

Copy link
Contributor Author

@zander zander Jul 24, 2021

Choose a reason for hiding this comment

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

I didn't test this and indeed if yggdrasil does not try to create the directory in which it wants to place the socket (which IMOHO is a bug) then this will fail.

An actual go progammer is required to make ygg create the directory before it attempts to create a file in it. I am not a Go programmer, would appreciate a patch. I guess its not too complex.

Choose a reason for hiding this comment

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

No problem, default socket path can be different if run with -autoconf. Probably, it should be created in user's home directory if run this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point. Likely best to use /run/user/{UID}/ which is where Linux by default makes those.

For instance wayland creates its sockets there.

Choose a reason for hiding this comment

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

No, it's bad to hardcode this path. The better way would be to use contents XDG_RUNTIME_DIR environment variable if it's present, otherwise something like .yggdrasil.sock in user's home directory. I presume, this will be a good default for those who still insist on not using systemd.

Choose a reason for hiding this comment

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

Actually, defaults should be dependent on whether config is generated by regular user or root (not on -autoconf option).

Regular user should have everything in the home directory (and socket $XDG_RUNTIME_DIR if this variable is present).
But for root user everything should be in LSB-conforming directories (config in /etc/yggdrasil, socket in /run/yggdrasil).

contrib/systemd/yggdrasil.sysusers Outdated Show resolved Hide resolved
src/defaults/defaults_linux.go Show resolved Hide resolved
@zander
Copy link
Contributor Author

zander commented Jul 24, 2021

To followup after the discussion on matrix.

  • the executable as compiled by build should, when using -autoconf use the XDG_RUNTIME_DIR (typically /run/user/UID) as a basdir instead. The rationale here is that services (daemons in Linux) do not have this variable set and thus finding this env-var means its started by a user. The yggdrasilctl executable can follow the same logic and thus find the socket.
  • the package-managers will be very happy to reuse the files this PR updates, they lack the XDG env-var and thus will make the file go to /run/yggdrasil/. Packagers for distro's not using systemd will need a packager-note to make clear that need to create this dir.
  • maybe users are used to running this as root, as such I suggest that yggdrasil auto-creates the directory the user requested the socket to be inserted in. This code will only hit when the two above cases are not used as they ensure the dir to exist.

I would be very sad if yggdrasil would turn off the admin interface before there is a replacement as that makes deployment and sysadmin-jobs much harder.

@Arceliar
Copy link
Member

Arceliar commented Jul 24, 2021

One more thing to add:

Running as an unprivileged user means the old AdminListen value (/var/run/yggdrasil.sock) will need to be recognized and replaced when updating. Otherwise, updating the package (via apt-get upgrade or similar) will leave the system in a broken state. The new systemd file would try to run ygg as an unprivileged user, but using a config file with the old (root-only) path, which leads to a runtime crash. This is true regardless of what the new default value is.

EDIT: One more thing to add. We can make the AdminListen something that's configurable at compile time. The default value would be "none" for all platforms, but the package maintainers could override. Then we'd update our scripts (mainly contrib/deb/generate.sh) accordingly, so packages that use the example systemd service file will also use that path for the admin socket by default. This is independent of any code changes we may want to introduce, to e.g. create the socket's directory at run time (if it does not already exist), or to migrate old configs (with the old default value of /var/run/yggdrasil.sock) to the new defaults.

@zander
Copy link
Contributor Author

zander commented Jul 24, 2021

Running as an unprivileged user means the old AdminListen value (/var/run/yggdrasil.sock) will need to be recognized and replaced when updating.

Yes, this is one of those problems: what is the ideal time to do this? Before 0.4.0, the second best time to do this? Now.

The solution there is for packagers to add an 'upgrade' line. Ygg does not have to do anything as the problem does not exist for people using its compiled binary.
perl -spi.orig -e 's/(AdminListen: unix:\/\/\/var\/run\/)(yggdrasil.sock)/$1\yggdrasil\/$2/;' /etc/yggdrasil.conf

We can make the AdminListen something that's configurable at compile time. The default value would be "none" for all platforms, but the package maintainers could override.

That is possible. Sounds complicated compared to simply doing a makedir.
You might consider instead having a new argument that is used in combination wih --genconf to create a useful line for AdminListen.

This has the advantage that your second systemd config file is the one that actually calls genconf and thus this change has lower impact.

@kotovalexarian
Copy link
Contributor

Why not just drop root privileges after socket file setup?

@jgoerzen
Copy link

jgoerzen commented Jan 5, 2022

Here are some suggestions to make this work better:

Don't create a yggdrasil sysusers file, and instead use the DynamicUser option:

User=yggdrasil-dyn
Group=yggdrasil
DynamicUser=true

(Note: User must not match yggdrasil here, since it isn't defined statically)

Also, set:

ProtectSystem=strict
NoNewPrivileges=true
ReadWritePaths=/var/run/yggdrasil /run/yggdrasil

ProtectSystem=strict prevents it from modifying basically anything on the filesystem, so therefore we have to specify the possible locations for the runtime directory under ReadWritePaths.

@zander
Copy link
Contributor Author

zander commented Jan 6, 2022

@jgoerzen sounds like an interesting idea. Seems to make the systemd deployment one file, which is nice.

Would you be able to comment on the minimum version of systemd needed for all the features your idea uses?

@jgoerzen
Copy link

jgoerzen commented Jan 6, 2022

Sure. Per the systemd NEWS file:

  • DynamicUser was added in version 232, 2016-11-03
  • NoNewPrivileges was introduced before NEWS started; it was already mentioned as existing in v227, 2015-10-07
  • ReadWritePaths was also introduced before NEWS started; it was already mentioned as existing in v231, 2016-07-25
  • ProtectSystem was already in use, and was introduced in v214, 2014-06-11

As far as I can tell, this is supported by all major distributions that are under active support.

Versions of systemd in Debian that are still under standard or Long-Term Support (LTS):

Versions of systemd in Ubuntu that are still under general support:

Versions of systemd in Fedora that are still under general support:

RHEL/CentOS forks RockyLinux and AlmaLinux are both shipping version 239, as did CentOS 8.

Various version comparisons can be found at https://pkgs.org/download/systemd

An Internet accessible service should aim to have as little as
possible attack surface, which is much easier to do when running
with the absolute minimum number of priviledges.

This makes the systemd setup run the service as a user 'yggdrasil'
and uses the systemd feature `RuntimeDirectory` to auto-create
the /var/run/yggdrasil dir so our non-elevated client can still
create the socket.

The sysusers file will cause the user be created on first install
using the sysusers subsystem.
The `AdminListen` option and `yggdrasilctl` now default to
`unix:///var/run/yggdrasil/yggdrasil.sock` on Linux

This allows yggdrasil to be run as its own user.

Closes yggdrasil-network#802
@zander
Copy link
Contributor Author

zander commented Jan 9, 2022

I tested this new idea on my archlinux (i.e. latest) and also on my server which is debian old-stable (v 9).
It works great on both;

systemctl --version gives;

systemd 249 (249.7-2-arch) and systemd 232 respectively

I'll push an update shortly.

Also use some stricter security features systemd provides.

This change from github user John Goerzen @jgoerzen as provided
in his comment:
yggdrasil-network#816 (comment)

ProtectSystem=strict prevents it from modifying basically anything on
the filesystem, so therefore we have to specify the possible locations
for the runtime directory under ReadWritePaths.
@jgoerzen
Copy link

An update:

Per discussion in Matrix, CAP_NET_RAW is no longer necessary.

You may be interested in the service file I'm shipping with the package that will go into Debian; it's linked at #874 (comment)

@neilalexander
Copy link
Member

I removed CAP_NET_RAW already in a4bdf3d.

@neilalexander
Copy link
Member

Closing in favour of #927.

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.

8 participants