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.
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)
## 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
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
The next chapter provides a quick overview of how you can use the API.
+### Creating ApiUsers
+
+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
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];
boost::shared_ptr<X509> I2_BASE_API CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
boost::shared_ptr<X509> I2_BASE_API CreateCertIcingaCA(const boost::shared_ptr<X509>& cert);
String I2_BASE_API PBKDF2_SHA1(const String& password, const String& salt, int iterations);
+String I2_BASE_API PBKDF2_SHA256(const String& password, const String& salt, int iterations);
String I2_BASE_API SHA1(const String& s, bool binary = false);
String I2_BASE_API SHA256(const String& s);
String I2_BASE_API RandomString(int length);
objectlistcommand.cpp objectlistutility.cpp
pkinewcacommand.cpp pkinewcertcommand.cpp pkisigncsrcommand.cpp pkirequestcommand.cpp pkisavecertcommand.cpp pkiticketcommand.cpp
variablegetcommand.cpp variablelistcommand.cpp variableutility.cpp
- troubleshootcommand.cpp
+ apiusercommand.cpp troubleshootcommand.cpp
)
if(ICINGA2_UNITY_BUILD)
String ApiSetupCommand::GetShortDescription(void) const
{
- return "setup for api";
+ return "setup for API";
}
ImpersonationLevel ApiSetupCommand::GetImpersonationLevel(void) const
--- /dev/null
+/******************************************************************************
+ * 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 "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")
+ ("passwd", po::value<std::string>(), "Password in clear text")
+ ("salt", po::value<std::string>(), "Optional salt (default: 8 random chars)");
+}
+
+/**
+ * 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
+{
+ if (!vm.count("user")) {
+ Log(LogCritical, "cli", "Username (--user) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("passwd")) {
+ Log(LogCritical, "cli", "Password (--passwd) must be specified.");
+ return 1;
+ }
+
+ String user = vm["user"].as<std::string>();
+ String passwd = vm["passwd"].as<std::string>();
+ String salt = vm.count("salt") ? String(vm["salt"].as<std::string>()) : RandomString(8);
+
+ String hashedPassword = ApiUser::CreateHashedPasswordString(passwd, salt, true);
+
+ std::cout
+ << "object ApiUser \"" << user << "\" {\n"
+ << " password_hash =\"" << hashedPassword << "\"\n"
+ << " // client_cn = \"\"\n"
+ << "\n"
+ << " permissions = [ \"*\" ]\n"
+ << "}\n";
+
+ return 0;
+}
--- /dev/null
+/******************************************************************************
+ * 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 */
#include "remote/apiuser.tcpp"
#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 (this->GetPasswordHash().IsEmpty())
+ SetPasswordHash(CreateHashedPasswordString(GetPassword(), RandomString(8), true));
+}
+
ApiUser::Ptr ApiUser::GetByClientCN(const String& cn)
{
for (const ApiUser::Ptr& user : ConfigType::GetObjectsByType<ApiUser>()) {
return user;
}
+
+bool ApiUser::ComparePassword(String password) const
+{
+ Dictionary::Ptr passwordDict = this->GetPasswordDict();
+ String thisPassword = passwordDict->Get("password");
+ String otherPassword = CreateHashedPasswordString(password, passwordDict->Get("salt"), false);
+
+ const char *p1 = otherPassword.CStr();
+ const char *p2 = thisPassword.CStr();
+
+ volatile char c = 0;
+
+ for (size_t i=0; i<64; ++i)
+ c |= p1[i] ^ p2[i];
+
+ return (c == 0);
+}
+
+Dictionary::Ptr ApiUser::GetPasswordDict(void) const
+{
+ String password = this->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;
+}
+
+String ApiUser::CreateHashedPasswordString(const String& password, const String& salt, const bool shadow)
+{
+ if (shadow)
+ //Using /etc/shadow password format. The 5 means SHA256 is being used
+ return String("$5$" + salt + "$" + PBKDF2_SHA256(password, salt, 1000));
+ else
+ return PBKDF2_SHA256(password, salt, 1000);
+
+}
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);
+ static String CreateHashedPasswordString(const String& password, const String& salt, const bool shadow = false);
+
+ Dictionary::Ptr GetPasswordDict(void) const;
+ bool ComparePassword(String password) const;
};
}
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;
};
base-value.cpp config-ops.cpp icinga-checkresult.cpp icinga-macros.cpp
icinga-notification.cpp
icinga-perfdata.cpp remote-url.cpp
+ remote-user.cpp
)
if(ICINGA2_UNITY_BUILD)
remote_url/get_and_set
remote_url/format
remote_url/illegal_legal_strings
+ api_user/password
)
if(ICINGA2_WITH_LIVESTATUS)
--- /dev/null
+/******************************************************************************
+ * 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(ApiUser::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(user->ComparePassword(passwd));
+ BOOST_CHECK(!user->ComparePassword("wrong password uwu!"));
+#endif
+}
+
+BOOST_AUTO_TEST_SUITE_END()