Correct issuance-path guidance: profiles for fleets, subscribers are per-identity #19

Merged
gsadmin merged 3 commits from dev into main 2026-07-30 23:08:58 +00:00
Owner

Follow-up to #18. The subscriber guidance that PR shipped was wrong for fleet enrollment, and this corrects it.

What was wrong

#18 recommended -PkiSubscriberSlug as "preferred for most setups" and showed an example that grabs the first subscriber then passes the local machine's own CommonName. That fails on every machine whose name is not that subscriber's CN.

From pki-subscriber-service.ts signSubscriberCert:

const cn = dn.commonName;
if (cn !== subscriber.commonName) {
  throw new BadRequestError({ message: "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 subscriber.subjectAlternativeNames) and requires CSR key usages to be a subset of the subscriber's. A subscriber is a single named identity, so enrolling N machines would require N subscribers.

The correct guidance

Certificate profiles are the fleet path. They take a per-request common name constrained by policy allowed/required/denied lists, and profile issuance is the only path that skips the CA direct-issuance gate:

if (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) { throw ... }

So -CertificateProfileId issues successfully against a CA whose EnableDirectIssuance is False.

Direct issuance cannot be toggled

Also documented, because it is not discoverable: enableDirectIssuance appears in no Infisical create or update schema. The generic CA schemas accept only name and status, and none of general-certificate-authority-router.ts, internal-certificate-authority-router.ts, or certificate-authority-endpoints.ts reference it. There is no UI toggle and no API field.

A CA can read False for a non-obvious reason. Migration 20250521110635_add-external-ca-pki.ts renamed the older requireTemplateForIssuance column and inverted every existing value:

t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
...
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });

Any CA that previously required a template now reads EnableDirectIssuance = False permanently. The remedies are a profile, or a new CA (the column defaults to true).

Changes

  • README end-to-end example switched from subscriber to profile issuance, with -Ttl restored.
  • Issuance-path table now leads with whether the common name varies per request, and states each path's CN behavior.
  • New "Discovering profiles (recommended)" section; subscriber section rewritten to state the CN-match requirement.
  • "Direct issuance on a CA" section documents that it is fixed at creation, with the migration explanation.
  • Cmdlet help for Request-InfisicalCertificate and Get-InfisicalCertificateAuthority updated to match.

Docs only, no behavior change. 268/268 tests pass; full build.ps1 -RunTests green including module import, manifest, and help validation.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Generated with Claude Code

Follow-up to #18. The subscriber guidance that PR shipped was wrong for fleet enrollment, and this corrects it. ## What was wrong #18 recommended `-PkiSubscriberSlug` as "preferred for most setups" and showed an example that grabs the first subscriber then passes the local machine's own `CommonName`. That fails on every machine whose name is not that subscriber's CN. From `pki-subscriber-service.ts` `signSubscriberCert`: ```ts const cn = dn.commonName; if (cn !== subscriber.commonName) { throw new BadRequestError({ message: "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 `subscriber.subjectAlternativeNames`) and requires CSR key usages to be a subset of the subscriber's. A subscriber is a **single named identity**, so enrolling N machines would require N subscribers. ## The correct guidance **Certificate profiles** are the fleet path. They take a per-request common name constrained by policy `allowed`/`required`/`denied` lists, and profile issuance is the only path that skips the CA direct-issuance gate: ```ts if (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) { throw ... } ``` So `-CertificateProfileId` issues successfully against a CA whose `EnableDirectIssuance` is `False`. ## Direct issuance cannot be toggled Also documented, because it is not discoverable: `enableDirectIssuance` appears in **no** Infisical create or update schema. The generic CA schemas accept only `name` and `status`, and none of `general-certificate-authority-router.ts`, `internal-certificate-authority-router.ts`, or `certificate-authority-endpoints.ts` reference it. There is no UI toggle and no API field. A CA can read `False` for a non-obvious reason. Migration `20250521110635_add-external-ca-pki.ts` renamed the older `requireTemplateForIssuance` column and **inverted** every existing value: ```ts t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance"); ... .update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance }); ``` Any CA that previously required a template now reads `EnableDirectIssuance = False` permanently. The remedies are a profile, or a new CA (the column defaults to `true`). ## Changes - README end-to-end example switched from subscriber to profile issuance, with `-Ttl` restored. - Issuance-path table now leads with whether the common name varies per request, and states each path's CN behavior. - New "Discovering profiles (recommended)" section; subscriber section rewritten to state the CN-match requirement. - "Direct issuance on a CA" section documents that it is fixed at creation, with the migration explanation. - Cmdlet help for `Request-InfisicalCertificate` and `Get-InfisicalCertificateAuthority` updated to match. Docs only, no behavior change. 268/268 tests pass; full `build.ps1 -RunTests` green including module import, manifest, and help validation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Generated with [Claude Code](https://claude.com/claude-code)
gsadmin added 1 commit 2026-07-30 22:40:51 +00:00
The subscriber guidance shipped in #18 was wrong for fleet enrollment.
signSubscriberCert rejects any CSR whose CN differs from the subscriber's
commonName, and allowlists the subscriber's subjectAlternativeNames, so a
subscriber is a single named identity rather than a template. Enrolling N
machines through subscribers would require N subscribers.

Certificate profiles are the correct path: they accept a per-request common
name constrained by policy allowed/required/denied lists, and profile issuance
is the only path that skips the CA direct-issuance gate
(!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate), so a
profile issues against a CA whose EnableDirectIssuance is False.

Also documents that enableDirectIssuance cannot be changed after CA creation:
it appears in no Infisical create or update schema (the generic CA schemas
accept only name and status). Migration 20250521110635_add-external-ca-pki.ts
renamed requireTemplateForIssuance to enableDirectIssuance and inverted every
existing value, so CAs that previously required a template now read False
permanently. The remedies are a profile, or a new CA (column defaults to true).

README end-to-end example switched from subscriber to profile issuance, and the
issuance-path table now leads with whether the common name varies per request.
Cmdlet help for Request-InfisicalCertificate and Get-InfisicalCertificateAuthority
updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gsadmin added 1 commit 2026-07-30 23:00:17 +00:00
Request-InfisicalCertificate -InstallChain could hang indefinitely. Adding a
root certificate to CurrentUser\Root makes Windows raise a modal trust
confirmation dialog, and X509Store.Add blocks until it is answered. When that
dialog was hidden or the session non-interactive (scheduled task, MECM task
sequence) the cmdlet appeared to stop right after installing the intermediate,
with no indication why. A warning is now emitted before the blocking call.

-StoreLocation now defaults to the process elevation when the caller does not
supply it: LocalMachine when elevated, CurrentUser otherwise. This is what most
callers want, and it sidesteps the trust prompt entirely because writing
LocalMachine\Root already required elevation. Applied to both
Request-InfisicalCertificate and Install-InfisicalCertificate; the resolved
value is reported on the verbose stream and an explicit -StoreLocation wins.

Chain routing is unchanged and already correct: self-signed certificates go to
the Root store and everything else to CertificateAuthority, within whichever
location was resolved.

When the resolved location is LocalMachine and -KeyStorageFlags was not
supplied, the private key is written to the machine key store. Without this 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.

Reuse detection now searches the store location the install will write to
rather than always searching CurrentUser, so -AllowRenewal and the existing
certificate short-circuit behave consistently with where certificates land.

Elevation detection moved to InfisicalCmdletBase (evaluated through the engine,
since the module targets netstandard2.0 and carries no
System.Security.Principal.Windows reference) and is shared with
Write-InfisicalScepMdmProfileToWmi, which loses its private copy.

README gains the fuller worked example, a genericized output transcript, and a
"Where certificates get installed" section; cmdlet help updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gsadmin added 1 commit 2026-07-30 23:06:19 +00:00
Install issuers before the leaf, verify the chain, and name the leaf by hostname
Publish to PowerShell Gallery / build (pull_request) Successful in 26s
Publish to PowerShell Gallery / release (pull_request) Successful in 9s
Publish to PowerShell Gallery / publish (pull_request) Failing after 8s
883322cadf
Chain members are now installed before the leaf, so the certificate is
chainable the moment it appears in the store rather than momentarily orphaned.

After -InstallChain the chain is validated against the machine's own stores.
An incomplete result is reported as a warning naming the certificate whose
issuer is missing, which is the exact condition Windows surfaces as "The issuer
of this certificate could not be found" - previously that was only discoverable
in certmgr after the fact.

Chain routing is unchanged and already handles arbitrary depth: 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. Only
the leaf honours -StoreName (default My). This is now stated in the docs,
because the split was not obvious.

The installed certificate's Windows friendly name defaults to the common name
in upper case, which is what operators look for in certmgr. -FriendlyName
overrides it and moves from the ByCa parameter set to all of them; the CA path
still forwards the same value to Infisical as the issued certificate's
friendlyName.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gsadmin merged commit c114bd3b9c into main 2026-07-30 23:08:58 +00:00
Sign in to join this conversation.