--- /dev/null
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#include "config.h"
+
+#include <stdexcept>
+
+#ifdef HAVE_LIBSODIUM
+#include <sodium.h>
+#endif
+
+#include "credentials.hh"
+
+std::string hashPassword(const std::string& password)
+{
+#ifdef HAVE_LIBSODIUM
+ std::string result;
+ result.resize(crypto_pwhash_STRBYTES);
+ sodium_mlock(result.data(), result.size());
+
+ int res = crypto_pwhash_str(const_cast<char*>(result.c_str()),
+ password.c_str(),
+ password.size(),
+ crypto_pwhash_OPSLIMIT_INTERACTIVE,
+ crypto_pwhash_MEMLIMIT_INTERACTIVE);
+ if (res != 0) {
+ throw std::runtime_error("Error while hashing the supplied password");
+ }
+
+ return result;
+#else
+ throw std::runtime_error("Hashing a password requires libsodium support, and it is not available");
+#endif
+}
+
+bool verifyPassword(const std::string& hash, const std::string& password)
+{
+#ifdef HAVE_LIBSODIUM
+ if (hash.size() > crypto_pwhash_STRBYTES) {
+ throw std::runtime_error("Invalid password hash supplied for verification, size is " + std::to_string(hash.size()) + ", expected at most " + std::to_string(crypto_pwhash_STRBYTES));
+ }
+
+ return crypto_pwhash_str_verify(hash.c_str(),
+ password.c_str(),
+ password.size()) == 0;
+#else
+ throw std::runtime_error("Verifying a hashed password requires libsodium support, and it is not available");
+#endif
+}
+
+bool isPasswordHashed(const std::string& password)
+{
+#ifdef HAVE_LIBSODIUM
+ if (password.size() > crypto_pwhash_STRBYTES) {
+ return false;
+ }
+
+ int res = crypto_pwhash_str_needs_rehash(password.c_str(),
+ crypto_pwhash_OPSLIMIT_INTERACTIVE,
+ crypto_pwhash_MEMLIMIT_INTERACTIVE);
+
+ if (res == -1) {
+ return false;
+ }
+ /* 1 means a rehashing is needed (different parameters), 0 is fine.
+ Either way this is a valid hash */
+ return true;
+#else
+ return false;
+#endif
+}
+
+/* if the password is in cleartext and hashing is available,
+ the hashed form will be kept in memory */
+CredentialsHolder::CredentialsHolder(std::string&& password)
+{
+ bool locked = false;
+
+ if (isHashingAvailable()) {
+ d_hashed = true;
+
+ if (!isPasswordHashed(password)) {
+ d_credentials = hashPassword(password);
+ locked = true;
+ }
+ else {
+ d_credentials = std::move(password);
+ }
+ }
+ else {
+ d_credentials = std::move(password);
+ }
+
+ if (!locked) {
+#ifdef HAVE_LIBSODIUM
+ sodium_mlock(d_credentials.data(), d_credentials.size());
+#endif
+ }
+
+}
+
+CredentialsHolder::~CredentialsHolder()
+{
+#ifdef HAVE_LIBSODIUM
+ sodium_munlock(d_credentials.data(), d_credentials.size());
+#endif
+}
+
+bool CredentialsHolder::matches(const std::string& password) const
+{
+ if (d_hashed) {
+ return verifyPassword(d_credentials, password);
+ }
+ else {
+#warning FIXME: would be better to do a poor-man hashing using burtle and a random seed first
+ return password == d_credentials;
+ }
+}
+
+bool CredentialsHolder::isHashingAvailable()
+{
+#ifdef HAVE_LIBSODIUM
+ return true;
+#else
+ return false;
+#endif
+}
+
--- /dev/null
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#pragma once
+
+#include <string>
+
+std::string hashPassword(const std::string& password);
+bool verifyPassword(const std::string& hash, const std::string& password);
+bool isPasswordHashed(const std::string& password);
+
+class CredentialsHolder
+{
+public:
+ /* if the password is in cleartext and hashing is available,
+ the hashed form will be kept in memory */
+ CredentialsHolder(std::string&& password);
+ ~CredentialsHolder();
+
+ CredentialsHolder(const CredentialsHolder&) = delete;
+ CredentialsHolder& operator=(const CredentialsHolder&) = delete;
+
+ bool matches(const std::string& password) const;
+ bool isHashed() const
+ {
+ return d_hashed;
+ }
+
+ static bool isHashingAvailable();
+
+private:
+ std::string d_credentials;
+ bool d_hashed{false};
+};
{ "setUDPMultipleMessagesVectorSize", true, "n", "set the size of the vector passed to recvmmsg() to receive UDP messages. Default to 1 which means that the feature is disabled and recvmsg() is used instead" },
{ "setUDPTimeout", true, "n", "set the maximum time dnsdist will wait for a response from a backend over UDP, in seconds" },
{ "setVerboseHealthChecks", true, "bool", "set whether health check errors will be logged" },
- { "setWebserverConfig", true, "[{hashedPassword=string, apiKey=string, customHeaders, statsRequireAuthentication}]", "Updates webserver configuration" },
+ { "setWebserverConfig", true, "[{password=string, apiKey=string, customHeaders, statsRequireAuthentication}]", "Updates webserver configuration" },
{ "setWeightedBalancingFactor", true, "factor", "Set the balancing factor for bounded-load weighted policies (whashed, wrandom)" },
{ "setWHashedPertubation", true, "value", "Set the hash perturbation value to be used in the whashed policy instead of a random one, allowing to have consistent whashed results on different instance" },
{ "show", true, "string", "outputs `string`" },
g_carbon.setState(ours);
});
- luaCtx.writeFunction("webserver", [client,configCheck](const std::string& address, const boost::optional<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) {
+ luaCtx.writeFunction("webserver", [client,configCheck](const std::string& address, boost::optional<std::string> password, 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 {
SListen(sock, 5);
auto launch=[sock, local, password, apiKey, customHeaders, acl]() {
if (password) {
- warnlog("Passing a plain-text password to 'webserver()' is deprecated, please use 'setWebserverConfig()' instead.");
- auto hashed = hashPassword(*password);
- setWebserverPassword(std::move(hashed));
+ auto holder = make_unique<CredentialsHolder>(std::string(*password));
+ if (!holder->isHashed() && holder->isHashingAvailable()) {
+ warnlog("Passing a plain-text password to 'webserver()' is deprecated, please use 'setWebserverConfig()' instead.");
+ }
+
+ setWebserverPassword(std::move(holder));
}
if (apiKey) {
- setWebserverAPIKey(apiKey);
+ auto holder = make_unique<CredentialsHolder>(std::string(*apiKey));
+ setWebserverAPIKey(std::move(holder));
}
if (customHeaders) {
}
if (vars->count("password")) {
- warnlog("Passing a plain-text password via the 'password' parameter to 'setWebserverConfig()' is deprecated, please generate a hashed one using 'hashPassword()' and pass it via 'hashedPassword' instead.");
-
- const std::string password = boost::get<std::string>(vars->at("password"));
- auto hashed = hashPassword(password);
- setWebserverPassword(std::move(hashed));
- }
-
- if (vars->count("hashedPassword")) {
- std::string hashedPassword = boost::get<std::string>(vars->at("hashedPassword"));
- sodium_mlock(hashedPassword.data(), hashedPassword.size());
+ std::string password = boost::get<std::string>(vars->at("password"));
+ auto holder = make_unique<CredentialsHolder>(std::move(password));
+ if (!holder->isHashed() && holder->isHashingAvailable()) {
+ warnlog("Passing a plain-text password via the 'password' parameter to 'setWebserverConfig()' is deprecated, please generate a hashed one using 'hashPassword()' instead.");
+ }
- setWebserverPassword(std::move(hashedPassword));
+ setWebserverPassword(std::move(holder));
}
if (vars->count("apiKey")) {
- const std::string apiKey = boost::get<std::string>(vars->at("apiKey"));
+ std::string apiKey = boost::get<std::string>(vars->at("apiKey"));
+ auto holder = make_unique<CredentialsHolder>(std::move(apiKey));
+ if (!holder->isHashed() && holder->isHashingAvailable()) {
+ warnlog("Passing a plain-text API key via the 'apiKey' parameter to 'setWebserverConfig()' is deprecated, please generate a hashed one using 'hashPassword()' instead.");
+ }
- setWebserverAPIKey(apiKey);
+ setWebserverAPIKey(std::move(holder));
}
if (vars->count("acl")) {
}
NetmaskGroup acl;
- std::string password;
- std::string apiKey;
+ std::unique_ptr<CredentialsHolder> password;
+ std::unique_ptr<CredentialsHolder> apiKey;
boost::optional<std::map<std::string, std::string> > customHeaders;
bool statsRequireAuthentication{true};
};
apiWriteConfigFile("acl", content);
}
-static bool checkAPIKey(const YaHTTP::Request& req, const string& expectedApiKey)
+static bool checkAPIKey(const YaHTTP::Request& req, const std::unique_ptr<CredentialsHolder>& apiKey)
{
- if (expectedApiKey.empty()) {
+ if (!apiKey) {
return false;
}
const auto header = req.headers.find("x-api-key");
if (header != req.headers.end()) {
- return (header->second == expectedApiKey);
+ return apiKey->matches(header->second);
}
return false;
}
-static bool checkWebPassword(const YaHTTP::Request& req, const string &expected_password)
+static bool checkWebPassword(const YaHTTP::Request& req, const std::unique_ptr<CredentialsHolder>& password)
{
static const char basicStr[] = "basic ";
stringtok(cparts, plain, ":");
if (cparts.size() == 2) {
- return verifyPassword(g_webserverConfig.password, cparts.at(1));
+ if (password) {
+ return password->matches(cparts.at(1));
+ }
+ return true;
}
}
}
}
-void setWebserverAPIKey(const boost::optional<std::string> apiKey)
+void setWebserverAPIKey(std::unique_ptr<CredentialsHolder>&& apiKey)
{
auto config = g_webserverConfig.lock();
if (apiKey) {
- config->apiKey = *apiKey;
+ config->apiKey = std::move(apiKey);
} else {
- config->apiKey.clear();
+ config->apiKey.reset();
}
}
-void setWebserverPassword(std::string&& password)
+void setWebserverPassword(std::unique_ptr<CredentialsHolder>&& password)
{
g_webserverConfig.lock()->password = std::move(password);
}
setThreadName("dnsdist/webserv");
warnlog("Webserver launched on %s", local.toStringWithPort());
- if (g_webserverConfig.lock()->password.empty()) {
+ if (!g_webserverConfig.lock()->password) {
warnlog("Webserver launched on %s without a password set!", local.toStringWithPort());
}
capabilities.cc capabilities.hh \
circular_buffer.hh \
connection-management.hh \
+ credentials.cc credentials.hh \
dns.cc dns.hh \
dnscrypt.cc dnscrypt.hh \
dnsdist-backend.cc \
--- /dev/null
+../credentials.cc
\ No newline at end of file
--- /dev/null
+../credentials.hh
\ No newline at end of file
#pragma once
-void setWebserverAPIKey(const boost::optional<std::string> apiKey);
-void setWebserverPassword(std::string&& password);
+#include "credentials.hh"
+
+void setWebserverAPIKey(std::unique_ptr<CredentialsHolder>&& apiKey);
+void setWebserverPassword(std::unique_ptr<CredentialsHolder>&& password);
void setWebserverACL(const std::string& acl);
void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders);
void setWebserverStatsRequireAuthentication(bool);
``statsRequireAuthentication``, ``maxConcurrentConnections`` optional parameters added.
.. versionchanged:: 1.7.0
- The optional ``password`` parameter has been deprecated and replaced with ``hashedPassword``.
+ The optional ``password`` parameter now accepts hashed passwords.
Setup webserver configuration. See :func:`webserver`.
Options:
- * ``password=newPassword``: string - Changes the password used to access the internal webserver. Deprecated, please use ``hashedPassword`` instead
- * ``hashedPassword=newPassword``: string - Set the password used to access the internal webserver. The new password needs to be hashed and salted via the :func:`hashPassword` command
+ * ``password=newPassword``: string - Set the password used to access the internal webserver. Since 1.7.0 the password needs to be hashed and salted via the :func:`hashPassword` command
* ``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".
};
return retval;
};
-
-std::string hashPassword(const std::string& password)
-{
- std::string result;
- result.resize(crypto_pwhash_STRBYTES);
- sodium_mlock(result.data(), result.size());
-
- int res = crypto_pwhash_str(const_cast<char*>(result.c_str()),
- password.c_str(),
- password.size(),
- crypto_pwhash_OPSLIMIT_INTERACTIVE,
- crypto_pwhash_MEMLIMIT_INTERACTIVE);
- if (res != 0) {
- throw std::runtime_error("Error while hashing the supplied password");
- }
-
- return result;
-}
-
-bool verifyPassword(const std::string& hash, const std::string& password)
-{
- if (hash.size() > crypto_pwhash_STRBYTES) {
- throw std::runtime_error("Invalid password hash supplied for verification, size is " + std::to_string(hash.size()) + ", expected at most " + std::to_string(crypto_pwhash_STRBYTES));
- }
-
- return crypto_pwhash_str_verify(hash.c_str(),
- password.c_str(),
- password.size()) == 0;
-}
std::string sodDecryptSym(const std::string& msg, const std::string& key, SodiumNonce&);
std::string newKey();
bool sodIsValidKey(const std::string& key);
-
-std::string hashPassword(const std::string& password);
-bool verifyPassword(const std::string& hash, const std::string& password);
setACL({"127.0.0.1/32", "::1/128"})
newServer{address="127.0.0.1:%s", pool={'', 'mypool'}}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
class TestAPIBasics(APITestsBase):
newServer{address="127.0.0.1:%s"}
getServer(0):setDown()
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
def testServerDownNoLatencyLocalhost(self):
setACL({"127.0.0.1/32", "::1/128"})
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
setAPIWritable(true, "%s")
"""
setACL({"127.0.0.1/32", "::1/128"})
newServer({address="127.0.0.1:%s"})
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} })
+ setWebserverConfig({password="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} })
"""
def testBasicHeaders(self):
setACL({"127.0.0.1/32", "::1/128"})
newServer({address="127.0.0.1:%s"})
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s", statsRequireAuthentication=false })
+ setWebserverConfig({password="%s", apiKey="%s", statsRequireAuthentication=false })
"""
def testAuth(self):
setACL({"127.0.0.1/32", "::1/128"})
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
def testBasicAuthChange(self):
"""
url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
- self.sendConsoleCommand('setWebserverConfig({{hashedPassword="{}"}})'.format(self._webServerBasicAuthPasswordNewHashed))
+ self.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self._webServerBasicAuthPasswordNewHashed))
r = requests.get(url, auth=('whatever', self._webServerBasicAuthPasswordNew), timeout=self._webTimeout)
self.assertTrue(r)
setACL({"127.0.0.1/32", "::1/128"})
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s", acl="192.0.2.1"})
+ setWebserverConfig({password="%s", apiKey="%s", acl="192.0.2.1"})
"""
def testACLChange(self):
setACL({"127.0.0.1/32", "::1/128"})
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s"})
+ setWebserverConfig({password="%s"})
function customHTTPHandler(req, resp)
if req.path ~= '/foo' then
_config_template = """
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s", maxConcurrentConnections=%d})
+ setWebserverConfig({password="%s", apiKey="%s", maxConcurrentConnections=%d})
"""
def testConcurrentConnections(self):
end
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
_config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKey']
end
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
_config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKey']
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
_config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKey']
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
_config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_dynBlockWarningQPS', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKey']
_config_template = """
newServer{address="127.0.0.1:%s"}
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
def checkPrometheusContentBasic(self, content):
_config_template = """
newServer{address="127.0.0.1:%s", useClientSubnet=true, tcpFastOpen=true, retries=%d }
webserver("127.0.0.1:%s")
- setWebserverConfig({hashedPassword="%s", apiKey="%s"})
+ setWebserverConfig({password="%s", apiKey="%s"})
"""
@classmethod