Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f9a5be3
ab93116
leefine02 Aug 3, 2026
10c8663
ab93116
leefine02 Aug 3, 2026
199c2b0
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 3, 2026
5f4f1e8
ab93116
leefine02 Aug 4, 2026
04d09b5
Merge branch 'ab93116' of https://github.com/Keyfactor/f5-rest-orches…
leefine02 Aug 4, 2026
a306518
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 4, 2026
2600849
ab93116
leefine02 Aug 5, 2026
91f8fed
Merge branch 'ab93116' of https://github.com/Keyfactor/f5-rest-orches…
leefine02 Aug 5, 2026
41347de
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 5, 2026
9dd4e5a
ab93116
leefine02 Aug 5, 2026
227ed5a
Merge remote-tracking branch 'origin/ab93116' into ab93116
leefine02 Aug 5, 2026
d152b4a
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 5, 2026
edfad23
Remove RemoveChainOnAdd custom field from F5-PF-REST
leefine02 Aug 5, 2026
9387815
Merge remote-tracking branch 'origin/ab93116' into ab93116
leefine02 Aug 5, 2026
ff39d87
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 5, 2026
c0833ae
ab93116
Aug 5, 2026
865e883
ab93116
Aug 7, 2026
934dd62
Validate inherited profile exists before creating F5-PF-REST profile
leefine02 Aug 7, 2026
4dae9fa
ab93116
Aug 10, 2026
5a641ab
ab93116
Aug 11, 2026
e545c08
ab93116
Aug 12, 2026
5f06025
ab93116
Aug 21, 2026
f272ccf
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 21, 2026
8a8a901
ab93116
Aug 21, 2026
b67a8e9
Merge branch 'ab93116' of https://github.com/Keyfactor/f5-rest-orches…
Aug 21, 2026
f2866e2
ab93116
Aug 26, 2026
58370bb
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 26, 2026
c223282
ab93116
Aug 26, 2026
62b4eb7
Merge branch 'ab93116' of https://github.com/Keyfactor/f5-rest-orches…
Aug 26, 2026
3ad3f38
ab93116
Aug 27, 2026
acf6127
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 27, 2026
f5886e0
ab93116
Aug 27, 2026
26083f3
Merge branch 'ab93116' of https://github.com/Keyfactor/f5-rest-orches…
Aug 27, 2026
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 @@
v2.1.0
- Added new store type - F5-PF-REST - to manage the certificate bound to a single F5 Big IP SSL Profile (client or server only).

v2.0.0
- Add option to sync node to device group pre-set up in F5 device for F5-SL-REST store type.

Expand Down
204 changes: 191 additions & 13 deletions F5Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,27 @@
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// thespecific language governing permissions and limitations under the
// License.
using Keyfactor.Orchestrators.Extensions;
using Keyfactor.Orchestrators.Common.Enums;
using Keyfactor.PKI.X509;
using Keyfactor.Orchestrators.Extensions;
using Keyfactor.PKI.PEM;
using Keyfactor.PKI.X509;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing.Printing;
using System.Linq;
using System.Reflection.Metadata;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;

using Newtonsoft.Json;
using System.Collections;
using System.Collections.Concurrent;
using System.Drawing.Printing;
using System.Diagnostics.CodeAnalysis;
using static Keyfactor.Extensions.Orchestrator.F5Orchestrator.F5ProfileStorePath;
using static Keyfactor.Orchestrators.Common.OrchestratorConstants;
using static Org.BouncyCastle.Math.EC.ECCurve;
using System.Reflection.Metadata;

namespace Keyfactor.Extensions.Orchestrator.F5Orchestrator
{
Expand All @@ -42,6 +43,7 @@
private const string INVALID_KEY_END_DELIM = ")";
private const int MIN_VERSION_SUPPORTED = 14;
private const string VERSION_DELIMITER = "?ver=";
private const int DEFAULT_PROFILE_PAGE_SIZE = 50;

public CertificateStore CertificateStore { get; set; }
public string ServerUserName { get; set; }
Expand Down Expand Up @@ -206,13 +208,13 @@
return exists;
}

public void BindCertificate(string alias, string sslProfile)
public void BindCertificate(string alias, string sslProfile, string certificatePassword)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "BindCertificate");

try
{
F5Binding binding = new F5Binding { cert = $"{alias}", key = $"{alias}", chain = $"{alias}" };
F5Binding binding = new F5Binding { cert = $"{alias}", key = $"{alias}", chain = $"{alias}", passphrase = $"{certificatePassword}" };
REST.Patch<F5Binding>($"/mgmt/tm/ltm/profile/client-ssl/{sslProfile}", binding);
}

Expand Down Expand Up @@ -334,6 +336,9 @@
utilCmdArgs = $"-c 'cat {path} | base64'"
});

