Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d9195c5
Update generated docs
Oct 13, 2025
c5af4f9
ab#77142
Oct 14, 2025
f2d91a6
Merge branch 'ab#77142' of https://github.com/Keyfactor/fortigate-orc…
Oct 14, 2025
a9a47d5
ab#77142
Oct 15, 2025
8f6e61a
Update generated docs
Oct 15, 2025
4675fd8
chore(ci): Bump build workflow to v4
spbsoluble Nov 19, 2025
7aeaaf1
Update generated docs
Nov 19, 2025
6da2f5b
Update generated docs
Nov 20, 2025
e052b1a
Merge pull request #7 from Keyfactor/ab#77142
spbsoluble Nov 20, 2025
6a300ee
Update generated docs
Dec 3, 2025
dbd9f2f
ab#79187
Dec 3, 2025
7e8ceac
ab#79187
Dec 15, 2025
bc47984
Update generated docs
Dec 15, 2025
7cf7b25
ab#79187
Dec 16, 2025
a9b8948
Merge branch 'ab#79187' of https://github.com/Keyfactor/fortigate-orc…
Dec 16, 2025
26fd998
ab#79187
Dec 17, 2025
18cadf5
ab#79187
Dec 17, 2025
ade74af
ab#79187
Dec 19, 2025
882afa9
ab#79187
Dec 22, 2025
55cfbf3
ab#79187
Dec 22, 2025
4309f22
Update generated docs
Dec 22, 2025
446c8de
ab#79187
Dec 26, 2025
4434423
Merge branch 'ab#79187' of https://github.com/Keyfactor/fortigate-orc…
Dec 26, 2025
79ea01d
ab#79187
Dec 26, 2025
e499a5b
Update generated docs
Dec 26, 2025
c3e5e5f
ab#79187
Dec 26, 2025
d9e3136
Merge branch 'ab#79187' of https://github.com/Keyfactor/fortigate-orc…
Dec 26, 2025
23b39a9
Update generated docs
Dec 26, 2025
94f6ef0
Merge branch 'ab#79187' into release-1.3
leefine02 Dec 26, 2025
8292db9
Merge pull request #10 from Keyfactor/release-1.3
leefine02 Dec 26, 2025
02aedf5
Update generated docs
Dec 26, 2025
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
13 changes: 10 additions & 3 deletions .github/workflows/keyfactor-starter-workflow.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Keyfactor Bootstrap Workflow
name: Keyfactor Bootstrap Workflow

on:
workflow_dispatch:
Expand All @@ -11,10 +11,17 @@ on:

jobs:
call-starter-workflow:
uses: keyfactor/actions/.github/workflows/starter.yml@3.1.2
uses: keyfactor/actions/.github/workflows/starter.yml@v4
with:
command_token_url: ${{ vars.COMMAND_TOKEN_URL }}
command_hostname: ${{ vars.COMMAND_HOSTNAME }}
command_base_api_path: ${{ vars.COMMAND_API_PATH }}
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
scan_token: ${{ secrets.SAST_TOKEN }}
entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }}
entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }}
command_client_id: ${{ secrets.COMMAND_CLIENT_ID }}
command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
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

v1.2.0
- Allow for the management (renew/replace) of bound certificates

Expand Down
36 changes: 36 additions & 0 deletions Fortigate/Api/HttpsUsage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//Copyright 2023 Keyfactor
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.

using Newtonsoft.Json;
using System.Text.Json.Serialization;

namespace Keyfactor.Extensions.Orchestrator.Fortigate.Api
{
public class HttpsUsage
{
[JsonProperty("admin-server-cert")]
public string AdminServerCert { get; set; }
}

public class HttpUsageRequest
{
[JsonProperty("admin-server-cert")]
public OriginKey AdminServerCert { get; set; }
}

public class OriginKey
{
[JsonProperty("q_origin_key")]
public string QOriginKey { get; set; }
}
}
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; }
}
}
4 changes: 2 additions & 2 deletions Fortigate/Fortigate.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
Expand All @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.0.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageReference Include="Keyfactor.Logging" Version="1.1.1" />
<PackageReference Include="Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="0.7.0" />

