Browse Source

Run as nonroot user on Linux (with CAP_NET_ADMIN and CAP_NET_RAW added).

- ZT will only drop root privileges if zerotier-one user exists. It is created by
Debian postinst script - in other cases the user has to be created by administrator.
- Linux >=4.3 with ambient capabilities is required, otherwise ZT will silently
- "-U" option now also disables privileges dropping
Michał Zieliński 8 years ago
parent
commit
344a25c133
5 changed files with 197 additions and 3 deletions
  1. 9 0
      debian/postinst
  2. 2 2
      make-linux.mk
  3. 13 1
      one.cpp
  4. 164 0
      osdep/LinuxDropPrivileges.cpp
  5. 9 0
      osdep/LinuxDropPrivileges.hpp

+ 9 - 0
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#

+ 2 - 2
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

+ 13 - 1
one.cpp

@@ -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

+ 164 - 0
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
+}

+ 9 - 0
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