Skip to content

Component mTLS authentication for the split runtime#383

Merged
fw2568 merged 56 commits into
mainfrom
feature/component-authentication
Jun 1, 2026
Merged

Component mTLS authentication for the split runtime#383
fw2568 merged 56 commits into
mainfrom
feature/component-authentication

Conversation

@fw2568
Copy link
Copy Markdown
Member

@fw2568 fw2568 commented Jun 1, 2026

Secures the split-runtime message bus (Rebus/RabbitMQ) with per-component mutual TLS, and serves the Identity and Compute API endpoints over HTTPS. eryph-zero (single process, in-memory bus) is unaffected.

What it adds

  • A component CA in the Identity service — a single self-signed root with client-auth and server-TLS intermediates — persisted in the machine store and reused across restarts. One root validates both directions, so there is a single trust anchor.
  • Bootstrap via a one-time enrollment file minted by eryph-identity new-enrollment (CA trust anchor + identity endpoint + signed token). The token is bound to one component type and one host FQDN; the component imports the file, pins the CA, and redeems the token over HTTPS to receive both its mTLS client certificate and its server-TLS certificate before completing startup.
  • Redeemed tokens are recorded in the Identity DB so a token is single-use; a wrong-type/wrong-host or otherwise-invalid request is refused without consuming the token. Enrollment authorization is a pluggable policy seam (IComponentEnrollmentPolicy) for enterprise host attestation.
  • The transport presents the client certificate as an ACL-protected PKCS#12 file and validates the broker against the host trust store (matching how Rebus.RabbitMq configures TLS).

The enrollment token binds a host but does not authenticate it (the FQDN is self-reported); attestation is left to the policy seam. Unit-tested across the CA, codec, redeemer, enrollment service and endpoint; validated live across Identity + Compute API (enroll → redeem → mTLS bus → register).

fw2568 added 24 commits May 31, 2026 13:59
The Identity service hosts the deployment'"'"'s component CA: a long-lived CA certificate
created on first use (mirroring TokenCertificateManager) plus issuance of per-component
client certificates signed by it. Issued certificates carry the FQDN (CN + DNS SAN), the
stable ComponentId (URN SAN), and the TLS client-auth EKU — the identities components will
present for mTLS on the bus. Uses the existing GenerateCaCertificate/IssueCertificate
crypto primitives. Unit tests verify CA basic constraints, leaf chaining to the CA, EKU and SANs.
Prepares for certificate rotation per the chosen model (short ~90d leaves auto-renewed at
half life; CA rollover bundle-ready now, operator tooling later):
- GetCaCertificate signs with the newest usable CA and no longer wipes other still-valid
  generations, so a future CA rollover can run without a flag-day.
- Add GetTrustedCaCertificates returning the CA trust bundle (all unexpired generations) for
  distribution to components and the broker CA file.
- Default issued leaf lifetime is now ~90 days. Tests cover the bundle, the non-destructive
  multi-generation behaviour, and the leaf validity window.
…milestone 2)

Server-side enrollment in the Identity service: ComponentEnrollmentService validates a request
against an IComponentEnrollmentPolicy, then issues a leaf from the component CA and returns it
with the CA trust bundle. The component id is derived server-side from the (authorized) type +
FQDN via ComponentIdentity.DeriveComponentId, so a requester cannot be issued a certificate for a
different identity. The default SharedSecretEnrollmentPolicy validates an operator-provisioned
secret (constant-time) and denies when none is configured; enterprise tooling replaces the policy.

The policy is the only pluggable seam — CA/issuance stays in-repo. Shared enrollment DTOs live in
ModuleCore for the (later) component-side client. Tests cover issuance+chaining, server-derived id,
policy denial, invalid key, and the secret policy. Shared in-memory cert test doubles extracted.
ICertificateGenerator/CertificateGenerator (and the Windows override) can now issue an
intermediate CA certificate (BasicConstraints CA=true, path length 0; KeyUsage
KeyCertSign|CrlSign) signed by a root, with the same serial/lifetime-clamping rules as leaf
issuance. This is the primitive for the single-root component PKI: root -> server-TLS and
client intermediates -> leaves, so server TLS and bus mTLS share one trust anchor. Test verifies
the intermediate is a CA and that a leaf chains root -> intermediate -> leaf.
The component CA is now a single self-signed root (the trust anchor) with two intermediates
signed by it: a client intermediate (component mTLS, clientAuth) and a server-TLS intermediate
(serverAuth). One root validates both directions, so there is one trust anchor and no second CA.

