Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
v1.4.0
- Add ability to manage custom VDOMs. PLEASE NOTE this release contains a breaking change. Store Path MUST contain the value for the VDOM the certificate will be managing. `root` must be entered to manage the default 'root' VDOM.

v1.3.0
- Add support for renewing certificate bound to the HTTPS server

Expand Down
2 changes: 2 additions & 0 deletions Fortigate/Api/cmdb_certificate_resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ public class cmdb_certificate_resource
public string file_content { get; set; }

public string scope { get; set; }

public string vdom { get; set; }
}
}
89 changes: 65 additions & 24 deletions Fortigate/FortigateStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
{
private ILogger logger { get; set; }
private string FortigateHost { get; set; }
private string VDOM { get; set; }


private static readonly string available_certificates = "/api/v2/monitor/system/available-certificates";
Expand All @@ -46,9 +47,8 @@
//private static readonly string certificate_api = "/api/v2/cmdb/certificate/local";
private static readonly string import_certificate_api = "/api/v2/monitor/vpn-certificate/local/import";

private static readonly string get_certificate_api = "/api/v2/cmdb/certificate/local/";

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

Check warning on line 50 in Fortigate/FortigateStore.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The field 'FortigateStore.get_certificate_api' is assigned but its value is never used

private static readonly string update_certificate_api = "/api/v2/cmdb/certificate/local/";
private static readonly string get_vdom_api = "/api/v2/cmdb/system/vdom/";

//api/v2/cmdb/vpn.certificate/local/test?vdom=root
private static readonly string delete_certificate_api = "/api/v2/cmdb/vpn.certificate/local/";
Expand All @@ -63,7 +63,7 @@
};
private readonly HttpClient client;

public FortigateStore(string fortigateHost, string accessToken)
public FortigateStore(string fortigateHost, string accessToken, string vdom)
{
logger = LogHandler.GetClassLogger(this.GetType());

Expand All @@ -72,6 +72,9 @@
client = new HttpClient(handler);
FortigateHost = fortigateHost;
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}");
VDOM = string.IsNullOrEmpty(vdom) ? "root" : vdom;

ValidateVDOM();

