A tool for auditing TLS certificates on DNS-over-TLS servers.
Analyzes TLS certificates on DoT servers (port 853). Resolves NS records for each domain, uses them as SNI during TLS handshake, and extracts certificate information.
The work is split across two programs. dot_auditor.py collects the data and writes a JSON audit; dot_report.py turns that JSON into a human-readable report. Collection and presentation are decoupled, so a single audit can be re-rendered in any format, kept as a machine-readable record, or diffed over time without re-probing the servers. The renderer depends only on the Python standard library.
- Automatic SNI selection from NS records
- Certificate analysis (CN, SAN, validity, chain trust, issuer)
- IP address validation (checks if connected IP is listed in certificate SAN IPs)
- JSON audit as the stored format, with a separate renderer for verbose, Markdown, and HTML reports
- Self-signed and expired certificate detection, flagged with clear status labels
- Interactive HTML reports with DataTables (sorting, filtering, search)
- Per-column filtering in HTML output
- Concurrent processing with configurable workers
- Detailed certificate validation and chain trust verification
Requires Python 3.10 or later.
pip install dnspython cryptographyCollect an audit into JSON, then render it:
python3 dot_auditor.py input.csv -o audit.json
python3 dot_report.py audit.json --format html -o report.htmlThe two programs also pipe together when you do not need to keep the JSON:
python3 dot_auditor.py input.csv | python3 dot_report.py --format markdownThe CSV file should contain at least two columns: IP address and domain name.
Example input.csv:
45.55.10.200,powerdns.com
206.189.140.177,technitium.com
2604:a880:1:20::132:5001,powerdns.com| Option | Description |
|---|---|
csv_file |
CSV file with IP and domain columns (required) |
--has-header |
Skip the first CSV row as header |
--delimiter |
CSV delimiter (default: ,) |
--ip-col |
Zero-based index of the IP column (default: 0) |
--domain-col |
Zero-based index of the domain column (default: 1) |
--port |
Port to check (default: 853) |
--timeout |
Timeout for DNS and TLS operations in seconds (default: 5.0) |
--workers |
Number of concurrent checks (default: 64) |
-o, --output |
Path to write the JSON audit (default: stdout) |
| Option | Description |
|---|---|
input |
Path to the collector's JSON, or - for stdin (default: stdin) |
--format |
Report format: verbose, markdown, or html (default: html) |
-o, --output |
Output file path (default: stdout) |
The collector emits a self-describing envelope: provenance about the run followed by one record per audited server under results.
{
"schema_version": 2,
"generated_at": "2026-07-10T12:00:00+00:00",
"tool": "dot_auditor",
"tool_version": "1.0.0",
"source": "input",
"params": { "port": 853, "timeout": 5.0 },
"results": [
{
"ip": "45.55.10.200",
"domain": "powerdns.com",
"port": 853,
"matching_ns": ["pdns-public-ns2.powerdns.com"],
"sni_used": "pdns-public-ns2.powerdns.com",
"tls_ok": true,
"error_tls": null,
"leaf_cert_received": true,
"connected_ip": "45.55.10.200",
"not_before": "2025-01-15T12:00:00+00:00",
"not_after": "2026-01-15T12:00:00+00:00",
"is_expired": false,
"is_self_signed": false,
"issued_by_trusted_ca": true,
"trust_error": null,
"issuer_cn": "Let's Encrypt (R12)",
"cn_list": ["*.powerdns.com"],
"san_dns": ["*.powerdns.com", "powerdns.com"],
"san_ips": [],
"connected_ip_in_cert": false
}
]
}issued_by_trusted_ca is tri-state: true (chain validates against the system CA store), false (a certificate was received but rejected), or null (the trust check could not complete, e.g. a timeout). When it is false or null, trust_error carries the reason. connected_ip_in_cert is computed only from the certificate's iPAddress SAN entries, never the CommonName.
Pass one of these to dot_report.py --format.
Detailed human-readable output with all certificate information:
python3 dot_report.py audit.json --format verbose=== 45.55.10.200 (powerdns.com) :853 ===
Matching NS hostname(s): pdns-public-ns2.powerdns.com
SNI used: pdns-public-ns2.powerdns.com
TLS: OK
Leaf certificate received: yes
CN(s):
- *.powerdns.com
SAN DNS:
- *.powerdns.com
- powerdns.com
Validity: 2025-01-15T12:00:00+00:00 -> 2026-01-15T12:00:00+00:00 (expired: False)
Issued by: Let's Encrypt (R12)
Self-signed: False
Chains to system CA: True
Connected IP listed in cert IP SANs: False
Formatted as a table for documentation and reports:
python3 dot_report.py audit.json --format markdown| IP | Domain | SNI Used | Matching NS | TLS | Chain Trusted | IP in Cert | Expired | Self-Signed | Issued By | CN(s) | SAN DNS | SAN IPs |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
45.55.10.200 |
powerdns.com |
pdns-public-ns2.powerdns.com |
pdns-public-ns2.powerdns.com |
OK | TRUSTED | NO | VALID | CA-ISSUED | Let's Encrypt (R12) |
*.powerdns.com |
*.powerdns.com, powerdns.com |
- |
Interactive HTML reports with DataTables integration for advanced filtering and sorting:
python3 dot_report.py audit.json --format html -o report.htmlFeatures:
- Sortable columns: Click any column header to sort
- Global search: Filter across all columns at once
- Per-column filtering: Individual search boxes for each column
- Pagination: Shows all rows by default, with a menu to page at 50, 100, or 200
- Status pills: Certificate status shown as color-tinted text labels (
OK,TRUSTED,UNVERIFIED,EXPIRED,SELF-SIGNED, and so on) that stay legible without color and are filterable as plain text - Collapsible lists: Long SAN lists show the first few names with a toggle to reveal the rest
- Column tooltips: Hover over column headers for descriptions
- Provenance: source file and collection time shown in the report header and footer
- Responsive design: Works on desktop and mobile browsers
The HTML output uses monospace fonts for technical data (IPs, domains, hostnames) and includes all certificate details in a clean, professional format.
Interactive features powered by DataTables - a powerful jQuery plugin for enhanced HTML tables.
Sample Reports: View live examples of HTML and Markdown output:
- Query NS records for the domain
- Resolve NS hostnames to find which matches the target IP
- Use matching NS hostname as SNI during TLS handshake
- Retrieve certificate and validate against system CA store
- Extract certificate details (CN, SAN, validity, chain trust)
- Audit DoT server certificate configurations
- Monitor certificate expiration
- Verify certificate chain trust
- Check for self-signed certificates
python3 dot_auditor.py public-dns-servers.csv -o audit.json
python3 dot_report.py audit.json --format markdown -o audit-report.mdpython3 dot_auditor.py servers.csv --port=8853 --timeout=10.0 --workers=32 -o audit.jsonpython3 dot_auditor.py servers.csv -o results.jsonThe results.json file is the audit of record; render it whenever needed without re-probing the servers.
See CONTRIBUTING.md for development setup, testing, and contribution guidelines.
This project relies on the following excellent open-source libraries:
- dnspython - DNS toolkit for Python
- cryptography - Cryptographic recipes and primitives
- DataTables - jQuery plugin for interactive HTML tables (used in HTML output)
This project is licensed under the BSD 2-Clause License - see the LICENSE file for details.