Expand Down
148 changes: 120 additions & 28 deletions Fortigate/FortigateStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@
using System.Text;
using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
using Org.BouncyCastle.Asn1.Ocsp;

namespace Keyfactor.Extensions.Orchestrator.Fortigate
{
public class FortigateStore
{
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 @@ -45,14 +47,14 @@
//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

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/";

private static readonly string cert_usage_api = "/api/v2/monitor/system/object/usage";
private static readonly string https_usage_api = "/api/v2/cmdb/system/global";

private readonly HttpClientHandler handler = new HttpClientHandler()
{
Expand All @@ -61,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 @@ -70,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 @@ -78,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 @@ -104,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 @@ -127,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 All @@ -149,6 +154,46 @@
}
}

public string HttpsServerUsage()
{
logger.MethodEntry(LogLevel.Debug);

try
{
var result = GetResource(https_usage_api, new Dictionary<String, String>());
return JsonConvert.DeserializeObject<FortigateResponse<HttpsUsage>>(result).results.AdminServerCert;
}
catch (Exception ex)
{
logger.LogError(FortigateException.FlattenExceptionMessages(ex, $"Error checking https bindings: "));
throw;
}
finally
{
logger.MethodExit(LogLevel.Debug);
}
}

public void UpdateHttpsServerUsage(string alias)
{
logger.MethodEntry(LogLevel.Debug);
HttpUsageRequest request = new HttpUsageRequest() { AdminServerCert = new OriginKey() { QOriginKey = alias } };

try
{
PutAsJson(https_usage_api, request, new Dictionary<string, string>());
}
catch (Exception ex)
{
logger.LogError(FortigateException.FlattenExceptionMessages(ex, $"Error updating https server binding: "));
throw;
}
finally
{
logger.MethodExit(LogLevel.Debug);
}
}

public void Insert(string alias, string cert, string privateKey, bool overwrite, string password = null)
{
logger.MethodEntry(LogLevel.Debug);
Expand All @@ -170,9 +215,10 @@

//check to see if it's in use
existingUsage = Usage(alias, certItem.q_type);
bool existingHttpsUsage = HttpsServerUsage() == alias;

//if it's currently in use
if (existingUsage != null && existingUsage.currently_using != null && existingUsage.currently_using.Length > 0)
if ((existingUsage != null && existingUsage.currently_using != null && existingUsage.currently_using.Length > 0) || existingHttpsUsage)
{
//if newAlias exists, end with error
if (byNewAlias.Length > 0)
Expand All @@ -184,10 +230,18 @@
logger.LogDebug("Inserting alias:" + newAlias);
Insert(newAlias, cert, privateKey, password);

foreach (var existingUsing in existingUsage.currently_using)
if (existingUsage != null && existingUsage.currently_using != null && existingUsage.currently_using.Length > 0)
{
logger.LogDebug($"Update binding for path/name/attribute {existingUsing.path}/{existingUsing.name}/{existingUsing.attribute} for new alias {newAlias}");
UpdateUsage(newAlias, existingUsing.path, existingUsing.name, existingUsing.attribute);
foreach (var existingUsing in existingUsage.currently_using)
{
logger.LogDebug($"Update binding for path/name/attribute {existingUsing.path}/{existingUsing.name}/{existingUsing.attribute} for new alias {newAlias}");
UpdateUsage(newAlias, existingUsing.path, existingUsing.name, existingUsing.attribute);
}
}

if (existingHttpsUsage)
{
UpdateHttpsServerUsage(newAlias);
}

logger.LogDebug("Deleting alias:" + alias);
Expand Down Expand Up @@ -238,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 @@ -267,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 @@ -288,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
Loading