logger.MethodExit(LogLevel.Debug);
}
Expand All @@ -80,9 +83,11 @@
{
logger.MethodEntry(LogLevel.Debug);

Dictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("vdom", VDOM);
try
{
DeleteResource(delete_certificate_api + alias);
DeleteResource(delete_certificate_api + alias, parameters);
}
catch (Exception ex)
{
Expand All @@ -106,8 +111,7 @@

var endpoint = "/api/v2/cmdb/" + path + "/" + name;

var parameters = new Dictionary<String, String>();
parameters.Add("vdom", "root");
var parameters = new Dictionary<String, String> { { "vdom", VDOM } };

try
{
Expand All @@ -129,8 +133,7 @@
logger.MethodEntry(LogLevel.Debug);

var parameters = new Dictionary<String, String>();
parameters.Add("vdom", "root");
parameters.Add("scope", "global");
parameters.Add("vdom", VDOM);
parameters.Add("mkey", alias);
parameters.Add("qtypes", $"[{qtype.ToString()}]");

Expand Down Expand Up @@ -289,16 +292,14 @@
certname = alias,
key_file_content = privateKey,
file_content = cert,
scope = "global",
//password = password,
scope = "vdom",
vdom = VDOM,
type = "regular"
};

var parameters = new Dictionary<String, String>();
parameters.Add("vdom", "root");
try
{
PostAsJson(import_certificate_api, cert_resource, parameters);
PostAsJson(import_certificate_api, cert_resource);
}
catch (Exception ex)
{
Expand All @@ -318,8 +319,11 @@

try
{
string endpoint = string.IsNullOrEmpty(mkey) ? available_certificates : available_certificates;
Dictionary<String, String> parameters = mkey == null ? null : new Dictionary<string, string> { { "mkey", mkey } };
string endpoint = available_certificates;
Dictionary<String, String> parameters = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(mkey))
parameters.Add("mkey", mkey);
parameters.Add("vdom", VDOM);
var result = GetResource(endpoint, parameters);
certificates = JsonConvert.DeserializeObject<FortigateResponse<Certificate[]>>(result).results;
}
Expand All @@ -339,47 +343,84 @@
return certificates;
}

public string DownloadFileAsString(string mkey, string type)
public string DownloadFileAsString(string mkey, string type, out bool isError)
{
logger.MethodEntry(LogLevel.Debug);

isError = false;
var parameters = new Dictionary<String, String>();
parameters.Add("mkey", mkey);
parameters.Add("type", type);
parameters.Add("vdom", VDOM);

string content = string.Empty;

try
{
var response = client.GetAsync(GetUrl(download_certificate, parameters)).GetAwaiter().GetResult();
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
throw new Exception($"Error retrieving certificate {mkey}: {content}");

return content;
}
catch (Exception ex)
{
logger.LogError(FortigateException.FlattenExceptionMessages(ex, $"Error retrieving downloading file {mkey}: "));
throw;
isError = true;
}
finally
{
logger.MethodExit(LogLevel.Debug);
}

return content;
}

public void ValidateVDOMScope(string alias)
{
logger.MethodEntry(LogLevel.Debug);

try
{
Certificate[] certs = List(alias);
if (certs.Length > 0 && certs[0].range.ToLower() == "global")
throw new Exception($"Certificate {alias} is scoped as global. Global certificates cannot be replaced or deleted by this integration.");
}
finally
{
logger.MethodExit(LogLevel.Debug);
}
}

private void ValidateVDOM()
{
logger.MethodEntry(LogLevel.Debug);

try
{
var response = client.GetAsync(GetUrl(get_vdom_api + VDOM, null)).GetAwaiter().GetResult();
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
throw new FortigateException($"VDOM {VDOM} not found.");
if (!response.IsSuccessStatusCode)
throw new FortigateException($"Error retrieving VDOM {VDOM}. Status={response.StatusCode.ToString()}, Error={response.Content} {response.ReasonPhrase}");
}
finally
{
logger.MethodExit(LogLevel.Debug);
}
}

private String PostAsJson(string endpoint, cmdb_certificate_resource obj, Dictionary<String, String> additionalParams = null)
private String PostAsJson(string endpoint, cmdb_certificate_resource obj)
{
logger.MethodEntry(LogLevel.Debug);

string content = "";
var url = GetUrl(endpoint, additionalParams);
var url = GetUrl(endpoint);
var stringContent = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json");
stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");

try
{
HttpResponseMessage responseMessage = client.PostAsync(url, stringContent).GetAwaiter().GetResult();
content = responseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var content = responseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (!responseMessage.IsSuccessStatusCode)
throw new Exception($"Error adding certificate {obj.certname}: {content}");

Expand Down
23 changes: 17 additions & 6 deletions Fortigate/Inventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Keyfactor.Logging;
using Microsoft.Extensions.Logging;
using Keyfactor.Orchestrators.Extensions.Interfaces;
using Org.BouncyCastle.Tls;

namespace Keyfactor.Extensions.Orchestrator.Fortigate
{
Expand All @@ -37,19 +38,25 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
ILogger logger = LogHandler.GetClassLogger(this.GetType());
logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}...");
logger.LogDebug($"Client Machine: {config.CertificateStoreDetails.ClientMachine}");

FortigateStore store = new FortigateStore(config.CertificateStoreDetails.ClientMachine, PAMUtilities.ResolvePAMField(_resolver, logger, "Fortigate Access Key", config.CertificateStoreDetails.StorePassword));
logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}");

List<CurrentInventoryItem> inventoryItems = new List<CurrentInventoryItem>();
bool atLeastOneError = false;

try
{
FortigateStore store = new FortigateStore(config.CertificateStoreDetails.ClientMachine, PAMUtilities.ResolvePAMField(_resolver, logger, "Fortigate Access Key", config.CertificateStoreDetails.StorePassword), config.CertificateStoreDetails.StorePath);

Api.Certificate[] certificates = store.List(null);

bool isError;

foreach (var cert in certificates)
{
if (cert.type == "local-cer")
{
var certFile = store.DownloadFileAsString(cert.name, cert.type);
var certFile = store.DownloadFileAsString(cert.name, cert.type, out isError);
if (isError) atLeastOneError = true;

var item = new CurrentInventoryItem()
{
Expand All @@ -66,16 +73,20 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
}
catch (Exception ex)
{
logger.LogError($"Exception for {config.Capability}: {FortigateException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}");
return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = FortigateException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.ClientMachine}") };
logger.LogError($"Exception for {config.Capability}: {FortigateException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId} ");
return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = FortigateException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.ClientMachine} ") };
}

try
{
logger.LogDebug("Sending certificates back to Command:" + inventoryItems.Count);
submitInventory.Invoke(inventoryItems);
logger.LogDebug($"...End {config.Capability} job for job id {config.JobId}");
return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId };

if (atLeastOneError)
return new JobResult() { Result = OrchestratorJobStatusJobResult.Warning, JobHistoryId = config.JobHistoryId, FailureMessage = "At least one certificate was unable to be retrieved, Please check the log for more information." };
else
return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId };
}
catch (Exception ex)
{
Expand Down
9 changes: 6 additions & 3 deletions Fortigate/Management.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ public JobResult ProcessJob(ManagementJobConfiguration config)

logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}...");
logger.LogDebug($"Client Machine: {config.CertificateStoreDetails.ClientMachine}");

FortigateStore store = new FortigateStore(config.CertificateStoreDetails.ClientMachine, PAMUtilities.ResolvePAMField(_resolver, logger, "Fortigate Access Key", config.CertificateStoreDetails.StorePassword));
logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}");

