Redis Authentication and Network Binding
Redis lacks protected mode, authentication, or a loopback-only bind.
- Why It Matters
- An unauthenticated network-reachable Redis instance can disclose or alter data and may permit dangerous administrative operations.
- How It Detects
- Parses redis.conf for protected-mode, requirepass or ACL users, and bind. It does not connect or authenticate.
- How to Fix
- Set protected-mode yes, configure ACL users with strong secrets, bind to 127.0.0.1 and ::1, and restrict remote access with a firewall.
- How to Verify
Review redis-server --test-memory only for health, then inspect the effective configuration and listening socket after reload.- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H- 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
- 3.3
- NIST CSF 2.0 Subcategory
- PR.AA-05, PR.DS-01
- CWE
- CWE-306, CWE-1188
- 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
- Redis Security
Source Code
The complete file that defines this check. Run it with openvapt scan --only NET-DB-002, or read it offline with openvapt explain NET-DB-002.
internal/checks/network/data_services.go · Lines 1-285View 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 | type configRule struct { |
| 14 | id, title, summary, rationale, detection, fix, verify, cvss string |
| 15 | severity check.Severity |
| 16 | patterns []string |
| 17 | ref check.Ref |
| 18 | evaluate func([]collect.ConfigFile) (bool, []string) |
| 19 | } |
| 20 | |
| 21 | func values(fs []collect.ConfigFile, keys ...string) []string { |
| 22 | var out []string |
| 23 | for _, f := range fs { |
| 24 | for _, k := range keys { |
| 25 | out = append(out, f.Values[strings.ToLower(k)]...) |
| 26 | } |
| 27 | } |
| 28 | return out |
| 29 | } |
| 30 | func hasValue(fs []collect.ConfigFile, want func(string) bool, keys ...string) bool { |
| 31 | for _, v := range values(fs, keys...) { |
| 32 | if want(strings.ToLower(strings.TrimSpace(v))) { |
| 33 | return true |
| 34 | } |
| 35 | } |
| 36 | return false |
| 37 | } |
| 38 | func anyIncomplete(fs []collect.ConfigFile) bool { |
| 39 | for _, f := range fs { |
| 40 | if !f.Complete { |
| 41 | return true |
| 42 | } |
| 43 | } |
| 44 | return false |
| 45 | } |
| 46 | func evidence(fs []collect.ConfigFile, keys ...string) []string { |
| 47 | var out []string |
| 48 | for _, f := range fs { |
| 49 | for _, k := range keys { |
| 50 | for _, v := range f.Values[strings.ToLower(k)] { |
| 51 | out = append(out, fmt.Sprintf("%s: %s = %s", f.Path, k, v)) |
| 52 | } |
| 53 | } |
| 54 | } |
| 55 | return out |
| 56 | } |
| 57 | |
| 58 | func registerConfig(r configRule) { |
| 59 | check.Register(&check.Check{ID: r.id, Title: r.title, Domain: check.DomainNetwork, Severity: r.severity, Category: check.CatData, Effort: check.EffortLow, CVSS: r.cvss, |
| 60 | Summary: r.summary, Rationale: r.rationale, Detection: r.detection, Fix: r.fix, Verify: r.verify, Refs: []check.Ref{r.ref}, |
| 61 | Standards: dataStandards(r.id), |
| 62 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 63 | fs := collect.ReadConfigs(ctx, s, r.patterns...) |
| 64 | if len(fs) == 0 { |
| 65 | return []check.Result{check.SkipR(strings.Join(r.patterns, ", "), "Service configuration was not found")} |
| 66 | } |
| 67 | bad, ev := r.evaluate(fs) |
| 68 | if bad { |
| 69 | return []check.Result{check.FailR(fs[0].Path, r.summary, ev...)} |
| 70 | } |
| 71 | if anyIncomplete(fs) { |
| 72 | return []check.Result{check.IncompleteR(fs[0].Path, "Configuration input was truncated; a secure result cannot be established")} |
| 73 | } |
| 74 | return []check.Result{check.PassR(fs[0].Path, "Required security settings are present", ev...)} |
| 75 | }, |
| 76 | }) |
| 77 | } |
| 78 | |
| 79 | func init() { |
| 80 | yes := func(v string) bool { return v == "yes" || v == "true" || v == "on" || v == "1" } |
| 81 | no := func(v string) bool { return v == "no" || v == "false" || v == "off" || v == "0" } |
| 82 | registerConfig(configRule{ |
| 83 | "NET-DB-002", |
| 84 | "Redis Authentication and Network Binding", |
| 85 | "Redis lacks protected mode, authentication, or a loopback-only bind.", |
| 86 | "An unauthenticated network-reachable Redis instance can disclose or alter data and may permit dangerous administrative operations.", |
| 87 | "Parses redis.conf for protected-mode, requirepass or ACL users, and bind. It does not connect or authenticate.", |
| 88 | "Set protected-mode yes, configure ACL users with strong secrets, bind to 127.0.0.1 and ::1, and restrict remote access with a firewall.", |
| 89 | "Review redis-server --test-memory only for health, then inspect the effective configuration and listening socket after reload.", |
| 90 | "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 91 | check.Critical, |
| 92 | []string{"/etc/redis/redis.conf", "/etc/redis.conf"}, |
| 93 | check.Ref{Title: "Redis Security", URL: "https://redis.io/docs/latest/operate/oss_and_stack/management/security/"}, |
| 94 | func(fs []collect.ConfigFile) (bool, []string) { |
| 95 | bad := !hasValue(fs, yes, "protected-mode") || len(values(fs, "requirepass", "user")) == 0 || !hasValue(fs, func(v string) bool { return strings.Contains(v, "127.0.0.1") && !strings.Contains(v, "0.0.0.0") }, "bind") |
| 96 | return bad, evidence(fs, "protected-mode", "bind", "user") |
| 97 | }}) |
| 98 | registerConfig(configRule{ |
| 99 | "NET-DB-003", |
| 100 | "MongoDB Authorization and Network Binding", |
| 101 | "MongoDB authorization is disabled or it binds beyond loopback.", |
| 102 | "MongoDB without authorization or with broad exposure can permit unauthenticated database access.", |
| 103 | "Parses mongod configuration for security.authorization and net.bindIp.", |
| 104 | "Set security.authorization to enabled and net.bindIp to 127.0.0.1, then allow only approved clients through a controlled network path.", |
| 105 | "Run mongod --config <file> --configExpand none in a maintenance workflow and confirm port 27017 is loopback-only.", |
| 106 | "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 107 | check.Critical, |
| 108 | []string{"/etc/mongod.conf", "/etc/mongodb.conf"}, |
| 109 | check.Ref{Title: "MongoDB Security Checklist", URL: "https://www.mongodb.com/docs/manual/administration/security-checklist/"}, |
| 110 | func(fs []collect.ConfigFile) (bool, []string) { |
| 111 | bad := !hasValue(fs, func(v string) bool { return v == "enabled" }, "authorization", "security.authorization") || !hasValue(fs, func(v string) bool { return v == "127.0.0.1" || v == "localhost" }, "bindip", "net.bindip") |
| 112 | return bad, evidence(fs, "authorization", "bindip") |
| 113 | }}) |
| 114 | registerConfig(configRule{ |
| 115 | "NET-DB-004", |
| 116 | "PostgreSQL Trust and Broad Client Access", |
| 117 | "PostgreSQL accepts trust authentication or unrestricted client networks.", |
| 118 | "Trust authentication and all-address client rules can allow database access without a password from untrusted networks.", |
| 119 | "Reads postgresql.conf and pg_hba.conf, flags listen_addresses '*' or 0.0.0.0, trust methods, and 0.0.0.0/0 or ::/0 client ranges.", |
| 120 | "Bind PostgreSQL to required interfaces only and replace trust or unrestricted pg_hba.conf rules with narrow CIDRs and scram-sha-256. Caution: users whose passwords are stored as md5 cannot log in with scram-sha-256 until their password is set again with password_encryption = scram-sha-256, so reset them first and reload, not restart, to test.", |
| 121 | "Run pg_ctl reload through normal operations, then use pg_hba_file_rules and ss -tlnp to verify the effective rules.", |
| 122 | "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 123 | check.Critical, |
| 124 | []string{"/etc/postgresql/*/main/postgresql.conf", "/etc/postgresql/*/main/pg_hba.conf", "/var/lib/pgsql/data/pg_hba.conf"}, |
| 125 | check.Ref{Title: "PostgreSQL Client Authentication", URL: "https://www.postgresql.org/docs/current/client-authentication.html"}, |
| 126 | func(fs []collect.ConfigFile) (bool, []string) { |
| 127 | var ev []string |
| 128 | bad := false |
| 129 | for _, f := range fs { |
| 130 | raw := strings.ToLower(strings.Join(flatten(f.Values), " ")) |
| 131 | if strings.Contains(raw, "0.0.0.0/0") || strings.Contains(raw, "::/0") || strings.Contains(raw, " trust") || hasValue([]collect.ConfigFile{f}, func(v string) bool { return strings.Contains(v, "*") || strings.Contains(v, "0.0.0.0") }, "listen_addresses") { |
| 132 | bad = true |
| 133 | ev = append(ev, f.Path+": broad or trust rule detected") |
| 134 | } |
| 135 | } |
| 136 | return bad, ev |
| 137 | }}) |
| 138 | registerConfig(configRule{ |
| 139 | "NET-DB-005", |
| 140 | "MySQL or MariaDB Unsafe Network Options", |
| 141 | "MySQL or MariaDB enables local file loading, skips grants, or listens broadly with networking enabled.", |
| 142 | "These options can bypass authorization, expand file access, or expose the database to untrusted networks.", |
| 143 | "Parses MySQL and MariaDB option files for local_infile, skip-grant-tables, skip-networking, and bind-address.", |
| 144 | "Disable local_infile and skip-grant-tables. Bind to 127.0.0.1 or enable skip-networking when TCP access is unnecessary.", |
| 145 | "Inspect SHOW VARIABLES through an approved administrator session and confirm the listening address locally.", |
| 146 | "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 147 | check.High, |
| 148 | []string{"/etc/mysql/my.cnf", "/etc/mysql/mysql.conf.d/mysqld.cnf", "/etc/my.cnf"}, |
| 149 | check.Ref{Title: "MySQL Server Security Guidelines", URL: "https://dev.mysql.com/doc/refman/8.4/en/security-guidelines.html"}, |
| 150 | func(fs []collect.ConfigFile) (bool, []string) { |
| 151 | bad := hasValue(fs, yes, "local_infile", "mysqld.local_infile") || len(values(fs, "skip-grant-tables", "mysqld.skip-grant-tables")) > 0 || (!hasValue(fs, yes, "skip-networking", "mysqld.skip-networking") && hasValue(fs, func(v string) bool { return v == "0.0.0.0" || v == "*" }, "bind-address", "mysqld.bind-address")) |
| 152 | return bad, evidence(fs, "local_infile", "skip-grant-tables", "skip-networking", "bind-address") |
| 153 | }}) |
| 154 | registerConfig(configRule{ |
| 155 | id: "NET-DB-006", |
| 156 | title: "Memcached UDP or Broad Listen Address", |
| 157 | summary: "Memcached enables UDP or listens on a non-loopback address.", |
| 158 | rationale: "Unauthenticated Memcached exposure can disclose cached data and UDP mode has been abused for amplification attacks.", |
| 159 | detection: "Parses memcached.conf (one option per line) and /etc/sysconfig/memcached (OPTIONS and UDP_PORT) for -U or UDP_PORT and -l. A UDP port other than 0 fails; a listen address fails unless every address is loopback (127.0.0.1, ::1 or localhost); no -l at all means every interface and fails. The actual values are the evidence.", |
| 160 | fix: "Set UDP_PORT=0 or -U 0 and bind with -l 127.0.0.1. Use a firewall for any explicitly required clients.", |
| 161 | verify: "Confirm ss -ltnup shows Memcached only on loopback and no UDP port 11211.", |
| 162 | cvss: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 163 | severity: check.High, |
| 164 | patterns: []string{"/etc/memcached.conf", "/etc/sysconfig/memcached"}, |
| 165 | ref: check.Ref{Title: "memcached Configuration", URL: "https://docs.memcached.org/serverguide/configuring/"}, |
| 166 | evaluate: memcachedEvaluate}) |
| 167 | registerConfig(configRule{ |
| 168 | id: "NET-DB-007", |
| 169 | title: "Elasticsearch Security and Network Binding", |
| 170 | summary: "Elasticsearch security is disabled or network.host is broadly reachable.", |
| 171 | rationale: "An exposed Elasticsearch cluster without authentication can disclose, alter, or delete indexed data.", |
| 172 | detection: "Parses elasticsearch.yml for xpack.security.enabled and network.host.", |
| 173 | fix: "Set xpack.security.enabled true, bind network.host to loopback or an approved private interface, require TLS, and filter access.", |
| 174 | verify: "Use the local node configuration API with approved credentials and confirm port 9200 is not broadly exposed.", |
| 175 | cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 176 | severity: check.Critical, |
| 177 | patterns: []string{"/etc/elasticsearch/elasticsearch.yml"}, |
| 178 | ref: check.Ref{Title: "Elastic Security Minimal Setup", URL: "https://www.elastic.co/docs/deploy-manage/security/set-up-minimal-security"}, |
| 179 | evaluate: func(fs []collect.ConfigFile) (bool, []string) { |
| 180 | bad := !hasValue(fs, yes, "xpack.security.enabled") || hasValue(fs, func(v string) bool { return v == "0.0.0.0" || v == "_site_" || v == "_global_" }, "network.host") |
| 181 | return bad, evidence(fs, "xpack.security.enabled", "network.host") |
| 182 | }}) |
| 183 | _ = no |
| 184 | } |
| 185 | |
| 186 | // memcachedEvaluate judges memcached's listen addresses and UDP port, quoting |
| 187 | // the values it saw. |
| 188 | func memcachedEvaluate(fs []collect.ConfigFile) (bool, []string) { |
| 189 | var listen, udp, ev []string |
| 190 | for _, f := range fs { |
| 191 | for k, vals := range f.Values { |
| 192 | for _, v := range vals { |
| 193 | v = strings.Trim(strings.TrimSpace(v), `"'`) |
| 194 | switch k { |
| 195 | case "-l": |
| 196 | listen = append(listen, v) |
| 197 | case "-u", "udp_port": |
| 198 | if k == "-u" { |
| 199 | k = "-U" |
| 200 | } |
| 201 | udp = append(udp, v) |
| 202 | case "options": |
| 203 | flds := strings.Fields(v) |
| 204 | for i := 0; i+1 < len(flds); i++ { |
| 205 | switch flds[i] { |
| 206 | case "-l": |
| 207 | listen = append(listen, flds[i+1]) |
| 208 | case "-U": |
| 209 | udp = append(udp, flds[i+1]) |
| 210 | } |
| 211 | } |
| 212 | default: |
| 213 | continue |
| 214 | } |
| 215 | ev = append(ev, f.Path+": "+k+" "+v) |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | bad := false |
| 220 | if len(listen) == 0 { |
| 221 | bad = true |
| 222 | ev = append(ev, "no -l listen address: memcached listens on every interface") |
| 223 | } |
| 224 | for _, l := range listen { |
| 225 | for _, a := range strings.Split(l, ",") { |
| 226 | a = strings.TrimSpace(a) |
| 227 | if i := strings.LastIndex(a, ":"); i > 0 && strings.Count(a, ":") == 1 { |
| 228 | a = a[:i] // 127.0.0.1:11211 |
| 229 | } |
| 230 | if a != "127.0.0.1" && a != "::1" && a != "localhost" { |
| 231 | bad = true |
| 232 | } |
| 233 | } |
| 234 | } |
| 235 | for _, u := range udp { |
| 236 | if strings.TrimSpace(u) != "0" { |
| 237 | bad = true |
| 238 | } |
| 239 | } |
| 240 | return bad, ev |
| 241 | } |
| 242 | |
| 243 | func flatten(m map[string][]string) []string { |
| 244 | var out []string |
| 245 | for k, v := range m { |
| 246 | for _, x := range v { |
| 247 | out = append(out, k+" "+x) |
| 248 | } |
| 249 | } |
| 250 | return out |
| 251 | } |
| 252 | |
| 253 | // dataCWE gives each data service check the weakness it actually detects: |
| 254 | // missing authentication on an insecure default, unsafe sharing permissions, |
| 255 | // amplification, information exposure or an unintended relay. |
| 256 | var dataCWE = map[string][]string{ |
| 257 | "NET-DB-002": {"CWE-306", "CWE-1188"}, // Redis |
| 258 | "NET-DB-003": {"CWE-306", "CWE-1188"}, // MongoDB |
| 259 | "NET-DB-004": {"CWE-306", "CWE-1188"}, // PostgreSQL trust |
| 260 | "NET-DB-005": {"CWE-306", "CWE-1188"}, // MySQL skip-grant-tables, local_infile |
| 261 | "NET-DB-006": {"CWE-406", "CWE-1188"}, // memcached UDP amplification |
| 262 | "NET-DB-007": {"CWE-306", "CWE-1188"}, // Elasticsearch |
| 263 | "NET-DB-008": {"CWE-732", "CWE-306"}, // NFS exports |
| 264 | "NET-DB-009": {"CWE-732", "CWE-306"}, // Samba guest |
| 265 | "NET-DB-010": {"CWE-406"}, // open recursion |
| 266 | "NET-DB-011": {"CWE-497"}, // zone transfer |
| 267 | "NET-DB-012": {"CWE-441"}, // open relay |
| 268 | "NET-DB-013": {"CWE-319", "CWE-523"}, // cleartext mail authentication |
| 269 | } |
| 270 | |
| 271 | func dataStandards(id string) check.Standards { |
| 272 | st := check.Standards{CWE: dataCWE[id], OWASP: []string{"A05:2021"}, NIST: []string{"AC-3", "CM-6", "SC-7"}, CISTopic: "Secure data service configuration"} |
| 273 | switch id { |
| 274 | case "NET-DB-006", "NET-DB-010": |
| 275 | st.NIST = []string{"SC-5", "CM-6", "SC-7"} |
| 276 | case "NET-DB-013": |
| 277 | st.OWASP = []string{"A02:2021"} |
| 278 | st.NIST = []string{"SC-8", "IA-5(1)"} |
| 279 | st.ASVS = []string{"9.1.1"} |
| 280 | } |
| 281 | if len(st.CWE) == 0 { |
| 282 | st.CWE = []string{"CWE-1188"} |
| 283 | } |
| 284 | return st |
| 285 | } |
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-002 Redis Authentication and Network Binding (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/NET-DB-002.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_002,
author = {{OpenVAPT}},
title = {{NET-DB-002 Redis Authentication and Network Binding}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/NET-DB-002.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.