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();