Reviewed-on: #23
PSInfisicalAPI
A C# binary PowerShell module for interacting with the Infisical REST API. It provides cmdlets for authentication, secret retrieval, structured export, and includes automatic environment-variable discovery so connections can be established with little or no inline configuration.
- License: AGPL-3.0
- Author: Grace Solutions
- Target framework: .NET Standard 2.0 (compatible with Windows PowerShell 5.1 and PowerShell 7+)
Installation
From the PowerShell Gallery
Install-Module -Name PSInfisicalAPI -Scope CurrentUser
Import-Module -Name PSInfisicalAPI
From source
git clone https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI.git
cd PSInfisicalAPI
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
Import-Module -Name .\Module\PSInfisicalAPI
Cmdlets
The module exports 68 cmdlets. Discovery cmdlets (Get-Infisical*) use a List (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
Session
| Cmdlet | Purpose |
|---|---|
Connect-Infisical |
Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. |
Disconnect-Infisical |
Clears the current Infisical session from the module-level session manager. |
Secrets
| Cmdlet | Purpose |
|---|---|
Get-InfisicalSecret |
Lists or retrieves Infisical secrets within a project, environment, and optional folder path. |
New-InfisicalSecret |
Creates a new Infisical secret, with support for SecureString values and bulk creation. |
Update-InfisicalSecret |
Updates an existing Infisical secret value, comment, name, or tags. |
Remove-InfisicalSecret |
Deletes one or many Infisical secrets by name. |
Copy-InfisicalSecret |
Duplicates one or more secrets into a different environment or secret path. |
ConvertTo-InfisicalSecretDictionary |
Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. |
Export-InfisicalSecrets |
Exports InfisicalSecret objects to disk or environment variables in a chosen file format. |
Organizations
| Cmdlet | Purpose |
|---|---|
Get-InfisicalOrganization |
Lists or retrieves Infisical organizations accessible to the current identity. |
New-InfisicalOrganization |
Creates a new Infisical organization. |
Update-InfisicalOrganization |
Updates the name or slug of an existing Infisical organization. |
Remove-InfisicalOrganization |
Deletes an Infisical organization. |
Sub-Organizations
| Cmdlet | Purpose |
|---|---|
Get-InfisicalSubOrganization |
Lists or retrieves Infisical sub-organizations, with optional search, paging, and ordering filters. |
New-InfisicalSubOrganization |
Creates a new Infisical sub-organization. |
Update-InfisicalSubOrganization |
Updates the name or slug of an existing Infisical sub-organization. |
Remove-InfisicalSubOrganization |
Deletes an Infisical sub-organization. |
Projects
| Cmdlet | Purpose |
|---|---|
Get-InfisicalProject |
Lists or retrieves Infisical projects accessible to the current identity. |
New-InfisicalProject |
Creates a new Infisical project in the active organization. |
Update-InfisicalProject |
Updates the name, description, or auto-capitalization flag on an existing project. |
Remove-InfisicalProject |
Deletes an Infisical project. |
Environments
| Cmdlet | Purpose |
|---|---|
Get-InfisicalEnvironment |
Lists or retrieves Infisical environments defined on a project. |
New-InfisicalEnvironment |
Creates a new environment on an Infisical project. |
Update-InfisicalEnvironment |
Updates the name, slug, or sort order of an existing Infisical environment. |
Remove-InfisicalEnvironment |
Deletes an Infisical environment from a project. |
Folders
| Cmdlet | Purpose |
|---|---|
Get-InfisicalFolder |
Lists or retrieves Infisical folders at a given secret path. |
New-InfisicalFolder |
Creates a new Infisical folder under the supplied parent path. |
Update-InfisicalFolder |
Renames an existing Infisical folder. |
Remove-InfisicalFolder |
Deletes an Infisical folder and all secrets it contains. |
Tags
| Cmdlet | Purpose |
|---|---|
Get-InfisicalTag |
Lists or retrieves Infisical tags defined on a project. |
New-InfisicalTag |
Creates a new Infisical tag on a project. |
Update-InfisicalTag |
Updates the slug, name, or color of an existing Infisical tag. |
Remove-InfisicalTag |
Deletes an Infisical tag from a project. |
PKI
| Cmdlet | Purpose |
|---|---|
Get-InfisicalCertificateAuthority |
Lists or retrieves Infisical internal Certificate Authorities. |
Get-InfisicalPkiSubscriber |
Lists or retrieves Infisical PKI subscribers in a project. |
Get-InfisicalCertificate |
Lists or retrieves Infisical certificates in a project, with optional filters and automatic paging. |
Request-InfisicalCertificate |
Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. |
ConvertTo-InfisicalCertificate |
Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. |
Install-InfisicalCertificate |
Installs an Infisical certificate (and optional chain) into a Windows certificate store. |
Uninstall-InfisicalCertificate |
Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. |
Export-InfisicalCertificate |
Exports an Infisical certificate to disk in PEM, PFX, or CER format. |
Get-InfisicalScepMdmProfile |
Projects an Infisical certificate profile into a Windows SCEP MDM profile model. |
Export-InfisicalScepMdmProfile |
Writes a SCEP MDM profile to disk as a SyncML payload suitable for MDM delivery. |
Write-InfisicalScepMdmProfileToWmi |
Submits a SCEP MDM profile to the local MDM Bridge WMI provider to trigger enrollment. |
Get-InfisicalSANList |
Builds a SAN candidate list (device name, <device>.<suffix> per adapter DNS suffix, RFC 1918 + CGNAT IPv4 addresses, IPv4/IPv6 loopback) for Request-InfisicalCertificate -DnsName. |
PKI configuration
Creating and changing the objects a Certificate Manager project is built from. -ProjectId is optional on all of them, and every one honours -WhatIf.
| Cmdlet | Purpose |
|---|---|
New-InfisicalCertificateAuthority |
Creates a root or intermediate internal CA, signing a subordinate with its parent so it can issue. |
Set-InfisicalCertificateAuthority |
Renames an internal CA or changes its status. |
Remove-InfisicalCertificateAuthority |
Deletes an internal CA. |
New-InfisicalCertificatePolicy |
Creates a certificate policy constraining subject, SANs, key usages, and validity. |
Set-InfisicalCertificatePolicy |
Updates a certificate policy; only supplied constraints are sent. |
Remove-InfisicalCertificatePolicy |
Deletes a certificate policy. |
New-InfisicalCertificateProfile |
Creates a certificate profile binding an issuing CA to a policy for enrollment. |
Set-InfisicalCertificateProfile |
Updates a certificate profile. |
Remove-InfisicalCertificateProfile |
Deletes a certificate profile. |
New-InfisicalCertificateApplication |
Creates a certificate application and optionally attaches profiles. |
Set-InfisicalCertificateApplication |
Renames an application, or attaches and detaches profiles. |
Remove-InfisicalCertificateApplication |
Deletes a certificate application. |
New-InfisicalPkiSubscriber |
Creates a PKI subscriber: one named identity with a fixed common name. |
Set-InfisicalPkiSubscriber |
Updates a PKI subscriber. |
Remove-InfisicalPkiSubscriber |
Deletes a PKI subscriber. |
Constraint dictionaries take Allowed, Required, and Denied in whatever casing reads naturally — they reach the API lower-cased — and an empty list is omitted rather than sent as "allow nothing":
$Root = New-InfisicalCertificateAuthority -Name 'root-ca' -Type Root -CommonName 'Contoso Root CA' -Organization 'Contoso' -Country 'US'
$Ca = New-InfisicalCertificateAuthority -Name 'issuing-ca' -Type Intermediate -ParentCaId $Root.Id -CommonName 'Contoso Issuing CA'
$Policy = New-InfisicalCertificatePolicy -Name 'server-auth' -MaxValidity '90d' `
-KeyAlgorithm 'RSA_2048','EC_secp384r1' `
-KeyUsage @{ Required = @('digital_signature','key_encipherment') } `
-ExtendedKeyUsage @{ Required = @('server_auth','client_auth') }
$CertificateProfile = New-InfisicalCertificateProfile -Slug 'server-auth' -CertificatePolicyId $Policy.Id -CaId $Ca.Id -AutoRenew -RenewBeforeDays 14
$Application = New-InfisicalCertificateApplication -Name 'platform' -ProfileId $CertificateProfile.Id
The intermediate comes back ready to issue: Infisical creates a subordinate pending a certificate, and New-InfisicalCertificateAuthority performs the remaining sequence — read the CSR, sign it with -ParentCaId, import the result.
Process
| Cmdlet | Purpose |
|---|---|
Start-InfisicalProcess |
Launches a child process with Infisical secrets injected directly into its environment block, capturing stdout/stderr and validating the exit code. |
Use Get-Help <Cmdlet> -Full for parameter details and Get-Help about_PSInfisicalAPI for the module overview.
Quick start
$secureSecret = Read-Host -AsSecureString 'Client Secret'
$connection = Connect-Infisical `
-BaseUri 'https://app.infisical.com' `
-OrganizationId '00000000-0000-0000-0000-000000000000' `
-ClientId 'machine-identity-client-id' `
-ClientSecret $secureSecret `
-PassThru
Get-InfisicalSecret -ProjectId '11111111-1111-1111-1111-111111111111' -Environment 'dev' -SecretPath '/'
Disconnect-Infisical
Connect-Infisical establishes the session; project, environment, and secret path are supplied per call. On the PKI cmdlets -ProjectId is optional — see Project scoping.
End-to-end: request and install a chained certificate
Connects, selects a cert-manager project, sources SANs from Get-InfisicalSANList, requests a certificate through a certificate profile, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted OrderedDictionary constructed with OrdinalIgnoreCase so parameter names round-trip case-insensitively.
This is the shape to use for fleet enrollment, where each machine needs its own common name. See Choosing an issuance path — a PKI subscriber is not the right tool for this, because it pins one fixed common name.
$ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com'
$ConnectInfisicalParameters.OrganizationId = '00000000-0000-0000-0000-000000000000'
$ConnectInfisicalParameters.ClientId = 'machine-identity-client-id'
$ConnectInfisicalParameters.ClientSecret = ConvertTo-SecureString -String 'ClientSecret' -AsPlainText -Force
$ConnectInfisicalParameters.PassThru = $True
$ConnectInfisicalParameters.Verbose = $True
$Connection = Connect-Infisical @ConnectInfisicalParameters
$Application = Get-InfisicalCertificateApplication | Where-Object {($_.Name -ieq 'platform')}
$CertificateProfile = Get-InfisicalCertificateProfile -ApplicationId ($Application.Id) -IncludeConfigs | Where-Object {($_.EnrollmentType -ieq 'api') -and ($_.Slug -imatch 'server')} | Select-Object -First 1
$SanList = Get-InfisicalSANList
$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$RequestInfisicalCertificateParameters.CertificateProfileId = $CertificateProfile.Id
$RequestInfisicalCertificateParameters.CommonName = $Env:ComputerName.ToUpper()
$RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
$RequestInfisicalCertificateParameters.DnsName.Add('app.contoso.com')
$RequestInfisicalCertificateParameters.Ttl = '90d'
$RequestInfisicalCertificateParameters.Metadata = [Ordered]@{ Environment = 'Production'; Owner = 'Platform' }
$RequestInfisicalCertificateParameters.Install = $True
$RequestInfisicalCertificateParameters.InstallChain = $True
$RequestInfisicalCertificateParameters.Verbose = $True
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
$Null = Disconnect-Infisical -Verbose
Four calls: find the application, pick its profile, gather SANs, request. No project lookup — -ProjectId resolves itself — and no CA lookup, because the profile already binds its issuing CA and -InstallChain installs the whole chain regardless.
-StoreName/-StoreLocation are omitted deliberately — see Where certificates get installed.
Renewing that certificate
Run the same request again with -AllowRenewal. Nothing else changes — the parameters below are the ones built above, so this is the shape to put on a schedule:
$RequestInfisicalCertificateParameters.AllowRenewal = $True
$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
The call is safe to repeat. Outside the renewal window it finds the installed certificate and returns it untouched; inside the window it issues a replacement and installs it:
VERBOSE: Reuse search for CN=WEB01 scoped to certificate profile 'a42f8446-...' returned 1 active certificate(s).
VERBOSE: Reusing existing certificate (Thumbprint=F480A920..., NotAfter=2026-10-28 19:36:49Z).
Reuse is matched on the issuing profile and the requested SAN set, so the same script issues a new certificate — rather than silently returning the old one — whenever the profile changes or a name is added to -DnsName. -Force issues unconditionally, ignoring both the renewal window and any existing certificate.
Run it daily and it does nothing until the certificate is within 30 days of expiry, then rotates it. Nothing to gate it with, and no state to keep.
Project scoping
Opening Certificate Manager in the Infisical console never asks which project to use. The project is in the URL — /organizations/{orgId}/projects/cert-manager/{projectId}/applications — but it is chosen for you, and everything below it is presented as applications. That is because Infisical's own resolver takes the single Certificate Manager project when an organization has exactly one:
const projects = await projectDAL.find({ orgId: actorOrgId, type: ProjectType.CertificateManager });
if (projects.length === 1) return projects[0].id;
The PKI cmdlets follow the same rule: -ProjectId is optional. Omit it and the module resolves the organization's only Certificate Manager project, reporting the choice on the verbose stream:
Get-InfisicalCertificateApplication
Get-InfisicalCertificateAuthority -Kind Internal
Get-InfisicalCertificateProfile -IncludeConfigs
Get-InfisicalPkiSubscriber
Get-InfisicalCertificate -Status active
VERBOSE: -ProjectId was not supplied; resolved the organization's only Certificate Manager project
'Microsoft Endpoint Configuration Manager' (2122628e-...).
An organization with no Certificate Manager project is not an error either. There is nothing to list, so the PKI Get-* cmdlets return nothing and -Verbose explains why:
VERBOSE: -ProjectId was not supplied and this organization has no Certificate Manager project, so there is
nothing to resolve to. Create one in Infisical (Certificate Management), or pass -ProjectId to
target a specific project.
Several Certificate Manager projects in one organization is not an error. Infisical designates one as the organization's active project, and that is what resolution picks:
VERBOSE: -ProjectId was not supplied; resolved the organization's active Certificate Manager project
'Platform PKI' (aaaa...).
If no active project is designated, the first is used and the verbose line says so; pass -ProjectId to target another.
This is resolved client-side rather than left to the server because several PKI endpoints carry the project in the URL path (/api/v1/projects/{projectId}/pki-subscribers, /certificates/search) and cannot defer to the server's resolver.
One project per organization, in practice
An organization can hold several Certificate Manager projects, but certificate applications are served only from the active one. The applications router rejects anything else outright:
if (req.internalCertManagerProjectId !== activeProjectId) {
throw new BadRequestError({ message: "Applications are only available on this organization's active Certificate Manager project." });
}
So an application-centric workflow is single-project by design. Additional Certificate Manager projects can exist and hold their own CAs, policies, profiles, and certificates, but they are reachable only by passing -ProjectId explicitly, and applications will not work in them.
Projects contain applications
The two are different levels, which is worth keeping straight when reading output:
| What it is | Cmdlet | |
|---|---|---|
| Project | The Certificate Manager project itself. One per organization in most setups. | Get-InfisicalProject -Type cert-manager |
| Application | A grouping of profiles, members, and certificates inside that project. | Get-InfisicalCertificateApplication |
So a project named pki can contain applications named platform and endpoint-management; listing projects returns only pki, because the others are not projects. Every application carries the ProjectId it belongs to, which is why that field is real rather than vestigial.
Profiles can be filtered to an application, matching how the console groups them:
$Application = Get-InfisicalCertificateApplication | Where-Object {($_.Name -ieq 'platform')}
Get-InfisicalCertificateProfile -ApplicationId $Application.Id -IncludeConfigs
Get-InfisicalCertificate -ApplicationId $Application.Id
Example output
Id : 11111111-1111-1111-1111-111111111111
ProjectId : 00000000-0000-0000-0000-000000000000
Name : platform
Description :
ProfileCount : 3
MemberCount : 2
CertificateCount : 0
CreatedAtUtc : 7/30/2026 10:05:37 PM +00:00
UpdatedAtUtc : 7/30/2026 10:05:37 PM +00:00
Id : 44444444-4444-4444-4444-444444444444
ProjectId : 00000000-0000-0000-0000-000000000000
CaId : 33333333-3333-3333-3333-333333333333
CertificatePolicyId : 55555555-5555-5555-5555-555555555555
Slug : serverauthentication
Description :
EnrollmentType : api
IssuerType : ca
EstConfigId :
ApiConfigId : 66666666-6666-6666-6666-666666666666
AcmeConfigId :
ScepConfigId :
CreatedAtUtc : 3/25/2026 4:53:53 PM +00:00
UpdatedAtUtc : 3/25/2026 4:53:53 PM +00:00
Defaults : PSInfisicalAPI.Models.InfisicalCertificateProfileDefaults
CertificateAuthority : PSInfisicalAPI.Models.InfisicalCertificateAuthoritySummary
CertificatePolicy :
ApiConfig : PSInfisicalAPI.Models.InfisicalCertificateProfileApiConfig
WEB01
10.20.30.40
WEB01.contoso.com
127.0.0.1
::1
VERBOSE: [...] - [Information] - [GetInfisicalCertificateApplicationCmdlet] - -ProjectId was not supplied; resolved the organization's only Certificate Manager project 'Platform PKI' (00000000-0000-0000-0000-000000000000).
VERBOSE: [...] - [Information] - [PkiClient] - Attempting to search Infisical certificates. Please Wait...
VERBOSE: [...] - [Verbose] - [HttpClient] - Attempting HTTP POST to https://infisical.contoso.com/api/v1/projects/00000000-0000-0000-0000-000000000000/certificates/search. Please Wait...
VERBOSE: [...] - [Verbose] - [HttpClient] - HTTP POST completed with status 200.
VERBOSE: [...] - [Information] - [PkiClient] - Infisical certificate search was successful.
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Process is elevated; defaulting -StoreLocation to LocalMachine. Pass -StoreLocation explicitly to override.
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing via certificate profile '44444444-4444-4444-4444-444444444444' in project '00000000-0000-0000-0000-000000000000'.
VERBOSE: Performing the operation "Request new certificate" on target "certificate profile '44444444-4444-4444-4444-444444444444' for CN=WEB01".
VERBOSE: [...] - [Information] - [PkiClient] - Attempting to issue certificate via profile '44444444-4444-4444-4444-444444444444'. Please Wait...
VERBOSE: [...] - [Verbose] - [HttpClient] - Attempting HTTP POST to https://infisical.contoso.com/api/v1/cert-manager/certificates. Please Wait...
VERBOSE: [...] - [Verbose] - [HttpClient] - HTTP POST completed with status 200.
VERBOSE: [...] - [Information] - [PkiClient] - Infisical certificate issuance (profile) was successful.
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Installed certificate to LocalMachine\My [F480A920DFB41EA8EE3E9178C1BC6A5EC7055B96].
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Installed certificate to LocalMachine\CertificateAuthority [89A486A532D94EFE4391BEF2EA7F5E7E2B654AB0].
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Installed certificate to LocalMachine\Root [1F3A77B0C2D45E6819AB3C7D0E5F2A9B4C81D6E7].
Where certificates get installed
When -StoreLocation is not supplied, the cmdlet picks it from the process's elevation, and says which it chose on the verbose stream:
| Session | Leaf | Intermediates | Roots |
|---|---|---|---|
| Elevated | LocalMachine\My |
LocalMachine\CertificateAuthority |
LocalMachine\Root |
| Not elevated | CurrentUser\My |
CurrentUser\CertificateAuthority |
CurrentUser\Root |
The routing split is deliberate:
- The leaf honours
-StoreName(defaultMy) and lands in the resolved location. - Chain members ignore
-StoreNameand are routed by what they are — a self-signed certificate is a root and goes to the trusted-root store; anything with an issuer above it is a subordinate CA and goes to the intermediate store. A chain of any depth is walked, so an issuing subordinate installs alongside every CA above it up to the root.
-StoreLocation still applies to all of them, so an elevated run puts the whole chain machine-wide and a non-elevated run puts it all under the user.
Issuers are installed before the leaf, so the certificate is chainable the moment it appears in the store. Afterwards the chain is validated against the machine's own stores, and an incomplete result is called out by name rather than left to be discovered in certmgr:
WARNING: [...] Certificate chain is incomplete (PartialChain). The highest certificate installed is
'CN=Contoso Intermediate Certificate Authority, OU=IT, O=Contoso, C=US', whose issuer
'CN=Contoso Root Certificate Authority, OU=IT, O=Contoso, C=US' is not present in the trusted stores.
Windows will report "The issuer of this certificate could not be found" until that issuer is installed.
The installed certificate's Windows friendly name defaults to the common name in upper case (WEB01), which is what shows in certmgr. Pass -FriendlyName on any parameter set to override it; on the -CertificateAuthorityId path the same value is also forwarded to Infisical as the issued certificate's friendlyName.
Metadata
-Metadata attaches key/value pairs to the certificate in Infisical, and accepts any IDictionary — a hashtable, an [Ordered] dictionary, or a generic Dictionary[String,String]:
$RequestInfisicalCertificateParameters.Metadata = [Ordered]@{
Environment = 'Production'
Owner = 'Platform Engineering'
ManagedBy = 'Invoke-SecretStaging'
Site = 'HQ'
}
Only the supplied keys are reconciled. Keys already on the certificate that this call does not mention are left alone, so several callers can each own their own keys without clobbering each other:
Request-InfisicalCertificate @Parameters -Metadata @{ Owner = 'Platform' } # certificate now has Owner
Request-InfisicalCertificate @Parameters -Metadata @{ Site = 'HQ' } # Owner survives; Site added
Request-InfisicalCertificate @Parameters -Metadata @{ Owner = 'Security' } # Owner updated; Site survives
This is done client-side. Infisical's PATCH /certificates/{id} replaces a certificate's metadata wholesale — the service deletes every row before inserting what it was sent — so the module reads the current set, merges the supplied keys over it, and writes back the union. When nothing would change, no request is sent at all.
Reconciliation runs on the reuse path too, so a metadata change lands without forcing reissuance. Values are flattened to strings (443 becomes "443", $True becomes "True", $Null becomes ""), keys are trimmed and compared case-insensitively, and blank keys are dropped.
The result carries the certificate's metadata after reconciliation:
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
$Certificate.Metadata['Environment'] # Production
Metadata never fails an issuance that otherwise succeeded. By the time it is applied the certificate exists and may already be installed, so a failure is reported as a warning and the certificate is still returned.
Metadata is also a search filter — Get-InfisicalCertificate accepts -Metadata to find certificates by the keys you stamped on them.
Reuse and renewal
A second run does not issue a new certificate if a still-valid one is already installed. That check is scoped to the issuer you asked for, not just the common name: the reuse search is filtered by -CertificateProfileId or -CertificateAuthorityId, so switching profiles issues a new certificate rather than handing back the old one.
This matters when two profiles over the same CA differ in key usage. Requesting from a client-authentication profile on a host that already holds a server-authentication certificate for the same name issues a new certificate, because a name match alone would return one with the wrong EKUs:
VERBOSE: Reuse search for CN=WEB01 scoped to certificate profile 'a42f8446-...' returned 0 active certificate(s).
Reuse also requires the existing certificate to carry every name being requested. Adding an entry to -DnsName and re-running issues a new certificate rather than returning one that would fail validation for the name you just added:
VERBOSE: An existing certificate for CN=WEB01 does not carry the requested name DNS:api.contoso.com;
requesting a new certificate rather than reusing one that would fail validation for it.
The rule is coverage, not equality — a certificate carrying more names than requested still satisfies the request. DNS names compare case-insensitively and IP addresses are normalized, so ::1 matches 0:0:0:0:0:0:0:1. Removing a name from the request therefore reuses the existing certificate; use -Force when you need the SAN set trimmed rather than extended.
-Force issues unconditionally, and -AllowRenewal with -RenewalThresholdDays rotates a certificate that is inside its renewal window.
If Infisical cannot be reached, the reuse check cannot confirm which certificates belong to which issuer and falls back to matching on the common name alone. That is announced as a warning, since it can return a certificate from a different profile.
When the resolved location is LocalMachine and -KeyStorageFlags was not supplied, the private key is written to the machine key store. Without that the key lands in the calling user's profile while the certificate sits in LocalMachine\My, which is the usual cause of an installed certificate that reports no usable private key to a service.
Non-elevated root installs prompt. Adding a root to
CurrentUser\Rootmakes Windows raise a modal trust dialog, and the call blocks until it is answered — if the dialog is hidden or the session is non-interactive (a scheduled task, an MECM task sequence), the cmdlet appears to hang indefinitely. It warns before blocking. Run elevated, or pass-StoreLocation LocalMachine, to install machine-wide with no prompt.
Choosing an issuance path
Request-InfisicalCertificate has three mutually exclusive issuance parameter sets. The deciding question is whether the common name varies per request:
| Parameter | Common name | Use when |
|---|---|---|
-CertificateProfileId |
Per request, constrained by policy | Fleet enrollment — many machines, each with its own CN. Works on any CA. |
-CertificateAuthorityId |
Per request, unconstrained | Rarely usable — needs direct issuance, which cannot be enabled (see below). |
-PkiSubscriberSlug |
Fixed by the subscriber record | One named identity — a specific service or host, provisioned in advance. |
A PKI subscriber is a per-identity object, not a fleet template. signSubscriberCert rejects any CSR whose CN differs from the subscriber's:
Common name (CN) in the CSR does not match the subscriber's common name
It also allowlists SANs — every dNSName/email SAN in the CSR must appear in the subscriber's subjectAlternativeNames — and requires CSR key usages to be a subset of the subscriber's. (IP SANs are not covered by that check.) Enrolling N machines through subscribers therefore means creating N subscribers. Prefer a profile.
There is no -CertificateTemplateId parameter. Infisical's REST API exposes no template-based issuance route — templates are consumed internally by EST and subscribers — so when the API says "Certificate template or subscriber is required for issuance", the reachable answers are a profile, a subscriber, or direct issuance.
The cmdlet resolves and reports the issuer before generating a keypair, so -Verbose tells you exactly what will sign the request:
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing via certificate profile 'a1b2c3d4-...' in project '2122628e-...'.
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing directly via certificate authority 'intermediate-ca' (bf661d78-...); direct issuance is enabled.
-WhatIf names the same issuer without issuing anything:
Request-InfisicalCertificate @RequestInfisicalCertificateParameters -WhatIf
# What if: Performing the operation "Request new certificate" on target
# "certificate profile 'a1b2c3d4-...' for CN=WEB01".
Discovering profiles (recommended)
A certificate profile binds a CA to a certificate policy. The policy constrains the subject, key usages, and extended key usages with allowed/required/denied lists, so the common name still varies per request while staying inside guardrails.
Get-InfisicalCertificateProfile | Format-Table Id, Slug, CaId, EnrollmentType
Get-InfisicalCertificatePolicy | Format-Table Id, Name
Profile issuance is the only path that does not consult the CA's direct-issuance flag — the service short-circuits it:
if (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) { throw ... }
So a profile issues successfully against a CA whose EnableDirectIssuance is False. If a project has no profiles, create a policy then a profile under Certificate Management in the Infisical UI.
Discovering subscribers
Get-InfisicalPkiSubscriber |
Format-Table Name, CommonName, Status, Ttl, CaId
Pass the subscriber's Name to -PkiSubscriberSlug, and set -CommonName to exactly that subscriber's CommonName. Because the subscriber owns the lifetime and usage policy, -Ttl, -KeyUsage, and -ExtendedKeyUsage are not accepted on this parameter set — set them on the subscriber in Infisical instead.
An empty result means the project has no subscribers; create one under Certificate Management > Subscribers. This module is read-only for subscribers, so creation is UI or raw API (POST /api/v1/pki/subscribers).
Direct issuance on a CA
Direct issuance lets a CA sign a bare CSR with no profile, subscriber, or template in front of it. When it is off, Infisical rejects the request with 400 Certificate template or subscriber is required for issuance; this module catches that before building a CSR.
There is no UI toggle or API field for this.
enableDirectIssuanceappears in no create or update schema — the generic CA schemas accept onlynameandstatus. It is set at CA creation (the column defaults totrue) and is not editable afterwards through the public API.
A CA can therefore read False for a reason that is not obvious. Migration 20250521110635_add-external-ca-pki.ts renamed the older requireTemplateForIssuance column to enableDirectIssuance and inverted every existing value:
t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
...
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
Any CA created before that migration with "require template for issuance" enabled now reads EnableDirectIssuance = False permanently.
Creating a new CA does not help either. Although the database column defaults to true, the creation service passes false explicitly:
const ca = await certificateAuthorityDAL.create({ projectId, name: resolvedCaName, status, enableDirectIssuance: false }, tx);
So every CA created through the API or UI has direct issuance disabled, and nothing can turn it on afterwards. Use a certificate profile, which ignores the flag entirely.
Get-InfisicalCertificateAuthority -Kind Internal |
Format-Table Name, CommonName, Status, EnableDirectIssuance
$Ca = Get-InfisicalCertificateAuthority -Kind Internal |
Where-Object {($_.EnableDirectIssuance -eq $True)} |
Select-Object -First 1
$RequestInfisicalCertificateParameters.CertificateAuthorityId = $Ca.Id
$RequestInfisicalCertificateParameters.Ttl = '90d' # required by the CA path
Subject and SAN handling
-CommonNametakes the bare value (WEB01.contoso.com), not an RDN.CN=WEB01is accepted and normalized, since the CSR builder adds theCN=prefix itself.Get-InfisicalSANListreturns DNS names and IP addresses in one list. Passing the whole list to-DnsNameis fine: IP literals are detected and emitted asiPAddressSAN entries rather than malformeddNSNameentries.-Ttl(or-NotAfter) applies to the-CertificateAuthorityIdand-CertificateProfileIdpaths. Subscriber-issued certificates take their lifetime from the subscriber definition.
Diagnostics and error handling
Every cmdlet derives from PSCmdlet, so the full set of common parameters is bound: -Verbose, -Debug, -ErrorAction, -ErrorVariable, -WarningAction, -WarningVariable, -InformationAction, -InformationVariable, -OutVariable, -PipelineVariable, and -WhatIf/-Confirm on the cmdlets that declare SupportsShouldProcess.
Output is routed by stream so those parameters mean what they say:
| Stream | Carries | Controlled by |
|---|---|---|
| Error | The failure itself, once, as a non-terminating ErrorRecord |
-ErrorAction, -ErrorVariable, 2> |
| Warning | Genuine advisories that are not failures (e.g. issuance returned no certificate) | -WarningAction, -WarningVariable |
| Verbose | Request/response trace and the diagnostic trail leading up to a failure | -Verbose |
| Debug | Low-level detail | -Debug |
A failed call surfaces exactly one error. The [Error]-tagged diagnostic lines that precede it are on the verbose stream, so they appear only under -Verbose and never compete with the ErrorRecord:
# One error, no warning noise.
Request-InfisicalCertificate @Parameters -ErrorVariable Failure -ErrorAction SilentlyContinue
# The ErrorRecord carries the API detail; no log scraping required.
$Failure[0].Exception.StatusCode # 400
$Failure[0].Exception.ApiErrorCode # BadRequest
$Failure[0].Exception.ApiErrorMessage # Certificate template or subscriber is required for issuance
$Failure[0].Exception.ApiRequestId # req-SSPFN1gc2zHvkV
-ErrorAction decides the outcome
Operation failures are reported as non-terminating errors, so -ErrorAction (or $ErrorActionPreference) governs what happens, exactly as it does for built-in cmdlets:
-ErrorAction |
Behavior |
|---|---|
Continue (default) |
Error is written; a pipeline keeps processing its remaining input |
SilentlyContinue |
Nothing is printed; the error is still in $Error and -ErrorVariable |
Ignore |
Nothing is printed and nothing is recorded in $Error |
Stop |
Promoted to a terminating error that try/catch catches |
Inquire |
Prompts |
A failing item does not abort the batch:
'web01', 'does-not-exist', 'web02' |
ForEach-Object { Get-InfisicalPkiSubscriber -Name $_ -ErrorAction SilentlyContinue }
# emits web01 and web02; the failure is available in $Error
To catch failures you must ask for it with -ErrorAction Stop or $ErrorActionPreference = 'Stop':
try {
$Certificate = Request-InfisicalCertificate @Parameters -ErrorAction Stop
} catch [PSInfisicalAPI.Errors.InfisicalApiException] {
Write-Warning "Issuance failed with HTTP $($_.Exception.StatusCode): $($_.Exception.ApiErrorMessage)"
}
Breaking change. Failures were previously terminating, so
try/catchcaught them without-ErrorAction Stop. Existingtry/catchblocks need-ErrorAction Stopadded (or$ErrorActionPreference = 'Stop'set) to keep catching.
Exception types are InfisicalApiException, InfisicalAuthenticationException, InfisicalHttpException, InfisicalSerializationException, InfisicalConfigurationException, InfisicalExportException, and InfisicalImportException, all deriving from InfisicalException.
Automatic environment-variable discovery
When Connect-Infisical is invoked with one or more parameters missing (or set to whitespace/empty), the cmdlet searches environment variables and uses the first value it finds. This makes invocation as simple as Connect-Infisical when variables are set up in advance.
Scope precedence
Scopes are searched in order; the first matching variable with a non-blank value wins:
ProcessUserMachine
Patterns
The resolver matches case-insensitively against patterns aligned with Infisical's CLI defaults plus common variants such as CLOUDINIT_INFISICAL_* and custom-prefixed names (e.g., myapp_infisical_client_id).
| Parameter | Example variable names matched |
|---|---|
BaseUri |
INFISICAL_API_URL, INFISICAL_BASE_URL, INFISICAL_BASE_URI, INFISICAL_HOST |
OrganizationId |
INFISICAL_ORG_ID, INFISICAL_ORGANIZATION_ID |
ClientId |
INFISICAL_CLIENT_ID, INFISICAL_UNIVERSAL_AUTH_CLIENT_ID |
ClientSecret |
INFISICAL_CLIENT_SECRET, INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET |
AccessToken |
INFISICAL_TOKEN, INFISICAL_ACCESS_TOKEN, INFISICAL_AUTH_TOKEN |
ApiVersion |
INFISICAL_API_VERSION |
Discovery covers the connection itself. Project, environment, and secret path are per-call parameters, so they are not resolved from the environment.
Sensitive values (ClientSecret, AccessToken) are read directly into a read-only SecureString and never logged.
Zero-configuration example
[Environment]::SetEnvironmentVariable('INFISICAL_API_URL', 'https://app.infisical.com', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ORG_ID', '00000000-0000-0000-0000-000000000000', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_PROJECT_ID', '11111111-1111-1111-1111-111111111111', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ENVIRONMENT', 'dev', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_ID', 'machine-identity-client-id', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_SECRET', 'super-secret-value', 'User')
Connect-Infisical
Get-InfisicalSecret
Mixed example (explicit values override discovery)
Explicit parameters always win over discovered values; blank/whitespace explicit values trigger discovery.
Connect-Infisical -Environment 'prod' # everything else discovered from environment
Logging
The resolver emits a single verbose line announcing the scan and one informational line per discovered variable (variable name and scope; values are never logged). Use -Verbose to see the scan announcement.
Building
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
The script builds the binary, runs unit tests, publishes binaries into Module/PSInfisicalAPI/bin/, regenerates the manifest, and validates that the module imports.
Extending the module
Adding a new API endpoint
All HTTP routes live in two files under src/PSInfisicalAPI/Endpoints/:
InfisicalEndpointNames.csdeclares aconst stringidentifier for each endpoint.InfisicalEndpointRegistry.csmaps each identifier to one or moreInfisicalEndpointDefinitionrecords grouped by resource (RegisterAuthentication,RegisterSecrets,RegisterPki, etc.).
To add a route:
- Add a constant in
InfisicalEndpointNames.cs(e.g.,public const string ListPkiSubscribers = "ListPkiSubscribers";). - In the matching
Register<Resource>method, callAdd(map, new InfisicalEndpointDefinition { ... })withName,Resource,Version,Method,Template, and theRequiresAuthorization/ContainsSecretMaterialInRequest/ContainsSecretMaterialInResponseflags. Use{placeholder}tokens inTemplate; they are substituted from thepathParametersdictionary passed by the caller. - If the same logical operation has more than one upstream path (legacy + current), register both definitions under the same
Name—InvokeWithCandidateFallbacktries each in order until one succeeds. - Invoke the endpoint from the appropriate client (
InfisicalPkiClient,InfisicalSecretsClient, etc.) via_invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.XYZ, "XYZ", pathParameters, query, body).
Adding a new cmdlet
Cmdlets live in src/PSInfisicalAPI/Cmdlets/ and derive from InfisicalCmdletBase, which exposes HttpClient, Logger, ResolveCertManagerProjectId, and ThrowTerminatingForException. Follow the consolidated discovery pattern when the cmdlet supports both list and single-record retrieval:
[Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")]
[OutputType(typeof(InfisicalPkiSubscriber))]
public sealed class GetInfisicalPkiSubscriberCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "ByName", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("SubscriberName", "Slug")]
public string Name { get; set; }
[Parameter] public string ProjectId { get; set; }
protected override void ProcessRecord() { /* dispatch on ParameterSetName */ }
}
After adding (or removing) a cmdlet:
-
Update
build.ps1in two places — theCmdletsToExportarray inside the generated manifest block, and the$expectedCmdsarray used byTest-ModuleImports. Both must list the same cmdlets; the build fails fast if they drift. -
Add a
<command:command>entry inModule/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml. Each entry must include a non-empty<maml:description>synopsis (do not let it start with the cmdlet name — the validation gate rejects PowerShell's auto-generated fallback), a non-empty<maml:description>body, and at least one<command:example>with a non-empty<dev:code>block. -
For consolidated
List/ single-record cmdlets, ship three examples: two straight-line invocations (one per parameter set) and oneOrderedDictionarysplat. The splat must construct the dictionary withOrdinalIgnoreCaseso parameter names round-trip case-insensitively:$Params = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) $Params.ProjectId = (Get-InfisicalProject -Type cert-manager | Select-Object -First 1).Id $Result = Get-InfisicalPkiSubscriber @Params -
Add a
## Unreleasedentry toCHANGELOG.mddescribing the change (mark removals of public cmdlets or parameters as BREAKING). -
Run
./build.ps1 -RunTests. The script enforces the cmdlet list, runs the xUnit suite, and verifies that every exported cmdlet has a valid synopsis, description, and at least one non-empty example.
Committing source and build artifacts in lockstep
The embedded BuildCommitHash in Module/PSInfisicalAPI/PSInfisicalAPI.psd1 and the bundled DLL is captured from git rev-parse HEAD at build time. To keep the embedded hash truthful, commit source and build artifacts as two ordered commits:
- Stage and commit your source changes first. Suppose this produces commit
S. - Run
./build.ps1 -RunTests -CommitArtifacts. The build picks upSasHEAD, embeds it asBuildCommitHash, then stages and commits only the build outputs (Module/PSInfisicalAPI/bin/**,Module/PSInfisicalAPI/PSInfisicalAPI.psd1, and theCHANGELOG.mdbuild-stamp insertion). The commit message referencesSso the binary commit always traces back to its source. git push.
-CommitArtifacts only touches the three artifact paths above; any other dirty files in your working tree are left alone. Use the older -CommitOnSuccess switch only when you intentionally want a single commit covering everything (git add -A + git commit -m "Build <version>"); the two switches are mutually exclusive.
Continuous integration
.gitea/workflows/publish-psgallery.yml publishes the module to the PowerShell Gallery whenever a pull request is merged into main. The workflow expects a repository secret named PSGALLERY_API_KEY containing a valid Gallery API key.
License
Distributed under the GNU Affero General Public License v3.0. See LICENSE.