STARTTLS Services¶
Not every TLS service starts encrypted. Mail servers, directory servers, and databases often greet you in plaintext and switch to TLS only after a short application-protocol exchange. Connecting to such a port and immediately sending a TLS handshake fails.
CertMonitor handles this on its own. When a port does not answer a TLS handshake, it works out which service is listening and runs that service's STARTTLS preamble first. After the preamble, every validator works exactly as it does for an HTTPS endpoint. You don't need to know the protocol up front, and you don't need the service to sit on its usual port.
Try it¶
from certmonitor import CertMonitor
with CertMonitor("mail.example.com", 587) as monitor:
print(monitor.validate()["expiration"])
print(monitor.starttls) # "smtp", discovered
That's it. Nothing about port 587 told CertMonitor this was SMTP; the server's greeting did. The same call against a PostgreSQL database or an LDAP directory works just as well, and monitor.starttls tells you what was found.
From the shell:
certmonitor check mail.example.com:587
certmonitor check db.internal:5432 --cafile /etc/pki/private-ca.pem
How discovery works¶
Discovery never looks at the port number, so a mail server on 2525 or a directory server on 10389 is found just like one on its usual port. It reads the service instead:
- A service that speaks first is named from its greeting.
* OKis IMAP,+OKis POP3, and anSSH-banner is SSH. SMTP and FTP both greet with220; the greeting text usually says which, and when it does not, CertMonitor sendsEHLOand treats a250reply as SMTP. - A silent service is asked. CertMonitor sends the PostgreSQL
SSLRequestand treats any of its one-byte answers as PostgreSQL. If that gets nothing, a fresh connection sends the LDAP StartTLS request and treats any LDAP reply as LDAP. - Anything else keeps the original TLS error. A port that is neither TLS nor a known STARTTLS service reports the handshake failure it would have reported before.
Discovery costs nothing on ports that speak TLS directly: it only starts after the TLS handshake fails, and the whole exchange is bounded by timeout. On a STARTTLS port it adds one connection for the greeting, plus one more if the service is silent and turns out not to be PostgreSQL. The result is stored on the monitor as starttls and reused by every later connection, including the verified trust handshake, so nothing is discovered twice.
SSH is found the same way
An SSH server also greets in plaintext. If its banner arrives late enough that detection assumed TLS, the failed handshake triggers the same discovery, which reads the banner and switches to the SSH handler. See Protocol Detection for the full decision flow.
Choosing the protocol yourself¶
Pass starttls to skip detection and discovery entirely. CertMonitor then runs that preamble before every handshake and never peeks or probes. Reach for it when:
- You already know the service. A fleet of mail relays doesn't need to rediscover SMTP on every run.
- The server greets slowly. Some SMTP servers pause before their greeting to slow down spammers, and discovery waits for it. Pinning the protocol skips the failed handshake and the wait.
- You want a refusal, not a guess. With the protocol pinned, a server that won't upgrade to TLS is reported as exactly that, with its own reply in the message.
with CertMonitor("mail.example.com", 587, starttls="smtp") as monitor:
print(monitor.validate()["expiration"])
Supported protocols¶
starttls |
Service | Typical port | What CertMonitor sends |
|---|---|---|---|
smtp |
Mail submission and relay | 587, 25 | EHLO, then STARTTLS (RFC 3207) |
imap |
Mail access | 143 | STARTTLS (RFC 2595) |
pop3 |
Mail access | 110 | STLS (RFC 2595) |
ftp |
File transfer | 21 | AUTH TLS (RFC 4217) |
postgres |
PostgreSQL | 5432 | The 8-byte SSLRequest message |
ldap |
Directory | 389 | An ExtendedRequest for the StartTLS OID 1.3.6.1.4.1.1466.20037 (RFC 4511) |
The Typical port column is only a hint for you. CertMonitor never uses it: discovery reads the service, so a relay on 2525 or a directory on 10389 is handled the same way.
Ports that are TLS from the first byte (465 for SMTPS, 993 for IMAPS, 995 for POP3S, 636 for LDAPS) need no starttls at all. The handshake succeeds on the first try, so discovery never runs.
What changes with STARTTLS¶
- An explicit
starttlsskips detection and discovery. The first bytes on a STARTTLS port are the service banner, not a TLS record, so CertMonitor trusts your choice instead of peeking or probing. - Every connection runs the preamble. The permissive collection connection, the separate verified trust handshake, and the post-quantum probe all negotiate STARTTLS, so
root_certificateandpq_key_exchangework the same way they do for HTTPS. - The post-quantum probe runs it too. The native probe performs the same preamble before its ClientHello, so
pq_key_exchangereports the negotiated group for STARTTLS endpoints just as it does for HTTPS. - A refusal is an error, not a guess. If the server does not offer STARTTLS, or declines it, the collection fails with the server's own reply in the message (for example
SMTP server does not advertise STARTTLSorPostgreSQL server declined SSL).
Fleets¶
Fleets need nothing special: each endpoint discovers its own service, so a mixed list of web, mail, and database hosts just works. scan_hosts() also accepts starttls for the whole scan or per endpoint when you want to pin it:
from certmonitor import scan_hosts
targets = [
{"host": "mail.example.com", "port": 587, "starttls": "smtp"}, # pinned
{"host": "db.internal", "port": 5432, "cafile": "/etc/pki/private-ca.pem"}, # discovered
"www.example.com", # plain TLS, discovery never runs
]
for scan in scan_hosts(targets):
print(scan["host"], scan["results"]["expiration"]["status"])
Reference¶
The preambles, discover(), and StartTLSError are documented with the other connection code in Protocol Handlers.