Cleartext Legacy Services Listening
Checks for Telnet, FTP, rsh/rlogin, TFTP, plain POP3/IMAP/LDAP, SNMP v1/v2c and rsync daemons on network-reachable addresses.
- Why It Matters
- These protocols send passwords and data unencrypted, so anyone on the path can read or alter them. Most also have weak or no authentication.
- How It Detects
- Matches network-reachable listening ports from /proc/net against well-known cleartext service ports.
- How to Fix
- Replace with encrypted equivalents: SSH/SFTP instead of Telnet/FTP/rsh, IMAPS (993)/POP3S (995), LDAPS (636), SNMPv3. Disable the old service: systemctl disable --now <unit>.
- How to Verify
ss -tulpn should not show ports 21, 23, 69, 110, 143, 161, 389, 512-514 or 873 on public addresses.- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N- Control Category
- Network Exposure: CIS Controls v8 4, 12, 13; NIST CSF 2.0 PR.IR, PR.PS
- Fix Effort
- Medium
- CIS Controls v8 Safeguard
- 4.8
- NIST CSF 2.0 Subcategory
- PR.PS-01, PR.IR-01
- CWE
- CWE-319
- OWASP Top 10
- A02:2021
- NIST SP 800-53 Rev. 5
- SC-8, CM-7
- Benchmark Topic
- Remove insecure legacy services (topic only, no benchmark text)
- References
- NIST SP 800-123, Guide to General Server Security
Source Code
The complete file that defines this check. Run it with openvapt scan --only NET-SVC-001, or read it offline with openvapt explain NET-SVC-001.
internal/checks/network/listeners.go · Lines 1-162View on GitHub at This Commit
| 1 | package network |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/openvapt/openvapt/internal/check" |
| 9 | "github.com/openvapt/openvapt/internal/checks/collect" |
| 10 | "github.com/openvapt/openvapt/internal/sys" |
| 11 | ) |
| 12 | |
| 13 | // dataPorts are services that should almost never be reachable from other |
| 14 | // machines. Port numbers per the IANA Service Name and Port Number Registry. |
| 15 | var dataPorts = map[int]string{ |
| 16 | 3306: "MySQL/MariaDB", 5432: "PostgreSQL", 1433: "Microsoft SQL Server", 1521: "Oracle DB", |
| 17 | 27017: "MongoDB", 6379: "Redis", 11211: "Memcached", 9200: "Elasticsearch/OpenSearch", |
| 18 | 5984: "CouchDB", 9042: "Cassandra", 5672: "RabbitMQ", |
| 19 | } |
| 20 | |
| 21 | // cleartextPorts are legacy services that send credentials unencrypted. |
| 22 | var cleartextPorts = map[int]string{ |
| 23 | 21: "FTP", 23: "Telnet", 69: "TFTP", 110: "POP3 (no TLS)", 143: "IMAP (no TLS)", |
| 24 | 512: "rexec", 513: "rlogin", 514: "rsh", 161: "SNMP (v1/v2c community strings)", 389: "LDAP (no TLS)", 873: "rsync daemon", |
| 25 | } |
| 26 | |
| 27 | func init() { |
| 28 | check.Register(&check.Check{ |
| 29 | ID: "NET-LSN-001", |
| 30 | Title: "Inventory of Listening Network Services", |
| 31 | Domain: check.DomainNetwork, |
| 32 | Severity: check.Info, |
| 33 | CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N", |
| 34 | Summary: "Lists every TCP and UDP port this machine listens on, and whether it is reachable from other machines.", |
| 35 | Rationale: "Attack surface starts with what is listening. Every exposed port should be known and justified (NIST SP 800-115, discovery phase).", |
| 36 | Detection: "Parses /proc/net/tcp, tcp6, udp and udp6. Sockets bound to 127.0.0.1 or ::1 are local only; anything else (0.0.0.0, ::, a public address) is reachable from the network unless a firewall blocks it.", |
| 37 | Fix: "Stop or uninstall services you do not need (systemctl disable --now <unit>). Bind internal services to 127.0.0.1. Block the rest at the firewall.", |
| 38 | Verify: "ss -tulpn shows the same list with process names.", |
| 39 | Refs: []check.Ref{{Title: "NIST SP 800-115, Technical Guide to Information Security Testing and Assessment", URL: "https://csrc.nist.gov/pubs/sp/800/115/final"}, {Title: "proc_net(5), Linux man-pages", URL: "https://man7.org/linux/man-pages/man5/proc_net.5.html"}}, |
| 40 | Standards: check.Standards{CWE: []string{"CWE-1327"}, NIST: []string{"CM-7", "CM-8"}, CISTopic: "Network services inventory"}, |
| 41 | Run: func(_ context.Context, s sys.System) []check.Result { |
| 42 | ls, err := collect.Listeners(s) |
| 43 | if err != nil { |
| 44 | return []check.Result{check.IncompleteR("/proc/net", "One or more socket tables were unreadable or partial")} |
| 45 | } |
| 46 | var pub, local []string |
| 47 | for _, l := range ls { |
| 48 | if l.Public() { |
| 49 | pub = append(pub, l.String()+label(l.Port)) |
| 50 | } else { |
| 51 | local = append(local, l.String()+label(l.Port)) |
| 52 | } |
| 53 | } |
| 54 | ev := append([]string{fmt.Sprintf("network-reachable: %d", len(pub))}, pub...) |
| 55 | ev = append(ev, fmt.Sprintf("local only: %d", len(local))) |
| 56 | ev = append(ev, local...) |
| 57 | return []check.Result{check.PassR("sockets", fmt.Sprintf("%d listening sockets, %d reachable from the network", len(ls), len(pub)), ev...)} |
| 58 | }, |
| 59 | }) |
| 60 | |
| 61 | check.Register(&check.Check{ |
| 62 | ID: "NET-DB-001", |
| 63 | Title: "Database or Cache Service Exposed to the Network", |
| 64 | Domain: check.DomainNetwork, |
| 65 | Severity: check.High, |
| 66 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 67 | Summary: "Checks whether databases, caches, search engines or the Docker API listen on a network-reachable address.", |
| 68 | Rationale: "Exposed databases and caches (Redis, MongoDB, Elasticsearch, Memcached) are among the most common causes of mass data breaches and ransom wipes, often because they ship without authentication.", |
| 69 | Detection: "Matches network-reachable listening ports from /proc/net against a list of well-known data service ports (IANA registry).", |
| 70 | Fix: "Bind the service to 127.0.0.1 (MySQL: bind-address = 127.0.0.1; Redis: bind 127.0.0.1 ::1 and protected-mode yes; MongoDB: net.bindIp: 127.0.0.1) and restart it. Caution: confirm first that no application on another machine connects to it, or that application loses its database. If remote access is needed, allow only specific source IPs in the firewall and require TLS and authentication.", |
| 71 | Verify: "ss -tlnp | grep -E ':(3306|5432|6379|27017|9200|11211)' should show only 127.0.0.1 or ::1.", |
| 72 | Refs: []check.Ref{{Title: "OWASP Database Security Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Database_Security_Cheat_Sheet.html"}, |
| 73 | {Title: "Redis security", URL: "https://redis.io/docs/latest/operate/oss_and_stack/management/security/"}}, |
| 74 | Standards: check.Standards{CWE: []string{"CWE-1327", "CWE-306"}, OWASP: []string{"A05:2021"}, NIST: []string{"SC-7", "CM-7"}, CISTopic: "Network exposure of data services"}, |
| 75 | Run: func(_ context.Context, s sys.System) []check.Result { |
| 76 | ls, err := collect.Listeners(s) |
| 77 | if err != nil { |
| 78 | return []check.Result{check.IncompleteR("/proc/net", "One or more socket tables were unreadable or partial")} |
| 79 | } |
| 80 | var bad []string |
| 81 | for _, l := range ls { |
| 82 | if name, ok := dataPorts[l.Port]; ok && l.Public() && strings.HasPrefix(l.Proto, "tcp") { |
| 83 | bad = append(bad, l.String()+" "+name) |
| 84 | } |
| 85 | } |
| 86 | if len(bad) > 0 { |
| 87 | return []check.Result{check.FailR("sockets", "Data services listen on network-reachable addresses", bad...)} |
| 88 | } |
| 89 | return []check.Result{check.PassR("sockets", "No database or cache ports are network-reachable")} |
| 90 | }, |
| 91 | }) |
| 92 | |
| 93 | check.Register(&check.Check{ |
| 94 | ID: "NET-SVC-001", |
| 95 | Title: "Cleartext Legacy Services Listening", |
| 96 | Domain: check.DomainNetwork, |
| 97 | Severity: check.High, |
| 98 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N", |
| 99 | Summary: "Checks for Telnet, FTP, rsh/rlogin, TFTP, plain POP3/IMAP/LDAP, SNMP v1/v2c and rsync daemons on network-reachable addresses.", |
| 100 | Rationale: "These protocols send passwords and data unencrypted, so anyone on the path can read or alter them. Most also have weak or no authentication.", |
| 101 | Detection: "Matches network-reachable listening ports from /proc/net against well-known cleartext service ports.", |
| 102 | Fix: "Replace with encrypted equivalents: SSH/SFTP instead of Telnet/FTP/rsh, IMAPS (993)/POP3S (995), LDAPS (636), SNMPv3. Disable the old service: systemctl disable --now <unit>.", |
| 103 | Verify: "ss -tulpn should not show ports 21, 23, 69, 110, 143, 161, 389, 512-514 or 873 on public addresses.", |
| 104 | Refs: []check.Ref{{Title: "NIST SP 800-123, Guide to General Server Security", URL: "https://csrc.nist.gov/pubs/sp/800/123/final"}}, |
| 105 | Standards: check.Standards{CWE: []string{"CWE-319"}, OWASP: []string{"A02:2021"}, NIST: []string{"SC-8", "CM-7"}, CISTopic: "Remove insecure legacy services"}, |
| 106 | Run: func(_ context.Context, s sys.System) []check.Result { |
| 107 | ls, err := collect.Listeners(s) |
| 108 | if err != nil { |
| 109 | return []check.Result{check.IncompleteR("/proc/net", "One or more socket tables were unreadable or partial")} |
| 110 | } |
| 111 | var bad []string |
| 112 | for _, l := range ls { |
| 113 | if name, ok := cleartextPorts[l.Port]; ok && l.Public() { |
| 114 | bad = append(bad, l.String()+" "+name) |
| 115 | } |
| 116 | } |
| 117 | if len(bad) > 0 { |
| 118 | return []check.Result{check.FailR("sockets", "Cleartext services are network-reachable", bad...)} |
| 119 | } |
| 120 | return []check.Result{check.PassR("sockets", "No cleartext legacy services are network-reachable")} |
| 121 | }, |
| 122 | }) |
| 123 | } |
| 124 | |
| 125 | func label(port int) string { |
| 126 | for _, m := range []map[int]string{dataPorts, cleartextPorts, controlPorts, managementPorts} { |
| 127 | if n, ok := m[port]; ok { |
| 128 | return " (" + n + ")" |
| 129 | } |
| 130 | } |
| 131 | switch port { |
| 132 | case 22: |
| 133 | return " (SSH)" |
| 134 | case 25: |
| 135 | return " (SMTP)" |
| 136 | case 53: |
| 137 | return " (DNS)" |
| 138 | case 80: |
| 139 | return " (HTTP)" |
| 140 | case 443: |
| 141 | return " (HTTPS)" |
| 142 | case 587: |
| 143 | return " (SMTP submission)" |
| 144 | case 993: |
| 145 | return " (IMAPS)" |
| 146 | case 995: |
| 147 | return " (POP3S)" |
| 148 | } |
| 149 | return "" |
| 150 | } |
| 151 | |
| 152 | // exposure describes how a network-reachable socket is bound, so a reader |
| 153 | // can tell a service on every interface from one on a single address. |
| 154 | func exposure(l collect.Listener) string { |
| 155 | if l.IP.IsUnspecified() { |
| 156 | return "all interfaces" |
| 157 | } |
| 158 | if l.IP.IsPrivate() || l.IP.IsLinkLocalUnicast() { |
| 159 | return "private address" |
| 160 | } |
| 161 | return "public address" |
| 162 | } |
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-SVC-001 Cleartext Legacy Services Listening (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/NET-SVC-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_svc_001,
author = {{OpenVAPT}},
title = {{NET-SVC-001 Cleartext Legacy Services Listening}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/NET-SVC-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.