Remove ApiUser password_hash functionality

This affects and fixes

- Windows reload
- Config validation
- RHEL 7.5 OpenSSL memory corruption
- Hash algorithm, requested changes

refs #6378
refs #6279
refs #6278
This commit is contained in:
Michael Friedrich 2018-06-18 11:05:56 +02:00
parent eb1f37905d
commit 2fd6709952
14 changed files with 8 additions and 298 deletions

View File

@ -112,7 +112,6 @@ Configuration Attributes:
Name | Type | Description
--------------------------|-----------------------|----------------------------------
password | String | **Optional.** Password string. Note: This attribute is hidden in API responses.
password\_hash | String | **Optional.** A hashed password string in the form of /etc/shadow. Note: This attribute is hidden in API responses.
client\_cn | String | **Optional.** Client Common Name (CN).
permissions | Array | **Required.** Array of permissions. Either as string or dictionary with the keys `permission` and `filter`. The latter must be specified as function.

View File

@ -20,7 +20,6 @@ Usage:
Supported commands:
* api setup (setup for API)
* api user (API user creation helper)
* ca list (lists all certificate signing requests)
* ca sign (signs an outstanding certificate request)
* console (Icinga console)

View File

@ -21,25 +21,6 @@ If you prefer to set up the API manually, you will have to perform the following
The next chapter provides a quick overview of how you can use the API.
### Creating ApiUsers <a id="icinga2-api-creating-users"></a>
The CLI command `icinga2 api user` allows you to create an ApiUser object with a hashed password string, ready to be
added to your configuration. Example:
```
$ icinga2 api user --user icingaweb2 --password icinga
object ApiUser "icingaweb2" {
password_hash ="$5$d5f1a17ea308acb6$9e9fd5d24a9373a16e8811765cc5a5939687faf9ef8ed496db6e7f1d0ae9b2a9"
// client_cn = ""
permissions = [ "*" ]
}
```
Optionally a salt can be provided with `--salt`, otherwise a random value will be used. When ApiUsers are stored this
way, even somebody able to read the configuration files won't be able to authenticate using this information. There is
no way to recover your password should you forget it, you'd need to create it anew.
## Introduction <a id="icinga2-api-introduction"></a>
The Icinga 2 API allows you to manage configuration objects

View File

