tlswatch
Self-hosted · MIT · one binary

Know before a certificate expires. Every port, every address.

tlswatch probes the certificates your servers actually serve, not the ones you think you installed. Expiry, chain of trust, hostname, legacy TLS and silent changes, with an alert before anything goes dark.

Go binary + SQLite, nothing else to run Configured in the browser Light and dark UI

What is served, not what is installed

Checks happen over the network, the way a client sees it. Every address behind a name is probed, so the one stale node behind a load balancer is caught.

Any port, not just 443

Implicit TLS or STARTTLS on SMTP, IMAP, POP3, FTP and PostgreSQL. Mail relays, SIP trunks and PBXes get the same attention as the website.

Alerts on transitions, not on every run

A finding opens once and alerts once, with reminders, a recovery notice when it clears and acknowledgements when you already know.

What it checks

More than an expiry date

A certificate can be valid and still wrong. tlswatch looks at the whole picture on every check.

Expiry with thresholds

Warning and critical days, with separate defaults for manual and ACME-managed certificates.

Chain of trust

Verified for server authentication like a browser does, against the system store, your own CA bundle, or a pinned self-signed certificate.

Hostname and signature

Does the certificate match the name you are connecting to, and is the signature algorithm still acceptable?

Legacy TLS versions

A daily handshake pinned to TLS 1.0 and 1.1. A server on TLS 1.3 can still accept them unnoticed; now it is a finding.

Change detection

A new certificate or issuer opens a finding. Let's Encrypt and other ACME renewals are recognised and stay quiet.

Inconsistent addresses

When the addresses behind one name serve different certificates, you hear about it before a customer does.

Certificate inventory

Every certificate seen, where it is served, when it expires. Exportable as CSV.

Live pages

Overview, targets and findings update as checks land. No refresh, no WebSocket, nothing to configure.

Roles and audit log

Viewer, operator and admin. Local accounts and Google sign-in. Every change is in the audit log.

Alerting

The alert goes where your team already looks

Each channel has its own severity floor, so Telegram sees everything and the on-call pager only the critical ones. Mail carries a plain-text and an HTML part. Rate limits and retries are handled for you, and an optional weekly digest sums up what is due.

E-mailTelegramSlackDiscordntfyWebhookMattermostRocket.Chat
How it works

Three steps, no YAML

Add a target

A hostname or IP, a port, a protocol. Choose the trust store, set thresholds or keep the defaults, tag it. Test without saving to see the result first.

It keeps probing

Every address behind the name, on a schedule you set, with the legacy TLS test once a day. Results land on live pages and in the inventory.

You get told once

A finding opens when something changes and alerts your channels. Acknowledge it, get a reminder, and a recovery notice when it is fixed.

Install

Running in a minute

One binary, one SQLite file. On first start you create your admin account in the browser, or restore a backup from another installation. Everything else lives under Settings.

Docker compose

$ git clone https://github.com/sjoerd305/tlswatch
$ cd tlswatch
$ docker compose up --build -d
$ open http://127.0.0.1:8081/

Put a reverse proxy with TLS in front before exposing it, then set the public URL under Settings › General.

Binary go 1.27

$ make build          # or download a release
$ ./tlswatch serve    # creates ./data/ and a secret key
$ open http://127.0.0.1:8080/

Listen address, database path and secret key file are environment variables; everything else is in the UI.

Command line

Try one endpoint first, no server needed

The same checks as the server, printed as JSON, with an exit code for scripts: 0 ok, 1 warning, 2 critical. A server that still accepts TLS 1.0 exits 1.

$ tlswatch check mail.example.nl:587 -protocol smtp
{
  "target": "mail.example.nl:587",
  "protocol": "smtp",
  "status": "warning",
  "days_left": 22,
  "not_after": "2026-11-06T09:12:00Z",
  "issuer": "DigiCert Global G2 TLS RSA SHA256 2020 CA1",
  "addresses": [
    {
      "address": "203.0.113.21",
      "ok": true,
      "tls_version": "TLS 1.3",
      "verified": true,
      "legacy_accepted": []
    },
    { "address": "2001:db8:c0::21", … }
  ],
  "findings": [
    {
      "kind": "expiring",
      "severity": "warning",
      "details": "expires in 22 days"
    }
  ]
}
$ echo $?
1
Built to run unattended

Boring in the right places

1 binary
Go, statically built, SQLite inside
0 runtime deps
no database server, no queue, no Node
/metrics
optional Prometheus endpoint
1 file
encrypted backup and restore from the UI

Watch the certificates you actually serve.

MIT licensed. Source, releases and documentation on GitHub.