- IssueComponentCertificate / IssueServerCertificate return an IssuedCertificate (leaf + the
  issuing intermediate to present); GetTrustedCaCertificates returns the root bundle.
- Roots remain multi-generation/non-destructive for rollover; intermediates are bound to their
  key via CopyWithPrivateKey and persisted.
- Enrollment now returns the issuing chain alongside the leaf and root bundle, so a component
  presents leaf + intermediate and trusts only the root.
- Tests verify client/server leaves chain leaf -> intermediate -> root, the correct EKUs, distinct
  intermediates, ~90d leaves, and multi-root retention.
… 2b)

Anonymous POST /components/enroll in the Identity module wraps the enrollment service: a
component presents its identity, public key and enrollment credential over HTTPS (off the bus);
failures return a uniform 401 so the endpoint does not reveal which check failed. Wires the
component CA, the default shared-secret policy (secret read from configuration; unset = denied)
and the enrollment service in the module container as transient services (stateless over the
certificate store, matching the cross-wired certificate services). Endpoint unit tests plus the
integration suite (which verifies the full enrollment DI graph) pass.
IServerCertificateProvider yields the certificate a host presents on its HTTPS listener. The
default CaServerCertificateProvider issues it from the component CA server intermediate (chains
to the root components already trust, serverAuth EKU) with a per-process key, so the enrollment
endpoint and the bus mTLS share one trust root. An external/third-party provider can replace it
for a public/enterprise-issued issuer certificate. Test verifies key presence, EKU and chaining.
… core)

ComponentEnrollmentClient ensures the component holds a current mTLS certificate: it generates a
key pair, enrolls with the identity service (sending only the public key) and persists the result
via IComponentCertificateStore. Enrollment is retried with exponential backoff and never fails the
component on the first attempt, so a component starting before or alongside the identity service
keeps trying until enrollment succeeds. Transport and store are abstracted (IEnrollmentTransport /
IComponentCertificateStore) so the retry/persist logic is unit-tested without a live endpoint.
FileComponentCertificateStore persists the enrolled leaf, PKCS#8 key, issuing chain and CA trust
bundle as PEM files; HasValidCertificate/HasCurrentCertificate report validity and the renewal
window so the enrollment client knows when to (re)enroll. The bus mTLS transport reads from the
same store. The directory holds the private key and is expected to be ACL-restricted by the
deployment tooling. Test covers save/load, validity and the renewal window.
HttpEnrollmentTransport POSTs the enrollment request to the identity service at the authority
root (/components/enroll) and deserializes the result; a non-success response throws so the
enrollment client retries. Tests cover the request target/deserialization and the retry-trigger on
failure. (Seals the file-store test class to clear a Dispose-pattern analyzer warning.)
Runs enrollment on startup (retry-tolerant) and re-checks hourly to renew before expiry; disposes
its cancellation source on stop. Registered by split-runtime hosts that use bus mTLS; eryph-zero
(in-memory bus) does not need it.
RabbitMqRebusTransportConfigurer optionally takes the component client certificate + CA trust
bundle; when present it enables TLS, presents the client certificate and validates the broker
certificate against the bundle (custom-root trust, not the machine store). The certificate store
gains LoadClientCertificate (leaf+key, re-imported via PKCS#12 for TLS use) and LoadCaTrustBundle.
ComponentEnrollment.EnsureEnrolledTransport ensures the component is enrolled (blocking,
retry-tolerant) then returns a RabbitMQ transport configurer wired with the issued client
certificate and CA trust bundle. The enrollment HTTP client validates the identity server
certificate against the pre-provisioned CA root bundle (custom-root trust), so the same single
root secures enrollment and the bus. Hosts call this from a filter before the module bus connects.
…hase 2)

- C1/CON1: TLS server-cert validation rebuilt with empty ExtraStore would fail a real
  leaf->intermediate->root chain on a clean host. Extract shared TrustEvaluation that builds the
  chain through the peer-presented intermediates against the trusted roots and requires serverAuth.
