Postfix Open Relay Controls
Postfix relay restrictions are missing or mynetworks trusts overly broad ranges.
- Why It Matters
- An open relay enables spam and phishing, damages domain reputation, and can expose the host to blocklisting.
- How It Detects
- Parses main.cf for smtpd_relay_restrictions with reject_unauth_destination and flags 0.0.0.0/0 or ::/0 in mynetworks.
- How to Fix
- Set smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination and narrow mynetworks.
- How to Verify
Run postconf -n during approved administration and perform only an authorized relay test.- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L- Control Category
- Data Services: CIS Controls v8 3, 4, 12; NIST CSF 2.0 PR.DS, PR.PS
- Fix Effort
- Low
- CIS Controls v8 Safeguard
- 4.1
- NIST CSF 2.0 Subcategory
- PR.PS-01
- CWE
- CWE-441
- OWASP Top 10
- A05:2021
- NIST SP 800-53 Rev. 5
- AC-3, CM-6, SC-7
- Benchmark Topic
- Secure data service configuration (topic only, no benchmark text)
- References
- Postfix SMTP Relay and Access Control
Source Code
The complete file that defines this check. Run it with openvapt scan --only NET-DB-012, or read it offline with openvapt explain NET-DB-012.
internal/checks/network/services.go · Lines 1-198View on GitHub at This Commit
| 1 | package network |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "context" |
| 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 | func readAny(ctx context.Context, s sys.System, paths ...string) (string, string, bool, bool) { |
| 14 | var found []string |
| 15 | var text strings.Builder |
| 16 | complete := true |
| 17 | for _, p := range paths { |
| 18 | r, err := s.ReadFile(ctx, p) |
| 19 | if err == nil { |
| 20 | found = append(found, p) |
| 21 | text.Write(r.Data) |
| 22 | text.WriteByte('\n') |
| 23 | complete = complete && r.Complete |
| 24 | } |
| 25 | } |
| 26 | if len(found) == 0 { |
| 27 | return "", "", false, false |
| 28 | } |
| 29 | return strings.Join(found, ", "), text.String(), complete, true |
| 30 | } |
| 31 | func simpleFileCheck(id, title string, sev check.Severity, cat check.Category, summary, rationale, detection, fix, verify, cvss string, paths []string, ref check.Ref, bad func(string) []string) { |
| 32 | check.Register(&check.Check{ |
| 33 | ID: id, Title: title, Domain: check.DomainNetwork, Severity: sev, Category: cat, Effort: check.EffortLow, CVSS: cvss, Summary: summary, Rationale: rationale, Detection: detection, Fix: fix, Verify: verify, Refs: []check.Ref{ref}, Standards: dataStandards(id), Run: func(ctx context.Context, s sys.System) []check.Result { |
| 34 | p, text, complete, ok := readAny(ctx, s, paths...) |
| 35 | if !ok { |
| 36 | return []check.Result{check.SkipR(strings.Join(paths, ", "), "Configuration was not found")} |
| 37 | } |
| 38 | ev := bad(text) |
| 39 | if len(ev) > 0 { |
| 40 | return []check.Result{check.FailR(p, summary, ev...)} |
| 41 | } |
| 42 | if !complete { |
| 43 | return []check.Result{check.IncompleteR(p, "Configuration input was truncated")} |
| 44 | } |
| 45 | return []check.Result{check.PassR(p, "No unsafe setting was found")} |
| 46 | }}) |
| 47 | } |
| 48 | |
| 49 | func init() { |
| 50 | simpleFileCheck("NET-DB-008", "Unsafe NFS Export Options", check.High, check.CatData, "NFS exports are world-accessible or use no_root_squash or insecure.", "These export options can grant untrusted clients filesystem access or preserve remote root privileges.", "Reads /etc/exports and active exports.d fragments, ignoring comments, and flags *, no_root_squash, and insecure.", "Restrict every export to explicit client CIDRs, use root_squash, remove insecure, and reload exports.", "Run exportfs -v during approved administration and confirm only intended networks and safe options appear.", "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", []string{"/etc/exports"}, check.Ref{Title: "Linux exports(5)", URL: "https://man7.org/linux/man-pages/man5/exports.5.html"}, func(t string) []string { return matchingLines(t, "*", "no_root_squash", "insecure") }) |
| 51 | simpleFileCheck("NET-DB-009", "Unsafe Samba Guest or SMB1 Configuration", check.High, check.CatData, "Samba permits guest access, maps unknown users to guest, or allows SMB1.", "Guest shares weaken accountability, while SMB1 lacks modern protocol protections and has a long history of severe flaws.", "Reads smb.conf and flags guest ok = yes, permissive map to guest modes, or a server min protocol below SMB2.", "Set guest ok = no, map to guest = Never, and server min protocol = SMB2_02 or newer. Review each share ACL.", "Run testparm -s during approved administration and confirm the effective values.", "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", []string{"/etc/samba/smb.conf"}, check.Ref{Title: "Samba smb.conf Manual", URL: "https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html"}, func(t string) []string { |
| 52 | l := strings.ToLower(t) |
| 53 | var e []string |
| 54 | if strings.Contains(l, "guest ok = yes") || strings.Contains(l, "guest ok=yes") { |
| 55 | e = append(e, "guest ok enabled") |
| 56 | } |
| 57 | if strings.Contains(l, "map to guest = bad") || strings.Contains(l, "map to guest=bad") { |
| 58 | e = append(e, "unknown users map to guest") |
| 59 | } |
| 60 | if strings.Contains(l, "server min protocol = nt1") || strings.Contains(l, "server min protocol=nt1") { |
| 61 | e = append(e, "SMB1 NT1 enabled") |
| 62 | } |
| 63 | return e |
| 64 | }) |
| 65 | simpleFileCheck("NET-DB-010", "BIND Open Recursion", check.High, check.CatData, "BIND enables recursion without restricting recursive clients.", "An open recursive resolver can be abused for reflection attacks and permits outsiders to consume resolver resources.", "Reads named.conf and flags recursion yes when neither allow-recursion nor allow-query-cache restricts clients.", "Disable recursion on authoritative-only servers, or set allow-recursion and allow-query-cache to explicit trusted networks.", "Run named-checkconf and query recursion from an untrusted test network under an approved test plan.", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H", []string{"/etc/bind/named.conf.options", "/etc/named.conf"}, check.Ref{Title: "BIND 9 Administrator Reference Manual - Access Control", URL: "https://bind9.readthedocs.io/en/latest/reference.html#access-control"}, func(t string) []string { |
| 66 | l := strings.ToLower(t) |
| 67 | if strings.Contains(l, "recursion yes") && !strings.Contains(l, "allow-recursion") && !strings.Contains(l, "allow-query-cache") { |
| 68 | return []string{"recursion yes without a client ACL"} |
| 69 | } |
| 70 | return nil |
| 71 | }) |
| 72 | simpleFileCheck("NET-DB-011", "BIND Zone Transfers Allowed to Any", check.High, check.CatData, "BIND permits zone transfers to any client.", "Unrestricted transfers disclose the complete DNS namespace and provide attackers with a detailed target inventory.", "Reads named.conf and flags allow-transfer clauses containing any.", "Set allow-transfer to none or explicit secondary server addresses, preferably protected with TSIG.", "Run named-checkconf and verify AXFR is refused from an unauthorized test host.", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", []string{"/etc/bind/named.conf.options", "/etc/named.conf"}, check.Ref{Title: "BIND 9 Administrator Reference Manual - allow-transfer", URL: "https://bind9.readthedocs.io/en/latest/reference.html#namedconf-statement-allow-transfer"}, func(t string) []string { |
| 73 | l := strings.ToLower(t) |
| 74 | if strings.Contains(l, "allow-transfer") && strings.Contains(l, "any;") { |
| 75 | return []string{"allow-transfer includes any"} |
| 76 | } |
| 77 | return nil |
| 78 | }) |
| 79 | registerMailChecks() |
| 80 | registerExposureChecks() |
| 81 | } |
| 82 | |
| 83 | func registerMailChecks() { |
| 84 | simpleFileCheck("NET-DB-012", "Postfix Open Relay Controls", check.High, check.CatData, "Postfix relay restrictions are missing or mynetworks trusts overly broad ranges.", "An open relay enables spam and phishing, damages domain reputation, and can expose the host to blocklisting.", "Parses main.cf for smtpd_relay_restrictions with reject_unauth_destination and flags 0.0.0.0/0 or ::/0 in mynetworks.", "Set smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination and narrow mynetworks.", "Run postconf -n during approved administration and perform only an authorized relay test.", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L", []string{"/etc/postfix/main.cf"}, check.Ref{Title: "Postfix SMTP Relay and Access Control", URL: "https://www.postfix.org/SMTPD_ACCESS_README.html"}, func(t string) []string { |
| 85 | m := collect.ParseKV(t) |
| 86 | relay := strings.ToLower(strings.Join(m["smtpd_relay_restrictions"], " ")) |
| 87 | nets := strings.ToLower(strings.Join(m["mynetworks"], " ")) |
| 88 | var e []string |
| 89 | if !strings.Contains(relay, "reject_unauth_destination") { |
| 90 | e = append(e, "reject_unauth_destination missing") |
| 91 | } |
| 92 | if strings.Contains(nets, "0.0.0.0/0") || strings.Contains(nets, "::/0") { |
| 93 | e = append(e, "mynetworks trusts all addresses") |
| 94 | } |
| 95 | return e |
| 96 | }) |
| 97 | simpleFileCheck("NET-DB-013", "Mail Cleartext Authentication Allowed", check.Medium, check.CatCrypto, "Postfix or Dovecot permits plaintext authentication without requiring TLS.", "Credentials sent before transport encryption can be observed by any party able to intercept the connection.", "Reads Postfix smtpd_tls_auth_only and Dovecot disable_plaintext_auth, and flags no values.", "Set smtpd_tls_auth_only = yes and disable_plaintext_auth = yes. Use implicit TLS on ports 465, 993, and 995. STARTTLS assessment is out of scope.", "Confirm the effective mail configuration and that unauthenticated plaintext sessions do not advertise AUTH.", "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N", []string{"/etc/postfix/main.cf", "/etc/dovecot/conf.d/10-auth.conf"}, check.Ref{Title: "Dovecot Authentication Settings", URL: "https://doc.dovecot.org/main/core/config/auth/settings.html"}, func(t string) []string { |
| 98 | m := collect.ParseKV(t) |
| 99 | var e []string |
| 100 | for _, k := range []string{"smtpd_tls_auth_only", "disable_plaintext_auth"} { |
| 101 | for _, v := range m[k] { |
| 102 | if strings.EqualFold(strings.TrimSpace(v), "no") { |
| 103 | e = append(e, k+" is no") |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | return e |
| 108 | }) |
| 109 | } |
| 110 | |
| 111 | // controlPorts are control planes and remote desktops: reaching one of them |
| 112 | // from the network means administrative control of the host or cluster |
| 113 | // (vendor documentation and the IANA registry). SNMP is not here: it is a |
| 114 | // cleartext protocol and belongs to NET-SVC-001 only, so one socket never |
| 115 | // yields more than one finding per check family. |
| 116 | var controlPorts = map[int]string{2375: "Docker API without TLS", 6443: "Kubernetes API", 10250: "Kubelet API", 2379: "etcd client API", 5900: "VNC", 3389: "RDP"} |
| 117 | |
| 118 | // managementPorts are management panels, monitoring exporters and service |
| 119 | // discovery APIs: sensitive telemetry and configuration, usually behind their |
| 120 | // own authentication, so exposure is Medium rather than Critical. |
| 121 | var managementPorts = map[int]string{2376: "Docker API (TLS)", 8500: "Consul HTTP API", 9100: "Prometheus Node Exporter", 15672: "RabbitMQ Management", 10000: "Webmin", 2083: "cPanel", 2087: "WHM"} |
| 122 | |
| 123 | func registerExposureChecks() { |
| 124 | check.Register(&check.Check{ |
| 125 | ID: "NET-SVC-002", |
| 126 | Title: "Control Plane or Remote Desktop Ports Network-Reachable", |
| 127 | Domain: check.DomainNetwork, |
| 128 | Severity: check.Critical, |
| 129 | Category: check.CatNetwork, |
| 130 | Effort: check.EffortMedium, |
| 131 | CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 132 | Summary: "Docker API, Kubernetes API, kubelet, etcd, VNC or RDP listen on network-reachable addresses.", |
| 133 | Rationale: "These services hand out administrative control of the host or the cluster. The unauthenticated Docker API and etcd give code execution or every cluster secret to whoever connects; RDP and VNC are the most attacked remote access services.", |
| 134 | Detection: "Parses local TCP and UDP socket tables and matches listeners bound to a non-loopback address against the default ports of these control planes and remote desktops. Severity follows the service identity: management panels and exporters are rated separately by NET-SVC-004. The binding (all interfaces, private or public address) is recorded as evidence. No packets are sent.", |
| 135 | Fix: "Bind control planes to loopback or a dedicated management address, require mutual TLS or strong authentication, and filter access to approved source networks or a VPN.", |
| 136 | Verify: "Review ss -tulpn and the host firewall from a trusted administrative session.", |
| 137 | Refs: []check.Ref{{Title: "Docker Protect the Daemon Socket", URL: "https://docs.docker.com/engine/security/protect-access/"}, {Title: "Kubernetes Ports and Protocols", URL: "https://kubernetes.io/docs/reference/networking/ports-and-protocols/"}, {Title: "IANA Service Name and Transport Protocol Port Number Registry", URL: "https://www.iana.org/assignments/service-names-port-numbers"}}, |
| 138 | Standards: check.Standards{CWE: []string{"CWE-1327", "CWE-923"}, OWASP: []string{"A05:2021"}, NIST: []string{"AC-17", "CM-7", "SC-7"}}, |
| 139 | Run: portListRun(controlPorts, "Control plane or remote desktop ports are network-reachable", "No control plane or remote desktop port is network-reachable"), |
| 140 | }) |
| 141 | check.Register(&check.Check{ |
| 142 | ID: "NET-SVC-004", |
| 143 | Title: "Management Panel or Monitoring Ports Network-Reachable", |
| 144 | Domain: check.DomainNetwork, |
| 145 | Severity: check.Medium, |
| 146 | Category: check.CatNetwork, |
| 147 | Effort: check.EffortLow, |
| 148 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", |
| 149 | Summary: "Webmin, cPanel, WHM, RabbitMQ Management, Consul, the Docker API with TLS or the Prometheus node exporter listen on network-reachable addresses.", |
| 150 | Rationale: "Management panels and exporters publish host metrics, service configuration and often a login form. They are a reconnaissance gift and a password guessing target, but they sit behind their own authentication or TLS, so the risk is lower than an open control plane.", |
| 151 | Detection: "Parses local TCP and UDP socket tables and matches listeners bound to a non-loopback address against the default ports of these management panels, exporters and discovery APIs. The binding (all interfaces, private or public address) is recorded as evidence. No packets are sent.", |
| 152 | Fix: "Bind the panel or exporter to 127.0.0.1 or a management address, put it behind a reverse proxy with authentication, or restrict the port to monitoring hosts in the firewall.", |
| 153 | Verify: "ss -tulpn shows these ports on 127.0.0.1, ::1 or a management address only.", |
| 154 | Refs: []check.Ref{{Title: "Prometheus, Security model", URL: "https://prometheus.io/docs/operating/security/"}, {Title: "IANA Service Name and Transport Protocol Port Number Registry", URL: "https://www.iana.org/assignments/service-names-port-numbers"}}, |
| 155 | Standards: check.Standards{CWE: []string{"CWE-1188", "CWE-1327"}, OWASP: []string{"A05:2021"}, NIST: []string{"AC-17", "CM-7", "SC-7"}}, |
| 156 | Run: portListRun(managementPorts, "Management panel or monitoring ports are network-reachable", "No management panel or monitoring port is network-reachable"), |
| 157 | }) |
| 158 | } |
| 159 | |
| 160 | // portListRun builds the Run function of a check that reports every |
| 161 | // network-reachable listener whose port is in ports. |
| 162 | func portListRun(ports map[int]string, failMsg, passMsg string) func(context.Context, sys.System) []check.Result { |
| 163 | return func(_ context.Context, s sys.System) []check.Result { |
| 164 | ls, err := collect.Listeners(s) |
| 165 | if err != nil { |
| 166 | return []check.Result{check.IncompleteR("/proc/net", "Socket tables were unreadable or partial")} |
| 167 | } |
| 168 | var ev []string |
| 169 | for _, l := range ls { |
| 170 | if n, ok := ports[l.Port]; ok && l.Public() { |
| 171 | ev = append(ev, l.String()+" "+n+", bound to "+exposure(l)) |
| 172 | } |
| 173 | } |
| 174 | if len(ev) > 0 { |
| 175 | return []check.Result{check.FailR("sockets", failMsg, ev...)} |
| 176 | } |
| 177 | return []check.Result{check.PassR("sockets", passMsg)} |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | func matchingLines(t string, needles ...string) []string { |
| 182 | var out []string |
| 183 | s := bufio.NewScanner(strings.NewReader(t)) |
| 184 | for s.Scan() { |
| 185 | line := strings.TrimSpace(s.Text()) |
| 186 | if line == "" || strings.HasPrefix(line, "#") { |
| 187 | continue |
| 188 | } |
| 189 | l := strings.ToLower(line) |
| 190 | for _, n := range needles { |
| 191 | if strings.Contains(l, strings.ToLower(n)) { |
| 192 | out = append(out, line) |
| 193 | break |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | return out |
| 198 | } |
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-DB-012 Postfix Open Relay Controls (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/NET-DB-012.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_db_012,
author = {{OpenVAPT}},
title = {{NET-DB-012 Postfix Open Relay Controls}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/NET-DB-012.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.