Browse Source

Add a 2nd auth token only for access to /metrics (#2043)

* Add a 2nd auth token for /metrics

Allows administrators to distribute a token that only has access to read
metrics and nothing else.

Also added support for using bearer auth tokens for both types of tokens

Separate endpoint for metrics #2041

* Update readme

* fix a couple of cases of writing the wrong token
Grant Limberg 2 years ago
parent
commit
008a768f15
2 changed files with 127 additions and 60 deletions
  1. 16 17
      README.md
  2. 111 43
      service/OneService.cpp

+ 16 - 17
README.md

@@ -112,18 +112,18 @@ Additional help can be found in our [knowledge base](https://zerotier.atlassian.
 
 
 ### Prometheus Metrics
 ### Prometheus Metrics
 
 
-Prometheus Metrics are available at the `/metrics` API endpoint.  This endpoint is protected by an API key stored in `authtoken.secret` because of the possibility of information leakage.  Information that could be gleaned from the metrics include joined networks and peers your instance is talking to. 
+Prometheus Metrics are available at the `/metrics` API endpoint.  This endpoint is protected by an API key stored in `metricstoken.secret` to prevent unwanted information leakage.  Information that could be gleaned from the metrics include joined networks and peers your instance is talking to. 
 
 
-Access control is via the ZeroTier control interface itself and `authtoken.secret`. This can be sent as the `X-ZT1-Auth` HTTP header field or appended to the URL as `?auth=<token>`. You can see the current metrics via `cURL` with the following command:
+Access control is via the ZeroTier control interface itself and `metricstoken.secret`. This can be sent as a bearer auth token, via the `X-ZT1-Auth` HTTP header field, or appended to the URL as `?auth=<token>`. You can see the current metrics via `cURL` with the following command:
 
 
     // Linux
     // Linux
-    curl -H "X-ZT1-Auth: $(sudo cat /var/lib/zerotier-one/authtoken.secret)" http://localhost:9993/metrics
+    curl -H "X-ZT1-Auth: $(sudo cat /var/lib/zerotier-one/metricstoken.secret)" http://localhost:9993/metrics
 
 
     // macOS
     // macOS
-    curl -H "X-XT1-Auth: $(sudo cat /Library/Application\ Support/ZeroTier/One/authtoken.secret)" http://localhost:9993/metrics
+    curl -H "X-XT1-Auth: $(sudo cat /Library/Application\ Support/ZeroTier/One/metricstoken.secret)" http://localhost:9993/metrics
 
 
     // Windows PowerShell (Admin)
     // Windows PowerShell (Admin)
-    Invoke-RestMethod -Headers @{'X-ZT1-Auth' = "$(Get-Content C:\ProgramData\ZeroTier\One\authtoken.secret)"; } -Uri http://localhost:9993/metrics
+    Invoke-RestMethod -Headers @{'X-ZT1-Auth' = "$(Get-Content C:\ProgramData\ZeroTier\One\metricstoken.secret)"; } -Uri http://localhost:9993/metrics
 
 
 To configure a scrape job in Prometheus on the machine ZeroTier is running on, add this to your Prometheus `scrape_config`:
 To configure a scrape job in Prometheus on the machine ZeroTier is running on, add this to your Prometheus `scrape_config`:
 
 
@@ -136,23 +136,22 @@ To configure a scrape job in Prometheus on the machine ZeroTier is running on, a
         - 127.0.0.1:9993
         - 127.0.0.1:9993
         labels:
         labels:
           group: zerotier-one
           group: zerotier-one
-      params:
-        auth:
-        - $YOUR_AUTHTOKEN_SECRET
+          node_id: $YOUR_10_CHARACTER_NODE_ID
+      authorization:
+        credentials: $YOUR_METRICS_TOKEN_SECRET
 
 
-If your Prometheus instance is remote from the machine ZeroTier instance, you'll have to edit your `local.conf` file to allow remote access to the API control port.  If your local lan is `10.0.0.0/24`, edit your `local.conf` as follows:
+If neither of these methods are desirable, it is probably possible to distribute metrics via [Prometheus Proxy](https://github.com/pambrose/prometheus-proxy) or some other tool.  Note: We have not tested this internally, but will probably work with the correct configuration.
 
 
-    {
-      "settings": {
-        "allowManagementFrom:" ["10.0.0.0/24"]
-      }
-    }
+Metrics are also available on disk in ZeroTier's working directory:
 
 
-Substitute your actual network IP ranges as necessary.
+   // Linux
+   /var/lib/zerotier-one/metrics.prom
 
 
-It's also possible to access the metrics & control port over the ZeroTier network itself via the same method shown above.  Just add the address range of your ZeroTier network to the list. NOTE: Using this method means that anyone with your auth token can control your ZeroTier instance, including leaving & joining other networks.
+   // macOS
+   /Library/Application\ Support/ZeroTier/One/metrics.prom
 
 
-If neither of these methods are desirable, it is probably possible to distribute metrics via [Prometheus Proxy](https://github.com/pambrose/prometheus-proxy) or some other tool.  Note: We have not tested this internally, but will probably work with the correct configuration.
+   //Windows
+   C:\ProgramData\ZeroTier\One\metrics.prom
 
 
 #### Available Metrics
 #### Available Metrics
 
 

+ 111 - 43
service/OneService.cpp

@@ -201,6 +201,26 @@ std::string ssoResponseTemplate = R"""(
 </html>
 </html>
 )""";
 )""";
 
 
+bool bearerTokenValid(const std::string authHeader, const std::string &checkToken) {
+	std::vector<std::string> tokens = OSUtils::split(authHeader.c_str(), " ", NULL, NULL);
+	if (tokens.size() != 2) {
+		return false;
+	}
+
+	std::string bearer = tokens[0];
+	std::string token = tokens[1];
+	std::transform(bearer.begin(), bearer.end(), bearer.begin(), [](unsigned char c){return std::tolower(c);});
+	if (bearer != "bearer") {
+		return false;
+	}
+
+	if (token != checkToken) {
+		return false;
+	}
+
+	return true;
+}
+
 #if ZT_DEBUG==1
 #if ZT_DEBUG==1
 std::string dump_headers(const httplib::Headers &headers) {
 std::string dump_headers(const httplib::Headers &headers) {
   std::string s;
   std::string s;
@@ -753,6 +773,7 @@ public:
 
 
 	const std::string _homePath;
 	const std::string _homePath;
 	std::string _authToken;
 	std::string _authToken;
+	std::string _metricsToken;
 	std::string _controllerDbPath;
 	std::string _controllerDbPath;
 	const std::string _networksPath;
 	const std::string _networksPath;
 	const std::string _moonsPath;
 	const std::string _moonsPath;
@@ -950,6 +971,26 @@ public:
 				_authToken = _trimString(_authToken);
 				_authToken = _trimString(_authToken);
 			}
 			}
 
 
+			{
+				const std::string metricsTokenPath(_homePath + ZT_PATH_SEPARATOR_S "metricstoken.secret");
+				if (!OSUtils::readFile(metricsTokenPath.c_str(),_metricsToken)) {
+					unsigned char foo[24];
+					Utils::getSecureRandom(foo,sizeof(foo));
+					_metricsToken = "";
+					for(unsigned int i=0;i<sizeof(foo);++i)
+						_metricsToken.push_back("abcdefghijklmnopqrstuvwxyz0123456789"[(unsigned long)foo[i] % 36]);
+					if (!OSUtils::writeFile(metricsTokenPath.c_str(),_metricsToken)) {
+						Mutex::Lock _l(_termReason_m);
+						_termReason = ONE_UNRECOVERABLE_ERROR;
+						_fatalErrorMessage = "metricstoken.secret could not be written";
+						return _termReason;
+					} else {
+						OSUtils::lockDownFile(metricsTokenPath.c_str(),false);
+					}
+				}
+				_metricsToken = _trimString(_metricsToken);
+			}
+
 			{
 			{
 				struct ZT_Node_Callbacks cb;
 				struct ZT_Node_Callbacks cb;
 				cb.version = 0;
 				cb.version = 0;
@@ -1458,54 +1499,81 @@ public:
 
 
 
 
         auto authCheck = [=] (const httplib::Request &req, httplib::Response &res) {
         auto authCheck = [=] (const httplib::Request &req, httplib::Response &res) {
-            std::string r = req.remote_addr;
-            InetAddress remoteAddr(r.c_str());
-
-            bool ipAllowed = false;
-            bool isAuth = false;
-            // If localhost, allow
-            if (remoteAddr.ipScope() == InetAddress::IP_SCOPE_LOOPBACK) {
-                ipAllowed = true;
-            }
+			if (req.path == "/metrics") {
 
 
-            if (!ipAllowed) {
-                for (auto i = _allowManagementFrom.begin(); i != _allowManagementFrom.end(); ++i) {
-                    if (i->containsAddress(remoteAddr)) {
-                        ipAllowed  = true;
-                        break;
-                    }
-                }
-            }
+				if (req.has_header("x-zt1-auth")) {
+					std::string token = req.get_header_value("x-zt1-auth");
+					if (token == _metricsToken || token == _authToken) {
+						return httplib::Server::HandlerResponse::Unhandled;
+					}
+				} else if (req.has_param("auth")) {
+					std::string token = req.get_param_value("auth");
+					if (token == _metricsToken || token == _authToken) {
+						return httplib::Server::HandlerResponse::Unhandled;
+					}
+				} else if (req.has_header("authorization")) {
+					std::string auth = req.get_header_value("authorization");
+					if (bearerTokenValid(auth, _metricsToken) || bearerTokenValid(auth, _authToken)) {
+						return httplib::Server::HandlerResponse::Unhandled;
+					}
+				}
 
 
+				setContent(req, res, "{}");
+				res.status = 401;
+				return httplib::Server::HandlerResponse::Handled;
+			} else {
+				std::string r = req.remote_addr;
+				InetAddress remoteAddr(r.c_str());
 
 
-            if (ipAllowed) {
-                // auto-pass endpoints in `noAuthEndpoints`.  No auth token required
-                if (std::find(noAuthEndpoints.begin(), noAuthEndpoints.end(), req.path) != noAuthEndpoints.end()) {
-                    isAuth = true;
-                }
+				bool ipAllowed = false;
+				bool isAuth = false;
+				// If localhost, allow
+				if (remoteAddr.ipScope() == InetAddress::IP_SCOPE_LOOPBACK) {
+					ipAllowed = true;
+				}
 
 
-                if (!isAuth) {
-                    // check auth token
-                    if (req.has_header("x-zt1-auth")) {
-                        std::string token = req.get_header_value("x-zt1-auth");
-                        if (token == _authToken) {
-                            isAuth = true;
-                        }
-                    } else if (req.has_param("auth")) {
-                        std::string token = req.get_param_value("auth");
-                        if (token == _authToken) {
-                            isAuth = true;
-                        }
-                    }
-                }
-            }
+				if (!ipAllowed) {
+					for (auto i = _allowManagementFrom.begin(); i != _allowManagementFrom.end(); ++i) {
+						if (i->containsAddress(remoteAddr)) {
+							ipAllowed  = true;
+							break;
+						}
+					}
+				}
 
 
-            if (ipAllowed && isAuth) {
-                return httplib::Server::HandlerResponse::Unhandled;
-            }
-			setContent(req, res, "{}");
-            res.status = 401;
-            return httplib::Server::HandlerResponse::Handled;
+
+				if (ipAllowed) {
+					// auto-pass endpoints in `noAuthEndpoints`.  No auth token required
+					if (std::find(noAuthEndpoints.begin(), noAuthEndpoints.end(), req.path) != noAuthEndpoints.end()) {
+						isAuth = true;
+					}
+
+					if (!isAuth) {
+						// check auth token
+						if (req.has_header("x-zt1-auth")) {
+							std::string token = req.get_header_value("x-zt1-auth");
+							if (token == _authToken) {
+								isAuth = true;
+							}
+						} else if (req.has_param("auth")) {
+							std::string token = req.get_param_value("auth");
+							if (token == _authToken) {
+								isAuth = true;
+							}
+						} else if (req.has_header("authorization")) {
+							std::string auth = req.get_header_value("authorization");
+							isAuth = bearerTokenValid(auth, _authToken);
+						}
+					}
+				}
+
+				if (ipAllowed && isAuth) {
+					return httplib::Server::HandlerResponse::Unhandled;
+				}
+				setContent(req, res, "{}");
+				res.status = 401;
+				return httplib::Server::HandlerResponse::Handled;
+			}
         };
         };