From: Remi Gacogne Date: Wed, 10 Jun 2020 06:58:35 +0000 (+0200) Subject: dnsdist: Implement an ACL in the internal web server X-Git-Tag: dnsdist-1.5.0-rc3~1^2~1 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=1c90c6bd976560b5c21e56274ea6919ad9cdf219;p=thirdparty%2Fpdns.git dnsdist: Implement an ACL in the internal web server --- diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index bdde09e1f8..8b3c2412ad 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -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 apiKey, const boost::optional > customHeaders) { + g_lua.writeFunction("webserver", [client,configCheck](const std::string& address, const std::string& password, const boost::optional apiKey, const boost::optional > customHeaders, const boost::optional 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(vars->at("acl")); + + setWebserverACL(acl); + } if(vars->count("customHeaders")) { const boost::optional > headers = boost::get >(vars->at("customHeaders")); diff --git a/pdns/dnsdist-web.cc b/pdns/dnsdist-web.cc index a49a9f9136..99a7a4aca1 100644 --- a/pdns/dnsdist-web.cc +++ b/pdns/dnsdist-web.cc @@ -19,26 +19,27 @@ * 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 -#include "threadname.hh" +#include #include -#include -#include "namespaces.hh" #include #include +#include + #include "ext/incbin/incbin.h" -#include "htmlfiles.h" +#include "ext/json11/json11.hpp" +#include + #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 +#include "htmlfiles.h" +#include "threadname.hh" +#include "sstuff.hh" bool g_apiReadWrite{false}; WebserverConfig g_webserverConfig; @@ -221,6 +222,12 @@ static bool isMethodAllowed(const YaHTTP::Request& req) return false; } +static bool isClientAllowedByACL(const ComboAddress& remote) +{ + std::lock_guard 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"); @@ -1226,6 +1233,14 @@ void setWebserverPassword(const std::string& password) g_webserverConfig.password = password; } +void setWebserverACL(const std::string& acl) +{ + std::lock_guard lock(g_webserverConfig.lock); + + g_webserverConfig.acl.clear(); + g_webserverConfig.acl.toMasks(acl); +} + void setWebserverCustomHeaders(const boost::optional > customHeaders) { std::lock_guard lock(g_webserverConfig.lock); @@ -1237,15 +1252,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()); } } diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index 949e5a08c7..8454cd6954 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -1094,19 +1094,6 @@ struct dnsheader; vector> setupLua(bool client, const std::string& config); -struct WebserverConfig -{ - std::string password; - std::string apiKey; - boost::optional > customHeaders; - std::mutex lock; -}; - -void setWebserverAPIKey(const boost::optional apiKey); -void setWebserverPassword(const std::string& password); -void setWebserverCustomHeaders(const boost::optional > customHeaders); - -void dnsdistWebserverThread(int sock, const ComboAddress& local); void tcpAcceptorThread(void* p); #ifdef HAVE_DNS_OVER_HTTPS void dohThread(ClientState* cs); diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index a66086d89b..b0ca67ad42 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -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 index 0000000000..1b5d0015f4 --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-web.hh @@ -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 > customHeaders; + std::mutex lock; +}; + +void setWebserverAPIKey(const boost::optional apiKey); +void setWebserverPassword(const std::string& password); +void setWebserverACL(const std::string& acl); +void setWebserverCustomHeaders(const boost::optional > customHeaders); + +void dnsdistWebserverThread(int sock, const ComboAddress& local); diff --git a/pdns/dnsdistdist/docs/guides/webserver.rst b/pdns/dnsdistdist/docs/guides/webserver.rst index f8ee873de6..bc0de3eafe 100755 --- a/pdns/dnsdistdist/docs/guides/webserver.rst +++ b/pdns/dnsdistdist/docs/guides/webserver.rst @@ -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. diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index a5db904eca..d6c3f7c6e6 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -296,7 +296,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. @@ -304,6 +307,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]) @@ -318,6 +322,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. @@ -327,7 +334,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 ~~~~~~~~~~~~~~~~~~~~ diff --git a/regression-tests.dnsdist/test_API.py b/regression-tests.dnsdist/test_API.py index afb5401510..d73a1f8c16 100644 --- a/regression-tests.dnsdist/test_API.py +++ b/regression-tests.dnsdist/test_API.py @@ -516,3 +516,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)