@ -18,6 +18,11 @@ The CORS attributes `access_control_allow_credentials`, `access_control_allow_he
The `node setup` parameter `--master_host` was deprecated and replaced with `--parent_host`. This parameter is now optional to allow connection-less client setups similar to the `node wizard` CLI command. The `parent_zone` parameter has been added to modify the parent zone name e.g. for client-to-satellite setups.
The `api user` command which was released in v2.8.2 turned out to cause huge problems with
configuration validation, windows restarts and OpenSSL versions. It is therefore removed in 2.9,
the `password_hash` attribute for the ApiUser object stays intact but has no effect. This is to ensure
that clients don't break on upgrade. We will revise this feature in future development iterations.
## Upgrading to v2.8.2 <a id="upgrading-to-2-8-2"></a>
With version 2.8.2 the location of settings formerly found in `/etc/icinga2/init.conf` has changed. They are now

View File

@ -784,34 +784,4 @@ std::string to_string(const errinfo_openssl_error& e)
return "[errinfo_openssl_error]" + tmp.str() + "\n";
}
bool ComparePassword(const String& hash, const String& password, const String& salt)
{
String otherHash = PBKDF2_SHA256(password, salt, 1000);
VERIFY(otherHash.GetLength() == 64 && hash.GetLength() == 64);
const char *p1 = otherHash.CStr();
const char *p2 = hash.CStr();
/* By Novelocrat, https://stackoverflow.com/a/25374036 */
volatile char c = 0;
for (size_t i = 0; i < 64; ++i)
c |= p1[i] ^ p2[i];
return (c == 0);
}
/* Returns a String in the format $algorithm$salt$hash or returns an empty string in case of an error */
String CreateHashedPasswordString(const String& password, const String& salt, int algorithm)
{
// We currently only support SHA256
if (algorithm != 5)
return String();
if (salt.FindFirstOf('$') != String::NPos)
return String();
return String("$5$" + salt + "$" + PBKDF2_SHA256(password, salt, 1000));
}
}

View File

@ -56,8 +56,6 @@ String SHA1(const String& s, bool binary = false);
String SHA256(const String& s);
String RandomString(int length);
bool VerifyCertificate(const std::shared_ptr<X509>& caCertificate, const std::shared_ptr<X509>& certificate);
bool ComparePassword(const String& hash, const String& password, const String& Salt);
String CreateHashedPasswordString(const String& password, const String& salt, int algorithm = 5);
class openssl_error : virtual public std::exception, virtual public boost::exception { };

View File

@ -18,7 +18,6 @@
set(cli_SOURCES
i2-cli.hpp
apisetupcommand.cpp apisetupcommand.hpp
apiusercommand.cpp apiusercommand.hpp
apisetuputility.cpp apisetuputility.hpp
calistcommand.cpp calistcommand.hpp
casigncommand.cpp casigncommand.hpp

View File

@ -1,104 +0,0 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#include "cli/apiusercommand.hpp"
#include "base/logger.hpp"
#include "base/tlsutility.hpp"
#include "base/configwriter.hpp"
#include "remote/apiuser.hpp"
#include <iostream>
using namespace icinga;
namespace po = boost::program_options;
REGISTER_CLICOMMAND("api/user", ApiUserCommand);
String ApiUserCommand::GetDescription(void) const
{
return "Create a hashed user and password string for the Icinga 2 API";
}
String ApiUserCommand::GetShortDescription(void) const
{
return "API user creation helper";
}
void ApiUserCommand::InitParameters(boost::program_options::options_description& visibleDesc,
boost::program_options::options_description& hiddenDesc) const
{
visibleDesc.add_options()
("user", po::value<std::string>(), "API username")
("password", po::value<std::string>(), "Password in clear text")
("salt", po::value<std::string>(), "Optional salt (default: 8 random chars)")
("oneline", "Print only the password hash");
}
/**
* The entry point for the "api user" CLI command.
*
* @returns An exit status.
*/
int ApiUserCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
{
String passwd, salt;
if (!vm.count("user") && !vm.count("oneline")) {
Log(LogCritical, "cli", "Username (--user) must be specified.");
return 1;
}
if (!vm.count("password")) {
Log(LogCritical, "cli", "Password (--password) must be specified.");
return 1;
}
passwd = vm["password"].as<std::string>();
salt = vm.count("salt") ? String(vm["salt"].as<std::string>()) : RandomString(8);
if (salt.FindFirstOf('$') != String::NPos) {
Log(LogCritical, "cli", "Salt (--salt) may not contain '$'");
return 1;
}
String hashedPassword = CreateHashedPasswordString(passwd, salt, 5);
if (hashedPassword == String()) {
Log(LogCritical, "cli") << "Failed to hash password \"" << passwd << "\" with salt \"" << salt << "\"";
return 1;
}
if (vm.count("oneline"))
std::cout << hashedPassword << std::endl;
else {
std::cout << "object ApiUser ";
ConfigWriter::EmitString(std::cout, vm["user"].as<std::string>());
std::cout << "{\n"
<< " password_hash = ";
ConfigWriter::EmitString(std::cout, hashedPassword);
std::cout << "\n"
<< " // client_cn = \"\"\n"
<< "\n"
<< " permissions = [ \"*\" ]\n"
<< "}\n";
}
return 0;
}

View File

@ -1,47 +0,0 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#ifndef APIUSERCOMMAND_H
#define APIUSERCOMMAND_H
#include "cli/clicommand.hpp"
namespace icinga
{
/**
* The "api user" command.
*
* @ingroup cli
*/
class ApiUserCommand : public CLICommand
{
public:
DECLARE_PTR_TYPEDEFS(ApiUserCommand);
virtual String GetDescription(void) const override;
virtual String GetShortDescription(void) const override;
virtual void InitParameters(boost::program_options::options_description& visibleDesc,
boost::program_options::options_description& hiddenDesc) const override;
virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
};
}
#endif /* APIUSERCOMMAND_H */

View File

@ -27,18 +27,6 @@ using namespace icinga;
REGISTER_TYPE(ApiUser);
void ApiUser::OnConfigLoaded(void)
{
ObjectImpl<ApiUser>::OnConfigLoaded();
if (GetPasswordHash().IsEmpty()) {
String hashedPassword = CreateHashedPasswordString(GetPassword(), RandomString(8), 5);
VERIFY(hashedPassword != String());
SetPasswordHash(hashedPassword);
SetPassword("********");
}
}
ApiUser::Ptr ApiUser::GetByClientCN(const String& cn)
{
for (const ApiUser::Ptr& user : ConfigType::GetObjectsByType<ApiUser>()) {
@ -75,31 +63,9 @@ ApiUser::Ptr ApiUser::GetByAuthHeader(const String& auth_header)
*/
if (!user || password.IsEmpty())
return nullptr;
else if (user && user->GetPassword() != password) {
Dictionary::Ptr passwordDict = user->GetPasswordDict();
if (!passwordDict || !ComparePassword(passwordDict->Get("password"), password, passwordDict->Get("salt")))
return nullptr;
}
else if (user && user->GetPassword() != password)
return nullptr;
return user;
}
Dictionary::Ptr ApiUser::GetPasswordDict(void) const
{
String password = GetPasswordHash();
if (password.IsEmpty() || password[0] != '$')
return nullptr;
String::SizeType saltBegin = password.FindFirstOf('$', 1);
String::SizeType passwordBegin = password.FindFirstOf('$', saltBegin+1);
if (saltBegin == String::NPos || saltBegin == 1 || passwordBegin == String::NPos)
return nullptr;
Dictionary::Ptr passwordDict = new Dictionary();
passwordDict->Set("algorithm", password.SubStr(1, saltBegin - 1));
passwordDict->Set("salt", password.SubStr(saltBegin + 1, passwordBegin - saltBegin - 1));
passwordDict->Set("password", password.SubStr(passwordBegin + 1));
return passwordDict;
}

View File

@ -35,12 +35,8 @@ public:
DECLARE_OBJECT(ApiUser);
DECLARE_OBJECTNAME(ApiUser);
virtual void OnConfigLoaded(void) override;
static ApiUser::Ptr GetByClientCN(const String& cn);
static ApiUser::Ptr GetByAuthHeader(const String& auth_header);
Dictionary::Ptr GetPasswordDict(void) const;
};
}

View File

@ -29,7 +29,7 @@ class ApiUser : ConfigObject
{
/* No show config */
[no_user_view, no_user_modify] String password;
[config, no_user_view] String password_hash;
[deprecated, config, no_user_view] String password_hash;
[config] String client_cn (ClientCN);
[config] array(Value) permissions;
};

View File

@ -43,7 +43,6 @@ set(base_test_SOURCES
icinga-notification.cpp
icinga-perfdata.cpp
remote-url.cpp
remote-user.cpp
${base_OBJS}
$<TARGET_OBJECTS:config>
$<TARGET_OBJECTS:remote>
@ -142,7 +141,6 @@ add_boost_test(base
remote_url/get_and_set
remote_url/format
remote_url/illegal_legal_strings
api_user/password
)
if(ICINGA2_WITH_LIVESTATUS)

View File

@ -1,50 +0,0 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* 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 St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#include "remote/apiuser.hpp"
#include "base/tlsutility.hpp"
#include <BoostTestTargetConfig.h>
#include <iostream>
using namespace icinga;
BOOST_AUTO_TEST_SUITE(api_user)
BOOST_AUTO_TEST_CASE(password)
{
ApiUser::Ptr user = new ApiUser();
String passwd = RandomString(16);
String salt = RandomString(8);
user->SetPasswordHash(CreateHashedPasswordString(passwd, salt));
user->OnConfigLoaded();
user->OnAllConfigLoaded();
user->Start();
BOOST_CHECK(user->GetPasswordHash() != passwd);
Dictionary::Ptr passwdd = user->GetPasswordDict();
BOOST_CHECK(passwdd);
BOOST_CHECK(passwdd->Get("salt") == salt);
BOOST_CHECK(ComparePassword(passwdd->Get("password"), passwd, salt));
BOOST_CHECK(!ComparePassword(passwdd->Get("password"), "wrong password uwu!", salt));
}
BOOST_AUTO_TEST_SUITE_END()