Component mTLS authentication for the split runtime#383
Merged
Conversation
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.
There was a problem hiding this comment.
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.
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
…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.
There was a problem hiding this comment.
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>();
… 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.
… 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.
…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.
…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.
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.
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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.IComponentEnrollmentPolicy) for enterprise host attestation.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).