A lightweight self-hosted SSL/TLS integrity checker. Runs a Go server that handles its own TLS and serves a dashboard that performs security and network checks to identify TLS interception.
waxseal checks for nefariousness (like MITM interception) or misconfiguration across multiple protocol layers. It also provides some network performance testing tools.
- Certificate Identity (Ed25519 Signature) -- Verifies the TLS certificate fingerprint using a DNS-anchored Ed25519 signature. The server signs the cert fingerprint + a one-time nonce with its identity key. The client fetches the corresponding public key from a DNS TXT record via DoH and verifies the signature using WebCrypto.
- TLS Fingerprint Analysis -- Captures and analyzes the raw TLS ClientHello before Go's TLS library processes it. Checks for GREASE values (RFC 8701), proxy cipher suites, missing TLS 1.3 extensions, and known proxy fingerprints.
- HTTP Header Inspection -- Detects proxy injection headers (
Via,X-Forwarded-For,Proxy-Connection, etc.) and checks for missing browser security headers. - DNS Consistency -- Cross-checks the server's reported IP against an independent DoH lookup of the hostname.
- HTTP/2 Multiplexing Probe -- Fires 5 parallel ping requests and measures the response time spread. Genuine HTTP/2 servers handle these in parallel; transparent proxies serialize them.
- Latency / RTT -- 20 sequential pings measuring min/avg/max/jitter.
- Download Speed -- Streams random data from the server, measuring throughput.
- Upload Speed -- POSTs data to the server, measuring throughput.
git clone https://github.com/sebseager/waxseal.git
cd waxseal
go build -o waxseal .If you already have TLS certificates:
TLS_CERT=cert.pem TLS_KEY=key.pem ./waxsealThen visit https://localhost:8443.
If your server has port 443 reachable and you have a domain pointed at it:
DOMAIN=waxseal.yourdomain.com ./waxseal --listen :443DOMAIN=waxseal.yourdomain.com docker compose up -dThis uses Let's Encrypt autocert. The identity.pem file should be in the project root (see below). Adjust the volume mount in docker-compose.yml if your key is elsewhere.
The identity key enables automated cryptographic verification of the server's TLS certificate. Without it, users must manually compare certificate fingerprints.
Generate with the built-in keygen:
./waxseal keygen --domain yourdomain.com --out identity.pemThis outputs a private key and the DNS TXT record you need to add:
_waxseal.yourdomain.com TXT "v=waxseal1 pk=BASE64_PUBKEY"
Or generate with OpenSSL:
openssl genpkey -algorithm Ed25519 -out identity.pem
openssl pkey -in identity.pem -pubout -outform DER | tail -c 32 | base64Use the base64 output in the DNS TXT record above.
Then set the environment variable when running the server:
IDENTITY_KEY=identity.pem ./waxsealAll options can be set via environment variables or CLI flags (--domain, --listen, etc.).
| Variable | Default | Description |
|---|---|---|
DOMAIN |
(required for autocert) | Let's Encrypt domain |
LISTEN_ADDR |
:8443 |
TCP listen address |
TLS_CERT / TLS_KEY |
(empty) | BYO cert paths; skips autocert if set |
IDENTITY_KEY |
(empty) | Path to Ed25519 private key PEM |
LOG_LEVEL |
info |
debug / info / warn / error |
DOH_RESOLVER |
https://cloudflare-dns.com/dns-query |
DoH endpoint |
EXTERNAL_IP |
(auto-detected) | Server public IP override |
waxseal must handle its own TLS. Any reverse proxy in front must forward raw TCP without terminating TLS. If a proxy terminates TLS, it is the MITM, and waxseal will correctly detect it.
DOMAIN=waxseal.yourdomain.com docker compose up -dThe autocert cache is stored in a Docker volume so certificates persist across restarts. To use BYO certificates instead, uncomment the TLS_CERT and TLS_KEY variables in docker-compose.yml and mount your cert files.
Configure NPM to forward raw TCP (not HTTP proxy):
- Go to the Streams tab (not Proxy Hosts)
- Create a new stream:
- Incoming Port: 443 (or your chosen port)
- Forward Host: waxseal container IP or hostname
- Forward Port: 8443
- TCP Forwarding: enabled (no SSL termination)
Use a TCP router with TLS passthrough:
# traefik dynamic config
tcp:
routers:
waxseal:
rule: "HostSNI(`waxseal.yourdomain.com`)"
service: waxseal
tls:
passthrough: true
services:
waxseal:
loadBalancer:
servers:
- address: "waxseal:8443"Caddy requires the caddy-l4 module for TCP-level routing:
{
layer4 {
:443 {
@waxseal tls sni waxseal.yourdomain.com
route @waxseal {
proxy waxseal:8443
}
}
}
}
See LICENSE.
