]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add an option for unauthenticated access to the dashboard
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 25 Jan 2023 15:29:51 +0000 (16:29 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 25 Jan 2023 15:29:51 +0000 (16:29 +0100)
pdns/dnsdist-lua.cc
pdns/dnsdist-web.cc
pdns/dnsdistdist/dnsdist-web.hh
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_API.py

index c7d65cbb4d5cfe537990f3d5223b16ca3cdb3e67..2df956334d1850f9bd95bdec694ed3b1e423d358 100644 (file)
@@ -1190,6 +1190,10 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
       setWebserverAPIRequiresAuthentication(boost::get<bool>(vars->at("apiRequiresAuthentication")));
     }
 
+    if (vars->count("dashboardRequiresAuthentication")) {
+      setWebserverDashboardRequiresAuthentication(boost::get<bool>(vars->at("dashboardRequiresAuthentication")));
+    }
+
     if (vars->count("maxConcurrentConnections")) {
       setWebserverMaxConcurrentConnections(std::stoi(boost::get<std::string>(vars->at("maxConcurrentConnections"))));
     }
index c85a52a8ee9a339023b8c3b0ac9c8d6fc905e526..91ec2ba0a7e0cf2b27493477292eea6e2ba98a82 100644 (file)
@@ -53,6 +53,7 @@ struct WebserverConfig
   std::unique_ptr<CredentialsHolder> apiKey;
   boost::optional<std::unordered_map<std::string, std::string> > customHeaders;
   bool apiRequiresAuthentication{true};
+  bool dashboardRequiresAuthentication{true};
   bool statsRequireAuthentication{true};
 };
 
@@ -81,6 +82,7 @@ std::string getWebserverConfig()
       out << "None" << endl;
     }
     out << "API requires authentication: " << (config->apiRequiresAuthentication ? "yes" : "no") << endl;
+    out << "Dashboard requires authentication: " << (config->dashboardRequiresAuthentication ? "yes" : "no") << endl;
     out << "Statistics require authentication: " << (config->statsRequireAuthentication ? "yes" : "no") << endl;
     out << "Password: " << (config->password ? "set" : "unset") << endl;
     out << "API key: " << (config->apiKey ? "set" : "unset") << endl;
@@ -275,8 +277,12 @@ static bool checkAPIKey(const YaHTTP::Request& req, const std::unique_ptr<Creden
   return false;
 }
 
-static bool checkWebPassword(const YaHTTP::Request& req, const std::unique_ptr<CredentialsHolder>& password)
+static bool checkWebPassword(const YaHTTP::Request& req, const std::unique_ptr<CredentialsHolder>& password, bool dashboardRequiresAuthentication)
 {
+  if (!dashboardRequiresAuthentication) {
+    return true;
+  }
+
   static const char basicStr[] = "basic ";
 
   const auto header = req.headers.find("authorization");
@@ -323,7 +329,7 @@ static bool handleAuthorization(const YaHTTP::Request& req)
   if (isAStatsRequest(req)) {
     if (config->statsRequireAuthentication) {
       /* Access to the stats is allowed for both API and Web users */
-      return checkAPIKey(req, config->apiKey) || checkWebPassword(req, config->password);
+      return checkAPIKey(req, config->apiKey) || checkWebPassword(req, config->password, config->dashboardRequiresAuthentication);
     }
     return true;
   }
@@ -334,10 +340,10 @@ static bool handleAuthorization(const YaHTTP::Request& req)
       return true;
     }
 
-    return isAnAPIRequestAllowedWithWebAuth(req) && checkWebPassword(req, config->password);
+    return isAnAPIRequestAllowedWithWebAuth(req) && checkWebPassword(req, config->password, config->dashboardRequiresAuthentication);
   }
 
-  return checkWebPassword(req, config->password);
+  return checkWebPassword(req, config->password, config->dashboardRequiresAuthentication);
 }
 
 static bool isMethodAllowed(const YaHTTP::Request& req)
@@ -1586,7 +1592,7 @@ static void connectionThread(WebClientConnection&& conn)
     else if (!handleAuthorization(req)) {
       YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
       if (header != req.headers.end()) {
-        errlog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, conn.getClient().toStringWithPort());
+        vinfolog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, conn.getClient().toStringWithPort());
       }
       resp.status = 401;
       resp.body = "<h1>Unauthorized</h1>";
@@ -1660,6 +1666,11 @@ void setWebserverAPIRequiresAuthentication(bool require)
   g_webserverConfig.lock()->apiRequiresAuthentication = require;
 }
 
