diff --git a/doc/06-distributed-monitoring.md b/doc/06-distributed-monitoring.md index dca0aea95..fd1b1fd60 100644 --- a/doc/06-distributed-monitoring.md +++ b/doc/06-distributed-monitoring.md @@ -451,6 +451,16 @@ information/cli: Signed certificate for 'CN = icinga2-client2.localdomain'. > `ca list` cannot be used as historical inventory. Certificate > signing requests older than 1 week are automatically deleted. +You can also remove an undesired CSR using the `ca remove` command using the +syntax as the `ca sign` command. + +``` +[root@pym ~]# icinga2 ca remove 5c31ca0e2269c10363a97e40e3f2b2cd56493f9194d5b1852541b835970da46e +information/cli: Certificate 5c31ca0e2269c10363a97e40e3f2b2cd56493f9194d5b1852541b835970da46e removed. +``` +If you want to restore a certificate you have removed, you can use `ca restore`. + + ## Client/Satellite Setup This section describes the setup of a satellite and/or client connected to an diff --git a/doc/11-cli-commands.md b/doc/11-cli-commands.md index dff5bc390..d05079a08 100644 --- a/doc/11-cli-commands.md +++ b/doc/11-cli-commands.md @@ -21,6 +21,8 @@ Usage: Supported commands: * api setup (setup for API) * ca list (lists all certificate signing requests) + * ca restore (restores a removed certificate request) + * ca remove (removes an outstanding certificate request) * ca sign (signs an outstanding certificate request) * console (Icinga debug console) * daemon (starts Icinga 2) @@ -185,6 +187,8 @@ Usage: Supported commands: * ca list (lists all certificate signing requests) * ca sign (signs an outstanding certificate request) + * ca restore (restores a removed certificate request) + * ca remove (removes an outstanding certificate request) Global options: -h [ --help ] show this help message @@ -232,6 +236,7 @@ Command options: --all List all certificate signing requests, including signed. Note: Old requests are automatically cleaned by Icinga after 1 week. + --removed List all removed CSRs (for use with 'ca restore') --json encode output as JSON Report bugs at diff --git a/doc/16-upgrading-icinga-2.md b/doc/16-upgrading-icinga-2.md index a0b016e3d..39d35a5ab 100644 --- a/doc/16-upgrading-icinga-2.md +++ b/doc/16-upgrading-icinga-2.md @@ -164,6 +164,16 @@ or with sudo. You can use the new `--all` parameter to show all signing requests. Note that Icinga automatically purges signed requests older than 1 week. +#### New: CA Remove/Restore The deprecated `concurrent_checks` attribute in the [checker feature](09-object-types.md#objecttype-checkercomponent) diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt index 7a5e91887..38756b5ce 100644 --- a/lib/cli/CMakeLists.txt +++ b/lib/cli/CMakeLists.txt @@ -5,6 +5,8 @@ set(cli_SOURCES apisetupcommand.cpp apisetupcommand.hpp apisetuputility.cpp apisetuputility.hpp calistcommand.cpp calistcommand.hpp + caremovecommand.cpp caremovecommand.hpp + carestorecommand.cpp carestorecommand.hpp casigncommand.cpp casigncommand.hpp clicommand.cpp clicommand.hpp consolecommand.cpp consolecommand.hpp diff --git a/lib/cli/calistcommand.cpp b/lib/cli/calistcommand.cpp index 829086b98..f693ad72f 100644 --- a/lib/cli/calistcommand.cpp +++ b/lib/cli/calistcommand.cpp @@ -14,32 +14,49 @@ namespace po = boost::program_options; REGISTER_CLICOMMAND("ca/list", CAListCommand); +/** + * Provide a long CLI description sentence. + * + * @return text + */ String CAListCommand::GetDescription() const { return "Lists pending certificate signing requests."; } +/** + * Provide a short CLI description. + * + * @return text + */ String CAListCommand::GetShortDescription() const { return "lists pending certificate signing requests"; } +/** + * Initialize available CLI parameters. + * + * @param visibleDesc Register visible parameters. + * @param hiddenDesc Register hidden parameters. + */ void CAListCommand::InitParameters(boost::program_options::options_description& visibleDesc, boost::program_options::options_description& hiddenDesc) const { visibleDesc.add_options() ("all", "List all certificate signing requests, including signed. Note: Old requests are automatically cleaned by Icinga after 1 week.") + ("removed", "List all removed CSRs (for use with 'ca restore')") ("json", "encode output as JSON"); } /** * The entry point for the "ca list" CLI command. * - * @returns An exit status. + * @return An exit status. */ int CAListCommand::Run(const boost::program_options::variables_map& vm, const std::vector& ap) const { - Dictionary::Ptr requests = PkiUtility::GetCertificateRequests(); + Dictionary::Ptr requests = PkiUtility::GetCertificateRequests(vm.count("removed")); if (vm.count("json")) std::cout << JsonEncode(requests); diff --git a/lib/cli/caremovecommand.cpp b/lib/cli/caremovecommand.cpp new file mode 100644 index 000000000..d8944944e --- /dev/null +++ b/lib/cli/caremovecommand.cpp @@ -0,0 +1,93 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "cli/caremovecommand.hpp" +#include "base/logger.hpp" +#include "base/application.hpp" +#include "base/tlsutility.hpp" +#include "remote/apilistener.hpp" + +using namespace icinga; + +REGISTER_CLICOMMAND("ca/remove", CARemoveCommand); + +/** + * Provide a long CLI description sentence. + * + * @return text + */ +String CARemoveCommand::GetDescription() const +{ + return "Removes an outstanding certificate request."; +} + +/** + * Provide a short CLI description. + * + * @return text + */ +String CARemoveCommand::GetShortDescription() const +{ + return "removes an outstanding certificate request"; +} + +/** + * Define minimum arguments without key parameter. + * + * @return number of arguments + */ +int CARemoveCommand::GetMinArguments() const +{ + return 1; +} + +/** + * Impersonate as Icinga user. + * + * @return impersonate level + */ +ImpersonationLevel CARemoveCommand::GetImpersonationLevel() const +{ + return ImpersonateIcinga; +} + +/** + * The entry point for the "ca remove" CLI command. + * + * @returns An exit status. + */ +int CARemoveCommand::Run(const boost::program_options::variables_map& vm, const std::vector& ap) const +{ + String fingerPrint = ap[0]; + String requestFile = ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".json"; + + if (!Utility::PathExists(requestFile)) { + Log(LogCritical, "cli") + << "No request exists for fingerprint '" << fingerPrint << "'."; + return 1; + } + + Dictionary::Ptr request = Utility::LoadJsonFile(requestFile); + std::shared_ptr certRequest = StringToCertificate(request->Get("cert_request")); + + if (!certRequest) { + Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute."); + return 1; + } + + String cn = GetCertificateCN(certRequest); + + if (request->Contains("cert_response")) { + Log(LogCritical, "cli") + << "Certificate request for CN '" << cn << "' already signed, removal is not possible."; + return 1; + } + + Utility::SaveJsonFile(ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".removed", 0600, request); + + Utility::Remove(requestFile); + + Log(LogInformation, "cli") + << "Certificate request for CN " << cn << " removed."; + + return 0; +} diff --git a/lib/cli/caremovecommand.hpp b/lib/cli/caremovecommand.hpp new file mode 100644 index 000000000..2da92d39e --- /dev/null +++ b/lib/cli/caremovecommand.hpp @@ -0,0 +1,30 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef CAREMOVECOMMAND_H +#define CAREMOVECOMMAND_H + +#include "cli/clicommand.hpp" + +namespace icinga +{ + +/** + * The "ca remove" command. + * + * @ingroup cli + */ +class CARemoveCommand final : public CLICommand +{ +public: + DECLARE_PTR_TYPEDEFS(CARemoveCommand); + + String GetDescription() const override; + String GetShortDescription() const override; + int GetMinArguments() const override; + ImpersonationLevel GetImpersonationLevel() const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& ap) const override; +}; + +} + +#endif /* CAREMOVECOMMAND_H */ diff --git a/lib/cli/carestorecommand.cpp b/lib/cli/carestorecommand.cpp new file mode 100644 index 000000000..502036830 --- /dev/null +++ b/lib/cli/carestorecommand.cpp @@ -0,0 +1,88 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "cli/carestorecommand.hpp" +#include "base/logger.hpp" +#include "base/application.hpp" +#include "base/tlsutility.hpp" +#include "remote/apilistener.hpp" + +using namespace icinga; + +REGISTER_CLICOMMAND("ca/restore", CARestoreCommand); + +/** + * Provide a long CLI description sentence. + * + * @return text + */ +String CARestoreCommand::GetDescription() const +{ + return "Restores a previously removed certificate request."; +} + +/** + * Provide a short CLI description. + * + * @return text + */ +String CARestoreCommand::GetShortDescription() const +{ + return "restores a removed certificate request"; +} + +/** + * Define minimum arguments without key parameter. + * + * @return number of arguments + */ +int CARestoreCommand::GetMinArguments() const +{ + return 1; +} + +/** + * Impersonate as Icinga user. + * + * @return impersonate level + */ +ImpersonationLevel CARestoreCommand::GetImpersonationLevel() const +{ + return ImpersonateIcinga; +} + +/** + * The entry point for the "ca restore" CLI command. + * + * @returns An exit status. + */ +int CARestoreCommand::Run(const boost::program_options::variables_map& vm, const std::vector& ap) const +{ + String fingerPrint = ap[0]; + String removedRequestFile = ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".removed"; + + if (!Utility::PathExists(removedRequestFile)) { + Log(LogCritical, "cli") + << "Cannot find removed fingerprint '" << fingerPrint << "', bailing out."; + return 1; + } + + Dictionary::Ptr request = Utility::LoadJsonFile(removedRequestFile); + std::shared_ptr certRequest = StringToCertificate(request->Get("cert_request")); + + if (!certRequest) { + Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute."); + /* Purge the file when we know that it is broken. */ + Utility::Remove(removedRequestFile); + return 1; + } + + Utility::SaveJsonFile(ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".json", 0600, request); + + Utility::Remove(removedRequestFile); + + Log(LogInformation, "cli") + << "Restored certificate request for CN '" << GetCertificateCN(certRequest) << "', sign it with:\n" + << "\"icinga2 ca sign " << fingerPrint << "\""; + + return 0; +} diff --git a/lib/cli/carestorecommand.hpp b/lib/cli/carestorecommand.hpp new file mode 100644 index 000000000..74a27dff6 --- /dev/null +++ b/lib/cli/carestorecommand.hpp @@ -0,0 +1,30 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef CARESTORECOMMAND_H +#define CARESTORECOMMAND_H + +#include "cli/clicommand.hpp" + +namespace icinga +{ + +/** + * The "ca restore" command. + * + * @ingroup cli + */ +class CARestoreCommand final : public CLICommand +{ +public: + DECLARE_PTR_TYPEDEFS(CARestoreCommand); + + String GetDescription() const override; + String GetShortDescription() const override; + int GetMinArguments() const override; + ImpersonationLevel GetImpersonationLevel() const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& ap) const override; +}; + +} + +#endif /* CASTORECOMMAND_H */ diff --git a/lib/cli/casigncommand.cpp b/lib/cli/casigncommand.cpp index a6b547d98..96d2c2c87 100644 --- a/lib/cli/casigncommand.cpp +++ b/lib/cli/casigncommand.cpp @@ -1,30 +1,50 @@ /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ #include "cli/casigncommand.hpp" -#include "remote/apilistener.hpp" #include "base/logger.hpp" #include "base/application.hpp" #include "base/tlsutility.hpp" +#include "remote/apilistener.hpp" using namespace icinga; REGISTER_CLICOMMAND("ca/sign", CASignCommand); +/** + * Provide a long CLI description sentence. + * + * @return text + */ String CASignCommand::GetDescription() const { return "Signs an outstanding certificate request."; } +/** + * Provide a short CLI description. + * + * @return text + */ String CASignCommand::GetShortDescription() const { return "signs an outstanding certificate request"; } +/** + * Define minimum arguments without key parameter. + * + * @return number of arguments + */ int CASignCommand::GetMinArguments() const { return 1; } +/** + * Impersonate as Icinga user. + * + * @return impersonate level + */ ImpersonationLevel CASignCommand::GetImpersonationLevel() const { return ImpersonateIcinga; @@ -33,7 +53,7 @@ ImpersonationLevel CASignCommand::GetImpersonationLevel() const /** * The entry point for the "ca sign" CLI command. * - * @returns An exit status. + * @return An exit status. */ int CASignCommand::Run(const boost::program_options::variables_map& vm, const std::vector& ap) const { diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp index 9b537d138..27a21a6b3 100644 --- a/lib/remote/jsonrpcconnection-pki.cpp +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -129,6 +129,12 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona return result; } + } else if (Utility::PathExists(requestDir + "/" + certFingerprint + ".removed")) { + Log(LogInformation, "JsonRpcConnection") + << "Certificate for CN " << cn << " has been removed. Ignoring signing request."; + result->Set("status_code", 1); + result->Set("error", "Ticket for CN " + cn + " declined by administrator."); + return result; } std::shared_ptr newcert; diff --git a/lib/remote/pkiutility.cpp b/lib/remote/pkiutility.cpp index 350b99361..3fddd1a67 100644 --- a/lib/remote/pkiutility.cpp +++ b/lib/remote/pkiutility.cpp @@ -18,6 +18,7 @@ #include #include #include +#include using namespace icinga; @@ -368,8 +369,9 @@ static void CollectRequestHandler(const Dictionary::Ptr& requests, const String& Dictionary::Ptr result = new Dictionary(); - String fingerprint = Utility::BaseName(requestFile); - fingerprint = fingerprint.SubStr(0, fingerprint.GetLength() - 5); + namespace fs = boost::filesystem; + fs::path file(requestFile.Begin(), requestFile.End()); + String fingerprint = file.stem().string(); String certRequestText = request->Get("cert_request"); result->Set("cert_request", certRequestText); @@ -414,14 +416,19 @@ static void CollectRequestHandler(const Dictionary::Ptr& requests, const String& requests->Set(fingerprint, result); } -Dictionary::Ptr PkiUtility::GetCertificateRequests() +Dictionary::Ptr PkiUtility::GetCertificateRequests(bool removed) { Dictionary::Ptr requests = new Dictionary(); String requestDir = ApiListener::GetCertificateRequestsDir(); + String ext = "json"; + + if (removed) + ext = "removed"; if (Utility::PathExists(requestDir)) - Utility::Glob(requestDir + "/*.json", std::bind(&CollectRequestHandler, requests, _1), GlobFile); + Utility::Glob(requestDir + "/*." + ext, std::bind(&CollectRequestHandler, requests, _1), GlobFile); return requests; } + diff --git a/lib/remote/pkiutility.hpp b/lib/remote/pkiutility.hpp index cc5e67061..50d47e01a 100644 --- a/lib/remote/pkiutility.hpp +++ b/lib/remote/pkiutility.hpp @@ -29,7 +29,7 @@ public: const String& certfile, const String& cafile, const std::shared_ptr& trustedcert, const String& ticket = String()); static String GetCertificateInformation(const std::shared_ptr& certificate); - static Dictionary::Ptr GetCertificateRequests(); + static Dictionary::Ptr GetCertificateRequests(bool removed = false); private: PkiUtility();