if (crt.Length < 80 || crt.Contains("no such file", StringComparison.OrdinalIgnoreCase))
return new X509Certificate2Collection();

byte[] crtBytes;
switch (crt.Substring(0, 1))
{
Expand Down Expand Up @@ -371,10 +376,20 @@
}

public List<F5SSLProfile> GetSSLProfiles(int pageSize)
{
return GetSSLProfiles(pageSize, "client-ssl");
}

public List<F5SSLProfile> GetSSLProfiles(int pageSize, string profileEndpoint)
{
return GetSSLProfiles(pageSize, profileEndpoint, null);
}

public List<F5SSLProfile> GetSSLProfiles(int pageSize, string profileEndpoint, string partition)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "GetSSLProfiles");
string partition = CertificateStore.StorePath;
string query = $"/mgmt/tm/ltm/profile/client-ssl?$top={pageSize}&$skip=0";
string partitionFilter = string.IsNullOrEmpty(partition) ? string.Empty : $"&$filter=partition+eq+{partition}";
string query = $"/mgmt/tm/ltm/profile/{profileEndpoint}?$top={pageSize}&$skip=0{partitionFilter}";
F5PagedSSLProfiles pagedProfiles = REST.Get<F5PagedSSLProfiles>(query);
List<F5SSLProfile> profiles = new List<F5SSLProfile>();

Expand Down Expand Up @@ -439,7 +454,7 @@
certificateBase64 = Regex.Replace(certificateBase64, "\\n-----END CERTIFICATE-----(\\n|)", "");

LogHandlerCommon.Trace(logger, CertificateStore, "Getting X509 object from F5 certificate pem");
X509Certificate2 x509 = new X509Certificate2(Convert.FromBase64String(certificateBase64));

Check warning on line 457 in F5Client.cs

View workflow job for this annotation

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

'X509Certificate2.X509Certificate2(byte[])' is obsolete: 'Loading certificate data through the constructor or Import is obsolete. Use X509CertificateLoader instead to load certificates.' (https://aka.ms/dotnet-warnings/SYSLIB0057)

Check warning on line 457 in F5Client.cs

View workflow job for this annotation

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

'X509Certificate2.X509Certificate2(byte[])' is obsolete: 'Loading certificate data through the constructor or Import is obsolete. Use X509CertificateLoader instead to load certificates.' (https://aka.ms/dotnet-warnings/SYSLIB0057)
LogHandlerCommon.Trace(logger, CertificateStore, $"Looking for CMS thumbprint matching: '{x509.Thumbprint}'");
if (!keyfactorInventoryItem.Thumbprints.Any(t => t.Equals(x509.Thumbprint, StringComparison.OrdinalIgnoreCase)))
{
Expand Down Expand Up @@ -541,6 +556,33 @@
return pathParts[0];
}

// Parses a Profile store path in the form 'Partition\ProfileName\ProfileType[\InheritedProfile]'
public F5ProfileStorePath ParseProfileStorePath()
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "ParseProfileStorePath");
string[] pathParts = CertificateStore.StorePath.Split('/');
if (pathParts.Length != 3)
{
throw new Exception($"The store path '{CertificateStore.StorePath}' is invalid. Expecting 'Partition\\ProfileType\\ProfileName'");
}

if (!Enum.TryParse<ProfileTypeEnum>(pathParts[1], ignoreCase: true, out var profileType) ||
!Enum.IsDefined(typeof(ProfileTypeEnum), profileType))
{
throw new Exception($"Invalid value for profile type: {pathParts[1]}");
}

F5ProfileStorePath profileStorePath = new F5ProfileStorePath
{
Partition = pathParts[0],
ProfileType = profileType,
ProfileName = pathParts[2],
};

LogHandlerCommon.MethodExit(logger, CertificateStore, "ParseProfileStorePath");
return profileStorePath;
}

// Infrastructure
#endregion

Expand Down Expand Up @@ -580,7 +622,7 @@
byte[] devicePfx = Convert.FromBase64String(b64Certificate);
string password = PFXPassword;
CertificateCollectionConverter converter = CertificateCollectionConverterFactory.FromDER(devicePfx, password);
string pfxPem = converter.ToPEM(password);

Check warning on line 625 in F5Client.cs

View workflow job for this annotation

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

'CertificateCollectionConverter.ToPEM(string)' is obsolete: 'The CryptographicObjectFormatter.PEM class should be used for all certificate PEM conversions. '

Check warning on line 625 in F5Client.cs

View workflow job for this annotation

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

'CertificateCollectionConverter.ToPEM(string)' is obsolete: 'The CryptographicObjectFormatter.PEM class should be used for all certificate PEM conversions. '

Check warning on line 625 in F5Client.cs

View workflow job for this annotation

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

'CertificateCollectionConverter.ToPEM(string)' is obsolete: 'The CryptographicObjectFormatter.PEM class should be used for all certificate PEM conversions. '

Check warning on line 625 in F5Client.cs

View workflow job for this annotation

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

'CertificateCollectionConverter.ToPEM(string)' is obsolete: 'The CryptographicObjectFormatter.PEM class should be used for all certificate PEM conversions. '

Check warning on line 625 in F5Client.cs

View workflow job for this annotation

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

'CertificateCollectionConverter.ToPEM(string)' is obsolete: 'The CryptographicObjectFormatter.PEM class should be used for all certificate PEM conversions. '

Check warning on line 625 in F5Client.cs

View workflow job for this annotation

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

'CertificateCollectionConverter.ToPEM(string)' is obsolete: 'The CryptographicObjectFormatter.PEM class should be used for all certificate PEM conversions. '
List<X509Certificate2> clist = converter.ToX509Certificate2List(password);

StringBuilder certPemBuilder = new StringBuilder();
Expand Down Expand Up @@ -733,6 +775,142 @@
// SSL Certificates
#endregion

#region SSL Profiles (Client/Server)

private const string CLIENT_SSL_ENDPOINT = "client-ssl";
private const string SERVER_SSL_ENDPOINT = "server-ssl";

public static string GetProfileEndpoint(ProfileTypeEnum profileType)
{
return profileType == ProfileTypeEnum.Server ? SERVER_SSL_ENDPOINT : CLIENT_SSL_ENDPOINT;
}

public bool ProfileExists(string partition, string profileEndpoint, string profileName)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "ProfileExists");
bool exists = false;

try
{
string query = $"/mgmt/tm/ltm/profile/{profileEndpoint}/~{profileName.Replace($"/","~")}";
F5SSLProfile profile = REST.Get<F5SSLProfile>(query);
exists = (profile != null);
}
catch (F5RESTException rex)
{
// A 404 will be returned if the profile is not found
if (rex.code != 404)
{
throw;
}
}

LogHandlerCommon.MethodExit(logger, CertificateStore, "ProfileExists");
return exists;
}

public void CreateProfile(string partition, string profileEndpoint, string profileName, string inheritedProfile)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "CreateProfile");

string defaultsFrom = string.IsNullOrEmpty(inheritedProfile) ? null : $"/{inheritedProfile}";

F5ProfileCreate profile = new F5ProfileCreate
{
name = profileName,
partition = partition,
defaultsFrom = defaultsFrom
};

REST.Post<F5SSLProfile>($"/mgmt/tm/ltm/profile/{profileEndpoint}", JsonConvert.SerializeObject(profile));

LogHandlerCommon.MethodExit(logger, CertificateStore, "CreateProfile");
}

// Returns the certificate name (alias) currently bound to the profile represented by this store, or null if none bound
public string GetBoundCertificateAlias(string partition, string profileEndpoint, string profileName)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "GetBoundCertificateAlias");

string query = $"/mgmt/tm/ltm/profile/{profileEndpoint}/~{partition}~{profileName}";
F5SSLProfile profile = REST.Get<F5SSLProfile>(query);
string alias = null;
if (!string.IsNullOrEmpty(profile?.cert))
{
string[] certParts = profile.cert.Split('/');
alias = certParts[certParts.Length - 1];
}

LogHandlerCommon.MethodExit(logger, CertificateStore, "GetBoundCertificateAlias");
return alias;
}

// Returns the names (partition/profile) of every client-ssl and server-ssl profile bound to the given certificate alias,
// excluding the profile represented by this store definition (partition/excludeProfileName)
public List<string> GetProfilesBoundToCertificate(string partition, string alias, string excludeProfileName)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "GetProfilesBoundToCertificate");

string certName = $"/{partition}/{alias}";
List<string> boundProfiles = new List<string>();

foreach (string profileEndpoint in new[] { CLIENT_SSL_ENDPOINT, SERVER_SSL_ENDPOINT })
{
List<F5SSLProfile> profiles = GetSSLProfiles(DEFAULT_PROFILE_PAGE_SIZE, profileEndpoint);
boundProfiles.AddRange(profiles
.Where(p => p.cert == certName && !(p.name.Equals(excludeProfileName, StringComparison.OrdinalIgnoreCase)))
.Select(p => p.name));
}

LogHandlerCommon.MethodExit(logger, CertificateStore, "GetProfilesBoundToCertificate");
return boundProfiles;
}

// Reset the profile's cert/key/chain binding back to F5's built-in default, effectively unbinding any custom certificate
public void UnbindCertificate(string partition, string profileEndpoint, string profileName)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "UnbindCertificate");

