Migration discovery
Find out which connections depend on publicly issued client certificates before anything changes. mt discover probes endpoints, inventories client certificates and inspects gateway configuration, read-only, and labels every finding verified, inferred or unknown.
Why discover first
Sectigo published 10 February 2027 as the final cutoff for clientAuth in newly issued publicly trusted Sectigo SSL/TLS certificates. A connection is affected when its callers authenticate with publicly issued client certificates: the first renewal after the cutoff produces a certificate that no longer works as a client certificate. Inventory and renewal timing decide the effect on each deployment, which is why migration starts with discovery rather than a change.
The change is about client certificates. A public server certificate keeps working as it is, and mutual TLS itself is not going away. The usual outcome is the smallest safe change: keep the gateway, the application and the server certificate, and move callers to a private client-certificate issuer that the gateway trusts next to the current one until the new path is proven.
mt discover answers four questions for each connection and labels every answer:
- Does the endpoint ask for client certificates, from which issuers, and does it refuse callers without one?
- Which client certificates do callers present, who issued them, do they carry the clientAuth EKU, and when do they renew?
- What does the gateway configuration say about trusted client CAs, verification depth and how a certificate identity maps to access?
- Where does TLS terminate, and what are that gateway's limitations for a migration?
Find affected connections
Run the collectors you can, in any order, save each result with --out, then combine them. Each step is read-only.
# 1. The endpoint, from anywhere that can reach it (no certificate, no credentials)
mt discover endpoint api.example.com --http-probe --out endpoint.json
# 2. The gateway configuration, on the gateway host or from a sanitized copy
mt discover config /etc/nginx --out config.json
# 3. The client certificates, on the calling side
mt discover certs /etc/partner/client.p12 --target api.example.com --out certs.json
mt discover certs --os-store --target api.example.com --out keychain.json
# 4. One evidence document per endpoint, explained in plain language
mt discover report --from endpoint.json --from config.json --from certs.jsonmt discover report can also run the collectors itself: mt discover report --endpoint api.example.com --config /etc/nginx --certs client.pem. The summary has three parts: What we found (verified and inferred findings), What it means (the assessment in plain language) and What is unknown (what to collect or confirm next). --json prints the evidence documents instead.
| Assessment | Finding | Meaning |
|---|---|---|
| Affected | connection_affected | A client certificate is publicly issued with clientAuth. The connection fails at that certificate's first renewal after the cutoff unless callers move to a private issuer. |
| Likely affected | connection_likely_affected | The server trusts a public CA for client certificates. Callers that use such certificates are affected; inventory them to confirm. |
| Not affected | connection_not_affected | Every inventoried client certificate comes from a private issuer. Certificates not inventoried are not covered. |
| No client authentication | client_auth_not_in_use | The endpoint does not ask for client certificates. |
| Incomplete | assessment_incomplete | Not enough evidence yet; the unknowns say what to collect. |
Delegate discovery
The person who owns the relationship is often not the person who can reach the gateway. From the connection workspace, invite the network or security engineer to a discovery task. The invitation is scoped to that task and that workspace, the engineer authenticates without a full product account, and every submission is audited.
- The engineer installs
mt(CLI reference) on a host inside the network. - They run the collectors. Inside a private network,
--allow-privateletsmt discover endpointprobe internal addresses; without it, private, loopback and link-local addresses are refused. - They submit with the delegated task token from the invitation. Host redaction is on by default for submissions, so internal hostnames, private addresses, upstreams and file paths do not leave the network.
export MT_TASK_TOKEN=... # from the discovery task invitation
mt discover report \
--endpoint orders.internal.example:8443 --allow-private --http-probe \
--config /etc/haproxy/haproxy.cfg \
--certs /opt/partner/keystore.jks \
--submit --workspace cws_...--submit reads the workspace, then sends the documents that describe its endpoint to POST /v1/workspaces/{id}/assessments as {"evidence": [...]} (up to 20 per request) with Authorization: Bearer and an Idempotency-Key. Documents about any other endpoint are left out with a warning, because discovery is limited to the approved asset; bind certificates to the workspace endpoint with --target. The control plane scans and stores each document and returns the updated assessment. The credential is $MT_TASK_TOKEN, then $MT_API_KEY (an API credential), or the file named by --token-file; it is never accepted on the command line. A container password for certs comes from $MT_DISCOVER_PASSWORD or a hidden prompt and is never written anywhere.
Verified, inferred and unknown
Every finding carries one label and ends with its source (the probe, the file and line, or the certificate), so a reviewer can trace each claim.
| Label | Means | Examples |
|---|---|---|
verified | Observed directly by this collector. | The server sent a CertificateRequest naming these CAs; the handshake failed without a certificate; ssl_verify_client on at nginx.conf:14; the certificate's EKU is clientAuth and its chain verifies to a public root. |
inferred | Derived from indirect signals that can be wrong. | The terminator is Cloudflare (from the CF-RAY header); the issuer is public (its key identifier is in the Mozilla CCADB list, but the chain was not in the file); gateway limitations from the adapter manifest. |
unknown | Not observable by this collector. A person must confirm it. | Whether a server that accepts the handshake without a certificate refuses the request; trusted CAs referenced by a file that was not provided; the gateway version during offline inspection. |
| Question | endpoint | config | certs |
|---|---|---|---|
| Client certificates requested | Verified (CertificateRequest) | Verified (the directive) | No |
| Requests without a certificate refused | Verified when the handshake fails, or with --http-probe; otherwise unknown | Verified (required or optional) | No |
| Trusted client CAs | Verified when the server names them; many servers name none | Verified when the CA file is included | CA certificates found in stores |
| Authorization mapping | No | Verified (ACLs, subject checks, external authorizers); application checks inferred | No |
| Client certificate issuer, EKU, renewal | No | No | Verified |
| TLS termination point | Inferred from headers and certificates | Verified for the file inspected | No |
| Gateway limitations | Inferred from the detected kind | Inferred from the adapter manifest | No |
Endpoint probe
mt discover endpoint HOST[:PORT] opens TLS connections without presenting a certificate. The first handshake captures the server chain and, when the server sends a CertificateRequest, the acceptable CA names. One handshake per TLS version (1.3, 1.2, 1.1, 1.0) records the supported versions. With --http-probe, one GET / with no cookies, no authorization header and no redirects reads the Server, Via, CF-RAY, x-amzn-*, x-envoy-*, x-kong-* and similar headers and the names (never the values) of cookies. The response body is read only to match fixed error-page signatures and is not kept.
- How refusal is detected. A server that requires a certificate either ends the handshake (Envoy, HAProxy, Traefik, Go servers; with TLS 1.3 the alert arrives just after the handshake) or completes it and refuses the request at the HTTP layer. NGINX with
ssl_verify_client ondoes the second and answers 400;--http-probesees that, and without it the result isrequestedwith enforcement unknown. - Path-specific requests. A server can ask for a certificate only for some paths (TLS 1.2 renegotiation, TLS 1.3 post-handshake authentication). The probe does not exercise those, and says so.
- Limits. At most 8 connections per endpoint, at least
--intervalapart (default 250 ms), each bounded by--timeout(default 10 s). Only the addresses checked against the private-address rule are dialed, so a second DNS answer cannot redirect the probe. - Private addresses. Loopback, RFC 1918, link-local, CGNAT, unique local IPv6 and similar addresses are refused (exit code 6) unless
--allow-privateis given. A name that resolves to any such address is refused as a whole.
Certificate inventory
| Source | Support |
|---|---|
| PEM and DER files | Every certificate in the file; a private key block is noted (has a key) but never parsed. |
PKCS#12 (.p12, .pfx) | Key and certificate containers and trust stores. The empty password is tried first, then $MT_DISCOVER_PASSWORD or a hidden prompt. The format requires decrypting the key bag to reach the certificates; the key is dropped at once and never output. |
| JKS | Read natively. The store password verifies integrity; key entries stay encrypted (their certificate chains are stored in the clear). |
| JCEKS | Through the JDK keytool when installed (the password reaches it through an environment variable of the child process only); otherwise reported as unsupported with the conversion command. |
macOS Keychain (--os-store) | Identities (a certificate with a private key) in the keychain search list, through /usr/bin/security. Keys stay in the keychain; no unlock prompt. |
Windows certificate store (--os-store) | The CurrentUser and LocalMachine personal (MY) stores through CryptoAPI, read-only; key presence is checked without opening the key. |
| Linux | No system store of client identities exists; pass the files your clients load. |
For each end-entity certificate the inventory records subject, issuer, serial, validity, SHA-256 fingerprint, EKUs, key type and size, whether the container holds its key, and whether the issuer is public:
- Public, verified when the chain (from the same file, plus the bundled Sectigo intermediates) verifies to a root trusted by the operating system or to a Sectigo public root.
- Public, inferred when no chain can be built but the issuer's key identifier (or, without one, its name) matches a publicly trusted CA disclosed in the Mozilla CCADB list bundled with
mt. A private CA that copies a public CA's name does not match, because the key identifier differs. - Private otherwise: verified when the chain ends at a root in the same input, inferred when the issuer is simply not public.
Renewal is expected --renewal-lead days (default 30) before expiry. A public certificate with clientAuth whose renewal falls on or after the cutoff gets public_clientauth_renewal_at_risk; one that renews earlier gets public_clientauth_renews_before_cutoff, because the renewal after that will not carry clientAuth. Private certificates get private_clientauth_unaffected. --cutoff assesses against another date, for a public CA that publishes its own schedule.
Configuration inspection
mt discover config FILE|DIR runs the same adapter inspectors as mt adapt inspect, offline: no gateway binary is executed, nothing is written next to the files, and a directory is read recursively with includes followed once. The platform is detected from the content unless --platform names it.
| Platform | Input | Inspected by |
|---|---|---|
nginx | nginx.conf with includes, or a conf.d server block | NGINX adapter |
envoy | Bootstrap YAML or JSON (static listeners; xDS ownership reported) | Envoy adapter |
haproxy | haproxy.cfg | HAProxy adapter |
traefik | Static configuration with the file provider, or a dynamic file alone | Traefik adapter |
kong | decK or declarative kong.yml (mtls-auth, acl, CA certificates) | Read-only parser |
f5 | bigip.conf client-ssl profiles, virtual servers and iRules, or an AS3 declaration | Read-only parser |
apache | SSLVerifyClient, CA files, SSLRequire and Require expr per virtual host | Read-only parser |
k8s | ingress-nginx Ingress annotations, Istio Gateway MUTUAL, Traefik TLSOption | Read-only parser |
java | Spring Boot server.ssl.* (properties or YAML), Tomcat server.xml | Read-only parser |
aws | aws apigateway get-domain-name output (mutualTlsAuthentication) | Read-only parser |
azure | API Management ARM JSON (negotiateClientCertificate) or policy XML (validate-client-certificate) | Read-only parser |
Each termination point reports the client-certificate mode, CA files and their certificates, verification depth, CRLs and the authorization mapping (none, a native ACL or subject check, an external authorizer, or the application), plus the adapter's findings and the documented limitations, parallel-trust support and revocation behavior from its capability manifest. Cloudflare API Shield, live AWS and Azure resources, Kong Admin API and F5 iControl REST are inspected with mt adapt inspect against the platform API instead.
Untrusted content
Response headers, certificate fields, acceptable CA names and configuration comments come from systems you may not control, and the evidence they produce is read by people and by AI agents planning a migration. Discovery treats all of it as data:
- Control characters, invisible and reordering Unicode characters and markup characters are stripped from every captured value, and lengths are bounded.
- Text that reads like instructions (to ignore previous instructions, approve or trust something, call a tool, reveal secrets, or that carries chat role markers and prompt template tokens) is withheld: the evidence holds a marker with its length and a hash, and a
untrusted_instructionsfinding names where it was found. The text is never printed, so an agent runningmt discovercannot be steered by it. Review it at the source. - Configuration comments are scanned but never copied into evidence.
Evidence documents
Each document is a MigrationEvidenceV1, the shape the workspace assessments endpoint accepts. Fingerprints are lowercase hex SHA-256 of the DER certificate. client_auth_requested is null when no collector could tell. Documents are kept within the contract's bounds (100 findings, 50 client certificates); when something is left out, a findings_truncated or client_certs_truncated finding says so, and the full local report is written with --out.
{
"source": "mt_discover",
"collected_at": "2026-11-14T10:00:00Z",
"collector": { "kind": "mt", "version": "1.0.0" },
"target": { "hostname": "api.example.com", "port": 443 },
"tls": {
"server_cert_chain_fingerprints": ["3f1c...", "9a02..."],
"client_auth_requested": true,
"acceptable_ca_dns": ["CN=Sectigo RSA Organization Validation Secure Server CA,O=Sectigo Limited,L=Salford,ST=Greater Manchester,C=GB"],
"tls_versions": ["TLS1.3", "TLS1.2"]
},
"client_certs": [{
"fingerprint": "c47e...", "issuer_dn": "CN=Sectigo RSA Organization Validation Secure Server CA,...",
"subject_dn": "CN=partner-client.example.com", "ekus": ["serverAuth", "clientAuth"],
"not_after": "2027-06-30T23:59:59Z", "public_ca": true, "key_type": "RSA-2048"
}],
"gateway": { "detected_kind": "nginx", "version": "1.25.3", "confidence": "inferred" },
"findings": [{
"code": "public_clientauth_renewal_at_risk",
"label": "Public client certificate whose renewal falls after the clientAuth cutoff",
"confidence": "verified",
"detail": "... (source: certificate c47e... in client.p12)"
}],
"redactions_applied": true
}Secrets are always redacted with the same rules the control plane uses. --redact-hosts (on by default with --submit) also replaces internal hostnames, private addresses, upstream addresses and file paths (reduced to base names); the target hostname stays, because the workspace is about it. redactions_applied is true when anything was masked or host redaction was on.