- H1: stop silently accepting host-name mismatch in the custom validation callbacks (only a chain
  error is tolerated, then re-validated against the custom root).
- M1: component renewal no longer retries forever; only the initial (no-cert) enrollment blocks.
- M2: dispose the per-process server key in CaServerCertificateProvider.
- M4: ComponentEnrollment bridge accepts a CancellationToken so a stuck startup can be aborted.
- T3: enrollment endpoint returns 400 on a null body instead of 500.
- H2/H3: document the shared-secret policy impersonation limitation and the rate-limiting the
  endpoint requires before exposure.
- Adds TrustEvaluation tests (valid chain, name mismatch, untrusted root, missing serverAuth).
ComponentMtlsTransport.Register reads the componentMtls config section: when enabled it enrolls
(blocking, retry-tolerant) and registers the mTLS transport; otherwise it registers the plaintext
transport (default, no regression). The agent host filter uses it. Other hosts reuse the helper.
Agent, ComputeApi and Controller hosts enroll-then-connect via ComponentMtlsTransport (gated on
componentMtls:enabled; plaintext default = no regression). The Controller gains a ComponentType so
it has a bus identity. Identity hosts the CA, so it self-issues its own client certificate directly
from the CA (no HTTP enrollment loop against itself) and connects over mTLS.
…unbook

Live-testing the four-process cluster surfaced several issues that unit tests
and review missed because they never exercise a real Windows TLS handshake:

- Redesign RabbitMqRebusTransportConfigurer for Rebus.RabbitMq 9.4: it configures
  TLS per endpoint from SslSettings (a client-cert *file* + OS-trust validation)
  and ignores an in-memory cert / callback set on the connection factory. The
  transport now takes a client-cert PKCS#12 path; the deployment root is trusted
  via the host store. TrustEvaluation is now used only by the HTTP enroll client.
