Merge branch 'feature/security-features'

This commit is contained in:
Jean Flach 2018-02-21 16:19:54 +01:00
commit 6bb2ed4258
14 changed files with 332 additions and 11 deletions

View File

@ -108,8 +108,9 @@ Configuration Attributes:
Name | Type | Description
--------------------------|-----------------------|----------------------------------
password | String | **Optional.** Password string. Note: This attribute is hidden in API responses.
hashed\_password | 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.
permissions | Array | **Required.** Array of permissions. Either as string or dictionary with the keys `permission` and `filter`. The latter must be specified as function.
Available permissions are explained in the [API permissions](12-icinga2-api.md#icinga2-api-permissions)
chapter.

View File

@ -19,7 +19,8 @@ Usage:
icinga2 <command> [<arguments>]
Supported commands:
* api setup (setup for api)
* 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)
@ -135,8 +136,9 @@ added.
## CLI command: Api <a id="cli-command-api"></a>
Provides the setup CLI command to enable the REST API. More details
in the [Icinga 2 API](12-icinga2-api.md#icinga2-api-setup) chapter.
Provides the helper functions `api setup` and `api user`. The first to enable the REST API, the second to create
ApiUser objects with hashed password strings.
More details in the [Icinga 2 API](12-icinga2-api.md#icinga2-api-setup) chapter.
```
# icinga2 api --help
@ -146,7 +148,8 @@ Usage:
icinga2 <command> [<arguments>]
Supported commands:
* api setup (setup for api)
* api setup (setup for API)
* api user (API user creation helper)
Global options:
-h [ --help ] show this help message

View File

@ -21,6 +21,25 @@ 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 --passwd 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

@ -658,6 +658,19 @@ String PBKDF2_SHA1(const String& password, const String& salt, int iterations)
return output;
}
String PBKDF2_SHA256(const String& password, const String& salt, int iterations)
{
unsigned char digest[SHA256_DIGEST_LENGTH];
PKCS5_PBKDF2_HMAC(password.CStr(), password.GetLength(), reinterpret_cast<const unsigned char *>(salt.CStr()),
salt.GetLength(), iterations, EVP_sha256(), SHA256_DIGEST_LENGTH, digest);
char output[SHA256_DIGEST_LENGTH*2+1];
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++)
sprintf(output + 2 * i, "%02x", digest[i]);
return output;
}
String SHA1(const String& s, bool binary)
{
char errbuf[120];
@ -797,4 +810,34 @@ 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

@ -51,10 +51,13 @@ std::shared_ptr<X509> StringToCertificate(const String& cert);
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert);
String PBKDF2_SHA1(const String& password, const String& salt, int iterations);
String PBKDF2_SHA256(const String& password, const String& salt, int iterations);
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,6 +18,7 @@
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

@ -36,7 +36,7 @@ String ApiSetupCommand::GetDescription() const
String ApiSetupCommand::GetShortDescription() const
{
return "setup for api";
return "setup for API";
}
ImpersonationLevel ApiSetupCommand::GetImpersonationLevel() const

104
lib/cli/apiusercommand.cpp Normal file
View File

@ -0,0 +1,104 @@
/******************************************************************************
* 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

@ -0,0 +1,47 @@
/******************************************************************************
* 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

@ -21,11 +21,24 @@
#include "remote/apiuser-ti.cpp"
#include "base/configtype.hpp"
#include "base/base64.hpp"
#include "base/tlsutility.hpp"
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>()) {
@ -55,11 +68,38 @@ ApiUser::Ptr ApiUser::GetByAuthHeader(const String& auth_header)
const ApiUser::Ptr& user = ApiUser::GetByName(username);
/* Deny authentication if 1) given password is empty 2) configured password does not match. */
if (password.IsEmpty())
return nullptr;
else if (user && user->GetPassword() != password)
/* Deny authentication if:
* 1) user does not exist
* 2) given password is empty
* 2) configured password does not match.
*/
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;
}
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,8 +35,12 @@ 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

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

View File

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

52
test/remote-user.cpp Normal file
View File

@ -0,0 +1,52 @@
/******************************************************************************
* 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)
{
#ifndef I2_DEBUG
std::cout << "Only enabled in Debug builds..." << std::endl;
#else
ApiUser::Ptr user = new ApiUser();
String passwd = RandomString(16);
String salt = RandomString(8);
user->SetPassword("ThisShouldBeIgnored");
user->SetPasswordHash(CreateHashedPasswordString(passwd, salt, true));
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));
#endif
}
BOOST_AUTO_TEST_SUITE_END()