Security compatibility matrix¶
The question this page answers: for each technology vouchfx talks to, can a 1.0 suite reach a secured instance of it?
If your infrastructure is authenticated — a broker demanding mutual TLS, an API behind client certificates, a database that refuses plaintext — this is the page to read before you write your first suite. It is a reference table, not a tutorial. For the authoring syntax see §3.2.6b of the DSL specification; for where the boundary moves next see the roadmap.
For a suite you can actually run, see
examples/security-mtls.e2e.yaml.
It demonstrates both halves of the shape this page describes against one topology — a REST call
presenting a client certificate to a TLS-terminating API, and a Kafka publish and consume over a
mutually-authenticated broker, with both services chaining to one private certificate authority.
It also shows the part that is easy to miss: the certificates are generated by a script that runs
before vouchfx, because every path under a security: block is checked before the first
container starts.
The short answer¶
- Kafka — reachable over TLS and mutual TLS, both as a
kafkadependency and as anenvironment.servicesentry standing in for a customer-supplied broker. - The HTTP family (
http.rest,http.soap,metrics-assert.prometheus) — reachable over HTTPS, presenting a client certificate, against any declared service. - Everything else — RabbitMQ, NATS, MongoDB, Redis, PostgreSQL, SQL Server, MySQL, Elasticsearch, and the remaining families — not reachable over a secured connection in 1.0.
Read this first: vouchfx secures the client, never the server¶
This is the single most likely surprise for a new enterprise adopter, so it is stated before the tables rather than after them.
A security: block declares the client-side expectation for infrastructure whose server side is
already secured. It does not cause vouchfx to secure anything. Where vouchfx starts the
container itself — every managed dependency — it stages only the client half of the connection: the
trust anchor, the client certificate and key, the transport protocol on the emitted client. It does
not generate a server certificate, does not enable a listener's TLS, and does not reconfigure a
stock image to demand an identity.
So "vouchfx supports mutual-TLS Kafka" means this in practice:
- You supply your own broker image via
image:(on the dependency) or anenvironment.servicesentry, already configured to serve a secured listener. - You supply its server-side material — keystore, truststore, certificate — through
security.serverArtifacts, which vouchfx streams into that container at topology-build time. - vouchfx configures the client: the step's producer or consumer connects over TLS with the certificate you declared.
You configure the server; vouchfx configures the client. Declaring security: on a stock,
engine-provisioned dependency does not secure it — it states an expectation the engine then confirms
at run time, and fails the suite when the endpoint turns out not to speak TLS at all.
One consequence worth knowing up front: a secured service's default health check becomes a TCP
probe rather than an HTTP one, because a container health check cannot present a client certificate
and an HTTPS probe against a mutual-TLS listener would hold a working topology unhealthy forever.
Declare an explicit healthCheck against a separate unsecured port if you want a stronger probe.
The client pair must be PEM, inside the suite directory — an encrypted key needs clientKeyPassword¶
clientCert and clientKey are read as a PEM certificate and its matching PEM private key. A key
encrypted at rest — the form a corporate PKI hands out by default — is usable as it stands: leave it
encrypted and declare its passphrase beside it, as a whole ${secret:<source>/<path>} reference and
nothing else, since a literal passphrase is refused.
clientCert: certs/client.pem
clientKey: certs/client-key.pem
clientKeyPassword: ${secret:env/CLIENT_KEY_PASS}
The reference is resolved at first use of the certificate material — inside the certificate load,
once the topology is up — never at compile time, so no passphrase is baked into the compiled script
and the reproducibility envelope hashes the reference rather than the value. The Kafka providers set their
client library's ssl.key.password from the same resolved value, so both halves of a suite open the
same encrypted key. Only the ENCRYPTED PRIVATE KEY (PKCS#8) form can be opened this way: an openssl
legacy key, marked Proc-Type: 4,ENCRYPTED, must first be converted with openssl pkcs8 -topk8. A
passphrase declared against a key that is not encrypted, one declared with no clientKey at all, and
a reference resolving to an empty value are each refused with a diagnostic rather than ignored.
What remains unavailable is a PKCS#12/PFX bundle carrying the certificate and key together —
there is no field for one — and a passphrase for caCert, a trust anchor being a public certificate
with no private key to unlock.
Both files must also sit inside the directory tree containing the .e2e.yaml. Every path in the
block is written relative to that directory and must resolve inside it; an absolute path is rejected
by name even when it points at the file you meant, so material kept in a shared machine-wide location
has to be copied in beside the suite. A ${secret:…} reference is no way round that: in a
path-valued field — caCert, clientCert, clientKey, serverArtifacts[].source — it is refused,
naming the field, because the engine reads and copies the file such a field points at while the
secrets subsystem yields a value, which is not a file.
caCert is the asymmetric case, and it invites the wrong generalisation: the trust anchor is loaded
through a path constructor that auto-detects PEM and DER, so one field is format-tolerant and the
other is not.
How the division was derived¶
The table below is derived from the code, not from intent. A target is reachable over a secured connection in 1.0 only where all three of the following hold together:
- the language schema accepts a
security:block on that target — it is rejected outright on every dependency kind exceptkafka; - the engine's security-profile registry has a wiring registered for that target kind — the wired
set is exactly
{kafka dependency, any declared service}, for bothtlsandmtls, checked at validation time so a suite claiming anything else fails before a container starts; and - the provider that resolves the step's
targetactually configures its emitted client from the resolved security configuration — measured across all twenty-five Core providers, that is the two Kafka providers and the three HTTP-family providers, and no others.
Rule 3 is what makes the answer honest. Rules 1 and 2 alone would only tell you which suites validate; a suite can validate and still connect in plaintext if nothing on the provider side ever reads the declaration. Because both gates run at validation time, they decide only which suites are accepted at all — which is why widening them later is additive and safe, and why 1.0 deliberately rejects more than it might have.
Matrix by step family¶
Every Core provider, and what it can reach.
| Provider | Resolves target against |
Secured connection in 1.0 | Notes |
|---|---|---|---|
http.rest |
services | Yes — TLS and mutual TLS | image:-form service only |
http.soap |
services | Yes — TLS and mutual TLS | image:-form service only |
metrics-assert.prometheus |
services | Yes — TLS and mutual TLS | image:-form service only |
mq-publish.kafka |
kafka dependency, or a service |
Yes — TLS and mutual TLS | Plain-payload and Avro paths alike |
mq-expect.kafka |
kafka dependency, or a service |
Yes — TLS and mutual TLS | Plain-payload and Avro paths alike |
mq-publish.rabbitmq |
rabbitmq dependency |
No | Server-side TLS deferred to 1.1 |
mq-expect.rabbitmq |
rabbitmq dependency |
No | Server-side TLS deferred to 1.1 |
mq-publish.nats |
nats dependency |
No | Server-side TLS deferred to 1.1 |
mq-expect.nats |
nats dependency |
No | Server-side TLS deferred to 1.1 |
mq-publish.redis |
redis dependency |
No | Server-side TLS deferred to 1.1 |
mq-expect.redis |
redis dependency |
No | Server-side TLS deferred to 1.1 |
cache-assert.redis |
redis dependency |
No | Server-side TLS deferred to 1.1 |
mq-publish.azureservicebus |
azureservicebus dependency |
No | Reaches the engine-provisioned emulator only |
mq-expect.azureservicebus |
azureservicebus dependency |
No | Reaches the engine-provisioned emulator only |
db-assert.postgres |
postgres dependency |
No | Deferred to 1.1 |
db-assert.mysql |
mysql dependency |
No | Deferred to 1.1 |
db-assert.sqlserver |
sqlserver dependency |
No | Deferred to 1.1 |
db-assert.mongodb |
mongodb dependency |
No | Server-side TLS deferred to 1.1; mutual TLS further out |
db-assert.dynamodb |
dynamodb dependency |
No | Reaches the engine-provisioned local double only |
cache-assert.elasticsearch |
elasticsearch dependency |
No | Out of scope — see below |
storage-assert.s3 |
minio dependency |
No | Reaches the engine-provisioned MinIO only |
mail-expect.smtp |
mailpit dependency |
No | Deferred; not in 1.1's named set |
webhook-listen.http |
— (engine-hosted listener) | No — plaintext inbound | See Inbound listeners, below |
trace-expect.otlp |
— (engine-hosted receiver) | No — plaintext inbound | See Inbound listeners, below |
script.csharp |
— (no infrastructure target) | Not applicable | Reconciles no target |
Matrix by dependency kind¶
The security: block is accepted on exactly one of the thirteen dependency kinds. On the other
twelve it is rejected outright — the block, not merely a particular profile value, and tls and
mtls are refused identically.
environment.dependencies kind |
Accepts security: |
Status |
|---|---|---|
kafka |
Yes | Delivered — TLS and mutual TLS |
postgres |
No | Deferred to 1.1 |
sqlserver |
No | Deferred to 1.1 |
mysql |
No | Deferred to 1.1 |
mongodb |
No | Deferred to 1.1 (server-side TLS) |
redis |
No | Deferred to 1.1 (server-side TLS) |
rabbitmq |
No | Deferred to 1.1 |
nats |
No | Deferred to 1.1 |
elasticsearch |
No | Out of scope |
mailpit |
No | Not in 1.1's named set |
azureservicebus |
No | Emulator only |
dynamodb |
No | Local double only |
minio |
No | Local double only |
An environment.services entry accepts security: unconditionally — but only in its image: form.
A project:-form service cannot declare it: its endpoints come from its own launch profile, so the
engine has none of its own to give an https scheme, and the declaration is rejected at
topology-build time.
That refusal does not make a project:-form service plaintext by definition, and the
difference matters here more than anywhere else on this page. Such a service has three states, not
two. It may be addressed over plaintext, which is what the engine's fixed selection rule prefers
whenever the project offers a plaintext listener. It may be refused a security: block, as above.
And it may be addressed over TLS with no engine-configured trust — either because the author
named an https listener through the service's endpoint: field, or because the project offers only
an https listener and the fixed rule selected the one that exists. An advisory announces
that third state whenever a step addresses such a service, precisely because a reader who stopped
at the paragraph above would be contradicted by their own terminal. It is printed there, and it
also reaches the JSON Lines event stream as a transport-notice record — so the announcement
survives into an archive, not just a console. Its reach is --events and --events-stream
only: the JUnit and HTML renderers ignore an event type they do not recognise, and --watch is
wired to no report artefact whatever.
What is absent in that state is engine-configured trust, not verification, and conflating the
two in either direction is wrong. vouchfx contributes no trust anchor, pins no peer, presents no
client identity and asserts nothing about the transport: there is no security: block to declare
any of it from. What still happens is the platform's own verification — for a step that makes an
HTTP request, the certificate that listener presents is validated against the host's default trust
store, full chain, exactly as any other .NET HTTPS request is. A host that does not already trust
that certificate fails the handshake, which is classified an environment error, and an
environment error does not fail the run unless --fail-on-env-error is passed. So a suite in this
state can finish green having verified nothing about the service it addressed — which is why the
advisory above is on the event stream as well as the terminal: a run archiving --events or
--events-stream keeps the record that says which state it was in, while a --junit/--html-only
pipeline still archives a green report with nothing in it about the transport. Securing the system
under test, in the sense the rest of this page uses, still requires the image: form.
What "not reachable" actually means for the twelve excluded kinds¶
This distinction matters, because "the client library cannot do it" and "vouchfx does not wire it yet" are very different problems for an adopter to plan around. For these kinds it is the second.
The .NET client libraries behind most of the excluded kinds carry TLS through the connection
string: RabbitMQ flips on an amqps:// scheme, NATS on tls://, Redis on ,ssl=true, MongoDB
on tls=true, and the relational clients on their own equivalent keywords. Enabling server-side
TLS for those kinds is mechanically a scheme or keyword per kind, with no provider code change at
all — which is exactly why it is a cheap 1.1 extension rather than a redesign.
What is missing is a channel for it in 1.0. The connection string a provider consumes is built
by the engine from the orchestrated resource; there is no DSL field through which an author supplies
or amends one. The variables block cannot carry it either — names beginning with the engine's
reserved conn:: prefix are rejected by the schema, precisely so an author-supplied value cannot
overwrite a staged connection string. So there is no supported way to hand vouchfx a
TLS-enabled connection string for a dependency in 1.0.
There is also no substitute declaration that buys transport security back. Re-declaring the
technology under environment.services does not work: of the twenty-five Core providers, seventeen
resolve target exclusively against declared dependencies and reject a target that is not a
declared dependency of their own kind, and those seventeen cover all twelve excluded kinds. Moving
the declaration trades a schema rejection for a step-validation rejection and still leaves you
without a working suite. Only the three HTTP-family providers resolve target through declared
services (and the two Kafka providers accept either), so the service form is a working path for an
HTTP system under test and for a customer-supplied broker — and for nothing else.
Kafka needed provider-side code where these kinds do not, for the same underlying reason: its client configuration derives no transport decision from the bootstrap string, so the decision has to be made in the emitted client configuration rather than carried in a URL.
The deferrals, stated plainly¶
Rather than leaving blanks in the tables:
- Server-side TLS for RabbitMQ, NATS, MongoDB, Redis and the relational stores (PostgreSQL, SQL Server, MySQL) is deferred to 1.1 by owner decision, taken on 3 August 2026. The design and acceptance criteria are written and unchanged; only the release is deferred. Because both gates that exclude these kinds run at validation time, widening is additive and breaks nothing that validates today.
- Mutual TLS for MongoDB and Redis goes further out than server-side TLS for them. Neither .NET client accepts client certificates through a connection string — both are API-only — so the cheap connection-string path that delivers server-side TLS does not reach client identity for these two.
- Secured Elasticsearch is out of scope, not deferred. The engine provisions Elasticsearch with its security subsystem disabled, and enabling it requires an initialisation/setup container that this design does not add.
mailpit,azureservicebus,dynamodbandminioaddress engine-provisioned test doubles — an SMTP capture server, the Service Bus emulator, DynamoDB Local, MinIO — reached at fixed, engine-supplied local credentials. Pointing these providers at a real, secured, credentialed service is not supported in 1.0 and is not part of 1.1's named set.- Mutual TLS on the engine's own inbound listeners is out of scope; see below.
Inbound listeners are plaintext¶
Two providers do not connect out to infrastructure — the engine stands up an ephemeral listener and waits for the system under test to call in:
webhook-listen.httpcaptures inbound webhook deliveries.trace-expect.otlpreceives OTLP trace exports.
Both bind plaintext HTTP on an OS-assigned port. Your system under test must be able to reach the engine host over plain HTTP to deliver a callback or a span. A system under test that can only make mutually-authenticated outbound calls would need the listener to present a server certificate, which it does not do. Plan for this when the system under test is itself hardened: it is the one place in a secured topology where vouchfx is the server rather than the client.
What a green secured suite actually proves¶
Before the first step runs, the engine connects to every declared secured endpoint with the same
material a step will use, and reports one of two named confirmation levels. A declaration it cannot
confirm fails the suite as an environment error with no gating flags set — the one deliberate
exception to "only Fail breaks CI by default".
A green secured suite proves nothing about a secured file the run never executed, and it no longer
pretends to. A .e2e.yaml that parsed and was then refused for its contents never becomes a
scenario, so its declaration is confirmed by nothing — not even by a sibling whose probe confirmed
the same target, since nothing established that the refused file's environment block is the one the
topology started from. Such a file reddens the run with no gating flag, on run and
run --parallel N alike. See CI integration for the full rule.
TransportConfirmed— the endpoint speaks TLS and, undermtls, the engine presented the declared client certificate. It does not prove the peer demanded an identity: in TLS 1.3 a server cannot reject a missing client certificate during the handshake, so a listener that merely requests one is indistinguishable from an enforcing one at this level.AuthenticatedRoundTrip— the engine completed an application-protocol round trip over the secured connection, and undermtlsadditionally showed that a second connection presenting no client certificate failed the same exchange. Both halves are needed for the claim "the peer required an identity" to be true. Today this level is reached for Kafka-speaking targets.
Neither level says anything about authorisation — whether the presented identity may publish to or consume from a given topic is the broker's own per-request decision, and surfaces as an ordinary step-level environment error.
Where mutual TLS being enforced is itself the thing under test and the target is not one the engine can round-trip, assert it at the system under test — for example with a step that expects the rejection an unauthenticated caller should receive.
If your infrastructure is not on the delivered list¶
Three honest options, in order of how often they apply:
- Terminate TLS in front of the system under test, not in front of the store. Most enterprise requirements are about the API surface and the event bus, both of which 1.0 covers. A suite that drives a secured REST API and a mutual-TLS Kafka broker, and then asserts against a plaintext database inside the private topology vouchfx created, is a realistic and defensible shape — the database is not exposed beyond the suite's own network.
- Wait for 1.1 for the deferred kinds. The widening is additive; nothing you author today breaks.
- Raise it. If a specific kind blocks an adoption, say so on the issue tracker — the ordering within 1.1 is not fixed, and the deferred work is designed rather than merely intended.