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
This commit is contained in:
Grant Limberg 2023-07-07 16:43:32 -07:00 committed by GitHub
parent 33b2e6a856
commit 008a768f15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 129 additions and 62 deletions

View File

@ -112,18 +112,18 @@ Additional help can be found in our [knowledge base](https://zerotier.atlassian.
### 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
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
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)
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`:
@ -136,24 +136,23 @@ To configure a scrape job in Prometheus on the machine ZeroTier is running on, a
- 127.0.0.1:9993
labels:
group: zerotier-one
params:
auth:
- $YOUR_AUTHTOKEN_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:
{
"settings": {
"allowManagementFrom:" ["10.0.0.0/24"]
}
}
Substitute your actual network IP ranges as necessary.
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.
node_id: $YOUR_10_CHARACTER_NODE_ID
authorization:
credentials: $YOUR_METRICS_TOKEN_SECRET
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.
Metrics are also available on disk in ZeroTier's working directory:
// Linux
/var/lib/zerotier-one/metrics.prom
// macOS
/Library/Application\ Support/ZeroTier/One/metrics.prom
//Windows
C:\ProgramData\ZeroTier\One\metrics.prom
#### Available Metrics
| Metric Name | Labels | Metric Type | Description |

View File

@ -201,6 +201,26 @@ std::string ssoResponseTemplate = R"""(
</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
std::string dump_headers(const httplib::Headers &headers) {
std::string s;
@ -753,6 +773,7 @@ public:
const std::string _homePath;
std::string _authToken;
std::string _metricsToken;
std::string _controllerDbPath;
const std::string _networksPath;
const std::string _moonsPath;
@ -950,6 +971,26 @@ public:
_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;
cb.version = 0;
@ -1458,54 +1499,81 @@ public:
auto authCheck = [=] (const httplib::Request &req, httplib::Response &res) {
std::string r = req.remote_addr;
InetAddress remoteAddr(r.c_str());
if (req.path == "/metrics") {
bool ipAllowed = false;
bool isAuth = false;
// If localhost, allow
if (remoteAddr.ipScope() == InetAddress::IP_SCOPE_LOOPBACK) {
ipAllowed = true;
}
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;
}
}
if (!ipAllowed) {
for (auto i = _allowManagementFrom.begin(); i != _allowManagementFrom.end(); ++i) {
if (i->containsAddress(remoteAddr)) {
ipAllowed = true;
break;
}
}
}
setContent(req, res, "{}");
res.status = 401;
return httplib::Server::HandlerResponse::Handled;
} else {
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 (!ipAllowed) {
for (auto i = _allowManagementFrom.begin(); i != _allowManagementFrom.end(); ++i) {
if (i->containsAddress(remoteAddr)) {
ipAllowed = true;
break;
}
}
}
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 (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;
}
}
}
}
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;
if (ipAllowed && isAuth) {
return httplib::Server::HandlerResponse::Unhandled;
}
setContent(req, res, "{}");
res.status = 401;
return httplib::Server::HandlerResponse::Handled;
}
};