- Add SchannelCertificate.MakeUsable and load/write client certs with DefaultKeySet:
  Schannel cannot use ephemeral keys (CopyWithPrivateKey / PKCS#12 EphemeralKeySet)
  and fails the handshake. FileComponentCertificateStore writes a component.pfx
  (atomic) and exposes its path; ComponentEnrollment hands that to the transport.
- Register components with the controller in a background retry loop instead of
  blocking startup, so cluster components can start in any order.
- Enroll endpoint: mark [ApiVersionNeutral] (API versioning otherwise left the
  unversioned route unmatched -> require-auth fallback -> 401); disable
  RequireHttpsMetadata for an http authority in both Identity and Api modules.
- HttpEnrollmentTransport serializes/deserializes with the identity API JSON
  conventions (snake_case + string enums) so multi-word fields bind.

Adds docs/internal/cluster-mtls-bring-up.md, a dev provisioning harness
(dev/provisioning) that drives the in-repo CA, and tests for the JSON contract
and the cert-store PFX path.
…e; issue dual certs

Replaces the reusable shared-secret enrollment with the intended design:

- A single operator-delivered enrollment file (ComponentEnrollmentFile) carries the
  identity CA certificate (the out-of-band trust anchor), the identity endpoint, and a
  one-time token. The component imports it (componentMtls:enrollmentFile), pins the CA to
  trust the identity TLS endpoint, and enrolls — no separate trust-anchor provisioning.
- One-time token: IEnrollmentTokenService / EnrollmentTokenService signs the token with
  the component CA (self-validating — no token secret is stored), binds it to a component
  type with an expiry, and redeems it at most once. TokenEnrollmentPolicy replaces
  SharedSecretEnrollmentPolicy as the default IComponentEnrollmentPolicy.
- Enrollment now issues BOTH the client (mTLS) and the server-TLS certificate; the request
  carries both public keys + the server DNS name(s); FileComponentCertificateStore persists
  both and exposes each PKCS#12 path.

Tests updated/added: token-policy authorization, dual-cert issuance, and the snake_case
enrollment JSON contract.

Still to wire (next): the identity command that produces the enrollment file, and the HTTPS
listeners (identity self-issuing its server cert; components serving TLS with the enrolled
server cert), then a live re-test and cold review.
… file

Run on the identity host (elevated): mints a one-time token bound to a component type, exports
the component CA certificate (the trust anchor) and writes the self-contained enrollment file
(snake_case JSON) the operator delivers out-of-band to the component.
When component mTLS is enabled, the identity host self-issues its server certificate from its own
server-TLS sub-CA (presenting leaf + server intermediate) and configures Kestrel HTTPS with it, so
the enrollment endpoint is reachable over TLS that enrolling components validate against the CA they
pinned out-of-band. Verified live: a component imports the enrollment file, trusts the endpoint via
the pinned CA, redeems its one-time token and receives both its client and server certificates.
The compute API host configures Kestrel HTTPS from the server-TLS certificate it received at
enrollment (presenting leaf + server intermediate). Verified live: it enrolls over HTTPS via the
enrollment file, joins the mTLS bus, registers, and serves HTTPS with a certificate chaining to the
component CA root.
- CRITICAL — one-time tokens were not enforced across requests: the token service is transient
  (matching the transient cert services), so its in-memory redeemed-id set was recreated per
  request, allowing replay. Move the set into a singleton IRedeemedTokenStore. Adds direct
  EnrollmentTokenService tests (valid/one-time/expired/tampered/cross-CA) that cover this.
- Fail closed: the compute API throws if componentMtls is enabled but the enrolled server
  certificate is missing, instead of silently serving plaintext HTTP.
- Validate DNS names before issuing a server/leaf certificate (reject wildcards and malformed
  SANs); the token binds the component type, not the name.
- Read componentMtls from IConfiguration (not the environment directly) in the HTTPS-listener
  wiring, so appsettings work too.
- Require an explicit enrollment endpoint (drop the localhost default); refresh stale comments
  (rate-limiting rationale; note the CA-root-key reuse for token signing as an accepted choice).
Replace the bespoke in-memory redeemed-token store with a RedeemedEnrollmentToken
entity in IdentityDb, claimed once through the existing IIdentityDbRepository (the
Jti primary key enforces single use). EF Core is treated as the persistent store;
which provider backs it is deployment wiring. Split issuing from redeeming:
EnrollmentTokenCodec (CA-only sign/verify, accepts any trusted root for rollover)
and EnrollmentTokenRedeemer (CA + repository, prunes expired rows). The redemption
chain is now async.

Security hardening from cold review:
- Validate and import all client/server keys and server DNS names before the token
  is redeemed, so a recoverable client error cannot burn a one-time token.
- Bind the token type in the redeemer (a wrong-type request does not consume it).
- IssueServerCertificate covers every requested DNS name (no silent SAN truncation).
- new-enrollment writes the token file atomically under an owner-only ACL and is
  guarded to Windows; reject non-positive TTL and undefined component types.
Previously a one-time enrollment token bound only the component type, so a leaked
or misdirected token could enroll any host of that type. The token payload now
carries a bound FQDN ({jti, type, fqdn, exp}, signed by the component CA): the
redeemer accepts it only when the enrolling component's self-reported FQDN matches
(case-insensitive), and a wrong-host request is refused without consuming the token.
The issued certificate's identity (ComponentId, CN/SAN) is the same FQDN, so a file
provisions exactly one host identity.

new-enrollment now requires --fqdn <host> (validated as a DNS name, normalised to
lower case); the enrollment file surfaces it. ComponentMtlsTransport warns early if
a file is delivered to a host whose FQDN does not match. The FQDN is self-reported,
so this binds the token to one host but does not authenticate it - host attestation
remains the IComponentEnrollmentPolicy seam.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces per-component mutual TLS (mTLS) for the split-runtime message bus (Rebus/RabbitMQ) and adds an HTTPS-based enrollment/bootstrap flow so components can obtain certificates from the Identity service before joining the bus.

Changes:

  • Adds a component CA (root + client/server intermediates) plus token-based, one-time enrollment (codec + redeemer + policy + endpoint) in the Identity module.
  • Adds component-side enrollment bootstrapping (enrollment file), certificate persistence (PEM/PFX), and an mTLS-enabled RabbitMQ transport configuration.
  • Adds shared server-certificate trust evaluation and wires Kestrel TLS for Identity and Compute API when mTLS is enabled, plus extensive unit tests.

Reviewed changes

Copilot reviewed 66 out of 66 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/modules/test/Eryph.Modules.Identity.Test/Services/TrustEvaluationTests.cs Unit tests for custom-root server cert validation
src/modules/test/Eryph.Modules.Identity.Test/Services/InMemoryCertificateInfrastructure.cs In-memory cert/key services for Identity tests
src/modules/test/Eryph.Modules.Identity.Test/Services/EnrollmentTokenRedeemerTests.cs Tests for one-time token redemption + pruning
src/modules/test/Eryph.Modules.Identity.Test/Services/EnrollmentTokenCodecTests.cs Tests for token issue/verify format and tamper rejection
src/modules/test/Eryph.Modules.Identity.Test/Services/ComponentEnrollmentServiceTests.cs Tests for server-side enrollment issuance + policy behavior
src/modules/test/Eryph.Modules.Identity.Test/Services/ComponentCertificateAuthorityTests.cs Tests for CA root/intermediates and issued leaf properties
src/modules/test/Eryph.Modules.Identity.Test/Services/CaServerCertificateProviderTests.cs Tests for server cert provider issuing usable TLS certs
src/modules/test/Eryph.Modules.Identity.Test/Integration/IdentityModuleFactoryExtensions.cs Registers test certificate store for enrollment endpoint
src/modules/test/Eryph.Modules.Identity.Test/Endpoints/Components/EnrollEndpointTests.cs Endpoint tests for OK/Unauthorized behavior
src/modules/test/Eryph.ModuleCore.Tests/Components/HttpEnrollmentTransportTests.cs Tests for HTTP enrollment JSON contract + error behavior
src/modules/test/Eryph.ModuleCore.Tests/Components/FileComponentCertificateStoreTests.cs Tests for file-based cert store validity/currentness + PFX
src/modules/test/Eryph.ModuleCore.Tests/Components/ComponentEnrollmentClientTests.cs Tests for enrollment client retry + short-circuit on current cert
src/modules/src/Eryph.Modules.Identity/Services/TokenEnrollmentPolicy.cs Default enrollment authorization using one-time tokens
src/modules/src/Eryph.Modules.Identity/Services/IssuedCertificate.cs DTO for leaf + issuing chain
src/modules/src/Eryph.Modules.Identity/Services/IServerCertificateProvider.cs Abstraction for server TLS certificate source
src/modules/src/Eryph.Modules.Identity/Services/IEnrollmentTokenRedeemer.cs Abstraction for validating/consuming enrollment tokens
src/modules/src/Eryph.Modules.Identity/Services/IComponentEnrollmentService.cs Server-side enrollment service contract
src/modules/src/Eryph.Modules.Identity/Services/IComponentEnrollmentPolicy.cs Pluggable authorization seam for enrollment
src/modules/src/Eryph.Modules.Identity/Services/IComponentCertificateAuthority.cs Component PKI abstraction (root + intermediates + issuance)
src/modules/src/Eryph.Modules.Identity/Services/EnrollmentTokenRedeemer.cs Default redeemer persisting redeemed token JTIs
src/modules/src/Eryph.Modules.Identity/Services/EnrollmentTokenCodec.cs Token format/sign/verify implementation
src/modules/src/Eryph.Modules.Identity/Services/ComponentEnrollmentService.cs Enrollment request validation + certificate issuance
src/modules/src/Eryph.Modules.Identity/Services/ComponentEnrollmentException.cs Enrollment failure exception type
src/modules/src/Eryph.Modules.Identity/Services/ComponentCertificateAuthority.cs Component CA implementation with persisted tiers
src/modules/src/Eryph.Modules.Identity/Services/CaServerCertificateProvider.cs Default server cert provider issuing from component CA
src/modules/src/Eryph.Modules.Identity/IdentityModule.cs Wires CA/enrollment services and HTTP-metadata HTTPS requirement logic
src/modules/src/Eryph.Modules.Identity/Endpoints/Components/Enroll.cs Anonymous enrollment endpoint returning cert material
src/modules/src/Eryph.Modules.AspNetCore/ApiModule.cs RequireHttpsMetadata gating based on authority scheme
src/modules/src/Eryph.ModuleCore/Components/IEnrollmentTransport.cs Enrollment transport abstraction for testability
src/modules/src/Eryph.ModuleCore/Components/IComponentCertificateStore.cs Abstraction for persisted component cert material
src/modules/src/Eryph.ModuleCore/Components/HttpEnrollmentTransport.cs HTTPS enrollment client (snake_case + enum-string JSON)
src/modules/src/Eryph.ModuleCore/Components/FileComponentCertificateStore.cs PEM/PFX persistence and certificate freshness checks
src/modules/src/Eryph.ModuleCore/Components/ComponentRegistrationStartupHandler.cs Makes controller registration/config fetch retry in background
src/modules/src/Eryph.ModuleCore/Components/ComponentMtlsTransport.cs Host bootstrap: enrollment file → enroll → register mTLS transport
src/modules/src/Eryph.ModuleCore/Components/ComponentIdentity.cs Exposes deterministic DeriveComponentId
src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollmentResult.cs Enrollment response DTO (client/server certs + bundles)
src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollmentRequest.cs Enrollment request DTO (keys + token + DNS names)
src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollmentHostedService.cs Periodic renewal loop via enrollment client
src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollmentFile.cs Enrollment file bootstrap artifact DTO
src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollmentClientOptions.cs Enrollment retry/backoff + token + server DNS names
src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollmentClient.cs Enrollment/renewal logic with retry/backoff
src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollment.cs Host helper: ensure enrolled, return mTLS RabbitMQ configurer
src/infrastructure/src/Eryph.Rebus/TrustEvaluation.cs Shared custom-root server cert validation helper
src/infrastructure/src/Eryph.Rebus/RabbitMqRebusTransportConfigurer.cs Adds optional TLS client cert + strict server validation
src/data/src/Eryph.IdentityDb/Specifications/EnrollmentTokenSpecs.cs Spec for pruning expired redeemed tokens
src/data/src/Eryph.IdentityDb/IdentityDbContext.cs Adds redeemed token DbSet + model configuration
src/data/src/Eryph.IdentityDb/Entities/RedeemedEnrollmentToken.cs Entity recording redeemed JTIs + expiry
src/core/test/Eryph.Security.Cryptography.Test/CertificateGeneratorIntermediateTests.cs Tests for issuing intermediate CA certs
src/core/src/Eryph.Security.Cryptography/WindowsCertificateGenerator.cs Sets FriendlyName on issued intermediate certs
src/core/src/Eryph.Security.Cryptography/SchannelCertificate.cs Helper to make private keys usable by Schannel
src/core/src/Eryph.Security.Cryptography/ICertificateGenerator.cs Adds IssueIntermediateCaCertificate to interface
src/core/src/Eryph.Security.Cryptography/CertificateGenerator.cs Implements IssueIntermediateCaCertificate
src/core/src/Eryph.Messages/Components/ComponentType.cs Adds Controller component type
src/apps/src/Eryph.Identity/Program.cs Adds new-enrollment command and enables Identity TLS config
src/apps/src/Eryph.Identity/IdentityServerTls.cs Configures Kestrel HTTPS cert from component CA when enabled
src/apps/src/Eryph.Identity/HostIdentityModuleExtensions.cs Self-issues Identity bus client cert when componentMtls enabled
src/apps/src/Eryph.Identity/EnrollmentCommand.cs Operator command to mint enrollment files with restrictive ACL
src/apps/src/Eryph.Controller/HostControllerModuleExtensions.cs Registers mTLS transport bootstrap for controller
src/apps/src/Eryph.ApiEndpoint/Program.cs Enables compute API TLS config when componentMtls enabled
src/apps/src/Eryph.ApiEndpoint/HostComputeApiModuleExtensions.cs Registers mTLS transport bootstrap for compute API
src/apps/src/Eryph.ApiEndpoint/ComponentServerTls.cs Loads enrolled server cert for Kestrel HTTPS
src/apps/src/Eryph.Agent/HostVmHostAgentModuleExtensions.cs Registers mTLS transport bootstrap for agent
dev/provisioning/Eryph.ClusterProvision/Program.cs Dev provisioning harness for local mTLS cluster
dev/provisioning/Eryph.ClusterProvision/Eryph.ClusterProvision.csproj Provisioning harness project definition
dev/provisioning/cluster/run-component.ps1 Dev launcher script for running components with mTLS
dev/provisioning/cluster/00-provision.ps1 Dev script to provision CA + broker TLS material

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/modules/src/Eryph.Modules.Identity/Services/EnrollmentTokenCodec.cs Outdated
Comment thread src/modules/src/Eryph.ModuleCore/Components/ComponentMtlsTransport.cs Outdated
Comment thread dev/provisioning/Eryph.ClusterProvision/Program.cs Outdated
Comment thread dev/provisioning/Eryph.ClusterProvision/Program.cs
Comment thread src/modules/src/Eryph.ModuleCore/Components/ComponentMtlsTransport.cs Outdated
Comment thread dev/provisioning/Eryph.ClusterProvision/Program.cs Outdated
Comment thread dev/provisioning/Eryph.ClusterProvision/Program.cs
Comment thread dev/provisioning/cluster/run-component.ps1
The enrollment endpoint bound the shared ModuleCore DTOs directly and returned bare
status codes, ignoring the Identity API conventions. It is now a versioned endpoint
(v1/components/enroll) with Identity Models.V1 request/response models, mapping
extensions, and the LanguageExt Validation -> ProblemDetails pipeline: a malformed
request returns a 400 problem+json with field-keyed errors (before the one-time token
is redeemed), while the authorization decision stays an opaque 401. The component
client posts to the versioned path; the ModuleCore types remain the internal service
and wire contract.

Also addresses the PR review comments:
- sign enrollment tokens with a trusted root that actually has a private key
- require a non-empty componentMtls:certificateDirectory when mTLS is enabled
- stop the registration retry loop on host shutdown (ApplicationStopping)
- drop the raw user-supplied FQDN from the issuance log (log injection)
- fix the provisioning harness so it compiles; rewrite the launcher to the
  enrollment-file model and keep the compute API identity authority on HTTPS
@fw2568 fw2568 requested a review from Copilot June 1, 2026 09:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 4 comments.

Comment thread src/modules/test/Eryph.Modules.Identity.Test/Services/TrustEvaluationTests.cs Outdated
Comment thread src/modules/test/Eryph.Modules.Identity.Test/Services/TrustEvaluationTests.cs Outdated
Comment thread src/modules/test/Eryph.Modules.Identity.Test/Services/TrustEvaluationTests.cs Outdated
Comment thread src/modules/test/Eryph.Modules.Identity.Test/Services/TrustEvaluationTests.cs Outdated
…nit tests

Swept the whole test-handle class rather than one file at a time: TrustEvaluationTests wraps the
root/intermediate/leaf/chain in a disposable fixture; the IssuedCertificate locals in
ComponentCertificateAuthorityTests and CaServerCertificateProviderTests are now using vars (the type is
IDisposable); and the caller-owned certificates returned by GetTrustedCaCertificates are disposed in the
chain helper and the bundle assertions. No production change.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/apps/src/Eryph.Identity/IdentityContainerExtensions.cs:32

  • The enrollment token is intended to be single-use, but the identity host is still configured with an in-memory EF store (InMemoryDatabaseRoot + InMemoryIdentityDbContextConfigurer). This means redeemed-token rows (and therefore one-time enforcement) are lost on identity restart, allowing token replay within its TTL. If single-use across restarts is a security requirement, the IdentityDbContext configurer should use a persistent provider when component mTLS/enrollment is enabled (or the limitation should be made explicit).
            // Identity store stays in-memory for this milestone (mirrors eryph-zero).
            container.RegisterInstance(new InMemoryDatabaseRoot());
            container.Register<IDbContextConfigurer<IdentityDbContext>, InMemoryIdentityDbContextConfigurer>();

Comment thread src/modules/src/Eryph.ModuleCore/Components/ComponentEnrollmentClient.cs Outdated
… enrollment

A wrong pinned CA or host-name mismatch surfaces as an HttpRequestException with no status code wrapping
an AuthenticationException. IsNonTransient now classifies that (directly or as an inner exception) as
non-transient, so a misconfiguration aborts blocking startup with an actionable error instead of looping
forever; a plain connection failure has no such inner exception and stays transient. Added a test.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 2 comments.

… test store

RemoveFromMyStore (both overloads) now disposes the certificates it drops from the in-memory list, so
the test double does not leak native handles across the run (it already clones on add and read). Test-only.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 3 comments.

Comment thread src/modules/src/Eryph.ModuleCore/Components/FileComponentCertificateStore.cs Outdated
…tial-save brick

Enrollment tokens are single-use, so a torn multi-file Save that left the component looking un-enrolled
would force a retry with an already-consumed token and brick it. FileComponentCertificateStore now writes
the PKCS#12 (leaf+key+chain, built directly from the in-memory result) first and atomically, and treats
it as the source of truth:
- HasValidCertificate/HasCurrentCertificate, GetClientCertificatePfxPath/GetServerCertificatePfxPath and
  LoadClientCertificate read the PFX when present, falling back to the PEM copies only when it is absent.
- The PEM files (leaf/key/chain/bundle) are secondary copies, each written owner-only and atomically.
So a crash after the PFX lands leaves a complete, usable certificate; the remaining unavoidable window
(server-side token redemption to the first durable write) is a separate idempotent-re-enrollment concern.
Tests cover PFX-only survival, neither-source-present, and the corrupt-PEM-fallback path.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 2 comments.

…porting, server-TLS chain, redeemer

Findings from two independent cold reviews (Sonnet) over the current code, verified and fixed in one batch.

PKI / certificate store:
- IsValidCa compares NotAfter in UTC (NotAfter is Kind=Local and DateTime comparison ignores Kind, so a
  CA could read as valid past true expiry on a non-UTC host). Matches CertificateGenerator/TryLoadLeaf.
- FileComponentCertificateStore validity check loads the PFX with EphemeralKeySet (no user-key-store
  write per poll); Save rejects a server certificate supplied without its key; PEM files join with "\n".

Enrollment flow / bring-up:
- SecureDirectory creates the certificate directory with an owner-only ACL on Windows too (was 0700 on
  Unix only; the inherited %ProgramData% ACL grants Users read, exposing private keys).
- ComponentEnrollmentClient no longer reports a non-transient renewal failure (used token / TLS
  misconfig) as a misleading "will retry"; it logs at Error with an actionable message and still does not
  throw during renewal. Abort messages now mention a consumed token needing a new enrollment file.
- ComponentServerTls loads the issuing chain from the (self-contained) server PFX instead of a separate
  PEM, so a crash between writes cannot leave the leaf without its chain.
- EnrollmentTokenRedeemer preserves the original DbUpdateException if the concurrent-redemption re-check
  itself fails, instead of masking it.
- new-enrollment rejects a present-but-unparseable --ttl-hours instead of silently using the 1h default.
- Enrollment validation rejects server_dns_names supplied without a server public key.

Tests: server-cert persist/cleanup, Save key-guard, LoadClientCertificate (PFX + PEM fallback); renewal
non-transient no-throw; redeemer concurrent-race / real-DB-failure / original-error-preservation;
server_dns_names-without-key validation; assert the TrustEvaluation fixture chain builds; dispose chain
certs in ComponentEnrollmentServiceTests.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 1 comment.

Comment thread src/modules/src/Eryph.Modules.Identity/Services/ComponentCertificateAuthority.cs Outdated
IsValidCa now also requires NotBefore <= now (in UTC), so a CA whose validity has not begun is not
selected for signing nor included in the trust bundle (which would produce leaves that fail chain
validation until the start date). Added a regression test.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 1 comment.

Comment thread src/apps/src/Eryph.Identity/HostIdentityModuleExtensions.cs
… on Windows

Eryph.Security.Cryptography.SecureFile.CreateOwnerOnlyDirectory now applies an inheritance-protected
owner-only ACL on Windows (it previously only set 0700 on Unix and relied on inherited ACLs on Windows).
This protects identity.pfx (the identity host's bus client key, written by HostIdentityModuleExtensions)
and the new-enrollment output directory, whose parents under %ProgramData% otherwise grant Users read.
Mirrors the component-side SecureDirectory fix.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 80 changed files in this pull request and generated 1 comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants