diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml
index 6c03689..d906128 100644
--- a/.github/workflows/keyfactor-bootstrap-workflow.yml
+++ b/.github/workflows/keyfactor-bootstrap-workflow.yml
@@ -11,19 +11,9 @@ on:
jobs:
call-starter-workflow:
- uses: keyfactor/actions/.github/workflows/starter.yml@v4
- permissions:
- contents: write # Explicitly grant write permission
- with:
- command_token_url: ${{ vars.COMMAND_TOKEN_URL }}
- command_hostname: ${{ vars.COMMAND_HOSTNAME }}
- command_base_api_path: ${{ vars.COMMAND_API_PATH }}
+ uses: keyfactor/actions/.github/workflows/starter.yml@v5
secrets:
- token: ${{ secrets.V2BUILDTOKEN}}
- 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 }}
+ token: ${{ secrets.V2BUILDTOKEN}} # REQUIRED
+ gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} # Only required for golang builds
+ gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} # Only required for golang builds
+ scan_token: ${{ secrets.SAST_TOKEN }} # REQUIRED
\ No newline at end of file
diff --git a/AxisIPCamera/AxisIPCamera.csproj b/AxisIPCamera/AxisIPCamera.csproj
index 020f565..60e8b00 100644
--- a/AxisIPCamera/AxisIPCamera.csproj
+++ b/AxisIPCamera/AxisIPCamera.csproj
@@ -2,47 +2,52 @@
true
- net6.0;net8.0
+ net8.0;net10.0truedisableKeyfactor.Extensions.Orchestrator.AxisIPCamera
+ 1.1.0
-
-
-
-
- Always
-
-
-
-
-
-
-
- Always
-
-
-
- Always
-
-
-
- Always
-
-
-
- Always
-
-
-
- Always
-
-
-
- Always
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+ Always
+
+
+
+ Always
+
+
+
+ Always
+
+
+
+ Always
+
+
+
+ Always
+
+
+
+ Always
+
diff --git a/AxisIPCamera/Client/AxisHttpClient.cs b/AxisIPCamera/Client/AxisHttpClient.cs
index 7f594dc..0353ffc 100644
--- a/AxisIPCamera/Client/AxisHttpClient.cs
+++ b/AxisIPCamera/Client/AxisHttpClient.cs
@@ -1,4 +1,4 @@
-// Copyright 2025 Keyfactor
+// Copyright 2026 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,
@@ -17,12 +17,12 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RestSharp;
-using RestSharp.Authenticators;
using Keyfactor.Logging;
using Keyfactor.Orchestrators.Extensions;
-using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model;
using Keyfactor.Orchestrators.Extensions.Interfaces;
+using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Exceptions;
+using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model;
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers;
/* AxisHttpClient.cs
@@ -67,46 +67,62 @@ public AxisHttpClient(JobConfiguration config, CertificateStore store, IPAMSecre
try
{
var errorContext = new CertificateErrorContext();
-
+
Logger = LogHandler.GetClassLogger();
Logger.LogTrace("Entered AxisHttpClient constructor.");
Logger.LogTrace("Initializing Axis IP Camera HTTP client");
-
+
// ** NOTE: Ignoring the default config.UseSSL custom field --- we will always connect to the device via HTTPS
var baseRestClientUrl = $"https://{store.ClientMachine}";
-
+
Logger.LogDebug($"Base HTTP client URL: {baseRestClientUrl}");
- // Initialize custom HTTP handler to validate device identity
- RestClientOptions options = null;
- Logger.LogTrace($"Adding custom TLS cert validator to the HTTP client options...");
+ // Retrieve username and password credentials to connect to the device
+ Logger.LogTrace("Adding device credentials to the HTTP client options...");
+ string username = PAMUtilities.ResolvePAMField(resolver, Logger, "API Username", config.ServerUsername);
+ string password = PAMUtilities.ResolvePAMField(resolver, Logger, "API Password", config.ServerPassword);
+
+ // See ADR-0001 Axis Camera Authentication Negotiation
+ // https://keyfactor.atlassian.net/wiki/spaces/IoTStrategy/pages/2968584197/ADR-0001+Axis+Camera+Authentication+Negotiation
+ //
+ // The client intentionally uses HttpClientHandler credentials
+ // rather than RestSharp's HttpBasicAuthenticator to allow
+ // automatic negotiation of Basic vs Digest authentication
+ // based on the authentication challenge presented by the camera.
+ Logger.LogInformation($"Adding custom TLS cert validator to the HTTP client options.");
+ Logger.LogInformation($"Using HttpClientHandler credential negotiation for camera authentication.");
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
- DeviceCertValidator.GetValidator(store.StorePath, errorContext, Logger)
+ DeviceCertValidator.GetValidator(
+ store.StorePath,
+ errorContext,
+ Logger),
+
+ Credentials = new NetworkCredential(username, password),
+
+ PreAuthenticate =
+ false // PreAuthenticate is set to false to avoid the default behavior of sending the username and password in the Authorization header
};
+ // End ADR-0001
// Initialize HTTP client options with the base URL and custom TLS cert validator
- options = new RestClientOptions(baseRestClientUrl)
+ RestClientOptions options = new RestClientOptions(baseRestClientUrl)
{
ConfigureMessageHandler = _ => handler
};
- // Add Basic Auth username and password credentials
- Logger.LogTrace("Adding Basic Auth Credentials to the HTTP client options...");
- string username = PAMUtilities.ResolvePAMField(resolver, Logger, "API Username", config.ServerUsername);
- string password = PAMUtilities.ResolvePAMField(resolver, Logger, "API Password", config.ServerPassword);
-
- options.Authenticator = new HttpBasicAuthenticator(username, password);
-
// Add SSL validation
Logger.LogTrace("Validating connection to the device...");
_httpClient = new RestClient(options);
- var request = new RestRequest("/"); // Initiates the TLS handshake to retrieve the server cert
+ // Initiates the TLS handshake to verify the ability to authenticate to the camera
+ var request = new RestRequest("axis-cgi/param.cgi?action=list&group=Network.HTTP.AuthenticationPolicy");
var response = _httpClient.Execute(request);
- // Build the list of errors to log to the console
+ Logger.LogTrace($"Connection to the device response status code: {response.StatusCode}");
+
+ // Build the list of SSL certificate errors and log to the console
StringBuilder errorSb = new StringBuilder();
if (errorContext.HasErrors)
{
@@ -114,17 +130,55 @@ public AxisHttpClient(JobConfiguration config, CertificateStore store, IPAMSecre
{
errorSb.AppendLine(error);
}
- throw new Exception(errorSb.ToString());
+
+ throw new DeviceCertValidationException(
+ $"Device TLS cert validator errors encountered --- {errorSb}");
}
-
- Logger.LogTrace($"Connection to the device response status code: {response.StatusCode}");
+
+ // Begin ADR-0001 Axis Camera Authentication Negotiation
+ // Log the WWW-Authenticate headers if 401 Unauthorized returned
+ // Throw exception if connection cannot be made successfully to the camera
+ if (response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ Logger.LogWarning("Camera returned 401 Unauthorized");
+
+ foreach (var header in response.Headers)
+ {
+ Logger.LogDebug($"WWW-Authenticate header: {header.Value}");
+ }
+
+ throw new AuthenticationException(
+ "Authentication to the Axis camera failed due to 401 Unauthorized. Verify the configured credentials.");
+ }
+
+ if (!response.IsSuccessful)
+ {
+ throw new Exception(response.ErrorMessage);
+ }
+ // End ADR-0001
+
Logger.LogTrace("Completed Initialization of Axis IP Camera HTTP Client");
Logger.LogTrace("Leaving AxisHttpClient constructor.");
}
- catch (Exception e)
+ catch (DeviceCertValidationException ex)
+ {
+ Logger.LogError("Device TLS cert validation failed while connecting to the device: " + LogHandler.FlattenException(ex));
+ throw new Exception(ex.Message);
+ }
+ catch (AuthenticationException ex1)
{
- Logger.LogError("Error initializing Axis IP Camera HTTP Client: " + LogHandler.FlattenException(e));
- throw new Exception($"Device identity could not be verified successfully --- {e.Message}");
+ Logger.LogError("Authentication to the device failed: " + LogHandler.FlattenException(ex1));
+ throw new Exception(ex1.Message);
+ }
+ catch (HttpRequestException ex2)
+ {
+ Logger.LogError("Failed to communicate with the device: " + LogHandler.FlattenException(ex2));
+ throw new Exception(ex2.Message);
+ }
+ catch (Exception ex3)
+ {
+ Logger.LogError("Unexpected error while connecting to the device: " + LogHandler.FlattenException(ex3));
+ throw new Exception(ex3.Message);
}
}
@@ -524,30 +578,79 @@ public void RemoveCACertificate(string alias)
Logger.LogError($"HTTP Request unsuccessful - HTTP Response: {DecodeHttpStatus(httpResponse)}");
throw new Exception($"HTTP Request unsuccessful.");
}
+
// Decode the API response when HTTP response is successful
+ if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content))
+ {
+ throw new Exception("No content returned from HTTP Response");
+ }
+
+ RestApiResponse apiResponse = JsonConvert.DeserializeObject(httpResponse.Content);
+ if (apiResponse.Status == Constants.Status.Success)
+ {
+ Logger.MethodExit();
+ }
else
{
- if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content))
- {
- throw new Exception("No content returned from HTTP Response");
- }
+ ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content);
+ throw new Exception(
+ $"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})");
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.LogError("Error completing CA certificate remove: " + LogHandler.FlattenException(e));
+ throw new Exception(e.Message);
+ }
+ }
+
+ ///
+ /// Removes a certificate with private key from the device.
+ ///
+ /// Unique identifier of the CA certificate to be removed
+ public HttpResult RemoveCertificate(string alias)
+ {
+ try
+ {
+ Logger.MethodEntry();
+ var context = new HttpContext();
+
+ var deleteCertResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}";
+ var httpResponse = ExecuteHttp(deleteCertResource, Method.Delete);
+
+ // Decode the HTTP response if failed
+ if (httpResponse is { IsSuccessful: false })
+ {
+ var decodedStatus = DecodeHttpStatus(httpResponse);
+
+ Logger.LogWarning($"HTTP Request unsuccessful - HTTP Response: {decodedStatus}");
+ context.AddWarning(decodedStatus);
+ }
+
+ // Decode the API response for more information
+ if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content))
+ {
+ Logger.LogError("No content returned from HTTP Response");
+ context.AddError($"No content returned from HTTP Response for {nameof(Method.Delete)} {deleteCertResource}");
+ }
+ else
+ {
RestApiResponse apiResponse = JsonConvert.DeserializeObject(httpResponse.Content);
- if (apiResponse.Status == Constants.Status.Success)
- {
- Logger.MethodExit();
- }
- else
+ if (apiResponse.Status != Constants.Status.Success)
{
ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content);
- throw new Exception(
- $"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})");
+ Logger.LogWarning($"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})");
+ context.AddWarning($"HTTP Request {nameof(Method.Delete)} {deleteCertResource}: API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})");
}
}
+
+ Logger.MethodExit();
+ return context.ToResult();
}
catch (Exception e)
{
- Logger.LogError("Error completing CA certificate remove: " + LogHandler.FlattenException(e));
+ Logger.LogError("Error completing certificate remove: " + LogHandler.FlattenException(e));
throw new Exception(e.Message);
}
}
diff --git a/AxisIPCamera/Exceptions/AuthenticationException.cs b/AxisIPCamera/Exceptions/AuthenticationException.cs
new file mode 100644
index 0000000..735b7df
--- /dev/null
+++ b/AxisIPCamera/Exceptions/AuthenticationException.cs
@@ -0,0 +1,26 @@
+// Copyright 2026 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 System;
+
+namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Exceptions
+{
+ public class AuthenticationException : Exception
+ {
+ public AuthenticationException(string message)
+ : base(message)
+ {
+ }
+
+ public AuthenticationException(
+ string message,
+ Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/AxisIPCamera/Exceptions/DeviceCertValidationException.cs b/AxisIPCamera/Exceptions/DeviceCertValidationException.cs
new file mode 100644
index 0000000..f315b3b
--- /dev/null
+++ b/AxisIPCamera/Exceptions/DeviceCertValidationException.cs
@@ -0,0 +1,26 @@
+// Copyright 2026 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 System;
+
+namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Exceptions
+{
+ public class DeviceCertValidationException : Exception
+ {
+ public DeviceCertValidationException(string message)
+ : base(message)
+ {
+ }
+
+ public DeviceCertValidationException(
+ string message,
+ Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/AxisIPCamera/Helpers/DeviceCertValidator.cs b/AxisIPCamera/Helpers/DeviceCertValidator.cs
index f24a662..f705a04 100644
--- a/AxisIPCamera/Helpers/DeviceCertValidator.cs
+++ b/AxisIPCamera/Helpers/DeviceCertValidator.cs
@@ -73,6 +73,12 @@ public static class DeviceCertValidator
// Add TLS cert as leaf certificate to the end of the custom chain
customChain.Add(parser.ReadCertificate(cert.RawData));
+
+ if (!File.Exists(trustedIntCertPath))
+ {
+ logger.LogError($"{trustedIntCertPath} does not exist.");
+ return false;
+ }
logger.LogTrace($"Loading Trusted Intermediate Certs from {trustedIntCertPath}");
var trustedIntCerts = parser.ReadCertificates(File.ReadAllBytes(trustedIntCertPath));
@@ -92,6 +98,12 @@ public static class DeviceCertValidator
logger.LogTrace($"{trustedIntCerts.Count} Trusted Intermediate Certs found");
+ if (!File.Exists(trustedRootCertPath))
+ {
+ logger.LogError($"{trustedRootCertPath} does not exist.");
+ return false;
+ }
+
logger.LogTrace($"Loading Trusted Root Cert from {trustedRootCertPath}");
var trustedRootCerts = parser.ReadCertificates(File.ReadAllBytes(trustedRootCertPath));
@@ -214,8 +226,8 @@ private static bool VerifyAkiSkiChain(List customChain, ILogger
{
logger.MethodEntry();
- logger.LogTrace("Custom chain being validated includes: (1) Leaf cert from TLS session, (2) n-Intermediate certs from custom trust, &" +
- "n-Root certs from custom trust");
+ logger.LogTrace("Custom chain being validated includes: (1) Leaf cert from TLS session, (2) n-Intermediate certs from custom trust, & " +
+ "(3) n-Root certs from custom trust");
for (int i = 0; i < customChain.Count - 1; i++)
{
diff --git a/AxisIPCamera/Helpers/SANBuilder.cs b/AxisIPCamera/Helpers/SANBuilder.cs
new file mode 100644
index 0000000..5b569d8
--- /dev/null
+++ b/AxisIPCamera/Helpers/SANBuilder.cs
@@ -0,0 +1,64 @@
+// Copyright 2026 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 System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers
+{
+ public static class SANBuilder
+ {
+ public static List BuildSANList(Dictionary sans, ILogger logger)
+ {
+ var parts = new List();
+
+ if (sans == null || sans.Count == 0)
+ {
+ logger.LogTrace($"SANs is null or empty");
+ return parts;
+ }
+
+ foreach (var entry in sans)
+ {
+ string key = NormalizeSanKey(entry.Key);
+
+ // The Axis API only supports the addition of 'dns' and 'ip' SAN type keys
+ if (key is not ("DNS" or "IP"))
+ continue;
+
+ if (entry.Value == null || entry.Value.Length == 0)
+ continue;
+
+ // NOTE: We are separating the key and value pairs with a colon because this is the format
+ // required to send SANs to the Axis API endpoint
+ parts.AddRange(
+ entry.Value
+ .Where(v => !string.IsNullOrWhiteSpace(v))
+ .Select(v => $"{key}:{v.Trim()}")
+ );
+ }
+
+ return parts;
+ }
+
+ ///
+ /// Normalize SAN type keys to RFC-compliant names.
+ /// **NOTE: The Axis API only supports the addition of 'dns' and 'ip' SAN types.
+ /// Courtesy of B.Pokorny.
+ ///
+ private static string NormalizeSanKey(string key)
+ {
+ return key.Trim().ToLower() switch
+ {
+ "dns" => "DNS",
+ "ip" or "ip4" or "ip6" => "IP",
+ _ => key.ToLower() // default
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/AxisIPCamera/Inventory.cs b/AxisIPCamera/Inventory.cs
index ca579c9..921946a 100644
--- a/AxisIPCamera/Inventory.cs
+++ b/AxisIPCamera/Inventory.cs
@@ -48,7 +48,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
_logger.MethodEntry();
_logger.LogTrace($"Begin Inventory for Client Machine {config.CertificateStoreDetails.ClientMachine}...");
- string jsonConfig = JsonConvert.SerializeObject(config);
+ string jsonConfig = JsonConvert.SerializeObject(config, Formatting.Indented);
_logger.LogDebug($"Inventory Config: {jsonConfig.Replace(config.ServerPassword,"**********")}");
_logger.LogTrace("Create HTTPS client to connect to device");
@@ -62,8 +62,9 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
_logger.LogTrace("Retrieve all client certificates");
CertificateData data2 = client.ListCertificates();
+ // TODO: Remove this if not using
// Get the default keystore
- _logger.LogTrace("Retrieve the default keystore");
+ /*_logger.LogTrace("Retrieve the default keystore");
Constants.Keystore defaultKeystore = client.GetDefaultKeystore();
string defaultKeystoreString = defaultKeystore.ToString();
_logger.LogDebug($"Inventory - Default keystore: {defaultKeystoreString}");
@@ -78,7 +79,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
foreach (var cert in data2.Certs.Where(cert => cert.Keystore == defaultKeystore))
{
data2DefKey.Certs.Add(cert);
- }
+ }*/
_logger.LogTrace("Retrieve all certificate bindings for each possible certificate usage type");
// Lookup the certificate used for HTTPS, MQTT, IEEE802.X
@@ -88,7 +89,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
// Set the binding on the client certificates object if the aliases found for each certificate usage match
_logger.LogTrace("Mark each client certificate with the appropriate certificate usage type");
- foreach (Certificate c in data2DefKey.Certs)
+ foreach (Certificate c in data2.Certs)
{
if (c.Alias.Equals(httpAlias))
{
@@ -113,7 +114,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
}
}
- // Build the list of CA certificates and add to the InventoryItems object that is sent back to Command
+ // Build the list of CA certificates and add to the InventoryItems object sent back to Command
inventoryItems.AddRange(data1.CACerts.Select(
c =>
{
@@ -130,8 +131,8 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
}
}).Where(item => item?.Certificates != null).ToList());
- // Build the list of client certificates and add to the InventoryItems object that is sent back to Command
- inventoryItems.AddRange(data2DefKey.Certs.Select(
+ // Build the list of client certificates and add to the InventoryItems object sent back to Command
+ inventoryItems.AddRange(data2.Certs.Select(
c =>
{
try
diff --git a/AxisIPCamera/Management.cs b/AxisIPCamera/Management.cs
index 357e4f3..62dcf9a 100644
--- a/AxisIPCamera/Management.cs
+++ b/AxisIPCamera/Management.cs
@@ -1,4 +1,4 @@
-// Copyright 2025 Keyfactor
+// Copyright 2026 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,
@@ -258,7 +258,11 @@ private static bool IsCACertificate(string certBase64Der)
byte[] certBytes = Convert.FromBase64String(certBase64Der);
// Create an X509 object so we can analyze the contents
- X509Certificate2 certToAdd = new X509Certificate2(certBytes);
+ #if NET9_0_OR_GREATER
+ X509Certificate2 certToAdd = X509CertificateLoader.LoadCertificate(certBytes);
+ #else
+ X509Certificate2 certToAdd = new X509Certificate2(certBytes);
+ #endif
foreach (X509Extension ext in certToAdd.Extensions)
{
diff --git a/AxisIPCamera/Model/Constants.cs b/AxisIPCamera/Model/Constants.cs
index 2f182e0..7854103 100644
--- a/AxisIPCamera/Model/Constants.cs
+++ b/AxisIPCamera/Model/Constants.cs
@@ -1,4 +1,4 @@
-// Copyright 2025 Keyfactor
+// Copyright 2026 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,
@@ -6,7 +6,9 @@
// and limitations under the License.
using System;
+using System.Globalization;
using System.IO;
+using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
@@ -16,7 +18,7 @@ namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model
public static class Constants
{
// This is the API entry point for the REST VAPIX Cert Management API
- public static string RestApiEntryPoint = "/config/rest/cert/v1beta";
+ public static string RestApiEntryPoint = "/config/rest/cert/v1";
// This is the API entry point for the SOAP Cert Management API
public static string SoapApiEntryPoint = "/vapix/services";
@@ -94,16 +96,16 @@ public static string MapKeyType(string keyAlgorithm, string keySize)
{
"RSA" when keySize == "2048" => "RSA-2048",
"RSA" when keySize == "4096" => "RSA-4096",
- "ECP" when keySize == "256" => "EC-P256",
- "ECP" when keySize == "384" => "EC-P384",
- "ECP" when keySize == "521" => "EC-P521",
+ "ECDSA" when keySize == "256" => "EC-P256",
+ "ECDSA" when keySize == "384" => "EC-P384",
+ "ECDSA" when keySize == "521" => "EC-P521",
_ => "UNKNOWN"
};
}
///
/// Maps the certificate usage enum values to the corresponding string values that are configured
- /// for the "Certificate Usage" entry parameter inside of Command. The string values *MUST* match
+ /// for the "Certificate Usage" entry parameter for the certificate store type in Command. The string values *MUST* match
/// exactly the value configured for the certificate usage entry parameter in Command.
///
///
@@ -248,4 +250,31 @@ public override void WriteJson(
}
}
}
+
+ public static class CertificateName
+ {
+ ///
+ /// Returns a UTC-based suffix, i.e. "2602171544"
+ ///
+ public static string GetUtcSuffix() =>
+ DateTime.UtcNow.ToString("yyMMddHHmm", CultureInfo.InvariantCulture);
+
+ ///
+ /// Creates a unique certificate name by appending ['_' + Utc DateTime suffix] to the end of the user-supplied certificate name.
+ /// Example: "_2602171544"
+ ///
+ public static string CreateUniqueCertName(string certName)
+ {
+ // check to see if the old cert name had a previously appended timestamp
+ // EDGE CASE: Scenario under which this could happen - Cert name bound to usage is known and used to schedule an ODKG job
+ Regex rgx = new Regex(@"_[0-9]{10}$",RegexOptions.CultureInvariant);
+ var m = Regex.Match(certName,@"_[0-9]{10}$");
+ if (m.Success)
+ {
+ return certName.Remove(m.Index, m.Length) + "_" + GetUtcSuffix();
+ }
+
+ return certName + "_" + GetUtcSuffix();
+ }
+ }
}
diff --git a/AxisIPCamera/Model/HttpResponse.cs b/AxisIPCamera/Model/HttpResponse.cs
new file mode 100644
index 0000000..d1a0441
--- /dev/null
+++ b/AxisIPCamera/Model/HttpResponse.cs
@@ -0,0 +1,68 @@
+// Copyright 2026 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 System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model
+{
+ public sealed class HttpContext
+ {
+ private readonly List _warnings = new();
+ private readonly List _errors = new();
+
+ public IReadOnlyList Warnings => _warnings;
+ public IReadOnlyList Errors => _errors;
+
+ public void AddWarning(string message) =>
+ _warnings.Add(message);
+
+ public void AddError(string message) =>
+ _errors.Add(message);
+
+ public HttpResult ToResult()
+ {
+ if (_errors.Any())
+ return HttpResult.Error(FormatMessages(_errors));
+
+ if (_warnings.Any())
+ return HttpResult.Warning(FormatMessages(_warnings));
+
+ return HttpResult.Success();
+ }
+
+ private static string FormatMessages(IEnumerable messages)
+ {
+ return string.Join(
+ Environment.NewLine,
+ messages.Select((message, index) =>
+ $"({index + 1}) {message}")
+ );
+ }
+ }
+
+ public enum HttpStatus
+ {
+ Success,
+ Warning,
+ Error
+ }
+
+ public sealed record HttpResult(HttpStatus Status, string Message = null)
+ {
+ public static HttpResult Success()
+ => new(HttpStatus.Success);
+
+ public static HttpResult Warning(string message)
+ => new(HttpStatus.Warning, message);
+
+ public static HttpResult Error(string message)
+ => new(HttpStatus.Error, message);
+
+ }
+}
\ No newline at end of file
diff --git a/AxisIPCamera/Reenrollment.cs b/AxisIPCamera/Reenrollment.cs
index afd519b..691eb57 100644
--- a/AxisIPCamera/Reenrollment.cs
+++ b/AxisIPCamera/Reenrollment.cs
@@ -1,4 +1,4 @@
-// Copyright 2025 Keyfactor
+// Copyright 2026 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,
@@ -6,7 +6,6 @@
// and limitations under the License.
using System;
-using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
@@ -14,6 +13,7 @@
using Keyfactor.Logging;
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Client;
+using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers;
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model;
using Keyfactor.Orchestrators.Common.Enums;
using Keyfactor.Orchestrators.Extensions;
@@ -25,7 +25,7 @@ public class Reenrollment : IReenrollmentJobExtension
{
private readonly ILogger _logger;
- private readonly IPAMSecretResolver _resolver;
+ private readonly IPAMSecretResolver _resolver;
public string ExtensionName => "";
public Reenrollment(IPAMSecretResolver resolver)
@@ -42,7 +42,7 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
_logger.MethodEntry();
_logger.LogTrace($"Begin Reenrollment for Client Machine {config.CertificateStoreDetails.ClientMachine}");
- string jsonConfig = JsonConvert.SerializeObject(config);
+ string jsonConfig = JsonConvert.SerializeObject(config, Formatting.Indented);
_logger.LogDebug($"Reenrollment Config: {jsonConfig.Replace(config.ServerPassword,"**********")}");
// Log each key-value pair in the Job Properties for debugging
@@ -53,57 +53,73 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
}
_logger.LogDebug("--- End Job Properties");
+ // Log each SAN, if provided
+ _logger.LogDebug("Begin SANs ---");
+ var formattedSANs = SANBuilder.BuildSANList(config.SANs,_logger);
+ if (formattedSANs.Count == 0)
+ {
+ _logger.LogDebug($"No SAN values found.");
+ }
+ else
+ {
+ foreach (var san in formattedSANs)
+ {
+ _logger.LogDebug($"{san}");
+ }
+ }
+ _logger.LogDebug("--- End SANs");
+
// Get required reenrollment fields
string certUsage = config.JobProperties[Constants.CertUsageParamName].ToString() ?? throw new Exception($"{Constants.CertUsageParamDisplay} returned null");
var certUsageEnum = Constants.GetCertUsageAsEnum(certUsage);
string keyAlgorithm = config.JobProperties["keyType"].ToString() ?? throw new Exception("Key Algorithm returned null");
string keySize = config.JobProperties["keySize"].ToString() ?? throw new Exception("Key Size returned null");
string subject = config.JobProperties["subjectText"].ToString() ?? throw new Exception("Subject returned null");
- string reenrollAlias = config.Alias ?? throw new Exception("Alias returned null");
- _logger.LogDebug($"Alias: {reenrollAlias}");
+ string newAlias = config.Alias ?? throw new Exception("Alias returned null");
+ _logger.LogDebug($"Alias: {newAlias}");
// Prevent reenrollment on Trust certificates
if (certUsageEnum is Constants.CertificateUsage.Trust)
{
throw new Exception(
- "Reenrollment cannot be performed on a store when the certificate usage is marked as 'Trust' or 'None'");
+ "Reenrollment cannot be performed on a store when the certificate usage is marked as 'Trust' or 'Other'");
}
_logger.LogTrace("Create HTTPS client to connect to device");
var client = new AxisHttpClient(config, config.CertificateStoreDetails, _resolver);
- // Get current binding for reenrollment certificate usage provided
+ // Get the existing alias name associated with the supplied cert usage
_logger.LogTrace($"Check '{certUsage}' binding for same alias");
- var boundAlias = client.GetCertUsageBinding(Constants.GetCertUsageAsEnum(certUsage));
- if (!string.IsNullOrEmpty(boundAlias))
+ var oldAlias = client.GetCertUsageBinding(Constants.GetCertUsageAsEnum(certUsage));
+ var oldCertExists = false;
+ if (!string.IsNullOrEmpty(oldAlias))
{
- _logger.LogDebug($"Alias currently bound to certificate usage type '{certUsage}': {boundAlias}");
+ oldCertExists = true;
+ _logger.LogDebug($"Alias currently bound to certificate usage type '{certUsage}': {oldAlias}");
- if (boundAlias == reenrollAlias)
- {
- _logger.LogDebug($"Alias '{reenrollAlias}' provided for reenrollment matches alias '{boundAlias}' currently bound " +
- $"to certificate usage type {certUsage}");
-
- throw new Exception(
- $"Alias '{reenrollAlias}' already exists for certificate usage type {certUsage}. Reenroll using another alias.");
- }
-
- _logger.LogTrace($"Alias '{reenrollAlias}' provided for reenrollment differs from alias '{boundAlias}' currently bound " +
- $"to certificate usage type {certUsage}. Proceeding...");
+ // compare the old alias name with the new alias name ---
+ // 1) if the names are the same, append a reserved time-based suffix to the end of the name
+ // This new name [AliasA_Timestamp] will be used to create the new cert.
+ // OR
+ // 2) EDGE CASE: if the old alias name currently tied to the cert usage does NOT match the new alias name,
+ // also create a new name [CertB_Timestamp] for the new cert in case the user-supplied cert name is already
+ // associated with an existing certificate that is NOT bound to a cert usage
+ newAlias = CertificateName.CreateUniqueCertName(newAlias);
}
else
{
- _logger.LogDebug($"No alias currently bound to certificate usage type {certUsage}");
+ _logger.LogDebug($"No alias currently bound to certificate usage type {certUsage}. Proceeding with new key, CSR, and adding cert for new alias...");
}
// Map the key type and key size from the job properties to a corresponding key type available on the device
+ _logger.LogTrace($"Mapping key type and key size from job properties to a corresponding key type available on the device: '{keyAlgorithm}' '{keySize}'");
string keyType = Constants.MapKeyType(keyAlgorithm, keySize);
_logger.LogDebug($"Mapped Key Type: {keyType}");
if (keyType == "UNKNOWN")
{
throw new Exception(
$"The key algorithm '{keyAlgorithm}' and key size '{keySize}' selected for reenrollment " +
- $"do not correspond to a valid key algorithm and" +
+ $"do not correspond to a valid key algorithm and " +
$"key size on the device.");
}
@@ -112,10 +128,10 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
Constants.Keystore defaultKeystore = client.GetDefaultKeystore();
string defaultKeystoreString = defaultKeystore.ToString();
_logger.LogDebug($"Reenrollment - Default keystore: {defaultKeystoreString}");
-
- _logger.LogTrace("Generating self-signed cert with private key on device");
- List sansList = new List();
- if (certUsageEnum == Constants.CertificateUsage.Https)
+
+ // If no SANs are provided and the cert usage is 'HTTPS' ---
+ // Add 1 for DNS and 1 for IP address to eliminate TLS errors
+ if(formattedSANs.Count == 0 && certUsageEnum == Constants.CertificateUsage.Https)
{
_logger.LogTrace("Extracting CN and IP address to add as SANs to the certificate");
// Extract the CN from the Subject
@@ -126,8 +142,9 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
throw new Exception(
"No value provided in the Subject for 'CN'. This is required for HTTPS certificates.");
}
+
_logger.LogTrace($"Extracted CN attribute from the Subject: {cnMatch.Groups[1].Value}");
-
+
// Extract the IP address from the Client Machine
var ipMatch = Regex.Match(config.CertificateStoreDetails.ClientMachine,
@"^(?(?:\d{1,3}\.){3}\d{1,3})", RegexOptions.IgnoreCase);
@@ -137,25 +154,28 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
throw new Exception(
"Value provided for the Client Machine does not match IPv4 format.");
}
- _logger.LogTrace($"Extracted IP Address from the Client Machine: { ipMatch.Groups["ip"].Value}");
- sansList.Add("DNS:" + cnMatch.Groups[1].Value);
- sansList.Add("IP:" + ipMatch.Groups["ip"].Value);
+ _logger.LogTrace($"Extracted IP Address from the Client Machine: {ipMatch.Groups["ip"].Value}");
+
+ formattedSANs.Add($"DNS:{cnMatch.Groups[1].Value}");
+ formattedSANs.Add($"IP:{ipMatch.Groups["ip"].Value}");
}
- client.CreateSelfSignedCert(reenrollAlias,keyType,defaultKeystoreString,subject,sansList.ToArray());
- _logger.LogTrace("Obtaining CSR using self-signed certificate");
- var csr = client.ObtainCSR(reenrollAlias);
+ _logger.LogTrace("Generating private key pair on device");
+ client.CreateSelfSignedCert(newAlias,keyType,defaultKeystoreString,subject,formattedSANs.ToArray());
+
+ _logger.LogTrace("Obtaining CSR");
+ var csr = client.ObtainCSR(newAlias);
_logger.LogDebug($"CSR: \n{csr}");
-
+
_logger.LogTrace("Validating CSR");
Constants.ValidateCsr(csr);
_logger.LogTrace("CSR is valid");
- // Submit CSR to be signed in Keyfactor
- _logger.LogTrace("Submitting CSR to be signed in Command");
+ // Submit CSR to be signed
+ _logger.LogTrace("Submitting CSR to Command to enroll for signed certificate");
var x509Cert = submitReenrollment.Invoke(csr);
-
+
// Build PEM content
// ** NOTE: The static newline (\n) characters are required in the API request
StringBuilder pemBuilder = new StringBuilder();
@@ -165,16 +185,31 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
pemBuilder.Append(noLineBreaks);
pemBuilder.Append(@"\n-----END CERTIFICATE-----");
var pemCert = pemBuilder.ToString();
+
+ _logger.LogTrace($"Replacing cert '{newAlias}' with the following cert: " + pemCert);
+ client.ReplaceCertificate(newAlias,pemCert);
+
+ _logger.LogTrace($"Setting '{certUsage}' binding to alias '{newAlias}'");
+ client.SetCertUsageBinding(newAlias,certUsageEnum);
+
+ // Perform unused certificate cleanup ---
+ // 1) If a bound alias exists, delete the bound alias
+ if (oldCertExists)
+ {
+ _logger.LogTrace($"Removing certificate and private key associated with alias '{oldAlias}'");
+ var result = client.RemoveCertificate(oldAlias);
- _logger.LogTrace($"Replacing self-signed cert '{reenrollAlias}' with the following cert: " + pemCert);
- client.ReplaceCertificate(reenrollAlias,pemCert);
-
- _logger.LogTrace($"Setting '{certUsage}' binding to alias '{reenrollAlias}'");
- client.SetCertUsageBinding(reenrollAlias, certUsageEnum);
+ if (result.Status == HttpStatus.Warning)
+ {
+ return new JobResult() { Result = OrchestratorJobStatusJobResult.Warning, JobHistoryId = config.JobHistoryId,
+ FailureMessage = $"Reenrollment Job Had Warnings - Refer to logs for more detailed information." };
+ }
+ }
}
catch (Exception ex)
{
//Status: 2=Success, 3=Warning, 4=Error
+ _logger.LogError($"Reenrollment Job Failed: {ex.Message}");
return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId,
FailureMessage = $"Reenrollment Job Failed: {ex.Message} - Refer to logs for more detailed information." };
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12153e6..7955efa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+v1.1.0
+- fix(logging): Improved exception handling to be more robust for HTTP client initialization
+- fix(auth): Implemented .NET native credential negotiation to support both Basic & Digest auth policies
+- chore(docs): Updated doctool release actions to v5
+- chore(build): Updated .NET target frameworks to net8.0 and net10.0
+- fix(odkg): Fixed incorrect mapping of ECDSA algorithm to ECP. Updated to ECDSA.
+- feat(odkg): Implemented alias versioning to enable reuse of existing alias names in automation processes
+
v1.0.2
- fix(logs): Removed logging of plaintext cert store Server Password
- fix(keystore): Updated Keystore type to be dynamic instead of a fixed Enum to allow compatibility across different cameras/firmware
diff --git a/README.md b/README.md
index 4e275e1..d6674fb 100644
--- a/README.md
+++ b/README.md
@@ -32,34 +32,33 @@
## Overview
The AXIS IP Camera Orchestrator extension remotely manages certificates on AXIS IP Network Cameras. This
-orchestrator extension inventories certificates on the camera's certificate store, and it also supports adding new client-server certificates and adding/removing CA certificates.
-New client-server certificates are created in the AXIS camera certificate store via On Device Key Generation (ODKG aka Reenrollment).
+orchestrator extension inventories certificates on the camera's certificate store, and it also supports adding new identity certificates and adding/removing CA certificates.
+New identity certificates are created in the AXIS camera certificate store via On Device Key Generation (ODKG), also known as Reenrollment.
This means that certificates cannot be directly added to the AXIS camera, but instead the keypair is generated on the AXIS device and a certificate is issued for that keypair via a CSR submitted to Command for enrollment.
This workflow is completely automated in the AXIS IP Camera Orchestrator extension. CA certificates can be added to the camera from uploaded CA certificates in Command.
### Use Cases
-The AXIS IP Camera Orchestrator extension supports the following use cases:
+#### Supported
-1. Inventory of client-server & CA certificates
-2. Enrollment of client-server certificates with ability to bind the certificate for a specific usage*
+1. Inventory of identity & CA certificates
+2. Enrollment of identity certificates with ability to bind the certificate for a specific usage*
3. Ability to remove CA certificates from the camera
4. Ability to add CA certificates to the camera
-The Axis IP Camera Orchestrator extension DOES NOT support the following use cases:
+#### Not Supported
-1. Ability to remove client-server certificates from the camera
-2. Ability to add client-server certificates to the camera
+1. Ability to remove identity certificates from the camera
+2. Ability to add identity certificates to the camera
\* Currently supported certificate usages include: **HTTPS**, **IEEE802.X**, **MQTT**, **Other**
-
-
## Compatibility
-This integration is compatible with Keyfactor Universal Orchestrator version 10.1 and later.
+This integration is compatible with Keyfactor Universal Orchestrator version 25.4 and later.
## Support
+
The AXIS IP Camera Universal Orchestrator extension is supported by Keyfactor. If you require support for any issues or have feature request, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com.
> If you want to contribute bug fixes or additional enhancements, use the **[Pull requests](../../pulls)** tab.
@@ -68,52 +67,65 @@ The AXIS IP Camera Universal Orchestrator extension is supported by Keyfactor. I
Before installing the AXIS IP Camera Universal Orchestrator extension, we recommend that you install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command.
-
1. Out of the box, an AXIS IP Network Camera will typically have configured an **Administrator** account. It is
recommended to create a new account specifically for executing API calls. This account will need \'Administrator\'
privileges since the orchestrator extension is capable of making configuration changes, such as installing and removing certificates.
-2. Currently supports AXIS M2035-LE Bullet Camera, AXIS OS version 12.2.62. Has not been tested with any other firmware version.
+### Camera Compatibility
-## AxisIPCamera Certificate Store Type
+Supported on AXIS cameras running AXIS OS 11.6 or later. Available LTS tracks vary by camera model.
-To use the AXIS IP Camera Universal Orchestrator extension, you **must** create the AxisIPCamera Certificate Store Type. This only needs to happen _once_ per Keyfactor Command instance.
+- **Tested model:** AXIS M2035-LE Bullet Camera
+- **Tested AXIS OS version:** 12.2.62
+
+Has not been tested with any other model or firmware version.
+
+### Authentication
+
+The Axis IP Camera Orchestrator Extension uses .NET HttpClientHandler credential negotiation when connecting to Axis devices over HTTPS.
+This allows the orchestrator to automatically negotiate the authentication mechanism required by the camera.
+The orchestrator has been validated against Axis cameras configured with:
+- Basic
+- Digest
+- Basic & Digest
+- Recommended
+As a result, customer-side changes to camera authentication policies are generally not required.
+
+## AxisIPCamera Certificate Store Type
+
+To use the AXIS IP Camera Universal Orchestrator extension, you **must** create the AxisIPCamera Certificate Store Type. This only needs to happen _once_ per Keyfactor Command instance.
The AXIS IP Camera certificate store type represents a certificate store on an AXIS network camera
that maintains two separate collections of certificates:
-* Client-server certificates (certs with private keys)
+* Identity certificates (certs with private keys)
* CA certificates
It is expected that there be one (1) certificate store managed per AXIS network camera.
-
-
-
#### Axis IP Camera Requirements
1. User Account with \'Administrator\' privileges and password to access the camera
2. Camera serial number
3. Camera IP address (and likely port number)
-
-
#### Supported Operations
-| Operation | Is Supported |
-|--------------|------------------------------------------------------------------------------------------------------------------------|
-| Add | ✅ Checked |
-| Remove | ✅ Checked |
-| Discovery | 🔲 Unchecked |
+| Operation | Is Supported |
+|--------------|--------------|
+| Add | ✅ Checked |
+| Remove | ✅ Checked |
+| Discovery | 🔲 Unchecked |
| Reenrollment | ✅ Checked |
-| Create | 🔲 Unchecked |
+| Create | 🔲 Unchecked |
#### Store Type Creation
##### Using kfutil:
`kfutil` is a custom CLI for the Keyfactor Command API and can be used to create certificate store types.
For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out the [docs](https://github.com/Keyfactor/kfutil?tab=readme-ov-file#quickstart)
+
Click to expand AxisIPCamera kfutil details
##### Using online definition from GitHub:
@@ -132,10 +144,10 @@ For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out
```
-
#### Manual Creation
Below are instructions on how to create the AxisIPCamera store type manually in
the Keyfactor Command Portal
+
Click to expand manual AxisIPCamera details
Create a store type called `AxisIPCamera` with the attributes in the tables below:
@@ -146,11 +158,11 @@ the Keyfactor Command Portal
| Name | Axis IP Camera | Display name for the store type (may be customized) |
| Short Name | AxisIPCamera | Short display name for the store type |
| Capability | AxisIPCamera | Store type name orchestrator will register with. Check the box to allow entry of value |
- | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add |
- | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove |
- | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery |
- | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment |
- | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation |
+ | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add |
+ | Supports Remove | ✅ Checked | Indicates that the Store Type supports Management Remove |
+ | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery |
+ | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment |
+ | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation |
| Needs Server | ✅ Checked | Determines if a target server name is required when creating store |
| Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint |
| Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell |
@@ -159,18 +171,18 @@ the Keyfactor Command Portal
The Basic tab should look like this:
- 
+ 
##### Advanced Tab
| Attribute | Value | Description |
| --------- | ----- | ----- |
| Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
- | Private Key Handling | Forbidden | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
+ | Private Key Handling | Forbidden | This determines if Keyfactor can send the private key associated with a certificate to the store. |
| PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |
The Advanced tab should look like this:
- 
+ 
> For Keyfactor **Command versions 24.4 and later**, a Certificate Format dropdown is available with PFX and PEM options. Ensure that **PFX** is selected, as this determines the format of new and renewed certificates sent to the Orchestrator during a Management job. Currently, all Keyfactor-supported Orchestrator extensions support only PFX.
@@ -185,8 +197,7 @@ the Keyfactor Command Portal
The Custom Fields tab should look like this:
- 
-
+ 
###### Server Username
Enter the username of the configured "service" user on the camera
@@ -196,8 +207,6 @@ the Keyfactor Command Portal
> This field is created by the `Needs Server` on the Basic tab, do not create this field manually.
-
-
###### Server Password
Enter the password of the configured "service" user on the camera
@@ -206,16 +215,11 @@ the Keyfactor Command Portal
> This field is created by the `Needs Server` on the Basic tab, do not create this field manually.
-
-
###### Use SSL
Select True or False depending on if SSL (HTTPS) should be used to communicate with the camera. This should always be "True"
- 
- 
-
-
-
+ 
+ 
##### Entry Parameters Tab
@@ -226,15 +230,12 @@ the Keyfactor Command Portal
The Entry Parameters tab should look like this:
- 
-
-
+ 
##### Certificate Usage
The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later.
- 
- 
-
+ 
+ 
@@ -243,18 +244,17 @@ the Keyfactor Command Portal
1. **Download the latest AXIS IP Camera Universal Orchestrator extension from GitHub.**
- Navigate to the [AXIS IP Camera Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/axis-ipcamera-orchestrator/releases/latest). Refer to the compatibility matrix below to determine the asset should be downloaded. Then, click the corresponding asset to download the zip archive.
+ Navigate to the [AXIS IP Camera Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/axis-ipcamera-orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive.
| Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `axis-ipcamera-orchestrator` .NET version to download |
| --------- | ----------- | ----------- | ----------- |
- | 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` | `LatestMajor` | `net8.0` |
+ | `11.6` _and_ newer | `net8.0` | | `net8.0` |
+ | `25.5` _and_ newer | `net10.0` | | `net10.0` |
Unzip the archive containing extension assemblies to a known location.
- > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net6.0`.
+ > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net10.0`.
2. **Locate the Universal Orchestrator extensions directory.**
@@ -272,60 +272,16 @@ the Keyfactor Command Portal
Refer to [Starting/Restarting the Universal Orchestrator service](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/StarttheService.htm).
-
6. **(optional) PAM Integration**
The AXIS IP Camera Universal Orchestrator extension is compatible with all supported Keyfactor PAM extensions to resolve PAM-eligible secrets. PAM extensions running on Universal Orchestrators enable secure retrieval of secrets from a connected PAM provider.
To configure a PAM provider, [reference the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam) to select an extension and follow the associated instructions to install it on the Universal Orchestrator (remote).
-
> The above installation steps can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions).
-
-## Post Installation
-
-The AXIS IP Camera Orchestrator Extension *always* connects to an AXIS IP Network Camera via HTTPS, regardless
-of whether the \`Use SSL\` option on the certificate store is set to **false** (*The \`Use SSL\` option cannot be removed). This ensures the orchestrator
-is connecting to a valid camera.
-
-All network cameras come pre-loaded with one (1) or more device ID certificates, and one of these certificates is configured on the camera to be provided in the TLS handshake
-to the client during an HTTPS request.
-
-The orchestrator will not trust the device ID certificate, and will therefore deny the session to the camera.
-
-To trust the device ID certificate, you must create a custom trust and add the root and intermediate CA certificates from the AXIS PKI chain to it.
-
-### Steps to Create the Custom Trust:
-
-1. Once the DLLs from GitHub are installed, create two (2) files in the sub-directory called "Files" with the below names (*Note: The "Files" folder should already exist):
- * **Axis.Root**
- * **Axis.Intermediate**
-
-* **Default Path on Windows** - `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\[Axis IP Camera orchestrator extension folder]\Files`
-* **Default Path on Linux** - `/opt/keyfactor/orchestrator/extensions/[Axis IP Camera orchestrator extension folder]/Files`
-2. Copy and paste the PEM contents of the AXIS PKI root for the device ID cert configured for the HTTP server into the **Axis.Root** file
-3. Copy and paste the PEM contents of the AXIS PKI intermediate for the device ID configured for the HTTP server into the **Axis.Intermediate** file
-
-\* AXIS Device ID CA certificates can be found here: https://www.axis.com/support/public-key-infrastructure-repository
-
-After the device ID is verified against the custom trust, the \`Store Path\` value of the certificate store will be compared against the SERIALNUMBER Subject DN attribute of the device ID certificate.
-These values must match or the session will be denied.
-
-> [!IMPORTANT]
-> You will want to replace the device ID certificate bound to the HTTP server with a CA-signed certificate. To do this,
-> you will need to schedule a Reenrollment job and select **HTTPS** as the Certificate Usage.
-
-> [!IMPORTANT]
-> After associating a CA-signed certificate with the HTTP server via the Reenrollment job, you need to make sure the orchestrator server trusts the HTTPS certificate.
-> Therefore, you will need to install the full CA chain - including root and intermediate certificates - into the orchestrator server's local
-> certificate store.
-
-
## Defining Certificate Stores
-
-
### Store Creation
#### Manually with the Command UI
@@ -340,8 +296,8 @@ These values must match or the session will be denied.
Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form.
- | Attribute | Description |
- | --------- |---------------------------------------------------------|
+ | Attribute | Description |
+ | --------- | ----------- |
| Category | Select "Axis IP Camera" or the customized certificate store name from the previous step. |
| Container | Optional container to associate certificate store with. |
| Client Machine | The IP address of the Camera. Sample is "192.167.231.174:44444". Include the port if necessary. |
@@ -353,8 +309,6 @@ These values must match or the session will be denied.
-
-
#### Using kfutil CLI
Click to expand details
@@ -387,7 +341,6 @@ These values must match or the session will be denied.
-
#### PAM Provider Eligible Fields
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator
@@ -403,10 +356,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
-
> The content in this section can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store).
-
### Certificate Usage
Every certificate inventoried will have an Entry Parameter called \`Certificate Usage\`.
@@ -432,27 +383,153 @@ There are five (5) possible options:
5. Other
- This certificate usage identifies all other certificates on the camera that do not fall under the pre-defined usages above.
-> [!NOTE]
-> A Reenrollment (ODKG) job will not allow enrollment of certificates with **Trust** assigned as the \`Certificate Usage\`.
-> Trust CA certificates can be added to the camera via a Management - Add job.
+
+## Device Onboarding
+
+The AXIS IP Camera Orchestrator Extension *always* connects to an AXIS IP Network Camera via HTTPS, regardless of how the **Use SSL** option on the certificate store is configured. This ensures that the orchestrator always validates the camera's certificate against the configured trust before proceeding.
+
+All network cameras come pre-loaded with one (1) or more device ID certificates, and one of these certificates is configured on the camera to be provided in the TLS handshake
+to the client during an HTTPS request.
+
+The orchestrator will not trust the device ID certificate, and will therefore deny the session to the camera.
+
+To trust the device ID certificate, you must create a custom trust and add the root and intermediate CA certificates from the AXIS PKI chain to it.
+
+### Steps to Create the Custom Trust
+
+1. Once the DLLs from GitHub are installed, create two (2) files in the sub-directory called "Files" with the below names (*Note: The "Files" folder should already exist):
+ * **Axis.Root**
+ * **Axis.Intermediate**
+
+* **Default Path on Windows** - `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\[Axis IP Camera orchestrator extension folder]\Files`
+* **Default Path on Linux** - `/opt/keyfactor/orchestrator/extensions/[Axis IP Camera orchestrator extension folder]/Files`
+2. Copy and paste the PEM contents of the AXIS PKI root for the device ID cert configured for the HTTP server into the **Axis.Root** file
+3. Copy and paste the PEM contents of the AXIS PKI intermediate for the device ID configured for the HTTP server into the **Axis.Intermediate** file
+
+\* AXIS Device ID CA certificates can be found here: https://www.axis.com/support/public-key-infrastructure-repository
+
+> [!IMPORTANT]
+> You will want to replace the device ID certificate bound to the HTTP server with a CA-signed certificate. To do this,
+> you will need to schedule an ODKG job and select **HTTPS** as the Certificate Usage.
+
+> [!IMPORTANT]
+> After associating a CA-signed certificate with the HTTP server via the ODKG job, you need to make sure the orchestrator server trusts the HTTPS certificate.
+> Therefore, you will need to install the full CA chain - including root and intermediate certificates - into the orchestrator server's local
+> certificate store.
+
+### Camera-Specific Trust Validation
+
+After the device ID is verified against the custom trust, the **Store Path** value of the certificate store will be compared against the SERIALNUMBER Subject DN attribute of the device ID certificate.
+These values must match or the session will be denied.
+
+> [!NOTE]
+> This SERIALNUMBER validation only applies while the camera's factory device ID certificate remains bound to the HTTP server. Once a new certificate is enrolled via an ODKG job using the customer's PKI, the camera presents that certificate instead, and the orchestrator falls back to standard TLS certificate chain validation — requiring the customer's root and intermediate CA certificates to be installed in the orchestrator server's local certificate store.
+
+## Enrollment Behavior
+
+The following enrollment behaviors are specific to ODKG (On Device Key Generation) — also known as Reenrollment — on AXIS cameras, and should be considered when designing certificate automation workflows.
+
+### Alias Versioning
+
+AXIS cameras require each Alias to be unique, and each Alias is tightly coupled with the private key used to generate its certificate. Because of this, certificates cannot be reenrolled in place by replacing the certificate and private key associated with an existing Alias.
+
+To support certificate renewals and automation workflows, the orchestrator generates a unique Alias by appending the following suffix:
+
+`_yyMMddHHmm`
+
+where `yyMMddHHmm` represents the current UTC date and time.
+
+From an automation perspective, the same Alias can continue to be reused, as uniqueness is enforced by the integration.
> [!NOTE]
-> For a Reenrollment (ODKG) job, where the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS are added as SANS
-> to the enrolled certificate.
->
-> IP = Client Machine configured for the certificate store (excluding any port)
->
-> DNS = CN set in the Subject DN
+> As of v1.1.0, ODKG jobs automatically manage versioned Aliases. When reenrolling a certificate using the same Alias, the integration creates a new certificate using a versioned Alias and removes the previously enrolled certificate associated with the same base Alias.
+>
+> Because AXIS cameras do not support in-place certificate replacement, a new versioned Alias is still created during reenrollment. The integration identifies and removes the previous certificate by matching the base Alias name and ignoring the timestamp suffix.
+>
+> If a new Alias is supplied during reenrollment, the original certificate (if one exists) associated with the selected `Certificate Usage` is **not** automatically removed from the camera. Because AXIS cameras have limited certificate and key storage capacity, users should periodically review and remove unused certificates through the AXIS Network Camera GUI.
+
+#### Configuration Example
+
+The following example demonstrates how Alias versioning behaves in an ODKG job configuration:
+
+- **Store Path:** camera serial number, e.g. `0b7c3d2f9e8a`
+- **Overwrite:** `true` or `false` *(has no bearing on Alias or certificate behavior)*
+- **Alias:** `https-cert` *(the Alias that will appear on the camera)*
+- **Certificate Usage:** any of `HTTPS`, `IEEE802.X`, `MQTT`, `Other` *(`Trust` is not supported for ODKG jobs — see [Certificate Usage Considerations (Trust)](#certificate-usage-considerations-trust) below)*
+
+In this configuration:
+- The ODKG job generates a new identity certificate and assigns it a versioned Alias by appending a `_yyMMddHHmm` timestamp suffix, e.g. `https-cert_2508251200`
+- A later ODKG job using the same `https-cert` Alias and the same Certificate Usage creates another versioned Alias (e.g. `https-cert_2509031400`) and removes the certificate tied to the previous versioned Alias, matching on the base Alias name and ignoring the timestamp suffix
+- If a different Alias is supplied on a later job, the original certificate associated with the selected Certificate Usage is **not** automatically removed and must be cleaned up manually via the AXIS Network Camera GUI
+
+Operational behavior:
+- The **Overwrite** setting has no effect on Alias or certificate behavior — AXIS cameras do not support in-place certificate replacement, so a new versioned Alias is always created regardless of how Overwrite is set
+- The base Alias name (excluding the timestamp suffix) is what the integration uses to identify and remove a previously enrolled certificate
+
+### Certificate Usage Considerations (Trust)
+
+> [!NOTE]
+> If an ODKG job is configured with `Trust` selected as the `Certificate Usage`, the job will return a warning indicating that the operation is not supported.
+>
+> Trust CA certificates must be installed using a **Management - Add** job. These certificates establish trust for TLS connections initiated by the camera.
+
+### Subject Alternative Names (SANs)
+
+As of Keyfactor Command v25.4, Subject Alternative Names (SANs) can be specified for ODKG jobs. Support for passing SANs to the orchestrator also requires, at minimum, Keyfactor Universal Orchestrator v25.1.
+
+The AXIS IP Camera API only supports DNS and IP SAN types. Any other SAN types included in the ODKG job will be ignored and will not be added to the enrolled certificate.
+
+> [!NOTE]
+> If SANs are not provided and the selected `Certificate Usage` is `HTTPS`, IP and DNS SANs are automatically added when enrolling a certificate associated with a new Alias:
+>
+> - **IP** = The Client Machine configured for the certificate store (excluding any port number)
+> - **DNS** = The Common Name (CN) specified in the certificate Subject DN
+
+## Troubleshooting
+
+### 401 Unauthorized Responses
+
+The Axis IP Camera Orchestrator Extension supports authentication negotiation and has been validated against Axis cameras configured for:
+
+- Basic
+- Digest
+- Basic & Digest
+- Recommended
+
+If a 401 Unauthorized response is encountered:
+
+1. Verify the configured credentials.
+2. Verify connectivity to the device.
+3. Review orchestrator logs for authentication-related messages.
+
+## Operational Notes
+
+### AXIS OS 12 Firmware Recommendation
+
+> [!IMPORTANT]
+> Devices running the AXIS OS 12 release track should always be updated to the latest firmware version available from Axis. Previous firmware versions are no longer supported once a newer release becomes available.
+
+Axis identified a memory leak in older firmware releases that may cause device keystore storage to become exhausted over time. If keystore storage issues are observed, verify that the device is running the latest supported firmware version.
+## Release Notes
+**1.1.0**
+- Improved exception handling to be more robust for HTTP client initialization.
+- Implemented .NET native credential negotiation to support both Basic and Digest authentication policies.
+- Fixed incorrect mapping of the ECDSA algorithm to ECP; updated to ECDSA.
+- Implemented Alias versioning to enable reuse of existing Alias names in automation processes.
+- Updated doctool release actions to v5.
+- Updated .NET target frameworks to net8.0 and net10.0.
-## Caveats
+**1.0.2**
+- Removed logging of plaintext cert store Server Password.
+- Updated Keystore type to be dynamic instead of a fixed Enum, to allow compatibility across different cameras and firmware versions.
-> [!NOTE]
-> Reenrollment jobs will not replace or remove a client-server certificate with the same alias. They will also not remove
-> the original certificate if a particular \`Certificate Usage\` had an associated cert. Since the camera has limited storage,
-> it will be up to the user to remove any unused client-server certificates via the AXIS Network Camera GUI.
+**1.0.1**
+- Added screenshots to docs.
+**1.0.0**
+- Initial Public Version.
## License
@@ -460,4 +537,4 @@ Apache License 2.0, see [LICENSE](LICENSE).
## Related Integrations
-See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator).
\ No newline at end of file
+See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator).
diff --git a/docsource/axisipcamera.md b/docsource/axisipcamera.md
index af3aed2..55422fe 100644
--- a/docsource/axisipcamera.md
+++ b/docsource/axisipcamera.md
@@ -2,7 +2,7 @@
The AXIS IP Camera certificate store type represents a certificate store on an AXIS network camera
that maintains two separate collections of certificates:
-* Client-server certificates (certs with private keys)
+* Identity certificates (certs with private keys)
* CA certificates
It is expected that there be one (1) certificate store managed per AXIS network camera.
@@ -36,16 +36,4 @@ There are five (5) possible options:
4. Trust
- This certificate usage describes a public certificate issued by a CA used to establish trust.
5. Other
- - This certificate usage identifies all other certificates on the camera that do not fall under the pre-defined usages above.
-
-> [!NOTE]
-> A Reenrollment (ODKG) job will not allow enrollment of certificates with **Trust** assigned as the \`Certificate Usage\`.
-> Trust CA certificates can be added to the camera via a Management - Add job.
-
-> [!NOTE]
-> For a Reenrollment (ODKG) job, where the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS are added as SANS
-> to the enrolled certificate.
->
-> IP = Client Machine configured for the certificate store (excluding any port)
->
-> DNS = CN set in the Subject DN
\ No newline at end of file
+ - This certificate usage identifies all other certificates on the camera that do not fall under the pre-defined usages above.
\ No newline at end of file
diff --git a/docsource/content.md b/docsource/content.md
index 24e31b2..d4249ab 100644
--- a/docsource/content.md
+++ b/docsource/content.md
@@ -1,24 +1,24 @@
## Overview
The AXIS IP Camera Orchestrator extension remotely manages certificates on AXIS IP Network Cameras. This
-orchestrator extension inventories certificates on the camera's certificate store, and it also supports adding new client-server certificates and adding/removing CA certificates.
-New client-server certificates are created in the AXIS camera certificate store via On Device Key Generation (ODKG aka Reenrollment).
+orchestrator extension inventories certificates on the camera's certificate store, and it also supports adding new identity certificates and adding/removing CA certificates.
+New identity certificates are created in the AXIS camera certificate store via On Device Key Generation (ODKG), also known as Reenrollment.
This means that certificates cannot be directly added to the AXIS camera, but instead the keypair is generated on the AXIS device and a certificate is issued for that keypair via a CSR submitted to Command for enrollment.
This workflow is completely automated in the AXIS IP Camera Orchestrator extension. CA certificates can be added to the camera from uploaded CA certificates in Command.
### Use Cases
-The AXIS IP Camera Orchestrator extension supports the following use cases:
+#### Supported
-1. Inventory of client-server & CA certificates
-2. Enrollment of client-server certificates with ability to bind the certificate for a specific usage*
+1. Inventory of identity & CA certificates
+2. Enrollment of identity certificates with ability to bind the certificate for a specific usage*
3. Ability to remove CA certificates from the camera
4. Ability to add CA certificates to the camera
-The Axis IP Camera Orchestrator extension DOES NOT support the following use cases:
+#### Not Supported
-1. Ability to remove client-server certificates from the camera
-2. Ability to add client-server certificates to the camera
+1. Ability to remove identity certificates from the camera
+2. Ability to add identity certificates to the camera
\* Currently supported certificate usages include: **HTTPS**, **IEEE802.X**, **MQTT**, **Other**
@@ -27,13 +27,32 @@ The Axis IP Camera Orchestrator extension DOES NOT support the following use cas
1. Out of the box, an AXIS IP Network Camera will typically have configured an **Administrator** account. It is
recommended to create a new account specifically for executing API calls. This account will need \'Administrator\'
privileges since the orchestrator extension is capable of making configuration changes, such as installing and removing certificates.
-2. Currently supports AXIS M2035-LE Bullet Camera, AXIS OS version 12.2.62. Has not been tested with any other firmware version.
-## Post Installation
+### Camera Compatibility
-The AXIS IP Camera Orchestrator Extension *always* connects to an AXIS IP Network Camera via HTTPS, regardless
-of whether the \`Use SSL\` option on the certificate store is set to **false** (*The \`Use SSL\` option cannot be removed). This ensures the orchestrator
-is connecting to a valid camera.
+Supported on AXIS cameras running AXIS OS 11.6 or later. Available LTS tracks vary by camera model.
+
+- **Tested model:** AXIS M2035-LE Bullet Camera
+- **Tested AXIS OS version:** 12.2.62
+
+Has not been tested with any other model or firmware version.
+
+### Authentication
+
+The Axis IP Camera Orchestrator Extension uses .NET HttpClientHandler credential negotiation when connecting to Axis devices over HTTPS.
+This allows the orchestrator to automatically negotiate the authentication mechanism required by the camera.
+The orchestrator has been validated against Axis cameras configured with:
+
+- Basic
+- Digest
+- Basic & Digest
+- Recommended
+
+As a result, customer-side changes to camera authentication policies are generally not required.
+
+## Device Onboarding
+
+The AXIS IP Camera Orchestrator Extension *always* connects to an AXIS IP Network Camera via HTTPS, regardless of how the **Use SSL** option on the certificate store is configured. This ensures that the orchestrator always validates the camera's certificate against the configured trust before proceeding.
All network cameras come pre-loaded with one (1) or more device ID certificates, and one of these certificates is configured on the camera to be provided in the TLS handshake
to the client during an HTTPS request.
@@ -42,7 +61,7 @@ The orchestrator will not trust the device ID certificate, and will therefore de
To trust the device ID certificate, you must create a custom trust and add the root and intermediate CA certificates from the AXIS PKI chain to it.
-### Steps to Create the Custom Trust:
+### Steps to Create the Custom Trust
1. Once the DLLs from GitHub are installed, create two (2) files in the sub-directory called "Files" with the below names (*Note: The "Files" folder should already exist):
* **Axis.Root**
@@ -55,21 +74,125 @@ To trust the device ID certificate, you must create a custom trust and add the r
\* AXIS Device ID CA certificates can be found here: https://www.axis.com/support/public-key-infrastructure-repository
-After the device ID is verified against the custom trust, the \`Store Path\` value of the certificate store will be compared against the SERIALNUMBER Subject DN attribute of the device ID certificate.
-These values must match or the session will be denied.
-
> [!IMPORTANT]
> You will want to replace the device ID certificate bound to the HTTP server with a CA-signed certificate. To do this,
-> you will need to schedule a Reenrollment job and select **HTTPS** as the Certificate Usage.
+> you will need to schedule an ODKG job and select **HTTPS** as the Certificate Usage.
> [!IMPORTANT]
-> After associating a CA-signed certificate with the HTTP server via the Reenrollment job, you need to make sure the orchestrator server trusts the HTTPS certificate.
+> After associating a CA-signed certificate with the HTTP server via the ODKG job, you need to make sure the orchestrator server trusts the HTTPS certificate.
> Therefore, you will need to install the full CA chain - including root and intermediate certificates - into the orchestrator server's local
> certificate store.
-## Caveats
+### Camera-Specific Trust Validation
+
+After the device ID is verified against the custom trust, the **Store Path** value of the certificate store will be compared against the SERIALNUMBER Subject DN attribute of the device ID certificate.
+These values must match or the session will be denied.
+
+> [!NOTE]
+> This SERIALNUMBER validation only applies while the camera's factory device ID certificate remains bound to the HTTP server. Once a new certificate is enrolled via an ODKG job using the customer's PKI, the camera presents that certificate instead, and the orchestrator falls back to standard TLS certificate chain validation — requiring the customer's root and intermediate CA certificates to be installed in the orchestrator server's local certificate store.
+
+## Enrollment Behavior
+
+The following enrollment behaviors are specific to ODKG (On Device Key Generation) — also known as Reenrollment — on AXIS cameras, and should be considered when designing certificate automation workflows.
+
+### Alias Versioning
+
+AXIS cameras require each Alias to be unique, and each Alias is tightly coupled with the private key used to generate its certificate. Because of this, certificates cannot be reenrolled in place by replacing the certificate and private key associated with an existing Alias.
+
+To support certificate renewals and automation workflows, the orchestrator generates a unique Alias by appending the following suffix:
+
+`_yyMMddHHmm`
+
+where `yyMMddHHmm` represents the current UTC date and time.
+
+From an automation perspective, the same Alias can continue to be reused, as uniqueness is enforced by the integration.
+
+> [!NOTE]
+> As of v1.1.0, ODKG jobs automatically manage versioned Aliases. When reenrolling a certificate using the same Alias, the integration creates a new certificate using a versioned Alias and removes the previously enrolled certificate associated with the same base Alias.
+>
+> Because AXIS cameras do not support in-place certificate replacement, a new versioned Alias is still created during reenrollment. The integration identifies and removes the previous certificate by matching the base Alias name and ignoring the timestamp suffix.
+>
+> If a new Alias is supplied during reenrollment, the original certificate (if one exists) associated with the selected `Certificate Usage` is **not** automatically removed from the camera. Because AXIS cameras have limited certificate and key storage capacity, users should periodically review and remove unused certificates through the AXIS Network Camera GUI.
+
+#### Configuration Example
+
+The following example demonstrates how Alias versioning behaves in an ODKG job configuration:
+
+- **Store Path:** camera serial number, e.g. `0b7c3d2f9e8a`
+- **Overwrite:** `true` or `false` *(has no bearing on Alias or certificate behavior)*
+- **Alias:** `https-cert` *(the Alias that will appear on the camera)*
+- **Certificate Usage:** any of `HTTPS`, `IEEE802.X`, `MQTT`, `Other` *(`Trust` is not supported for ODKG jobs — see [Certificate Usage Considerations (Trust)](#certificate-usage-considerations-trust) below)*
+
+In this configuration:
+- The ODKG job generates a new identity certificate and assigns it a versioned Alias by appending a `_yyMMddHHmm` timestamp suffix, e.g. `https-cert_2508251200`
+- A later ODKG job using the same `https-cert` Alias and the same Certificate Usage creates another versioned Alias (e.g. `https-cert_2509031400`) and removes the certificate tied to the previous versioned Alias, matching on the base Alias name and ignoring the timestamp suffix
+- If a different Alias is supplied on a later job, the original certificate associated with the selected Certificate Usage is **not** automatically removed and must be cleaned up manually via the AXIS Network Camera GUI
+
+Operational behavior:
+- The **Overwrite** setting has no effect on Alias or certificate behavior — AXIS cameras do not support in-place certificate replacement, so a new versioned Alias is always created regardless of how Overwrite is set
+- The base Alias name (excluding the timestamp suffix) is what the integration uses to identify and remove a previously enrolled certificate
+
+### Certificate Usage Considerations (Trust)
+
+> [!NOTE]
+> If an ODKG job is configured with `Trust` selected as the `Certificate Usage`, the job will return a warning indicating that the operation is not supported.
+>
+> Trust CA certificates must be installed using a **Management - Add** job. These certificates establish trust for TLS connections initiated by the camera.
+
+### Subject Alternative Names (SANs)
+
+As of Keyfactor Command v25.4, Subject Alternative Names (SANs) can be specified for ODKG jobs. Support for passing SANs to the orchestrator also requires, at minimum, Keyfactor Universal Orchestrator v25.1.
+
+The AXIS IP Camera API only supports DNS and IP SAN types. Any other SAN types included in the ODKG job will be ignored and will not be added to the enrolled certificate.
+
+> [!NOTE]
+> If SANs are not provided and the selected `Certificate Usage` is `HTTPS`, IP and DNS SANs are automatically added when enrolling a certificate associated with a new Alias:
+>
+> - **IP** = The Client Machine configured for the certificate store (excluding any port number)
+> - **DNS** = The Common Name (CN) specified in the certificate Subject DN
+
+## Troubleshooting
+
+### 401 Unauthorized Responses
+
+The Axis IP Camera Orchestrator Extension supports authentication negotiation and has been validated against Axis cameras configured for:
+
+- Basic
+- Digest
+- Basic & Digest
+- Recommended
+
+If a 401 Unauthorized response is encountered:
+
+1. Verify the configured credentials.
+2. Verify connectivity to the device.
+3. Review orchestrator logs for authentication-related messages.
+
+## Operational Notes
+
+### AXIS OS 12 Firmware Recommendation
+
+> [!IMPORTANT]
+> Devices running the AXIS OS 12 release track should always be updated to the latest firmware version available from Axis. Previous firmware versions are no longer supported once a newer release becomes available.
+
+Axis identified a memory leak in older firmware releases that may cause device keystore storage to become exhausted over time. If keystore storage issues are observed, verify that the device is running the latest supported firmware version.
+
+## Release Notes
+
+**1.1.0**
+- Improved exception handling to be more robust for HTTP client initialization.
+- Implemented .NET native credential negotiation to support both Basic and Digest authentication policies.
+- Fixed incorrect mapping of the ECDSA algorithm to ECP; updated to ECDSA.
+- Implemented Alias versioning to enable reuse of existing Alias names in automation processes.
+- Updated doctool release actions to v5.
+- Updated .NET target frameworks to net8.0 and net10.0.
+
+**1.0.2**
+- Removed logging of plaintext cert store Server Password.
+- Updated Keystore type to be dynamic instead of a fixed Enum, to allow compatibility across different cameras and firmware versions.
+
+**1.0.1**
+- Added screenshots to docs.
-> [!NOTE]
-> Reenrollment jobs will not replace or remove a client-server certificate with the same alias. They will also not remove
-> the original certificate if a particular \`Certificate Usage\` had an associated cert. Since the camera has limited storage,
-> it will be up to the user to remove any unused client-server certificates via the AXIS Network Camera GUI.
+**1.0.0**
+- Initial Public Version.
diff --git a/docsource/images/AxisIPCamera-advanced-store-type-dialog.svg b/docsource/images/AxisIPCamera-advanced-store-type-dialog.svg
new file mode 100644
index 0000000..6957ae3
--- /dev/null
+++ b/docsource/images/AxisIPCamera-advanced-store-type-dialog.svg
@@ -0,0 +1,67 @@
+
+
\ No newline at end of file
diff --git a/docsource/images/AxisIPCamera-basic-store-type-dialog.svg b/docsource/images/AxisIPCamera-basic-store-type-dialog.svg
new file mode 100644
index 0000000..664fc3a
--- /dev/null
+++ b/docsource/images/AxisIPCamera-basic-store-type-dialog.svg
@@ -0,0 +1,84 @@
+
+
\ No newline at end of file
diff --git a/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg
new file mode 100644
index 0000000..08ec420
--- /dev/null
+++ b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg
@@ -0,0 +1,54 @@
+
+
\ No newline at end of file
diff --git a/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg
new file mode 100644
index 0000000..7993c23
--- /dev/null
+++ b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg
@@ -0,0 +1,39 @@
+
+
\ No newline at end of file
diff --git a/docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg b/docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg
new file mode 100644
index 0000000..2108f8a
--- /dev/null
+++ b/docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg
@@ -0,0 +1,71 @@
+
+
\ No newline at end of file
diff --git a/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg
new file mode 100644
index 0000000..c85b2b8
--- /dev/null
+++ b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg
@@ -0,0 +1,68 @@
+
+
\ No newline at end of file
diff --git a/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg
new file mode 100644
index 0000000..08b4668
--- /dev/null
+++ b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg
@@ -0,0 +1,51 @@
+
+
\ No newline at end of file
diff --git a/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg
new file mode 100644
index 0000000..6e2e97f
--- /dev/null
+++ b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg
@@ -0,0 +1,54 @@
+
+
\ No newline at end of file
diff --git a/integration-manifest.json b/integration-manifest.json
index 4c1144a..c009418 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -12,8 +12,8 @@
"short_description": "The Axis IP Camera Orchestrator Extension is used to inventory, manage Trust certs, and enroll for client certificates that can be bound to endpoints.",
"about": {
"orchestrator": {
- "UOFramework": "10.1",
- "keyfactor_platform_version": "9.10",
+ "UOFramework": "25.4",
+ "keyfactor_platform_version": "25.4",
"pam_support": true,
"store_types": [
{
@@ -27,7 +27,6 @@
"PrivateKeyAllowed": "Forbidden",
"SupportedOperations": {
"Add": true,
- "Create": false,
"Discovery": false,
"Enrollment": true,
"Remove": true
diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh
new file mode 100755
index 0000000..97f4d97
--- /dev/null
+++ b/scripts/store_types/bash/curl_create_store_types.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+# Store Type creation script using curl
+# Generated by Doctool
+
+set -e
+
+# Configuration - set these variables before running
+KEYFACTOR_HOSTNAME="${KEYFACTOR_HOSTNAME}"
+KEYFACTOR_API_PATH="${KEYFACTOR_API_PATH:-KeyfactorAPI}"
+KEYFACTOR_AUTH_TOKEN="${KEYFACTOR_AUTH_TOKEN}"
+
+echo "Creating store type: AxisIPCamera"
+curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/CertificateStoreTypes" \
+ -H "Authorization: Bearer ${KEYFACTOR_AUTH_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -H "x-keyfactor-requested-with: APIClient" \
+ -d '{
+ "Name": "Axis IP Camera",
+ "ShortName": "AxisIPCamera",
+ "Capability": "AxisIPCamera",
+ "ServerRequired": true,
+ "BlueprintAllowed": false,
+ "PowerShell": false,
+ "CustomAliasAllowed": "Required",
+ "PrivateKeyAllowed": "Forbidden",
+ "SupportedOperations": {
+ "Add": true,
+ "Discovery": false,
+ "Enrollment": true,
+ "Remove": true
+ },
+ "PasswordOptions": {
+ "EntrySupported": false,
+ "StoreRequired": false,
+ "Style": "Default"
+ },
+ "Properties": [
+ {
+ "Name": "ServerUsername",
+ "DisplayName": "Server Username",
+ "Type": "Secret",
+ "DependsOn": "",
+ "DefaultValue": "",
+ "Required": true,
+ "Description": "Enter the username of the configured \"service\" user on the camera"
+ },
+ {
+ "Name": "ServerPassword",
+ "DisplayName": "Server Password",
+ "Type": "Secret",
+ "DependsOn": "",
+ "DefaultValue": "",
+ "Required": true,
+ "Description": "Enter the password of the configured \"service\" user on the camera"
+ },
+ {
+ "Name": "ServerUseSsl",
+ "DisplayName": "Use SSL",
+ "Type": "Bool",
+ "DependsOn": "",
+ "DefaultValue": "true",
+ "Required": true,
+ "Description": "Select True or False depending on if SSL (HTTPS) should be used to communicate with the camera. This should always be \"True\""
+ }
+ ],
+ "EntryParameters": [
+ {
+ "Name": "CertUsage",
+ "DisplayName": "Certificate Usage",
+ "Type": "MultipleChoice",
+ "RequiredWhen": {
+ "HasPrivateKey": false,
+ "OnAdd": true,
+ "OnRemove": false,
+ "OnReenrollment": true
+ },
+ "Options": "HTTPS,IEEE802.X,MQTT,Trust,Other",
+ "Description": "The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later."
+ }
+ ],
+ "StorePathType": "",
+ "StorePathValue": "",
+ "JobProperties": []
+}'
+
diff --git a/scripts/store_types/bash/kfutil_create_store_types.sh b/scripts/store_types/bash/kfutil_create_store_types.sh
new file mode 100755
index 0000000..aecc07d
--- /dev/null
+++ b/scripts/store_types/bash/kfutil_create_store_types.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# Store Type creation script using kfutil
+# Generated by Doctool
+
+set -e
+
+echo "Creating store type: AxisIPCamera"
+kfutil store-types create AxisIPCamera
+
diff --git a/scripts/store_types/powershell/kfutil_create_store_types.ps1 b/scripts/store_types/powershell/kfutil_create_store_types.ps1
new file mode 100644
index 0000000..59f0203
--- /dev/null
+++ b/scripts/store_types/powershell/kfutil_create_store_types.ps1
@@ -0,0 +1,6 @@
+# Store Type creation script using kfutil
+# Generated by Doctool
+
+Write-Host "Creating store type: AxisIPCamera"
+kfutil store-types create AxisIPCamera
+
diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1
new file mode 100644
index 0000000..1c90eed
--- /dev/null
+++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1
@@ -0,0 +1,88 @@
+# Store Type creation script using Invoke-RestMethod
+# Generated by Doctool
+
+# Configuration - set these variables before running
+$KeyfactorHostname = $env:KEYFACTOR_HOSTNAME
+$KeyfactorApiPath = if ($env:KEYFACTOR_API_PATH) { $env:KEYFACTOR_API_PATH } else { "KeyfactorAPI" }
+$KeyfactorAuthToken = $env:KEYFACTOR_AUTH_TOKEN
+
+$Headers = @{
+ "Authorization" = "Bearer $KeyfactorAuthToken"
+ "Content-Type" = "application/json"
+ "x-keyfactor-requested-with" = "APIClient"
+}
+
+Write-Host "Creating store type: AxisIPCamera"
+$Body = @'
+{
+ "Name": "Axis IP Camera",
+ "ShortName": "AxisIPCamera",
+ "Capability": "AxisIPCamera",
+ "ServerRequired": true,
+ "BlueprintAllowed": false,
+ "PowerShell": false,
+ "CustomAliasAllowed": "Required",
+ "PrivateKeyAllowed": "Forbidden",
+ "SupportedOperations": {
+ "Add": true,
+ "Discovery": false,
+ "Enrollment": true,
+ "Remove": true
+ },
+ "PasswordOptions": {
+ "EntrySupported": false,
+ "StoreRequired": false,
+ "Style": "Default"
+ },
+ "Properties": [
+ {
+ "Name": "ServerUsername",
+ "DisplayName": "Server Username",
+ "Type": "Secret",
+ "DependsOn": "",
+ "DefaultValue": "",
+ "Required": true,
+ "Description": "Enter the username of the configured \"service\" user on the camera"
+ },
+ {
+ "Name": "ServerPassword",
+ "DisplayName": "Server Password",
+ "Type": "Secret",
+ "DependsOn": "",
+ "DefaultValue": "",
+ "Required": true,
+ "Description": "Enter the password of the configured \"service\" user on the camera"
+ },
+ {
+ "Name": "ServerUseSsl",
+ "DisplayName": "Use SSL",
+ "Type": "Bool",
+ "DependsOn": "",
+ "DefaultValue": "true",
+ "Required": true,
+ "Description": "Select True or False depending on if SSL (HTTPS) should be used to communicate with the camera. This should always be \"True\""
+ }
+ ],
+ "EntryParameters": [
+ {
+ "Name": "CertUsage",
+ "DisplayName": "Certificate Usage",
+ "Type": "MultipleChoice",
+ "RequiredWhen": {
+ "HasPrivateKey": false,
+ "OnAdd": true,
+ "OnRemove": false,
+ "OnReenrollment": true
+ },
+ "Options": "HTTPS,IEEE802.X,MQTT,Trust,Other",
+ "Description": "The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later."
+ }
+ ],
+ "StorePathType": "",
+ "StorePathValue": "",
+ "JobProperties": []
+}
+'@
+
+Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body
+