]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Merge pull request #9229 from rgacogne/dnsdist-webserver-allow-from
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 17 Jun 2020 09:09:20 +0000 (11:09 +0200)
committerGitHub <noreply@github.com>
Wed, 17 Jun 2020 09:09:20 +0000 (11:09 +0200)
dnsdist: Implement an ACL in the internal web server

pdns/dnsdist-lua.cc
pdns/dnsdist-web.cc
pdns/dnsdist.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-web.hh [new file with mode: 0644]
pdns/dnsdistdist/docs/guides/webserver.rst
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_API.py

index 4077d9b75bee3f220ab193aea855bcb15556a752..39d2e595ad94d6206e3447098856f7e5ef3ef8a7 100644 (file)
@@ -41,6 +41,7 @@
 #endif /* LUAJIT_VERSION */
 #include "dnsdist-rings.hh"
 #include "dnsdist-secpoll.hh"
+#include "dnsdist-web.hh"
 
 #include "base64.hh"
 #include "dnswriter.hh"
@@ -796,7 +797,7 @@ static void setupLuaConfig(bool client, bool configCheck)
       g_carbon.setState(ours);
   });
 
-  g_lua.writeFunction("webserver", [client,configCheck](const std::string& address, const std::string& password, const boost::optional<std::string> apiKey, const boost::optional<std::map<std::string, std::string> > customHeaders) {
+  g_lua.writeFunction("webserver", [client,configCheck](const std::string& address, const std::string& password, const boost::optional<std::string> apiKey, const boost::optional<std::map<std::string, std::string> > customHeaders, const boost::optional<std::string> acl) {
       setLuaSideEffect();
       ComboAddress local;
       try {
@@ -815,10 +816,13 @@ static void setupLuaConfig(bool client, bool configCheck)
        SSetsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
        SBind(sock, local);
        SListen(sock, 5);
-       auto launch=[sock, local, password, apiKey, customHeaders]() {
+       auto launch=[sock, local, password, apiKey, customHeaders, acl]() {
           setWebserverPassword(password);
           setWebserverAPIKey(apiKey);
           setWebserverCustomHeaders(customHeaders);
+          if (acl) {
+            setWebserverACL(*acl);
+          }
           thread t(dnsdistWebserverThread, sock, local);
          t.detach();
        };
@@ -852,6 +856,11 @@ static void setupLuaConfig(bool client, bool configCheck)
 
         setWebserverAPIKey(apiKey);
       }
+      if (vars->count("acl")) {
+        const std::string acl = boost::get<std::string>(vars->at("acl"));
+
+        setWebserverACL(acl);
+      }
       if(vars->count("customHeaders")) {
         const boost::optional<std::map<std::string, std::string> > headers = boost::get<std::map<std::string, std::string> >(vars->at("customHeaders"));
 
index 4766403924aa92b3274eab188bc4cbcf1fac2c4e..5a738ca0ac5af60a616b91e14943e25deca45f8e 100644 (file)
  * along with this program; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
-#include "dnsdist.hh"
-#include "dnsdist-healthchecks.hh"
-#include "dnsdist-prometheus.hh"
 
-#include "sstuff.hh"
-#include "ext/json11/json11.hpp"
-#include "ext/incbin/incbin.h"
-#include "dolog.hh"
-#include <thread>
-#include "threadname.hh"
+#include <boost/format.hpp>
 #include <sstream>
-#include <yahttp/yahttp.hpp>
-#include "namespaces.hh"
 #include <sys/time.h>
 #include <sys/resource.h>
+#include <thread>
+
 #include "ext/incbin/incbin.h"
-#include "htmlfiles.h"
+#include "ext/json11/json11.hpp"
+#include <yahttp/yahttp.hpp>
+
 #include "base64.hh"
+#include "dnsdist.hh"
+#include "dnsdist-healthchecks.hh"
+#include "dnsdist-prometheus.hh"
+#include "dnsdist-web.hh"
+#include "dolog.hh"
 #include "gettime.hh"
-#include  <boost/format.hpp>
+#include "htmlfiles.h"
+#include "threadname.hh"
+#include "sstuff.hh"
 
 bool g_apiReadWrite{false};
 WebserverConfig g_webserverConfig;
@@ -223,6 +224,12 @@ static bool isMethodAllowed(const YaHTTP::Request& req)
   return false;
 }
 
+static bool isClientAllowedByACL(const ComboAddress& remote)
+{
+  std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
+  return g_webserverConfig.acl.match(remote);
+}
+
 static void handleCORS(const YaHTTP::Request& req, YaHTTP::Response& resp)
 {
   const auto origin = req.headers.find("Origin");
@@ -1228,6 +1235,17 @@ void setWebserverPassword(const std::string& password)
   g_webserverConfig.password = password;
 }
 
+void setWebserverACL(const std::string& acl)
+{
+  NetmaskGroup newACL;
+  newACL.toMasks(acl);
+
+  {
+    std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
+    g_webserverConfig.acl = std::move(newACL);
+  }
+}
+
 void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders)
 {
   std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
@@ -1239,15 +1257,21 @@ void dnsdistWebserverThread(int sock, const ComboAddress& local)
 {
   setThreadName("dnsdist/webserv");
   warnlog("Webserver launched on %s", local.toStringWithPort());
+
   for(;;) {
     try {
       ComboAddress remote(local);
       int fd = SAccept(sock, remote);
-      vinfolog("Got connection from %s", remote.toStringWithPort());
+      if (!isClientAllowedByACL(remote)) {
+        vinfolog("Connection to webserver from client %s is not allowed, closing", remote.toStringWithPort());
+        close(fd);
+        continue;
+      }
+      vinfolog("Got a connection to the webserver from %s", remote.toStringWithPort());
       std::thread t(connectionThread, fd, remote);
       t.detach();
     }
-    catch(std::exception& e) {
+    catch (const std::exception& e) {
       errlog("Had an error accepting new webserver connection: %s", e.what());
     }
   }
index 1b532df341e329ee1a9744269ecaf7bb0e665d6b..1ffb3aeb7454981277997cba7aae49cc897a24f6 100644 (file)
@@ -1098,19 +1098,6 @@ struct dnsheader;
 
 vector<std::function<void(void)>> setupLua(bool client, const std::string& config);
 
-struct WebserverConfig
-{
-  std::string password;
-  std::string apiKey;
-  boost::optional<std::map<std::string, std::string> > customHeaders;
-  std::mutex lock;
-};
-
-void setWebserverAPIKey(const boost::optional<std::string> apiKey);
-void setWebserverPassword(const std::string& password);
-void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders);
-
-void dnsdistWebserverThread(int sock, const ComboAddress& local);
 void tcpAcceptorThread(void* p);
 #ifdef HAVE_DNS_OVER_HTTPS
 void dohThread(ClientState* cs);
index a66086d89b4dc122618b3cc10b9d2b90fcef1d4c..b0ca67ad4245007a82776c7e56936db21831415e 100644 (file)
@@ -159,7 +159,7 @@ dnsdist_SOURCES = \
        dnsdist-snmp.cc dnsdist-snmp.hh \
        dnsdist-systemd.cc dnsdist-systemd.hh \
        dnsdist-tcp.cc \
-       dnsdist-web.cc \
+       dnsdist-web.cc dnsdist-web.hh \
        dnsdist-xpf.cc dnsdist-xpf.hh \
        dnslabeltext.cc \
        dnsname.cc dnsname.hh \
diff --git a/pdns/dnsdistdist/dnsdist-web.hh b/pdns/dnsdistdist/dnsdist-web.hh
new file mode 100644 (file)
index 0000000..1b5d001
--- /dev/null
@@ -0,0 +1,22 @@
+#pragma once
+
+struct WebserverConfig
+{
+  WebserverConfig()
+  {
+    acl.toMasks("127.0.0.1, ::1");
+  }
+
+  NetmaskGroup acl;
+  std::string password;
+  std::string apiKey;
+  boost::optional<std::map<std::string, std::string> > customHeaders;
+  std::mutex lock;
+};
+
+void setWebserverAPIKey(const boost::optional<std::string> apiKey);
+void setWebserverPassword(const std::string& password);
+void setWebserverACL(const std::string& acl);
+void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders);
+
+void dnsdistWebserverThread(int sock, const ComboAddress& local);
index f8ee873de652cae025ddc7f4770741629e5c72fb..bc0de3eafe7f2be848c2cac2fbe8b7ea5c7c086d 100755 (executable)
@@ -9,6 +9,13 @@ To visually interact with dnsdist, try add :func:`webserver` to the configuratio
 
 Now point your browser at http://127.0.0.1:8083 and log in with any username, and that password. Enjoy!
 
+Since 1.5.0, only connections from 127.0.0.1 and ::1 are allowed by default. To allow connections from 192.0.2.1, instead:
+
+.. code-block:: lua
+
+  webserver("127.0.0.1:8083", "supersecretpassword", "supersecretAPIkey", {}, "192.0.2.1")
+
+
 Security of the Webserver
 -------------------------
 
@@ -27,7 +34,7 @@ For example, to remove the X-Frame-Options header and add a X-Custom one:
 
 .. code-block:: lua
 
-  webserver("127.0.0.1:8080", "supersecret", "apikey", {["X-Frame-Options"]= "", ["X-Custom"]="custom"}
+  webserver("127.0.0.1:8080", "supersecret", "apikey", {["X-Frame-Options"]= "", ["X-Custom"]="custom"})
 
 Credentials can be changed over time using the :func:`setWebserverConfig` function.
 
index 9f6c8a73e94e1441eb11c1bc56bee3c300845ea8..82fa5d9a30eac5b535c6dafe4bf6862941dc8776 100644 (file)
@@ -297,7 +297,10 @@ Control Socket, Console and Webserver
 Webserver configuration
 ~~~~~~~~~~~~~~~~~~~~~~~
 
-.. function:: webserver(listen_address, password[, apikey[, custom_headers]])
+.. function:: webserver(listen_address, password[, apikey[, custom_headers[, acl]]])
+
+  .. versionchanged:: 1.5.0
+    ``acl`` optional parameter added.
 
   Launch the :doc:`../guides/webserver` with statistics and the API.
 
@@ -305,6 +308,7 @@ Webserver configuration
   :param str password: The password required to access the webserver
   :param str apikey: The key required to access the API
   :param {[str]=str,...} custom_headers: Allows setting custom headers and removing the defaults
+  :param str acl: 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".
 
 .. function:: setAPIWritable(allow [,dir])
 
@@ -319,6 +323,9 @@ Webserver configuration
 
   .. versionadded:: 1.3.3
 
+  .. versionchanged:: 1.5.0
+    ``acl`` optional parameter added.
+
   Setup webserver configuration. See :func:`webserver`.
 
   :param table options: A table with key: value pairs with webserver options.
@@ -328,7 +335,8 @@ Webserver configuration
   * ``password=newPassword``: string - Changes the API password
   * ``apiKey=newKey``: string - Changes the API Key (set to an empty string do disable it)
   * ``custom_headers={[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".
+
 Access Control Lists
 ~~~~~~~~~~~~~~~~~~~~
 
index bfabe5c61552e145306cdd9ac2ad609a6a37b2bf..b894323cb4cd59d71272ae91beb0278a455aceb9 100644 (file)
@@ -517,3 +517,39 @@ class TestAPIAuth(DNSDistTest):
 
         r = requests.get(url, headers=headers, timeout=self._webTimeout)
         self.assertEquals(r.status_code, 401)
+
+class TestAPIACL(DNSDistTest):
+
+    _webTimeout = 2.0
+    _webServerPort = 8083
+    _webServerBasicAuthPassword = 'secret'
+    _webServerAPIKey = 'apisecret'
+    _consoleKey = DNSDistTest.generateConsoleKey()
+    _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+    _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
+    _config_template = """
+    setKey("%s")
+    controlSocket("127.0.0.1:%s")
+    setACL({"127.0.0.1/32", "::1/128"})
+    newServer{address="127.0.0.1:%s"}
+    webserver("127.0.0.1:%s", "%s", "%s", {}, "192.0.2.1")
+    """
+
+    def testACLChange(self):
+        """
+        API: Should be denied by ACL then allowed
+        """
+
+        url = 'http://127.0.0.1:' + str(self._webServerPort) + "/"
+        try:
+            r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
+            self.assertTrue(False)
+        except requests.exceptions.ConnectionError as exp:
+            pass
+
+        # reset the ACL
+        self.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
+
+        r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
+        self.assertTrue(r)
+        self.assertEquals(r.status_code, 200)