·
80 commits
to master
since this release
[7.6.0] - 2026-08-17
Changed
- The three Kubernetes container-integration cases that need Java-built images —
helm_sidecar_injection
(admission-webhook sidecar injection),helm_clustered_convergence, andhelm_jgroups_dns_ping(both the
-clusteredInfinispan image) — now run blocking in CI instead of recording a skip. Previously the CI helm step
ran with no JDK and never built those images, so all three recorded an honest but permanent SKIP — coverage that
looked green while proving nothing. A new:maven: build container-test images (jars)step in the
mockserver-container-testspipeline builds themockserver-netty,mockserver-k8s-webhook, and
mockserver-state-infinispanjars from the tree (once, via the Maven-in-Docker reactor) and hands them to the
helm step as Buildkite artifacts; the helm step downloads them and does cheapdocker builds (a COPY into
distroless) to produce the-clusteredandmockserver-webhookimages, then runs the suite. The jars travel as
artifacts (~200 MB total) rather thandocker saved images (~1.3 GB), mirroring the node-launcher / WAR hand-off. Both
layers fail closed: the helm step exits non-zero if any jar artifact is absent, and the harness runs with
REQUIRE_CLUSTERED_IMAGE=true/REQUIRE_WEBHOOK_IMAGE=trueso an image that is expected-but-absent is recorded as a
FAILURE, never a skip — a skip in CI is now impossible. Localcontainer_integration_tests/integration_tests.sh
is unchanged for a developer without the images: it still records a comprehensible SKIP (no fail-closed flag set),
and the harness now also builds themockserver-webhookimage locally (build_webhook_docker) so the sidecar case
runs blocking in local dev too — it was never built by the harness before, so that case had always skipped.
Added
- Two new rules in the always-on
check-false-green-guards.shCI gate. Rule 4 fails the build if a CI step runs the
container-integration harness with the helm/k3d cases active but does not export bothREQUIRE_CLUSTERED_IMAGE=trueand
REQUIRE_WEBHOOK_IMAGE=true— the exact way the three image-dependent Kubernetes cases could silently revert to a green
SKIP. It is keyed on the step's behaviour (invokesintegration_tests.shwithoutSKIP_HELM_TESTS=true), not on a
filename, so a rename or a second helm-running step is covered automatically. Rule 5 fails the build if a
mockserver-coretest that performs a JVM-global logging side effect (reachingLogManager.readConfiguration's handler
reset()via the staticConfigurationPropertieslogging setters or a forced fresh<clinit>) is not in the
sequential-testsinclude list — the shape behind a release-blocking flake thatParallelStaticStateGuardTest
structurally cannot catch. Both rules fail closed on an empty corpus and carry a rotating allow-list. See
docs/operations/false-green-guards.md. - A
jarPathlauncher option and matchingMOCKSERVER_JAR_PATHenvironment variable formockserver-node, pointing
start_mockserverat a pre-provisionedmockserver-nettyjar-with-dependencies instead of downloading one from
Maven Central. When set, that exact jar is launched and no download is attempted; a configured-but-missing path is a
hard error (... refusing to fall back to downloading a release) rather than a silent fall-back to a released
jar, so a missing artifact fails loudly. Mirrors the existingMOCKSERVER_BINARY_BASE_URLbring-your-own-artifact
path for the standalone binary, and serves air-gapped/corporate installs as well as testing a locally-built jar
(jarPathtakes precedence overmockServerVersion/artifactory*; the option beats the env var). The Node launcher
integration tests now use it in CI: a new:maven: build node launcher jarstep builds the jar from the tree and
the launcher-test step downloads it as an artifact and launches it viaMOCKSERVER_JAR_PATH— so the suite finally
tests the repo's own code instead of the last release. Previously it ran a downloaded release chosen by
package.json's version, so amockserver-corefix could not green it and a regression could not red it; that
released jar also carried the shipped dynamic-CA generation race (fixed on master in4cff56e61) and flaked ~8% of
runs. The launcher step fails closed if the tree-built jar is absent rather than reverting to a download, and
localnpm testoutside CI is unchanged (with neither the option nor the env var set it still downloads as before). - A structural wire-contract test for the LLM provider codecs (
LlmCodecStructuralContractTest), breaking the
self-derivation weakness in the golden-file drift test.LlmCodecGoldenFileTestregenerates its golden bodies
from the codec itself (-Dmockserver.updateLlmGoldens=true), so a structural codec defect — a renamed field, a
wrong SSE event name, a droppedfinish_reason— bakes straight into its own golden and the byte-for-byte drift
test then passes forever, confirming only that the codec is consistent with itself (token counts were already
pinned separately byshouldEncodeCanonicalTokenUsageCounts; the bodies were not). The new test asserts the live
codec output against hand-authored expectations taken from each provider's published API schema — required
fields, JSON types, the enum discriminators each provider uses (object/type/finish_reason/stop_reason/
finishReason/status/done), the tool-call envelope shape (arguments as a JSON string for OpenAI/Responses
vs a structured object for Anthropic/Gemini/Ollama), and the exact SSE event-name sequence for the event-typed
providers — across all seven chat/completion providers (Azure and Bedrock via their delegate codecs). Crucially it
never reads the golden files and is unaffected by-Dmockserver.updateLlmGoldens=true, so regenerating goldens
cannot silence it. Each named defect class was injected into a codec and confirmed to turn the test red without
regenerating goldens (OpenAI renamedfinish_reason; Anthropic SSEcontent_block_delta→content_delta; Gemini
droppedfinishReason; Responses renamedstatus; Ollama renamed terminaldone), then reverted. Residual
streaming-over-the-wire behaviour remains covered byLlmAgentLoopE2eTest. - Endpoint-level test (
HttpStateCassetteEndpointTest) and an authoritativeCassetteRegistryjavadoc note pinning
the settled decision that loading and recording register a cassette automatically. Therecord_llm_fixtures
andload_expectations_from_fileMCP tools already auto-register the fixture in the process-wideCassetteRegistry
(keyed by file path, originrecorded/loaded, upserting on re-load/re-record) so it surfaces under
GET /mockserver/cassettesand in the dashboard's Cassettes tab without a manualPUT /mockserver/cassettes— but
nothing pinned that a so-registered cassette is actually retrievable through the GET endpoint, and the registration
policy lived only in the two MCP callers, inviting the question to be re-opened. The new test drives
GET /mockserver/cassettes(and the bare/cassettesalias) against a registry populated the way the load/record
handlers populate it and asserts the documented body shape (path, derived filename, expectation count, origin,
lastUsed), that record-then-load on one path yields a single upserted entry, and that a server reset clears it. The
MCP tools and consumer docs (ai_mcp_tools.html) already stated this behaviour; both are now clarified to say the
fixture is registered automatically. This closes the "CassetteRegistry auto-population" product-decision item by
recording the decision in code, docs, and a test rather than leaving it to resurface. - Compile gates for the dashboard composer's generated client code in Python, Ruby, Go and Rust
(.buildkite/scripts/steps/ui-client-codegen-compile.sh, wired intopipeline-ui.yml), closing the gap where
five of the composer's seven languages had no compile check — only Java (ui-java-codegen-compile.sh) and C#
(ComposerCodegenEquivalenceTests.cs) were gated, so an emitter bug or a client-API rename would ship broken
generated code to users caught by nothing (the existing per-language tests only string/byte-compare the emitter
output, never feed it to a compiler). Each gate drives the shared representative composer matrix
(extractParityCases.ts— the exactcombosthe byte-identity parity tests use) through that language's emitter
and runs the lightest credible toolchain check:python -m py_compileandruby -ccatch any emitter bug that
produces malformed source (the strongest static check for a dynamically-typed client with no shipped type stubs),
while Go (go build/go vet ./...) and Rust (cargo check) compile the generated code against the real
in-repomockserver-client-go/mockserver-client-rust, so a renamed client method fails the build — the direct
analog of the Javajavacgate. Node was already covered: thetsctype-proof innode.test.ts
(typecheck-node-codegen.mjs) runs undernpm test, so the orphaned-script concern was already resolved by that
test. All four new gates are proven to go red (emitter drift → non-zero exit) and green. Each phase runs in its
toolchain's Docker image viarun-in-docker.sh; setCODEGEN_COMPILE_USE_DOCKER=falsefor host validation. - Config-to-client wiring tests for the GCS and Azure blob-store backends
(GcsBlobStoreRegistrarConfigWiringTest,AzureBlobStoreRegistrarConfigWiringTest), closing the gap where
only S3 (S3BlobStoreRegistrarConfigWiringTest) proved thatblobStoreTypeconfiguration is turned into a
working client. The GCS and Azure contract tests hand-build their clients (new GcsBlobStore(storage, …),
new AzureBlobStore(containerClient, …)), so the registrar that reads the bucket/container name, endpoint,
credentials and project from configuration and constructs the client was never exercised — a wiring bug there
would ship silently because the contract tests bypass it. The new tests drive each registrar from configuration
only (as production does) against the same Docker emulator the contract tests use (fake-gcs-server for GCS,
Azurite3.36.0 --skipApiVersionCheckfor Azure) and assert the wiring that can actually be got wrong:
endpoint override, credentials, project id, and that a round-tripped object lands in the configured
bucket/container under the configured key prefix — verified through an independent admin client so a
mis-wired name cannot pass. Docker-gated via the canonicalDockerAvailability.isAvailable(...)probe. - The Node and Python Testcontainers modules (
mockserver-testcontainers/node,mockserver-testcontainers/python)
now start a real MockServer container in CI and assert against it, closing a false-green gap where both published
client libraries had jobs that passed having exercised nothing (npm run test:unitandpytest -m "not docker"
both skipped the container). Their CI steps now mount the Docker socket, run the existing integration tests that
start amockserver/mockservercontainer and drive it over HTTP, and — mirroring the Go/.NET/Rust steps — fail
closed by grepping for an evidence marker the test prints only after a real container answered
PUT /mockserver/statuswith200. A skip (test filtered out, renamed, or Docker unusable) therefore fails the
CI build loudly instead of reading as green, while the tests still degrade gracefully to a skip off-CI. - A GitHub Actions workflow (
.github/workflows/dependency-submission.yml) now submits the resolved Maven dependency
graph so Dependabot vulnerability alerts stay accurate for the monorepo layout. GitHub's managed Maven
auto-submission only discovers a project at the repository root, so it silently stopped when the Java project moved
intomockserver/— freezing the alerting graph at a pre-move snapshot that produced phantom Spring advisories and
hid already-landedlog4j-api/jsoupfixes. The workflow resolves and submits themockserver/reactor (which
includesexamples/java) and the separatemockserver-maven-pluginbuild under distinct correlators, path-gated to
mockserver/**/pom.xml. Dependabot's security-update pull requests were unaffected — they read manifests directly;
it was only the alerting graph that had gone stale. - A CI guard (
.buildkite/scripts/steps/check-certificate-expiry.sh, wired into the Java pipeline) now sweeps every
committed certificate PEM and fails the build when any certificate is already expired or expires within 30 days,
warning between 30 and 180 days. It checks every certificate in a chain file, allow-lists the one intentional
expired test fixture while asserting it stays expired, and enforces two structural invariants that previously had
no automation: everyleaf-cert.pemmust expire on or before its siblingca.pem, and the shipped default CA
files must stay in lockstep (the PKCS#1 and PKCS#8 CA private keys are the same key, that key matches the CA
certificate, and the two committed copies of the CA certificate remain byte-identical). Certificate expiry had
previously only ever been discovered by the build going red. - A standing CI guard (
check-false-green-guards.sh) that fails closed when a new "false-green" test shape is
introduced — a test or CI step that reports success while verifying nothing. The 2026-07-21 coverage audit named
these shapes but they lived only in plan documents, and the repository then produced ~a dozen fresh instances in a
single day. The guard enforces the three that can be pinned down precisely and each caused a real shipped false
green: (1) every JUnit suite gated byAssume.assumeTrue(DockerAvailability.isAvailable(...))must be paired with
assert-suite-ran.shover its reports, or a broken Docker socket skips it while the build stays green; (2) no CI
step may mount the Docker socket and then deselect the Docker-marked tests (e.g.pytest -m "not docker"),
starting no container yet passing; (3) no container-integrationlogTestSkipmay park deferred work ("CI wiring is
a follow-up") as a green skip. It runs always-on (a new false green can enter from any of several pipelines) and
carries a justified, self-verifying allow-list that fails if an entry no longer names what it claims. Wiring this
up also closed a live gap it found — theGcs/AzureRegistrarConfigWiringTestcloud suites ran under a Docker
socket in CI but were not fail-closed-asserted. Seedocs/operations/false-green-guards.md. - A local-only, opt-in
K3D_LOCAL_CA_BUNDLEhook incontainer_integration_tests/helm-deploy.shso the Helm
integration suite's k3d cluster can be stood up on a developer machine behind a corporate TLS-inspection proxy.
When set,start-up-k8sovermounts the given combined CA bundle (system/public roots + corporate root) as the
k3s node's containerd trust store at cluster-create time; when unset thek3d cluster createcommand is
byte-identical to before, so CI (which never sets it) is unchanged. Warns rather than fails if the variable is set
but the file is missing, mirroringLOCAL_DOCKER_CA_BUNDLEin.buildkite/scripts/run-in-docker.sh. This unblocks
the three Kubernetes test-coverage gaps that were previously (and incorrectly) deferred as impossible behind the
proxy: the host Docker daemon already trusts the corporate root so the node image pulls, but the in-node
containerd has its own public-roots-only trust store and otherwise cannot pull even therancher/mirrored-pause
sandbox image (every pod fails sandbox creation withx509: certificate signed by unknown authority). See
docs/operations/build-system.md→ Local Development Behind a Corporate TLS-Inspection Proxy. - Two live Kubernetes container-integration tests that exercise admission-webhook and JGroups discovery paths which
previously shipped unproven.helm_sidecar_injectiondeploys the chart withwebhook.enabled=true(self-signed TLS
bootstrap Jobs + webhook handler Deployment +MutatingWebhookConfiguration), drives a real labelled podCREATE
through the admission path, and asserts the resulting pod spec carries the injectedmockserver-sidecarcontainer,
mockserver-iptables-initinit container, andmockserver.org/injectedannotation — with a negative-control pod (no
opt-in annotation) that must not be injected, so a webhook that injects unconditionally fails the test.
helm_jgroups_dns_pingdeploys two clustered replicas and asserts the headless Service is truly headless
(clusterIP: None), thatJGROUPS_DNS_QUERYis wired to its FQDN, that it resolves to ≥2 pod IPs (Endpoints plus an
in-clusternslookup), that a ≥2-node JGroups/Infinispan view forms (the anti "two clusters of one" guard), and that
state converges across the pods — exercising the Kubernetes DNS discovery path thatJGroupsKubernetesStackTest
(XML-parse only) andClusteredTwoNodeTest(loopback MPING) never run. Both were proven red by degrading the exact
behaviour they name (deleting theMutatingWebhookConfiguration; deleting the headless Service and rolling the pods).
Both depend on Java-built images (the-clusteredvariant and themockserver-webhookhandler); when those images
are absent — e.g. the CI helm step runs withSKIP_JAVA_BUILD=trueand no JDK — the cases record an honest SKIP
rather than a misleading pass, and run blocking only where the images exist.
Changed
helm_clustered_convergenceis now a blocking container-integration test rather thannon_blocking || true. The
swallowedk3d image import ... 2>/dev/null || trueis replaced by a deterministic import that verifies the image is
present in the k3d node's containerd (viacrictl) before deploying, and a pre-deployensure_namespace_absentguard
removes the real back-to-back flake (helm installinto a still-Terminatingnamespace left by a prior run/retry).
When the-clusteredimage is absent (CI helm step, no JDK) the case records an honest SKIP; when present it runs
blocking so a genuine clustering regression reds the suite.- The
docker_compose_war_tomcatcontainer integration test (MockServer deployed as a WAR into Tomcat 10.1) now
actually runs in CI, closing a false-green gap where it silently skipped with "WAR artifact not present … CI wiring
is a follow-up" — a working behavioural test that never ran, in a demonstrated weak spot (the ROOT-context
percent-decode regression66b5d51d2shipped and broke builds, and this is the suite that would have caught it).
Buildkite steps share no filesystem, so the WAR (already built by the reactor in the:maven: buildstep but never
published) is now uploaded via that step'sartifact_pathsand downloaded bycontainer-tests-run.shinto the path
the test globs. A missing WAR now fails the step closed (both an explicit presence check incontainer-tests-run.sh
and, defensively,prepare_warinintegration_test.shred the case) instead of skipping — a skip that reads as green
is the exact defect being closed. The case runs in the Java pipeline's master-only:docker: container integration testsstep (triggered bymockserver/,mockserver-ui/,test-fixtures/changes); it was already declared required
inexpected_tests.manifest, so no manifest change was needed. - Upgraded Netty from
4.2.16.Finalto4.2.17.Finaland, in lockstep,netty-tcnative-boringssl-staticfrom
2.0.78.Finalto2.0.81.Final(the tcnative version the Netty 4.2.17 BOM aligns to). The two must move together:
the Netty BOM pins the transitively-resolved native-classifier tcnative jars to2.0.81.Final, so a mismatched
main-artifact pin fails the enforcerDependencyConvergencerule inmockserver-core. TheNETTY_TCNATIVEbuild
args in everydocker/*/Dockerfilewere updated to match. This unblocks Dependabot PRs #2523 and #2532. - Removed the manual
netty-tcnativeversion-synchronisation step that made every Netty upgrade a convergence trap.
netty-tcnative-boringssl-staticno longer has its own version property ordependencyManagementoverride — the
importednetty-bomnow governs the base artifact and every OS/arch native classifier together, so the two can no
longer diverge and break the enforcerDependencyConvergencerule. The server jars (which ship tcnative classes
but, per #1778, no natives) are stamped at build time with their resolved tcnative version at
META-INF/mockserver-tcnative.version, and everydocker/*/Dockerfilenow derives the native.sodownload from
that stamp instead of a hardcodedNETTY_TCNATIVE=build arg (SHA256 verification of the download is unchanged).
Both jars a Dockerfile can consume carry the stamp: the shadedmockserver-netty-no-dependenciesjar used by the
source=copypath (release/snapshot/CI) and themockserver-nettyassembly-jar-with-dependencies.jarused by
the defaultsource=downloadpath (the public reference build), stamped via the same script so they cannot drift. A
Netty bump therefore needs no tcnative pin update and no Docker edit, and the native.socan never be a different
version than the tcnative classes it pairs with. - BREAKING BEHAVIOUR: the default enabled TLS protocols are now
TLSv1.2,TLSv1.3(previously
TLSv1,TLSv1.1,TLSv1.2), andtlsAllowInsecureProtocolsnow defaults tofalse(previouslytrue).
TLSv1 and TLSv1.1 are deprecated by RFC 8996 and vulnerable to BEAST/POODLE, and TLSv1.3 was previously never
negotiated unless explicitly configured. This is a breaking change for a client that can only speak TLSv1 or
TLSv1.1: its handshake to MockServer will now fail. To
restore the legacy protocols setmockserver.tlsProtocols=TLSv1,TLSv1.1,TLSv1.2AND
mockserver.tlsAllowInsecureProtocols=true(both are required — the insecure-protocol filter strips TLSv1/TLSv1.1
unless it is explicitly allowed). The inbound server always applies the strongHttp2SecurityUtilcipher suites, so
no weak-cipher combination becomes reachable as a result of this change. - Renewed the TLS/mTLS test-certificate fixtures. The two mutual-TLS authentication CAs (which were 151 days from
expiry) and the three Netty TLS integration CAs were re-issued with a 10-year validity, and every leaf they sign
was re-issued with a shorter 5-year validity so that a leaf can no longer outlive its issuing CA. The existing CA
and leaf private keys were preserved (so key encodings, Subject/Authority Key Identifiers and existing signatures
are unchanged); only the certificates were re-minted.
Security
- Pinned
org.apache.logging.log4j:log4j-apito2.25.5to resolve GHSA-qv9r-c865-cp47. It is pulled in transitively
at compile scope (viaspring-boot-starter-logging->log4j-to-slf4j) and lands in a shaded artifact, so it is
shipped; the pin manageslog4j-apionly (log4j-coreis not on the dependency tree). - Pinned
org.jsoup:jsoupto1.23.1to resolve GHSA-pmhh-3w7g-xqp8. jsoup is used only at test scope (never shipped);
the pin guards against a future transitive downgrade below the fixed version. - Upgraded
com.azure:azure-storage-blobfrom12.29.1to12.35.0in the optionalmockserver-blob-azuremodule to
resolve theio.projectreactor.netty:reactor-netty-httpchained-redirect credential-leak advisory (fixed in
reactor-netty1.2.8). The old stack pulledazure-core-http-netty:1.15.10, which pins the vulnerable
reactor-netty1.0.48pair transitively;12.35.0pullsazure-core-http-netty:1.16.5, which advances both
reactor-netty-httpandreactor-netty-coreto1.2.18as a matched pair. This exposure surfaced only when
GitHub's Maven dependency-graph submission was restored (submission had silently frozen at a pre-move snapshot, so
the alert had been invisible to Dependabot). The whole Azure stack (azure-core1.58.1, reactor-netty1.2.18,
reactor-core3.7.19) remains Java-8 bytecode, so the Java 17 floor is preserved. A module-scoped
dependencyManagementpin ofio.projectreactor:reactor-coreto3.7.19reconciles the one internal off-by-one in
the 12.35.0 stack (azure-core declares3.7.18, reactor-netty declares3.7.19) so the enforcer
DependencyConvergencerule stays satisfied. The Docker-gated Azurite contract test moves to Azurite3.36.0with
--skipApiVersionCheck, since the newer SDK negotiates a Storage REST API version that runs ahead of every released
Azurite build. - Pinned three transitive dependencies to close vulnerability alerts that surfaced only when GitHub's Maven
dependency-graph submission was restored (submission had silently frozen at a pre-move snapshot on 5 May, so these
real exposures had been invisible to Dependabot). None required moving the direct dependency that introduces them:- Jackson 3 (
tools.jackson.core:jackson-databind,:jackson-core,tools.jackson.dataformat:jackson-dataformat-yaml)
pinned to3.1.5to resolve GHSA-5gvw-p9qm-jgwh (vulnerable>=3.0.0, <=3.1.4). It arrives at compile scope via
com.networknt:json-schema-validator:3.0.6and is shade-relocated intoshaded_package.tools.jackson, so it
ships in every distributed jar — the one broad, shipped, runtime exposure of the three. All three artifacts
resolve in lockstep and are pinned together so the enforcerDependencyConvergencerule stays satisfied. The pin
is inherited by the separatemockserver-maven-pluginbuild (same parent pom), closing its alert too. at.yawk.lz4:lz4-javapinned to1.11.1to resolve GHSA-6qcp-4vqm-vf35 (native XXHash JVM crash; vulnerable
<=1.11.0). Arrives viaorg.apache.kafka:kafka-clients:3.9.2— optional inmockserver-netty, runtime in
mockserver-async. Note this is theat.yawk.lz4fork coordinate, notorg.lz4.org.apache.commons:commons-compresspinned to1.27.1to resolve the>=1.21, <1.26.0advisories. Arrives at
test scope viaorg.testcontainers:testcontainers:1.21.4, which resolves it at1.24.0. The direct pin overrides
that transitive version without bumping Testcontainers, which is deliberately held at1.21.4for Docker Desktop
4.67+ compatibility.
- Jackson 3 (
- Made outbound TLS host name verification consistent and controllable for the forward/proxy client (Wave 3). When
forwardProxyTLSX509CertificatesTrustManagerTypeisJVMorCUSTOM— the modes that actually validate the
upstream certificate chain — MockServer now verifies the upstream host name against the certificate (RFC 2818 / HTTPS
endpoint identification) on every outbound path. Netty already enabled this for client connections opened with a
known host/port, but NOT for the no-host relay paths, so verification was silently skipped on some paths and there
was no way to turn it off; without the host-name binding a certificate signed by a trusted CA for any host name would
be accepted, leaving a user who had opted into real upstream validation open to a man-in-the-middle. It is now forced
uniformly at the single point every outbound path shares (HTTP/1.1, HTTP/2, CONNECT-tunnelled relay, websocket relay,
the reverse-proxy relay — which verifies against the CONNECT target host/port, not the connected socket address — and
the LLM forward paths). It has no effect on
the defaultANYtrust manager, which deliberately trusts everything and performs no host-name verification. A new
forwardProxyTLSHostnameVerificationEnabledproperty (defaulttrue) turns off just the host-name check while
keeping chain validation, for the legitimate testing case of an upstream whose certificate host name does not match
the address connected to (it actively clears the algorithm Netty would otherwise default on); it is carried by
ConfigurationDTOand folded into the client SSL-context cache key so a runtime change takes effect. The
trust-manager javadoc, which previously implied onlyANYskipped host-name verification, has been corrected. - Added a single startup WARN when the publicly-published bundled Certificate Authority (whose private key ships in the
MockServer jar) is the trust anchor signing served traffic, naming the two supported fixes
(dynamicallyCreateCertificateAuthorityCertificate=true, or--proxy-setup). Shipping the CA key is intentional and
documented and the default is unchanged; the warning just makes the trade-off visible so an operator does not mistake
the bundled CA for real interception security. Logged once per JVM, never per handshake. - A user-supplied FIXED server certificate is now re-checked on a cheap, time-bounded schedule (at most once a minute,
stat only — never a per-handshake re-parse) so a long-running server surfaces a problem instead of silently serving
it: a certificate rotated in place on disk forces a rebuild (which re-runs validation and picks up the replacement,
failing loudly if it too is expired), and an unchanged-but-expired certificate is surfaced with a single WARN. Wave 1
had deliberately left this gap (self-generated leaves already self-renew; fixed certificates were validated only at
startup). - Added a control-plane audit WARN when a
PUT /mockserver/configurationlowers or alters TLS security posture —
downgrading the forward-proxy trust manager toANY, turning offtlsMutualAuthenticationRequired, turning off
forwardProxyTLSHostnameVerificationEnabled, or repointing the TLS key/certificate/CA paths. Control-plane
authentication is off by default, so such a runtime downgrade would otherwise be silent. The change is audited, not
blocked (blocking would be an init-only breaking change). - Hardened the dynamic TLS certificate cache so it can no longer serve stale, torn or over-broad material (Wave 1,
resilience only — certificate validity periods and extensions are unchanged). The cached server SSL context now
regenerates a fresh leaf once the current one passes a renewal threshold (80% of its validity elapsed) instead of
serving an expired certificate for the JVM lifetime; the cache-reuse decision is driven by a content signature over
the Subject-Alternative-Name set, certificate-authority identity, key/cert paths, mTLS and protocol inputs
(replacing a single consumable boolean that could return a certificate missing a just-added SAN under concurrency);
client SSL contexts and the memoised certificate authority now self-invalidate when their inputs are rotated at
runtime. Certificate generation now publishes the new private key and certificate atomically (a mid-flight failure
keeps the previous working pair instead of leaving a new key paired with the old certificate), and SNI-driven
provisioning runs off the Netty event loop with per-host coalescing. Only the leaf drives that renewal trigger: a
dynamically-generated certificate authority nearing its own expiry is warned about once (it is never rotated
automatically, which would invalidate every client trust store that imported it) rather than demanding a leaf
regeneration the certificate-authority guard can never satisfy — which would otherwise re-mint the leaf on every
handshake indefinitely. - Bounded the dynamically-grown Subject-Alternative-Name list with a new
maxSubjectAlternativeNamesproperty
(default 100; when the cap is reached the genuinely oldest dynamically-discovered entry is evicted first, in FIFO
order, with a warning, while configured and default SANs such as localhost are never evicted) and now
normalise/validate each SNI hostname andHostheader (lowercase, length and label-charset checked) before it is
added, closing a denial-of-service vector where any client could force the leaf certificate to be re-minted with an
unbounded SAN list. BothmaxSubjectAlternativeNamesandsslCertificateLeafValidityInDaysare now carried by
ConfigurationDTO, so they round-trip throughGET/PUT /mockserver/configurationand can be set per-instance
rather than only via the static store (a runtime change tomaxSubjectAlternativeNamestakes effect on the next SAN
added). - Dynamically-generated private key material (leaf key, certificate-authority key, and the JKS key store) is now
written owner-readable-only (0600) and atomically, and public certificates0644; a corrupt or unreadable
certificate-authority PEM now fails loudly instead of being silently treated as absent and overwritten (which would
invalidate every pinned client trust store), with cross-process locking around certificate-authority generation. - Deferred BouncyCastle registration in
PEMToFileoff the class-load path, restoring the lazy-BouncyCastle startup
optimisation. - Shortened the auto-generated TLS leaf (server) certificate to a 397-day validity by default (Wave 2), so it
stays inside Apple's 825-day maximum for TLS server certificates (iOS 13 / macOS 10.15) — the previous 10-year leaf
exceeded that cap and is the likely cause of TLS handshake failures on Apple platforms
(#2531). The generated Certificate Authority keeps
its long (10-year) life, and the Wave 1 proactive renewal (regenerating the leaf once 80% of its validity has
elapsed) means a long-running server never serves an expired leaf; the previous long-lived behaviour can be restored
with the newsslCertificateLeafValidityInDaysproperty (e.g.3650). That override is clamped to a usable range of
30–3650 days (a WARN is logged when a value is clamped): a value below 30 would mint a leaf that — because the
notBeforeis back-dated 5 days — is either already expired at issuance or already past its renewal threshold (so it
would be re-minted on every handshake), and a value above 3650 could push the expiry past the X.509 date ceiling; a
non-positive value still falls back to the 397-day default. The generated leaf now also carries a
serverAuth+clientAuthextended-key-usage (Apple requiresserverAuthon the leaf independently of validity), a
criticaldigitalSignature+keyEnciphermentkey-usage, and an authority-key-identifier derived from the CA; the
root CA no longer carries a (non-idiomatic) extended-key-usage. Certificate serial numbers are now forced positive
as required by RFC 5280, and the HTTP/3 legacy echo-mode self-signed fallback gained a back-datednotBefore,
subject-alternative-names,serverAuthextended-key-usage and a key-usage (it keeps the long validity because it is
both trust anchor and server certificate with no renewal loop).
Removed
- Removed the unused
PKCS1CertificateAuthorityPrivateKey.pemresource from the publishedmockserver-corejar. It
was the explicit PKCS#1 half of an encoding pair added in 2020 for the since-removed JDK key/certificate builder,
whose deletion in 2022 left it with no references for around four years. It was byte-identical to the original
CertificateAuthorityPrivateKey.pem, which is retained (it backs a published raw-URL link and remains the stable
PKCS#1 form); the code continues to loadPKCS8CertificateAuthorityPrivateKey.pem.
Fixed
- Fixed a rare
mockserver-coreunit-test flake (ConfigurationValueRedactionTest/
ConfigurationDTOCredentialMaskingTestfailing withExpected: is <1> but: was <0>when asserting an exact count of
captured log records). These tests attach ajava.util.logginghandler and assert what is emitted. Two other tests —
ClassInitializationDeadlockTestandConfigurationPropertiesInitializationTest— force a fresh<clinit>of
MockServer classes in an isolated child-first classloader; that initialisation reaches
MockServerLogger.installDefaultJavaLoggingFormat()→LogManager.getLogManager().readConfiguration(...), and
java.util.logging.LogManageris a JVM-global singleton the child-first classloader does not isolate, so its
readConfigurationperforms areset()that removes every handler from every logger in the JVM. When one of those
classes ran in the parallel Surefire phase alongside a handler-capturing test, the reset detached the capturing
handler mid-test and a subsequent emit went uncounted. Both fresh-<clinit>classes are now pinned to the sequential
(parallel=none) Surefire phase inmockserver-core/pom.xml, so they can never run concurrently with a
handler-capturing test. No production behaviour changes. - Fixed the Helm chart's sidecar-injection webhook being broken out of the box. The default TLS-bootstrap image
webhook.tls.setupImage: bitnami/kubectl:1.31no longer exists — Bitnami withdrew the tag from Docker Hub — so both
bootstrap Jobs failed, theMutatingWebhookConfiguration'scaBundlewas never patched, and because
failurePolicy: Failthe webhook then rejected every matched pod CREATE. Anyone enablingwebhook.enabled=true
from the shipped defaults got an admission path that blocked pod creation rather than injecting a sidecar. The
bootstrap now usesregistry.k8s.io/ingress-nginx/kube-webhook-certgen, the purpose-built tool for exactly this
job, published on the most stable-publication registry available and self-contained (no shell, noopensslCLI, no
install-at-runtime step that would fail on an airgapped or proxied cluster). Phase 2's ClusterRole also gains the
updateverb, which the patch step genuinely needs.helm_sidecar_injectionnow renders and asserts the chart
default is pullable rather than overriding it, so this cannot silently rot again. - Fixed the Kubernetes sidecar-injection webhook rejecting valid TLS private keys.
WebhookServerparsed PKCS#8 only
and explicitly rejected anything else, which broke the cert-manager path as well: the chart'sCertificate
requestsalgorithm: RSAwithoutencoding: PKCS8, i.e. PKCS#1, so a cert-manager-issued key could not be loaded.
It now accepts any standard unencrypted PEM private key — PKCS#8, PKCS#1, and SEC1 EC. This coupling is why the
broken bootstrap image above could not simply be swapped: the handler only tolerated the one format that one
withdrawn image happened to emit. - Fixed a thread race in dynamic Certificate Authority generation that could leave a standalone / CLI / Docker
MockServer serving broken TLS for the rest of the process's lifetime. When
dynamicallyCreateCertificateAuthorityCertificateis enabled, two startup paths could generate the CA
concurrently — the proxy-setup log (proxySetupLogging, on by default in the CLI) writing the CA to disk, and the
first HTTPS handshake building the server certificate. Generation was serialised only by a cross-process file lock;
a second lock attempt within the same JVM threwOverlappingFileLockException, which was caught and treated as
"proceed without serialisation", so both threads minted different CA key pairs and interleaved their writes. The
result was a torn CA key/certificate pair on disk: every leaf certificate was then signed with one generation's CA
key but verified against the other's CA public key, failing withSignatureException: certificate does not verify with supplied key— and because the mismatched CA was memoised, every subsequent TLS handshake failed for the
life of the process. CA generation (and the paired load of the CA key + certificate) is now serialised within the
JVM on a per-directory monitor in addition to the cross-process file lock, so the key and certificate are always
published from the same generation. EmbeddedClientAndServerusers were unaffected (they default
proxySetupLoggingoff); standalone, CLI and Docker users generating a dynamic CA could hit it intermittently. - Unblocked the daily Dependabot updater, which had been failing and raising no PRs. The Maven wrappers under
mockserver/andmockserver/mockserver-maven-plugin/were modernised from the legacy Takari format to the
Apacheonly-scriptwrapper (wrapperVersion3.3.4, Maven pinned at 3.9.16), so themaven-wrapper-updatercan
parse them;mockserver-vscodewas split into its own npm job (a lockfileEOVERRIDEthere no longer aborts the
other npm directories) and pinnedjs-yamlto^4.1.1viaoverrides; and/.opencode(which had only an
orphan lockfile and no tracked manifest) was removed from the npm directories and its lockfile untracked. - Cloning an
X509Certificatemodel that contains certificate metadata without an underlying Java certificate no
longer throws aNullPointerException; metadata-only and certificate-backed models now both preserve their state
when cloned (#2527). - JSON body matching no longer depends on which JSON provider json-unit
resolves to. MockServer parses both documents with Jackson and hands json-unit the resulting nodes to avoid
re-parsing on every match, but json-unit picks its provider by asking each in turn whether it claims the value
and falling back to the last one registered — and only its Jackson provider claims a Jackson node. Where another
provider won (for exampleorg.json, whether because Jackson was not visible to json-unit or because
json-unit.librariespinned it) every JSON match threwUnsupported type class com.fasterxml.jackson.databind.node.ObjectNodeand no JSON body ever matched. MockServer now falls back to
giving json-unit the raw JSON text, which it parses with whichever provider it resolved to
(#2496). - Match failures from the JSON body matcher now report why the match failed. When JSON matching threw, the log said
onlyexception while perform json match failedand the exception was recorded solely atTRACE, so at the
default log level there was no way to tell a malformed body from a missing class from a runtime error. The cause
is now included in the reported difference, as it already was for the XML schema, JSON path and JSON-RPC matchers. - Resolved the outstanding npm security advisories in the shipped and published Node packages:
dompurify
3.4.12→3.4.13in the bundled dashboard (mockserver-ui),brace-expansion
5.0.8→5.0.9(ReDoS) inmockserver-testcontainers/node, andjs-yaml
4.3.0→4.3.1inmockserver-client-node,mockserver-nodeand
mockserver-testcontainers/node. Thebrace-expansionfix bumps only the parent so theminimatch/archiver
glob split is preserved (a blanket override previously brokearchiver); nobrace-expansionoverride was added. - Documented and mitigated a clustered-deployment TLS trust defect: with the default dynamic Certificate Authority
generation, every MockServer node in a cluster mints its own distinct CA, so a client that trusts one node's
mockserver-ca.pemgets an intermittent TLS validation failure when a load balancer routes it to another node.
StateBackendFactory.create()now logs a WARN when it detectsclusterEnabled=truetogether with
dynamicallyCreateCertificateAuthorityCertificate=true, and the limitation and its fix are now documented in
docs/code/clustered-state.md,docs/code/tls-and-security.md, the Helm chart README, and the Centralized
Deployment consumer page. - Added first-class Helm chart support for supplying one shared TLS Certificate Authority to every replica via a
Kubernetes Secret — the supported fix for the clustered CA defect above. New opt-in valuesapp.tls.*create (or
reference an existing) Secret, mount it read-only, and setcertificateAuthorityCertificate/
certificateAuthorityPrivateKeywithdynamicallyCreateCertificateAuthorityCertificate=falseon every pod. A CA
private key now lands in a Secret rather than a ConfigMap. Also addedapp.dynamicCertificateDir.*(a writable
emptyDirfor the single-replica dynamic-CA case, so certificate writes no longer depend on a non-writable working
directory) andapp.extraEnv(arbitrary container environment variables, enabling anyMOCKSERVER_*property the
chart does not expose directly). All new values are opt-in and default to today's behaviour. - Fixed a latent dead condition in the Helm chart Deployment template: the
MOCKSERVER_PROPERTY_FILEenv var was
gated on an undeclaredapp.mountConfigMapvalue (always false).app.mountConfigMapis now a declared value
(defaultfalse, preserving prior behaviour) so external-ConfigMap users can opt into having MockServer pointed at
the mountedmockserver.properties.