+void setWebserverDashboardRequiresAuthentication(bool require)
+{
+  g_webserverConfig.lock()->dashboardRequiresAuthentication = require;
+}
+
 void setWebserverMaxConcurrentConnections(size_t max)
 {
   s_connManager.setMaxConcurrentConnections(max);
@@ -1668,13 +1679,16 @@ void setWebserverMaxConcurrentConnections(size_t max)
 void dnsdistWebserverThread(int sock, const ComboAddress& local)
 {
   setThreadName("dnsdist/webserv");
-  warnlog("Webserver launched on %s", local.toStringWithPort());
+  infolog("Webserver launched on %s", local.toStringWithPort());
 
-  if (!g_webserverConfig.lock()->password) {
-    warnlog("Webserver launched on %s without a password set!", local.toStringWithPort());
+  {
+    auto config = g_webserverConfig.lock();
+    if (!config->password && config->dashboardRequiresAuthentication) {
+      warnlog("Webserver launched on %s without a password set!", local.toStringWithPort());
+    }
   }
 
-  for(;;) {
+  for (;;) {
     try {
       ComboAddress remote(local);
       int fd = SAccept(sock, remote);
index fde00de6c9f6aa1bf0b80be9de7e68819a57e44f..0a8a0bcab9a1f348c360f6eb4f9fe5a82e736a72 100644 (file)
@@ -7,6 +7,7 @@ void setWebserverPassword(std::unique_ptr<CredentialsHolder>&& password);
 void setWebserverACL(const std::string& acl);
 void setWebserverCustomHeaders(const boost::optional<std::unordered_map<std::string, std::string> > customHeaders);
 void setWebserverAPIRequiresAuthentication(bool);
+void setWebserverDashboardRequiresAuthentication(bool);
 void setWebserverStatsRequireAuthentication(bool);
 void setWebserverMaxConcurrentConnections(size_t);
 
index 65ca67cedd1e23bf93dd209e58e80ff641e8cb75..506b7663d6af64175a25ff160815bd64847656a2 100644 (file)
@@ -365,8 +365,8 @@ Webserver configuration
     The optional ``password`` and ``apiKey`` parameters now accept hashed passwords.
     The optional ``hashPlaintextCredentials`` parameter has been added.
 
-  .. versionchanged:: 1.6.0
-    ``apiRequiresAuthentication`` optional parameters added.
+  .. versionchanged:: 1.8.0
+    ``apiRequiresAuthentication``, ``dashboardRequiresAuthentication`` optional parameters added.
 
   Setup webserver configuration. See :func:`webserver`.
 
@@ -379,6 +379,7 @@ Webserver configuration
   * ``customHeaders={[str]=str,...}``: map of string - Allows setting custom headers and removing the defaults.
   * ``acl=newACL``: string - List of IP addresses, as a string, that are allowed to open a connection to the web server. Defaults to "127.0.0.1, ::1".
   * ``apiRequiresAuthentication``: bool - Whether access to the API (/api endpoints) require a valid API key. Defaults to true.
+  * ``dashbpardRequiresAuthentication``: bool - Whether access to the internal dashboard requires a valid password. Defaults to true.
   * ``statsRequireAuthentication``: bool - Whether access to the statistics (/metrics and /jsonstat endpoints) require a valid password or API key. Defaults to true.
   * ``maxConcurrentConnections``: int - The maximum number of concurrent web connections, or 0 which means an unlimited number. Defaults to 100.
   * ``hashPlaintextCredentials``: bool - Whether passwords and API keys provided in plaintext should be hashed during startup, to prevent the plaintext versions from staying in memory. Doing so increases significantly the cost of verifying credentials. Defaults to false.
index 68526a7cb1516b0ac0a61f77c67c771195f87080..bf8684183bb708ddb5e26629f22126414cfbdd9b 100644 (file)
@@ -677,6 +677,30 @@ class TestAPIWithoutAuthentication(APITestsBase):
             self.assertTrue(r)
             self.assertEqual(r.status_code, 200)
 
+class TestDashboardWithoutAuthentication(APITestsBase):
+    __test__ = True
+    _basicPath = '/'
+    _config_params = ['_testServerPort', '_webServerPort']
+    _config_template = """
+    setACL({"127.0.0.1/32", "::1/128"})
+    newServer({address="127.0.0.1:%d"})
+    webserver("127.0.0.1:%d")
+    setWebserverConfig({ dashboardRequiresAuthentication=false })
+    """
+    _verboseMode=True
+
+    def testDashboard(self):
+        """
+        API: Dashboard do not require authentication
+        """
+
+        for path in [self._basicPath]:
+            url = 'http://127.0.0.1:' + str(self._webServerPort) + path
+
+            r = requests.get(url, timeout=self._webTimeout)
+            self.assertTrue(r)
+            self.assertEqual(r.status_code, 200)
+
 class TestCustomLuaEndpoint(APITestsBase):
     __test__ = True
     _config_template = """