A common assumption among developers and sysadmins is that a new server is safe until someone finds it — and that finding it requires guessing its name or IP address. Neither is true once you request a TLS certificate.
Every certificate issued by a publicly-trusted CA (Let's Encrypt, DigiCert, etc.) must be submitted to one or more Certificate Transparency logs — a set of public, append-only, globally-mirrored ledgers. Anyone can watch the live feed. Automated scanners do exactly this, 24 hours a day, and will probe a newly-appearing hostname within minutes of certificate issuance — long before any real user has visited the site.
This means:
- A staging server, internal tool, or test environment that gets a TLS cert is immediately and publicly announced to every scanner on the internet.
- Attackers do not need to guess your hostname or brute-force your IP range. The CT feed hands it to them directly.
- "Security through obscurity" — keeping a hostname secret — is not possible once a certificate has been issued for it.
This demo makes that visible in real time.
- Zeek captures all incoming traffic on the server's public interface
- The monitor displays a live feed of probes hitting ports 22, 80, and 443, with separate IPv4 and IPv6 rates shown side by side
- Before a certificate is requested, the IPv6 rate should be near zero — mass internet scanners are overwhelmingly IPv4, and the host's IPv6 address has never appeared anywhere public
- Press
Rto request a Let's Encrypt certificate for your domain - The hostname is published to the CT logs within seconds of issuance
- Watch both rates climb as automated scanners discover and target the host — typically within minutes, without any other announcement
The split IPv4/IPv6 display makes the CT-driven discovery clearly visible: IPv6 climbs from a clean zero baseline while IPv4 climbs from a low background noise floor. Both protocols get targeted because CT log scanners resolve the domain and probe whatever addresses they find — A and AAAA alike.
- Linux host with a public IP address (IPv4 and/or IPv6)
- DNS
Arecord (IPv4) and/orAAAArecord (IPv6) pointing at your host — dual-stack gives the best demo: maximum scanner coverage with a clean IPv6 baseline to show CT-driven discovery against zeek,certbot,nginxinstalled (handled byinstall.sh)- Python 3.11+ with
rich(pip install rich, handled byinstall.sh) asciinema(optional, for recording — also installed byinstall.sh)
git clone <this repo>
cd certificate_transparency_discovery
sudo bash install.shThe installer will prompt for your domain, email address, and network
interface, then install all dependencies, configure nginx, and write
demo.conf.
- Docker and Docker Compose installed
- Linux host with a public IP address — see the note below
Mac and Windows are not supported. Docker Desktop on Mac and Windows runs containers inside a Linux VM with NAT.
--network=hostexposes the VM's network, not the Mac/Windows host's network, so nginx cannot bind to your public IP and Zeek cannot see the host's traffic. Use a Linux VM or cloud instance.
The container runs with --network=host, so it shares the host's network
stack entirely — no port mapping required. nginx binds directly to ports 80
and 443 on the host. Zeek captures packets at the NIC level using raw
AF_PACKET sockets; it observes traffic to all ports (including 22) without
binding any of them. Port 22 remains in use by the host's SSH daemon and
Zeek simply watches it passively.
cp .env.example .env
# Edit .env — set DOMAIN, EMAIL, and IFACE
# IFACE must match the host's public interface name, e.g. eth0, ens5, enp0s3
# Find yours: ip -4 addr show scope global
./run.shrun.sh fetches your public IP(s), prints the DNS A/AAAA records you need to
create and the firewall ports to open, then waits for you to press Enter before
building and starting the container. Zeek logs are written to ./logs/ on
the host. Press R inside the TUI to request a Let's Encrypt certificate;
nginx reloads automatically and begins serving HTTPS on port 443.
asciinema rec ct-demo.cast --command "./run.sh"cp demo.conf.example demo.confEdit demo.conf:
[demo]
domain = www.your-domain.example
email = you@example.com
log_dir = .Create DNS records pointing at your host's public address(es). Dual-stack is recommended — both records together give maximum scanner coverage while keeping a clean IPv6 baseline to contrast against:
# IPv4
dig A www.your-domain.example
# IPv6
dig AAAA www.your-domain.exampleIf you only have one address type, a single record is fine — the demo still works, you just won't see the IPv4/IPv6 split in the rate display.
python3 -m venv venv && source venv/bin/activate
pip install rich
sudo apt install zeek nginx certbot asciinemasudo sed "s/DOMAIN/www.your-domain.example/g" nginx.conf \
> /etc/nginx/sites-available/ct-demo
sudo ln -s /etc/nginx/sites-available/ct-demo /etc/nginx/sites-enabled/ct-demo
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl start nginxRun Zeek on your public-facing interface from the demo directory so it writes logs there:
cd /path/to/demo
sudo zeek -i <interface> -C-C disables checksum validation, which is needed on most cloud instances
where the NIC offloads checksum computation.
venv/bin/python3 monitor.pyOr with explicit overrides:
venv/bin/python3 monitor.py --domain www.your-domain.example \
--email you@example.com \
--log-dir /path/to/zeek/logs| Key | Action |
|---|---|
R |
Request a Let's Encrypt certificate via certbot (runs in the background) |
Q |
Quit |
┌─ CT Discovery Demo ──────────────────────────── Q to quit ─┐
│ Domain: www.your-domain.example | Watching: :22(SSH) :80(HTTP) :443(HTTPS) │
│ Rate 1s: 0.00/s 10s: 0.00/s 60s: 0.00/s Total: 0 │
│ [R] request certificate │
└───────────────────────────────────────────────────────────────────────┘
Time Source Port State Info
──────────────────────────────────────────────────────────────────────────────
14:32:01 2a01:4f8::1 HTTP SYN, no reply
14:32:04 2600:1f18::dead:beef SSH RST
After pressing R, the header updates to show a live timer and cert status:
│ Cert requested 00:01:23 ago | obtained — nginx reloaded │
Once the certificate is obtained, nginx is automatically reloaded and begins serving HTTPS on port 443.
Rate colours: white = quiet, yellow = light scanning, red = active targeting.
HTTP scanner requests (method, path, User-Agent) are pulled from Zeek's
http.log and shown in the Info column. You will quickly see scanners
probing for /.env, /wp-admin, /phpmyadmin, /actuator, etc.
If the certificate request fails, the full certbot output is written to
certbot.log in the log directory. Check it with:
cat certbot.logCommon causes of failure:
- Port 80 not reachable from the internet (firewall / security group rule)
- DNS not yet propagated when
Rwas pressed - Domain does not resolve to this host's IP
certbot uses the webroot method: it places a challenge file under
/var/www/html/.well-known/acme-challenge/ and Let's Encrypt fetches it
over HTTP through nginx. This means:
- nginx must be running and serving port 80 before pressing
R - Nothing else needs to stop or restart
- Port 80 stays up throughout the challenge
After the certificate is issued, nginx is reloaded automatically and begins serving HTTPS.
Install asciinema:
sudo apt install asciinemaWrap the demo directly — recording starts and stops with the monitor:
asciinema rec ct-demo.cast --command "sudo bash run.sh"Or start a recorded shell session and run commands manually:
asciinema rec ct-demo.cast
sudo bash run.sh
# press R when ready, then Q to exit
exitPlay back locally:
asciinema play ct-demo.cast
asciinema play --speed 2 ct-demo.cast # 2× faster
asciinema play --speed 0.5 ct-demo.cast # half speedShare online:
asciinema upload ct-demo.castOr self-host: the .cast file is plain JSON, playable with the
asciinema-player embedded
in any webpage.