Connector transport security
BurnLedger signs a record count into a deletion certificate. That number is read over the connector's link to the customer datastore, so an attacker who can sit on that link can change what BurnLedger attests to. Every connection config is therefore classified before a connector is built, and anything below verified TLS is refused.
This page is for the operator running a BurnLedger deployment: what the four
levels mean, exactly what each connector config needs in order to reach
verified, and what the escape hatch does.
Enforcement lives in connector.TransportPolicy and runs in
Registry.Create — the single point both cmd/server and cmd/enclave build
connectors through. Classification is pure config parsing (no DNS, no network),
which is why it also runs inside the enclave rather than being delegated to the
parent.
The four levels
| Level | Meaning | Accepted by default? |
|---|---|---|
verified |
TLS with full certificate chain and hostname verification, against the system trust store or an operator-supplied CA. | Yes |
encrypted |
TLS is negotiated but the server certificate is not verified (sslmode=require, tls=skip-verify, TrustServerCertificate=true, bolt+ssc://). An on-path attacker can terminate the session, be the peer, and rewrite the record count. |
No |
plaintext |
No TLS, or a negotiation that can silently fall back to no TLS (sslmode=prefer, MySQL tls=preferred). |
No |
unknown |
The config could not be classified. Never assumed secure. | No — and not permitted by the opt-out either |
unknown is deliberately not the same as "insecure": it means BurnLedger cannot
tell what the link will be, so it refuses regardless of the opt-out.
What a refusal looks like
Registration itself still succeeds — the system is stored, so you can see and fix it — but no connector is ever built for it, so every attestation against the system fails.
On attestation (POST /v1/attestations) the refusal is reported in full,
because a policy rejection is a classification verdict built from a connector
type and two level names — it contains nothing taken from your connection config,
so it is safe to hand back verbatim:
{
"error": {
"code": "CONNECTION_FAILED",
"message": "connector postgresql: transport security encrypted is below the required minimum verified",
"request_id": "..."
}
}
with HTTP 502. For an unclassifiable config the message is:
connector mysql: transport security could not be classified and is never assumed secure; the required minimum is verified
On registration and health check you do not get that message. The system is
stored unhealthy, and both POST /v1/systems and
POST /v1/systems/{id}/health-check return the generic
{ "health_status": "unhealthy", "health_error": "connection failed" }
health_error is always one of a small set of fixed strings. It never carries
the connector's own error text, because connector errors routinely embed the DSN
— password included — and the scrubber that cleans them is best-effort. The
enclave has no way to tell the host "this particular failure is a policy verdict
and its message is safe", so on this path every connection failure reads alike.
That is a known gap, not the intended experience.
What to read instead. The level measured at registration is stored on the
system row and served as transport_security on every system response (unknown
/ plaintext / encrypted / verified), and the dashboard shows it as a badge
on the systems list and system detail pages. If a system is unhealthy with
connection failed and its transport_security is anything other than
verified, that is the cause — the table below says what that connector needs.
Attempt an attestation to get the verdict stated outright.
In the logs (operator-side) the full verdict is there on both paths: the
enclave logs connector connect failed with the driver-level error, and the
server logs system: health check failed with the scrubbed detail, which for a
policy rejection is the verdict string above.
The transport level is not bound into the signed certificate. A third party verifying a certificate offline cannot tell what transport was used to produce it; the control is an enforcement gate plus a recorded field, not a cryptographic claim.
What each connector needs to reach verified
| Connector | Reaches verified when |
Refused |
|---|---|---|
postgresql, redshift |
sslmode=verify-full in the DSN. sslrootcert=system also works (pgx rewrites the mode to verify-full). A private CA stays verified via ca_cert (base64 PEM in the connection config); sslrootcert=<path> also works, but only where you control the filesystem of the process that dials — never in the enclave. |
sslmode absent (defaults to prefer → plaintext fallback), disable, allow, prefer, require, verify-ca (chain checked, hostname never bound) |
mysql |
?tls=true and an explicit port in @tcp(host:port). Without a port the driver derives no ServerName on the production dial path and the config classifies unknown. A private CA stays verified via ca_cert. |
no tls parameter, tls=false, tls=skip-verify (encrypted), tls=preferred (silently falls back to cleartext when the server clears one capability bit), tls=custom (no custom config is registered — parse error → unknown) |
sqlserver |
encrypt=true (or encrypt=strict) and no TrustServerCertificate=true. A private CA stays verified via ca_cert. |
encrypt absent — the driver's default TLS-wraps the login packet then reverts the transport to the bare socket, so rows travel in cleartext — encrypt=disable, or TrustServerCertificate=true (encrypted) |
oracle |
?SSL=true in the DSN, with SSL VERIFY left at its default (true). SSL=true is what sets the protocol to tcps; the URL scheme (oracle://, tcps://) is ignored by the driver. A private CA stays verified via ca_cert, on a DSN naming exactly one server address (go-ora rewrites the TLS server name per dial on the shared config). |
no SSL=true (plaintext), SSL VERIFY=false (encrypted), Oracle Native Network Encryption alone (ENCRYPTION=REQUIRED authenticates no peer), a DSN setting both connStr and server (unknown) |
mongodb |
?tls=true in the URI. There is no custom-CA field, so a private CA needs a publicly trusted certificate in front of the datastore. |
no tls=true (plaintext — note tlsInsecure=true alone does not turn TLS on), tls=true&tlsInsecure=true (encrypted), any mongodb+srv:// URI (unknown — its seed list lives in DNS, which classification does not resolve; list the hosts explicitly) |
neo4j |
neo4j+s://host:7687 or bolt+s://host:7687. There is no custom-CA field. |
bolt:// / neo4j:// (plaintext), bolt+ssc:// / neo4j+ssc:// (encrypted, self-signed accepted), any other scheme (unknown) |
elasticsearch |
Every entry in addresses is https://. A private CA stays verified via ca_cert. |
any http:// address — the client round-robins, so one plaintext entry means a fraction of real traffic is plaintext and the whole config classifies plaintext |
redis |
Omit tls (defaults to on) or set "tls": true. TLS 1.2+, certificate verified against the system trust store or against ca_cert when supplied, hostname bound to the host in addr. |
"tls": false (plaintext); a config naming no addr (unknown). addr must be host:port — a bare host classifies fine but the connector then refuses to build, because a hostname is what the certificate is checked against |
cassandra |
Omit tls (defaults to on) or set "tls": true. There is no custom-CA field. |
"tls": false (plaintext) |
hbase, marklogic |
Omit tls (defaults to on) or set "tls": true; the REST base URL becomes https:// and the certificate is verified against the system trust store. There is no custom-CA field. |
"tls": false (plaintext) |
teradata |
Same tls flag and system-trust-store verification as above. Additionally accepts server_cert (base64 of the server's PEM or DER certificate), which pins the exact leaf: the handshake is accepted only if the presented certificate is byte-identical. Use it for appliances whose factory certificate has no SANs and can never chain-verify (Vantage Express, ClearScape trials); a pin still classifies as verified because it is a stronger identity check than chain-plus-hostname. server_cert with "tls": false is refused. |
"tls": false (plaintext) |
s3, dynamodb |
No endpoint field (the SDK resolves the real AWS endpoint over https), or an endpoint that is https://. |
an http:// endpoint; for DynamoDB, region: "local" with no endpoint — the AWS ruleset resolves that to http://localhost:8000 |
gcs |
No endpoint field (compiled-in https://storage.googleapis.com/...), or an https:// endpoint. |
an http:// endpoint; also unknown — and therefore refused outright — if STORAGE_EMULATOR_HOST is set in the server or enclave environment, because the SDK then uses the emulator endpoint and the connection config cannot describe it |
azure_blob |
account_name mode (the URL is a hardcoded https:// literal), or a connection_string that resolves to an https:// blob endpoint. |
a connection string whose BlobEndpoint is http:// (the Azurite shape), regardless of what DefaultEndpointsProtocol says |
bigquery, snowflake, databricks |
Always — no config field can reach the scheme. | a config missing a required field (project_id/dataset, account, host/http_path/token) classifies unknown: nothing was parsed, so nothing was established |
Redis systems registered before this release
Until this release the Redis connector sent everything in cleartext on every
production path: go-redis ignores Options.TLSConfig whenever a dialer is
injected, and BurnLedger injects one. A system whose config said "tls": true
therefore connected in the clear, and the attestation completed and certified
anyway.
Any Redis certificate issued before this release was issued on evidence gathered
over an unauthenticated link, whatever the stored config claimed. The record
count could have been altered in transit, and the ACL username and password
crossed the same link in cleartext. Rotate those credentials, re-run the
attestation against a verified link, and re-issue any certificate that has to
stand on its own. There is no way to tell after the fact whether a given
connection was tampered with — that is what the floor now prevents.
The opt-out: DP_ALLOW_UNVERIFIED_TRANSPORT
export DP_ALLOW_UNVERIFIED_TRANSPORT=true # DOWNGRADE — development and test only
Set on cmd/server (via internal/config) and read independently by
cmd/enclave from its own environment. Any value other than the exact string
true leaves the floor in place.
What it does:
- Permits a connector to be built whose link classifies
plaintextorencrypted. It is the direct sibling ofDP_ALLOW_PRIVATE_NETWORKS, which relaxes the SSRF host policy; the two are independent, and a local Docker datastore usually needs both. - Does not permit an
unknownclassification. What cannot be classified is never assumed secure. - Leaves behind exactly one
WARNlog line per connector construction:connector transport below policy floor, permitted by opt-out, withconnector_type,transport_levelandminimum_transport_level. There is no other marker — certificates issued over a downgraded link are indistinguishable from any other certificate to anyone holding only the certificate.
Do not set it in production. It was, before #443, the only way to run a
deployment whose datastores use an internal CA. It is no longer: postgresql,
redshift, mysql, sqlserver, oracle, elasticsearch and redis all take
a ca_cert (base64-encoded PEM) in the connection config, which keeps the link
verified instead of downgrading the floor for everything that deployment
attests. mongodb, cassandra, neo4j, hbase, marklogic and teradata
still expose no CA field; a private CA there needs a publicly trusted certificate
in front of the datastore, or the opt-out. See
ADR-014.
In enclave (vsock) mode the enclave reads this variable from its own
environment, which is baked into the measured EIF. Dockerfile.enclave sets no
such variable, so the enclave's floor is verified with no opt-out as shipped;
turning it on means adding it to that image and rebuilding, which changes PCR0
and requires re-pinning the KMS key policy. That is intentional: an operator
cannot silently downgrade the enclave's transport floor from the parent. See
VSOCK_DEPLOYMENT.md.
Related
- Per-connector configuration detail: integration guides
- SSRF host policy (
DP_ALLOW_PRIVATE_NETWORKS):docs/security-audit-2026-06.md(NET-2) - Decision record: ADR-012, whose private-CA consequence is superseded by ADR-014