mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-01-18 18:56:24 +00:00
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:
parent
33b2e6a856
commit
008a768f15
39
README.md
39
README.md
@ -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 |
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user