F5Binding binding = new F5Binding { cert = "/Common/default.crt", key = "/Common/default.key", chain = "none" };
REST.Patch<F5Binding>($"/mgmt/tm/ltm/profile/{profileEndpoint}/~{partition}~{profileName}", binding);

LogHandlerCommon.MethodExit(logger, CertificateStore, "UnbindCertificate");
}

// Bind a certificate/key (and matching chain) already installed in the given partition to the named profile
public void BindCertificateToProfile(string partition, string profileEndpoint, string profileName, string alias, string certificatePassword, bool certificateExists)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "BindCertificateToProfile");

F5Binding binding = new F5Binding { cert = alias, key = alias, chain = alias, passphrase = certificatePassword };
REST.Patch<F5Binding>($"/mgmt/tm/ltm/profile/{profileEndpoint}/~{partition}~{profileName}", binding);

LogHandlerCommon.MethodExit(logger, CertificateStore, "BindCertificateToProfile");
}

// Returns the inventory (single certificate) bound to the profile represented by this store definition
public List<CurrentInventoryItem> GetProfileCertificateInventory(string partition, string profileEndpoint, string profileName)
{
LogHandlerCommon.MethodEntry(logger, CertificateStore, "GetProfileCertificateInventory");
List<CurrentInventoryItem> inventory = new List<CurrentInventoryItem>();

string alias = GetBoundCertificateAlias(partition, profileEndpoint, profileName);
if (string.IsNullOrEmpty(alias) || alias.Equals("none", StringComparison.OrdinalIgnoreCase))
{
LogHandlerCommon.Trace(logger, CertificateStore, $"Profile '{profileName}' in partition '{partition}' has no certificate bound");
LogHandlerCommon.MethodExit(logger, CertificateStore, "GetProfileCertificateInventory");
return inventory;
}

CurrentInventoryItem inventoryItem = GetInventoryItem(partition, alias, true);
LogHandlerCommon.MethodExit(logger, CertificateStore, "GetProfileCertificateInventory");
inventory.Add(inventoryItem);
return inventory;
}

// SSL Profiles (Client/Server)
#endregion

#region Auth & Version

private string GetToken(string userName, string userPassword)
Expand Down
28 changes: 28 additions & 0 deletions F5DataModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ internal class F5PagedSSLProfiles : F5PagedResult
internal class F5SSLProfile
{
public string name { get; set; }
public string partition { get; set; }
public string fullPath { get; set; }
public string cert { get; set; }

[Newtonsoft.Json.JsonProperty("defaultsFrom")]
public string defaultsFrom { get; set; }
}

internal class F5Key
Expand Down Expand Up @@ -141,6 +146,16 @@ internal class F5Binding
public string cert { get; set; }
public string key { get; set; }
public string chain { get; set; }
public string passphrase { get; set; }
}

internal class F5ProfileCreate
{
public string name { get; set; }
public string partition { get; set; }

[Newtonsoft.Json.JsonProperty("defaultsFrom", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string defaultsFrom { get; set; }
}

public class F5Transaction
Expand Down Expand Up @@ -180,6 +195,19 @@ public class F5Version
public string selfLink { get; set; }
}

public class F5ProfileStorePath
{
public enum ProfileTypeEnum
{
Client,
Server
}

public string Partition { get; set; }
public string ProfileName { get; set; }
public ProfileTypeEnum ProfileType { get; set; }
}

public class SyncRequest
{
public SyncRequest(string deviceGroupName)
Expand Down
2 changes: 2 additions & 0 deletions ManagementBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public abstract class ManagementBase : F5JobBase, IManagementJobExtension
protected bool RemoveChain { get; set; }
protected bool SyncDevice { get; set; }
protected string SyncDeviceGroup { get; set; }
protected string InheritedProfile { get; set; }

public string ExtensionName => "Keyfactor.Extensions.Orchestrator.F5Orchestrator.Management";

Expand Down Expand Up @@ -86,6 +87,7 @@ protected void ParseStoreProperties()
SyncDevice = properties.SyncDevice == null || string.IsNullOrEmpty(properties.SyncDevice.Value) ? false : bool.Parse(properties.SyncDevice.Value);
if (SyncDevice)
SyncDeviceGroup = properties.SyncDeviceGroup == null || string.IsNullOrEmpty(properties.SyncDeviceGroup.Value) ? string.Empty : properties.SyncDeviceGroup.Value.ToString();
InheritedProfile = properties.InheritedProfile == null || string.IsNullOrEmpty(properties.InheritedProfile.Value) ? string.Empty : properties.InheritedProfile.Value.ToString();

LogHandlerCommon.Trace(logger, JobConfig.CertificateStoreDetails, $"Ignore SSL Warnings '{IgnoreSSLWarning.ToString()}'");
}
Expand Down
Loading
Loading