try
{
FortigateStore store = new FortigateStore(config.CertificateStoreDetails.ClientMachine, PAMUtilities.ResolvePAMField(_resolver, logger, "Fortigate Access Key", config.CertificateStoreDetails.StorePassword), config.CertificateStoreDetails.StorePath);

store.ValidateVDOMScope(config.JobCertificate.Alias);

//Management jobs, unlike Discovery, Inventory, and Reenrollment jobs can have 3 different purposes:
switch (config.OperationType)
{
Expand All @@ -74,7 +77,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
}
catch (Exception ex)
{
logger.LogError($"Exception for {config.Capability}: {FortigateException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}");
logger.LogError($"Exception for {config.Capability}: {FortigateException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId} ");
return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = FortigateException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.ClientMachine}:") };
}

Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,18 @@

## Overview

The Fortigate Orchestrator Extension supports the following use cases:
1. Inventory of local user and factory cerificates
2. Ability to add new local certificates
3. Ability to replace bound* and unbound local user certificates (usually after renewal in Keyfactor Command)
4. Ability to delete **unbound** local user certificates
The Fortigate Orchestrator Extension supports the following use cases against a specified VDOM:
1. Inventory of local user and factory VDOM and globally scoped cerificates
2. Ability to add new local VDOM scoped certificates
3. Ability to replace bound* and unbound local user VDOM scoped certificates
4. Ability to delete **unbound** local user VDOM scoped certificates

The Fortigate Orchestrator Extension DOES NOT support the following use cases:
1. The renewal or removal of certificates enrolled through the internal Fortigate CA
2. The renewal or removal of factory certificates
3. The removal of ANY certificate bound to a Fortigate object
4. Certificate enrollment using the internal Fortigate CA (Keyfactor's "reenrollment" or "on device key generation" use case)
4. The renewal/replacement of any globally scoped certificate.
5. Certificate enrollment using the internal Fortigate CA (Keyfactor's "reenrollment" or "on device key generation" use case)

\* Because the Fortigate API does not allow for updating certificates in place, and to avoid temporary outages, when replacing local certificates that are bound, it is necessary to create a new name (alias) for the certificate. The new name is created using the first 8 characters of the previous name (larger names truncated due to Fortigate name length constraints) allong with a suffix comprised of "--" and a 15 character hash of the current date/time. The replaced certificate with the old name is then removed from the Fortigate instance. For example, a bound certificate with the name "CertName" would be replaced and the name would then be "CertName--8DD76A97A98E4C1". The existing bindings would remain in place with the new name. At no point during the management job would any of the bound objects be left without a valid certificate binding.
Currently, the ability to renew bound certificates is limited to these binding types:
Expand Down Expand Up @@ -178,9 +179,8 @@ the Keyfactor Command Portal
| --------- | ----------- | ----------- | ----------- |
| Older than `11.0.0` | | | `net6.0` |
| Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` |
| Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` |
| Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` |
| `11.6` _and_ newer | `net8.0` | | `net8.0` |
| Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` || Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` |
| `11.6` _and_ newer | `net8.0` | | `net8.0` |

Unzip the archive containing extension assemblies to a known location.

Expand Down Expand Up @@ -237,7 +237,7 @@ the Keyfactor Command Portal
| Category | Select "Fortigate" or the customized certificate store name from the previous step. |
| Container | Optional container to associate certificate store with. |
| Client Machine | The IP address or DNS of the Fortigate server |
| Store Path | This is not used in this integration, but is a required field in the UI. Just enter any value here |
| Store Path | Value must contain the VDOM this certificate store will be managing. `root` must be entered to manage the default 'root' VDOM. |
| Store Password | Enter the Fortigate API Token here |
| Orchestrator | Select an approved orchestrator capable of managing `Fortigate` certificates. Specifically, one with the `Fortigate` capability. |

Expand All @@ -263,7 +263,7 @@ the Keyfactor Command Portal
| Category | Select "Fortigate" or the customized certificate store name from the previous step. |
| Container | Optional container to associate certificate store with. |
| Client Machine | The IP address or DNS of the Fortigate server |
| Store Path | This is not used in this integration, but is a required field in the UI. Just enter any value here |
| Store Path | Value must contain the VDOM this certificate store will be managing. `root` must be entered to manage the default 'root' VDOM. |
| Store Password | Enter the Fortigate API Token here |
| Orchestrator | Select an approved orchestrator capable of managing `Fortigate` certificates. Specifically, one with the `Fortigate` capability. |

Expand Down
Loading
Loading