diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bb41c8..dee5a69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
- `Get-InfisicalCertificateProfile` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-profiles` (optional `-Limit`, `-Offset`, `-IncludeConfigs`); ById binds to `GET /api/v1/cert-manager/certificate-profiles/{certificateProfileId}`. New `InfisicalCertificateProfile` model surfaces ca/policy ids, slug, enrollment type, per-profile defaults (ttl, key/extended key usages), and the embedded CA/policy/apiConfig summaries.
- `Get-InfisicalCertificatePolicy` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-policies` (optional `-Limit`, `-Offset`); ById binds to `GET /api/v1/cert-manager/certificate-policies/{certificatePolicyId}`. New `InfisicalCertificatePolicy` model surfaces subject, SANs, key usages, extended key usages, algorithms, and validity. Polymorphic string-or-array fields (`allowed`, `required`, `keyAlgorithm`) are normalized to arrays; `sans` is normalized whether the API returns an object or an array.
- `Get-InfisicalCertificateAuthority` gains a `-Kind` parameter on the List parameter set with values `Internal` (default, preserves prior behavior against `/api/v1/cert-manager/ca/internal`), `Any` (binds to the generic `/api/v1/cert-manager/ca` endpoint which returns both internal and ACME CAs), and `Acme` (uses the generic endpoint and client-side filters to ACME issuers only). ById retrieval is unchanged and still resolves against the internal CA endpoint.
+- `Request-InfisicalCertificate` gains a `ByProfile` parameter set bound by the new `-CertificateProfileId` parameter (alias `ProfileId`). The cmdlet generates a local keypair and CSR as usual, then POSTs to `/api/v1/cert-manager/certificates` with the profile id, the CSR, and a subject/attribute envelope (commonName, organization, organizationalUnit, country, state, locality, ttl, notBefore, notAfter, keyUsages, extendedKeyUsages). The wrapped response (`{certificate:{certificate,certificateChain,issuingCaCertificate,serialNumber,certificateId,privateKey}, certificateRequestId, status, message}`) is unwrapped into the existing `InfisicalSignedCertificate` shape so the install / reuse / chain-completion paths continue to work unchanged. Issuance that returns without a certificate (e.g. status `pending_approval`) raises a configuration exception that surfaces the reported status and message.
## 2026.06.04.1920
diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
index 82c903b..799d13e 100644
--- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
@@ -1269,7 +1269,7 @@ $SearchInfisicalCertificateResult = Search-InfisicalCertificate @SearchInfisical
InfisicalCertificate
- Generates a keypair locally, builds a CSR, and submits it for signing either via a PKI subscriber (-PkiSubscriberSlug, default parameter set) or by direct CA signing (-CertificateAuthorityId). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm.
+ Generates a keypair locally, builds a CSR, and submits it for signing via one of three parameter sets: a PKI subscriber (-PkiSubscriberSlug, default), direct CA signing (-CertificateAuthorityId), or a certificate profile (-CertificateProfileId, POSTs to /api/v1/cert-manager/certificates with the profile bound). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm.
Notes
@@ -1306,6 +1306,12 @@ $RequestInfisicalCertificateParameters.Verbose = $True
$RequestInfisicalCertificateResult = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
Issues (or renews within 30 days) a 3072-bit RSA certificate for the local FQDN, installs the leaf and chain into LocalMachine\My with a non-exportable machine-bound persistent key.
+
+ EXAMPLE 3
+ $Profile = Get-InfisicalCertificateProfile | Where-Object { $_.Slug -eq 'web-tier-profile' }
+Request-InfisicalCertificate -CertificateProfileId $Profile.Id -CommonName 'web01.contoso.com' -Ttl '90d'
+ Issues a certificate via the modern profile API (POST /api/v1/cert-manager/certificates). The profile binds the CA, policy, and defaults so no subscriber is required.
+
diff --git a/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs
index 95c8232..48f32ab 100644
--- a/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs
+++ b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs
@@ -158,6 +158,7 @@ namespace PSInfisicalAPI.Tests
Assert.NotNull(cmdletType.GetProperty("PkiSubscriberSlug"));
Assert.NotNull(cmdletType.GetProperty("CertificateAuthorityId"));
+ Assert.NotNull(cmdletType.GetProperty("CertificateProfileId"));
Assert.NotNull(cmdletType.GetProperty("Subject"));
Assert.NotNull(cmdletType.GetProperty("CommonName"));
Assert.NotNull(cmdletType.GetProperty("DnsName"));
diff --git a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs
index 1fc3b09..7f0ac2e 100644
--- a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs
@@ -23,6 +23,10 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("CaId")]
public string CertificateAuthorityId { get; set; }
+ [Parameter(ParameterSetName = "ByProfile", Mandatory = true, Position = 0)]
+ [Alias("ProfileId")]
+ public string CertificateProfileId { get; set; }
+
[Parameter] public string ProjectId { get; set; }
[Parameter] public IDictionary Subject { get; set; }
[Parameter] public string CommonName { get; set; }
@@ -38,13 +42,18 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter] public int KeySize { get; set; } = 2048;
[Parameter] public InfisicalEcCurve Curve { get; set; } = InfisicalEcCurve.P256;
- [Parameter(ParameterSetName = "ByCa")] public string Ttl { get; set; }
- [Parameter(ParameterSetName = "ByCa")] public string NotBefore { get; set; }
- [Parameter(ParameterSetName = "ByCa")] public string NotAfter { get; set; }
+ [Parameter(ParameterSetName = "ByCa")]
+ [Parameter(ParameterSetName = "ByProfile")] public string Ttl { get; set; }
+ [Parameter(ParameterSetName = "ByCa")]
+ [Parameter(ParameterSetName = "ByProfile")] public string NotBefore { get; set; }
+ [Parameter(ParameterSetName = "ByCa")]
+ [Parameter(ParameterSetName = "ByProfile")] public string NotAfter { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string FriendlyName { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string PkiCollectionId { get; set; }
- [Parameter(ParameterSetName = "ByCa")] public string[] KeyUsage { get; set; }
- [Parameter(ParameterSetName = "ByCa")] public string[] ExtendedKeyUsage { get; set; }
+ [Parameter(ParameterSetName = "ByCa")]
+ [Parameter(ParameterSetName = "ByProfile")] public string[] KeyUsage { get; set; }
+ [Parameter(ParameterSetName = "ByCa")]
+ [Parameter(ParameterSetName = "ByProfile")] public string[] ExtendedKeyUsage { get; set; }
[Parameter] public SwitchParameter Install { get; set; }
[Parameter] public StoreName StoreName { get; set; } = StoreName.My;
@@ -104,7 +113,7 @@ namespace PSInfisicalAPI.Cmdlets
return;
}
- string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "' or CA '", CertificateAuthorityId ?? "(n/a)", "' for CN=", csrSubject.CommonName);
+ string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "', CA '", CertificateAuthorityId ?? "(n/a)", "', or profile '", CertificateProfileId ?? "(n/a)", "' for CN=", csrSubject.CommonName);
if (!ShouldProcess(target, "Request new certificate")) { return; }
InfisicalCsrOptions csrOptions = new InfisicalCsrOptions { KeyAlgorithm = KeyAlgorithm, RsaKeySize = KeySize, EcCurve = Curve };
@@ -198,6 +207,12 @@ namespace PSInfisicalAPI.Cmdlets
return client.SignCertificateBySubscriber(connection, PkiSubscriberSlug, projectId, csrPem);
}
+ if (string.Equals(ParameterSetName, "ByProfile", StringComparison.Ordinal))
+ {
+ InfisicalCsrSubject subject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress);
+ return client.IssueCertificateByProfile(connection, CertificateProfileId, csrPem, subject.CommonName, subject.Organization, subject.OrganizationalUnit, subject.Country, subject.State, subject.Locality, Ttl, NotBefore, NotAfter, KeyUsage, ExtendedKeyUsage);
+ }
+
return client.SignCertificateByCa(connection, CertificateAuthorityId, csrPem, CommonName, null, Ttl, NotBefore, NotAfter, FriendlyName, PkiCollectionId, KeyUsage, ExtendedKeyUsage);
}
}
diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs
index 7ccdd03..fa32617 100644
--- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs
+++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs
@@ -51,6 +51,7 @@ namespace PSInfisicalAPI.Endpoints
public const string GetCertificateBundle = "GetCertificateBundle";
public const string SignCertificateBySubscriber = "SignCertificateBySubscriber";
public const string SignCertificateByCa = "SignCertificateByCa";
+ public const string IssueCertificateByProfile = "IssueCertificateByProfile";
public const string ListPkiSubscribers = "ListPkiSubscribers";
public const string GetPkiSubscriber = "GetPkiSubscriber";
diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs
index 06abc80..242a202 100644
--- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs
+++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs
@@ -623,6 +623,17 @@ namespace PSInfisicalAPI.Endpoints
ContainsSecretMaterialInResponse = true
});
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.IssueCertificateByProfile,
+ Resource = "Pki",
+ Version = "v1",
+ Method = "POST",
+ Template = "/api/v1/cert-manager/certificates",
+ RequiresAuthorization = true,
+ ContainsSecretMaterialInResponse = true
+ });
+
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListPkiSubscribers,
diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs
index 65e52b2..03f76e7 100644
--- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs
+++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs
@@ -300,6 +300,66 @@ namespace PSInfisicalAPI.Pki
};
}
+ public InfisicalSignedCertificate IssueCertificateByProfile(InfisicalConnection connection, string profileId, string csrPem, string commonName, string organization, string organizationalUnit, string country, string state, string locality, string ttl, string notBefore, string notAfter, IEnumerable keyUsages, IEnumerable extendedKeyUsages)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("CertificateProfileId is required."); }
+ if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); }
+
+ InfisicalIssueCertificateAttributesDto attributes = new InfisicalIssueCertificateAttributesDto
+ {
+ CommonName = commonName,
+ Organization = organization,
+ OrganizationalUnit = organizationalUnit,
+ Country = country,
+ State = state,
+ Locality = locality,
+ Ttl = ttl,
+ NotBefore = notBefore,
+ NotAfter = notAfter,
+ KeyUsages = keyUsages != null ? new List(keyUsages) : null,
+ ExtendedKeyUsages = extendedKeyUsages != null ? new List(extendedKeyUsages) : null
+ };
+
+ InfisicalIssueCertificateByProfileRequestDto request = new InfisicalIssueCertificateByProfileRequestDto
+ {
+ ProfileId = profileId,
+ Csr = csrPem,
+ Attributes = attributes
+ };
+ string body = _serializer.Serialize(request);
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to issue certificate via profile '", profileId, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.IssueCertificateByProfile, "IssueCertificateByProfile", null, null, body);
+ InfisicalIssueCertificateResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ if (dto == null || dto.Certificate == null || string.IsNullOrEmpty(dto.Certificate.Certificate))
+ {
+ string status = dto != null ? dto.Status : "unknown";
+ string message = dto != null ? dto.Message : null;
+ throw new InfisicalConfigurationException(string.Concat("Certificate was not issued (status='", status ?? "unknown", "'", string.IsNullOrEmpty(message) ? "" : string.Concat(", message='", message, "'"), "). The certificate profile may require manual approval or additional validation."));
+ }
+
+ InfisicalSignedCertificate signed = new InfisicalSignedCertificate
+ {
+ SerialNumber = dto.Certificate.SerialNumber,
+ CertificatePem = dto.Certificate.Certificate,
+ CertificateChainPem = dto.Certificate.CertificateChain,
+ IssuingCaCertificatePem = dto.Certificate.IssuingCaCertificate
+ };
+ _logger.Information(Component, "Infisical certificate issuance (profile) was successful.");
+ return signed;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical certificate issuance (profile) failed.");
+ throw;
+ }
+ }
+
public InfisicalPkiSubscriber[] ListPkiSubscribers(InfisicalConnection connection, string projectId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
diff --git a/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs
index e6a82eb..71d3647 100644
--- a/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs
+++ b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs
@@ -30,4 +30,44 @@ namespace PSInfisicalAPI.Pki
[JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; }
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
}
+
+ internal sealed class InfisicalIssueCertificateByProfileRequestDto
+ {
+ [JsonProperty("profileId")] public string ProfileId { get; set; }
+ [JsonProperty("csr", NullValueHandling = NullValueHandling.Ignore)] public string Csr { get; set; }
+ [JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] public InfisicalIssueCertificateAttributesDto Attributes { get; set; }
+ }
+
+ internal sealed class InfisicalIssueCertificateAttributesDto
+ {
+ [JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; }
+ [JsonProperty("organization", NullValueHandling = NullValueHandling.Ignore)] public string Organization { get; set; }
+ [JsonProperty("organizationalUnit", NullValueHandling = NullValueHandling.Ignore)] public string OrganizationalUnit { get; set; }
+ [JsonProperty("country", NullValueHandling = NullValueHandling.Ignore)] public string Country { get; set; }
+ [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] public string State { get; set; }
+ [JsonProperty("locality", NullValueHandling = NullValueHandling.Ignore)] public string Locality { get; set; }
+ [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; }
+ [JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; }
+ [JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; }
+ [JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List KeyUsages { get; set; }
+ [JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List ExtendedKeyUsages { get; set; }
+ }
+
+ internal sealed class InfisicalIssueCertificateResponseDto
+ {
+ [JsonProperty("certificate")] public InfisicalIssueCertificateInnerDto Certificate { get; set; }
+ [JsonProperty("certificateRequestId")] public string CertificateRequestId { get; set; }
+ [JsonProperty("status")] public string Status { get; set; }
+ [JsonProperty("message")] public string Message { get; set; }
+ }
+
+ internal sealed class InfisicalIssueCertificateInnerDto
+ {
+ [JsonProperty("certificate")] public string Certificate { get; set; }
+ [JsonProperty("certificateChain")] public string CertificateChain { get; set; }
+ [JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; }
+ [JsonProperty("serialNumber")] public string SerialNumber { get; set; }
+ [JsonProperty("certificateId")] public string CertificateId { get; set; }
+ [JsonProperty("privateKey")] public string PrivateKey { get; set; }
+ }
}