diff options
-rw-r--r-- | debian/postinst | 9 | ||||
-rw-r--r-- | make-linux.mk | 4 | ||||
-rw-r--r-- | one.cpp | 14 | ||||
-rw-r--r-- | osdep/LinuxDropPrivileges.cpp | 164 | ||||
-rw-r--r-- | osdep/LinuxDropPrivileges.hpp | 9 |
5 files changed, 197 insertions, 3 deletions
diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 00000000..ecd148a4 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,9 @@ +#!/bin/sh -e + +case "$1" in + configure) + adduser --system --group --home /var/lib/zerotier-one --no-create-home zerotier-one + ;; +esac + +#DEBHELPER# diff --git a/make-linux.mk b/make-linux.mk index 016f7b7c..9dfd39bf 100644 --- a/make-linux.mk +++ b/make-linux.mk @@ -111,8 +111,8 @@ endif all: one manpages -one: $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o - $(CXX) $(CXXFLAGS) $(LDFLAGS) -o zerotier-one $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o $(LDLIBS) +one: $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o osdep/LinuxDropPrivileges.o + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o zerotier-one $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o osdep/LinuxDropPrivileges.o $(LDLIBS) $(STRIP) zerotier-one ln -sf zerotier-one zerotier-idtool ln -sf zerotier-one zerotier-cli @@ -44,6 +44,10 @@ #include <sys/types.h> #include <sys/stat.h> #include <signal.h> + +#ifdef __linux__ +#include "osdep/LinuxDropPrivileges.hpp" +#endif #endif #include <string> @@ -900,7 +904,7 @@ static void printHelp(const char *cn,FILE *out) fprintf(out,"Available switches:" ZT_EOL_S); fprintf(out," -h - Display this help" ZT_EOL_S); fprintf(out," -v - Show version" ZT_EOL_S); - fprintf(out," -U - Run as unprivileged user (skip privilege check)" ZT_EOL_S); + fprintf(out," -U - Skip privilege check and do not attempt to drop privileges" ZT_EOL_S); fprintf(out," -p<port> - Port for UDP and TCP/HTTP (default: 9993, 0 for random)" ZT_EOL_S); #ifdef __UNIX_LIKE__ @@ -1141,6 +1145,14 @@ int main(int argc,char **argv) #endif // __WINDOWS__ #ifdef __UNIX_LIKE__ + +#ifndef ZT_ONE_RUN_AS_ROOT +#ifdef __linux__ + if (!skipRootCheck) + dropPrivileges(homeDir); +#endif +#endif + std::string pidPath(homeDir + ZT_PATH_SEPARATOR_S + ZT_PID_PATH); { // Write .pid file to home folder diff --git a/osdep/LinuxDropPrivileges.cpp b/osdep/LinuxDropPrivileges.cpp new file mode 100644 index 00000000..dab85bd8 --- /dev/null +++ b/osdep/LinuxDropPrivileges.cpp @@ -0,0 +1,164 @@ +#include "LinuxDropPrivileges.hpp" +#include <linux/capability.h> +#include <linux/securebits.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <pwd.h> +#include <stdlib.h> +#include <unistd.h> + +namespace ZeroTier { + +#ifndef PR_CAP_AMBIENT +// if we are on old libc, dropPrivileges is nop +void dropPrivileges(std::string homeDir) {} + +#else + +const char* TARGET_USER_NAME = "zerotier-one"; + +struct cap_header_struct { + __u32 version; + int pid; +}; + +struct cap_data_struct { + __u32 effective; + __u32 permitted; + __u32 inheritable; +}; + +// libc doesn't export capset, it is instead located in libcap +// We ignore libcap and call it manually. + +int capset(cap_header_struct* hdrp, cap_data_struct* datap) { + return syscall(SYS_capset, hdrp, datap); +} + +void notDropping(std::string homeDir) { + struct stat buf; + if (lstat(homeDir.c_str(), &buf) < 0) { + if (buf.st_uid != 0 || buf.st_gid != 0) { + fprintf(stderr, "ERROR: failed to drop privileges. Refusing to run as root, because %s was already used in nonprivileged mode.\n", homeDir.c_str()); + exit(1); + } + } + fprintf(stderr, "WARNING: failed to drop privileges, running as root\n"); +} + +int setCapabilities(int flags) { + cap_header_struct capheader = {_LINUX_CAPABILITY_VERSION_1, 0}; + cap_data_struct capdata; + capdata.inheritable = capdata.permitted = capdata.effective = flags; + return capset(&capheader, &capdata); +} + +void createOwnedHomedir(std::string homeDir, struct passwd* targetUser) { + struct stat buf; + if (lstat(homeDir.c_str(), &buf) < 0) { + if (errno == ENOENT) { + mkdir(homeDir.c_str(), 0755); + } else { + perror("cannot access home directory"); + exit(1); + } + } + + if (buf.st_uid != 0 || buf.st_gid != 0) { + // should be already owned by zerotier-one + if (targetUser->pw_uid != buf.st_uid) { + fprintf(stderr, "ERROR: %s not owned by zerotier-one or root\n", homeDir.c_str()); + exit(1); + } + return; + } + + // Change homedir owner to zerotier-one user. This is safe, because this directory is writable only by root, so no one could have created malicious hardlink. + long p = (long)fork(); + int exitcode = -1; + if (p > 0) { + waitpid(p, &exitcode, 0); + } else if (p == 0) { + std::string ownerString = std::to_string(targetUser->pw_uid) + ":" + std::to_string(targetUser->pw_gid); + execlp("chown", "chown", "-R", ownerString.c_str(), "--", homeDir.c_str(), NULL); + _exit(-1); + } + + if (exitcode != 0) { + fprintf(stderr, "failed to change owner of %s to %s\n", homeDir.c_str(), targetUser->pw_name); + exit(1); + } +} + +void dropPrivileges(std::string homeDir) { + // dropPrivileges switches to zerotier-one user while retaining CAP_NET_ADMIN + // and CAP_NET_RAW capabilities. + struct passwd* targetUser = getpwnam(TARGET_USER_NAME); + if (targetUser == NULL) { + // zerotier-one user not configured by package + return; + } + + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_NET_RAW, 0, 0) < 0) { + // Kernel has no support for ambient capabilities. + notDropping(homeDir); + return; + } + + if (prctl(PR_SET_SECUREBITS, SECBIT_KEEP_CAPS | SECBIT_NOROOT) < 0) { + notDropping(homeDir); + return; + } + + createOwnedHomedir(homeDir, targetUser); + + if (setCapabilities((1 << CAP_NET_ADMIN) | (1 << CAP_NET_RAW) | (1 << CAP_SETUID) | (1 << CAP_SETGID)) < 0) { + fprintf(stderr, "ERROR: failed to set capabilities (not running as real root?)\n"); + exit(1); + } + + int oldDumpable = prctl(PR_GET_DUMPABLE); + + if (prctl(PR_SET_DUMPABLE, 0) < 0) { + // Disable ptracing. Otherwise there is a small window when previous + // compromised ZeroTier process could ptrace us, when we still have CAP_SETUID. + // (this is mitigated anyway on most distros by ptrace_scope=1) + perror("prctl(PR_SET_DUMPABLE)"); + exit(1); + } + + if (setgid(targetUser->pw_gid) < 0) { + perror("setgid"); + exit(1); + } + if (setuid(targetUser->pw_uid) < 0) { + perror("setuid"); + exit(1); + } + + if (setCapabilities((1 << CAP_NET_ADMIN) | (1 << CAP_NET_RAW)) < 0) { + perror("could not drop capabilities after setuid"); + exit(1); + } + + if (prctl(PR_SET_DUMPABLE, oldDumpable) < 0) { + perror("could not restore dumpable flag"); + exit(1); + } + + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_ADMIN, 0, 0) < 0) { + perror("could not raise ambient CAP_NET_ADMIN"); + exit(1); + } + + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_RAW, 0, 0) < 0) { + perror("could not raise ambient CAP_NET_RAW"); + exit(1); + } +} + +#endif +} diff --git a/osdep/LinuxDropPrivileges.hpp b/osdep/LinuxDropPrivileges.hpp new file mode 100644 index 00000000..111f682e --- /dev/null +++ b/osdep/LinuxDropPrivileges.hpp @@ -0,0 +1,9 @@ +#ifndef ZT_LINUXDROPPRIVILEGES_HPP +#define ZT_LINUXDROPPRIVILEGES_HPP +#include <string> + +namespace ZeroTier { + void dropPrivileges(std::string homeDir); +} + +#endif |