Deprecated TLS 1.0 or 1.1 Accepted
Checks whether local TLS services still accept TLS 1.0 or TLS 1.1.
- Why It Matters
- TLS 1.0 and 1.1 were formally deprecated by RFC 8996 in 2021. They rely on outdated constructions (e.g. SHA-1 in the handshake) and fail PCI DSS and most compliance baselines.
- How It Detects
- For each local TLS port and site name, attempts a handshake with 127.0.0.1 limited to TLS 1.0 only, then TLS 1.1 only. A completed handshake fails the check.
- How to Fix
- Apache: SSLProtocol -all +TLSv1.2 +TLSv1.3 . nginx: ssl_protocols TLSv1.2 TLSv1.3; . Postfix/Dovecot: set the minimum protocol to TLSv1.2. Reload the service afterwards.
- How to Verify
openssl s_client -connect 127.0.0.1:443 -tls1_1 </dev/null should fail to connect.- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N- Control Category
- Cryptography and TLS: CIS Controls v8 3; NIST CSF 2.0 PR.DS
- Fix Effort
- Low
- CIS Controls v8 Safeguard
- 3.10
- NIST CSF 2.0 Subcategory
- PR.DS-02
- OWASP WSTG v4.2
- WSTG-CRYP-01
- CWE
- CWE-326, CWE-327
- OWASP Top 10
- A02:2021
- OWASP ASVS 4.0.3
- 9.1.3
- NIST SP 800-53 Rev. 5
- SC-8, SC-13
- Benchmark Topic
- TLS protocol versions (topic only, no benchmark text)
- References
- RFC 8996, Deprecating TLS 1.0 and TLS 1.1
NIST SP 800-52 Rev. 2, Guidelines for TLS Implementations
Mozilla Server Side TLS guidelines
Source Code
The complete file that defines this check. Run it with openvapt scan --only NET-TLS-001, or read it offline with openvapt explain NET-TLS-001.
internal/checks/network/tls.go · Lines 1-203View on GitHub at This Commit
| 1 | package network |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/tls" |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/openvapt/openvapt/internal/check" |
| 11 | "github.com/openvapt/openvapt/internal/checks/collect" |
| 12 | "github.com/openvapt/openvapt/internal/sys" |
| 13 | ) |
| 14 | |
| 15 | var refTLS = []check.Ref{ |
| 16 | {Title: "RFC 8996, Deprecating TLS 1.0 and TLS 1.1", URL: "https://www.rfc-editor.org/rfc/rfc8996.html"}, |
| 17 | {Title: "NIST SP 800-52 Rev. 2, Guidelines for TLS Implementations", URL: "https://csrc.nist.gov/pubs/sp/800/52/r2/final"}, |
| 18 | {Title: "Mozilla Server Side TLS guidelines", URL: "https://wiki.mozilla.org/Security/Server_Side_TLS"}, |
| 19 | } |
| 20 | |
| 21 | // tlsTargets returns (port, SNI name) pairs: port 443 for every local vhost, |
| 22 | // plus other common TLS ports that are listening, using the first vhost name. |
| 23 | func tlsTargets(s sys.System) [][2]string { |
| 24 | ls, _ := collect.Listeners(s) |
| 25 | vh := collect.VHosts(s) |
| 26 | var out [][2]string |
| 27 | if collect.ListensOn(ls, 443) { |
| 28 | for _, v := range vh { |
| 29 | out = append(out, [2]string{"443", v}) |
| 30 | } |
| 31 | if len(vh) == 0 { |
| 32 | out = append(out, [2]string{"443", "localhost"}) |
| 33 | } |
| 34 | } |
| 35 | name := "localhost" |
| 36 | if len(vh) > 0 { |
| 37 | name = vh[0] |
| 38 | } |
| 39 | for _, p := range []int{465, 993, 995, 8443, 10000} { |
| 40 | if collect.ListensOn(ls, p) { |
| 41 | out = append(out, [2]string{fmt.Sprint(p), name}) |
| 42 | } |
| 43 | } |
| 44 | return out |
| 45 | } |
| 46 | |
| 47 | func init() { |
| 48 | check.Register(&check.Check{ |
| 49 | ID: "NET-TLS-001", |
| 50 | Title: "Deprecated TLS 1.0 or 1.1 Accepted", |
| 51 | Domain: check.DomainNetwork, |
| 52 | Severity: check.Medium, |
| 53 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", |
| 54 | Summary: "Checks whether local TLS services still accept TLS 1.0 or TLS 1.1.", |
| 55 | Rationale: "TLS 1.0 and 1.1 were formally deprecated by RFC 8996 in 2021. They rely on outdated constructions (e.g. SHA-1 in the handshake) and fail PCI DSS and most compliance baselines.", |
| 56 | Detection: "For each local TLS port and site name, attempts a handshake with 127.0.0.1 limited to TLS 1.0 only, then TLS 1.1 only. A completed handshake fails the check.", |
| 57 | Fix: "Apache: SSLProtocol -all +TLSv1.2 +TLSv1.3 . nginx: ssl_protocols TLSv1.2 TLSv1.3; . Postfix/Dovecot: set the minimum protocol to TLSv1.2. Reload the service afterwards.", |
| 58 | Verify: "openssl s_client -connect 127.0.0.1:443 -tls1_1 </dev/null should fail to connect.", |
| 59 | Refs: refTLS, |
| 60 | Standards: check.Standards{CWE: []string{"CWE-326", "CWE-327"}, OWASP: []string{"A02:2021"}, WSTG: []string{"WSTG-CRYP-01"}, ASVS: []string{"9.1.3"}, NIST: []string{"SC-8", "SC-13"}, CISTopic: "TLS protocol versions"}, |
| 61 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 62 | targets := tlsTargets(s) |
| 63 | if len(targets) == 0 { |
| 64 | return []check.Result{check.SkipR("tls", "No local TLS services found")} |
| 65 | } |
| 66 | var out []check.Result |
| 67 | for _, t := range targets { |
| 68 | target := t[1] + " on port " + t[0] |
| 69 | if _, err := s.TLS(ctx, t[0], t[1], tls.VersionTLS12, tls.VersionTLS13); err != nil { |
| 70 | out = append(out, check.IncompleteR(target, "Modern TLS handshake failed, so legacy protocol rejection could not be verified", err.Error())) |
| 71 | continue |
| 72 | } |
| 73 | var weak []string |
| 74 | uncertain := false |
| 75 | for _, v := range []struct { |
| 76 | n string |
| 77 | v uint16 |
| 78 | }{{"TLS 1.0", tls.VersionTLS10}, {"TLS 1.1", tls.VersionTLS11}} { |
| 79 | if _, err := s.TLS(ctx, t[0], t[1], v.v, v.v); err == nil { |
| 80 | weak = append(weak, v.n+" accepted") |
| 81 | } else if ctx.Err() != nil || strings.Contains(strings.ToLower(err.Error()), "timeout") || strings.Contains(strings.ToLower(err.Error()), "connection reset") { |
| 82 | uncertain = true |
| 83 | } |
| 84 | } |
| 85 | if len(weak) > 0 { |
| 86 | out = append(out, check.FailR(target, "Deprecated TLS versions accepted", weak...)) |
| 87 | } else if uncertain { |
| 88 | out = append(out, check.IncompleteR(target, "A legacy TLS probe ended without a protocol-rejection result")) |
| 89 | } else { |
| 90 | out = append(out, check.PassR(target, "TLS 1.0 and 1.1 refused")) |
| 91 | } |
| 92 | } |
| 93 | return out |
| 94 | }, |
| 95 | }) |
| 96 | |
| 97 | check.Register(&check.Check{ |
| 98 | ID: "NET-TLS-002", |
| 99 | Title: "TLS Certificate Expired or Expiring Soon", |
| 100 | Domain: check.DomainNetwork, |
| 101 | Severity: check.Medium, |
| 102 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L", |
| 103 | Summary: "Checks the expiry date of the certificate each local TLS service presents.", |
| 104 | Rationale: "An expired certificate breaks the site for visitors and trains users to click through warnings, which is exactly what an attacker needs for interception.", |
| 105 | Detection: "Performs a normal handshake with 127.0.0.1 using each site name as SNI and reads the leaf certificate's NotAfter date. Expired fails; fewer than 21 days left also fails.", |
| 106 | Fix: "Renew the certificate (certbot renew, or your provider's process) and make sure renewal is automated (systemctl list-timers | grep certbot).", |
| 107 | Verify: "echo | openssl s_client -connect 127.0.0.1:443 -servername <site> 2>/dev/null | openssl x509 -noout -enddate", |
| 108 | Refs: append([]check.Ref{{Title: "Let's Encrypt, certificate lifetimes and renewal", URL: "https://letsencrypt.org/docs/faq/"}}, refTLS[1]), |
| 109 | Standards: check.Standards{CWE: []string{"CWE-298"}, OWASP: []string{"A02:2021"}, WSTG: []string{"WSTG-CRYP-01"}, ASVS: []string{"9.2.1"}, NIST: []string{"SC-12", "SC-17"}, CISTopic: "Certificate validity"}, |
| 110 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 111 | targets := tlsTargets(s) |
| 112 | if len(targets) == 0 { |
| 113 | return []check.Result{check.SkipR("tls", "No local TLS services found")} |
| 114 | } |
| 115 | var out []check.Result |
| 116 | for _, t := range targets { |
| 117 | target := t[1] + " on port " + t[0] |
| 118 | info, err := s.TLS(ctx, t[0], t[1], tls.VersionTLS12, tls.VersionTLS13) |
| 119 | if err != nil || info.NotAfter.IsZero() { |
| 120 | continue |
| 121 | } |
| 122 | days := int(time.Until(info.NotAfter).Hours() / 24) |
| 123 | ev := "notAfter " + info.NotAfter.UTC().Format("2006-01-02") |
| 124 | switch { |
| 125 | case days < 0: |
| 126 | out = append(out, check.FailR(target, "Certificate has expired", ev)) |
| 127 | case days < 21: |
| 128 | out = append(out, check.FailR(target, fmt.Sprintf("Certificate expires in %d days", days), ev)) |
| 129 | default: |
| 130 | out = append(out, check.PassR(target, fmt.Sprintf("Certificate valid for %d more days", days), ev)) |
| 131 | } |
| 132 | } |
| 133 | if len(out) == 0 { |
| 134 | return []check.Result{check.SkipR("tls", "No certificate could be read")} |
| 135 | } |
| 136 | return out |
| 137 | }, |
| 138 | }) |
| 139 | |
| 140 | check.Register(&check.Check{ |
| 141 | ID: "NET-TLS-003", |
| 142 | Title: "Self-Signed or Mismatched TLS Certificate", |
| 143 | Domain: check.DomainNetwork, |
| 144 | Severity: check.Medium, |
| 145 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:L/A:N", |
| 146 | Summary: "Checks whether each site presents a self-signed certificate, or one that does not cover its name.", |
| 147 | Rationale: "Browsers reject these certificates, so visitors either leave or learn to ignore warnings, which removes protection against interception.", |
| 148 | Detection: "Reads the leaf certificate from a loopback handshake per site name. Issuer equal to subject means self-signed; the site name must appear in the certificate's DNS names (wildcards honoured).", |
| 149 | Fix: "Issue a certificate from a public CA covering every site name, for example with certbot --apache or certbot --nginx -d example.com -d www.example.com.", |
| 150 | Verify: "openssl s_client -connect 127.0.0.1:443 -servername <site> </dev/null | openssl x509 -noout -subject -issuer -ext subjectAltName", |
| 151 | Refs: append([]check.Ref{{Title: "RFC 6125, Representation and Verification of Domain-Based Application Service Identity", URL: "https://www.rfc-editor.org/rfc/rfc6125.html"}}, refTLS[1]), |
| 152 | Standards: check.Standards{CWE: []string{"CWE-295", "CWE-297"}, OWASP: []string{"A02:2021", "A07:2021"}, WSTG: []string{"WSTG-CRYP-01"}, ASVS: []string{"9.2.1"}, NIST: []string{"SC-17"}, CISTopic: "Certificate trust"}, |
| 153 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 154 | var out []check.Result |
| 155 | for _, t := range tlsTargets(s) { |
| 156 | if t[0] != "443" || t[1] == "localhost" { |
| 157 | continue |
| 158 | } |
| 159 | info, err := s.TLS(ctx, t[0], t[1], tls.VersionTLS12, tls.VersionTLS13) |
| 160 | if err != nil || info.NotAfter.IsZero() { |
| 161 | continue |
| 162 | } |
| 163 | switch { |
| 164 | case info.SelfSigned: |
| 165 | out = append(out, check.FailR(t[1], "Certificate is self-signed", "subject "+info.Subject)) |
| 166 | case !covers(info.DNSNames, t[1]): |
| 167 | out = append(out, check.FailR(t[1], "Certificate does not cover this site name", fmt.Sprintf("names %v", info.DNSNames))) |
| 168 | case !info.Trusted: |
| 169 | out = append(out, check.FailR(t[1], "Certificate chain is not trusted", info.TrustError)) |
| 170 | default: |
| 171 | out = append(out, check.PassR(t[1], "Certificate is CA-issued and covers the site name")) |
| 172 | } |
| 173 | } |
| 174 | if len(out) == 0 { |
| 175 | return []check.Result{check.SkipR("tls", "No HTTPS sites found")} |
| 176 | } |
| 177 | return out |
| 178 | }, |
| 179 | }) |
| 180 | } |
| 181 | |
| 182 | func covers(names []string, host string) bool { |
| 183 | for _, n := range names { |
| 184 | if n == host { |
| 185 | return true |
| 186 | } |
| 187 | if len(n) > 2 && n[:2] == "*." { |
| 188 | if i := indexDot(host); i > 0 && host[i:] == n[1:] { |
| 189 | return true |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | return false |
| 194 | } |
| 195 | |
| 196 | func indexDot(s string) int { |
| 197 | for i := 0; i < len(s); i++ { |
| 198 | if s[i] == '.' { |
| 199 | return i |
| 200 | } |
| 201 | } |
| 202 | return -1 |
| 203 | } |
Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.
Cite This Page
If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.
OpenVAPT. (2026). NET-TLS-001 Deprecated TLS 1.0 or 1.1 Accepted (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/NET-TLS-001.html
In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
@misc{openvapt_net_tls_001,
author = {{OpenVAPT}},
title = {{NET-TLS-001 Deprecated TLS 1.0 or 1.1 Accepted}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/NET-TLS-001.html}},
note = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}
Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.