AI Model Server or Dashboard Reachable From the Network
Checks whether a self-hosted AI model server, AI web interface, AI cluster dashboard or vector database listens on a network-reachable address.
- Why It Matters
- Many self-hosted AI tools have no login enabled by default. Reached from the network, anyone can use the models and the computing power, read stored documents and embeddings, pull or delete models, and on some dashboards submit jobs that run code.
- How It Detects
- Matches network-reachable listening TCP sockets from /proc/net against the documented default ports of Ollama, LM Studio, Gradio, ComfyUI, the Ray dashboard, Qdrant, Milvus and TensorBoard, and records how each is bound. No request, prompt or model call is sent.
- How to Fix
- Bind each service to 127.0.0.1 (Ollama: OLLAMA_HOST=127.0.0.1:11434 in its service environment; Ray: --dashboard-host=127.0.0.1; Gradio: server_name="127.0.0.1"), and if others need it, publish it only through a reverse proxy with authentication and TLS.
- How to Verify
ss -tlnp shows these ports on 127.0.0.1 or ::1 only.- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L- Control Category
- Network Exposure: CIS Controls v8 4, 12, 13; NIST CSF 2.0 PR.IR, PR.PS
- Fix Effort
- Low
- CIS Controls v8 Safeguard
- 4.8
- NIST CSF 2.0 Subcategory
- PR.PS-01, PR.IR-01
- CWE
- CWE-306, CWE-1327
- OWASP Top 10
- A05:2021, A07:2021
- NIST SP 800-53 Rev. 5
- SC-7, CM-7, AC-3
- Benchmark Topic
- Expose applications only through a hardened front end (topic only, no benchmark text)
- References
- Ollama FAQ, How do I configure Ollama server?
Ray documentation, Security
OWASP Top 10 for Large Language Model Applications
Source Code
The complete file that defines this check. Run it with openvapt scan --only NET-AI-001, or read it offline with openvapt explain NET-AI-001.
internal/checks/network/ai_services.go · Lines 1-129View on GitHub at This Commit
| 1 | package network |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "regexp" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/openvapt/openvapt/internal/check" |
| 9 | "github.com/openvapt/openvapt/internal/sys" |
| 10 | ) |
| 11 | |
| 12 | // NET-AI: self-hosted AI services. Detection only: listening sockets are read |
| 13 | // from /proc/net and configuration files are read from disk. No request, |
| 14 | // prompt or model call is ever sent to any AI service. |
| 15 | |
| 16 | // aiPorts are the documented default ports of self-hosted AI model servers, |
| 17 | // AI web interfaces, AI cluster dashboards and vector databases. Most of them |
| 18 | // ship with no authentication and are designed to listen on loopback only. |
| 19 | var aiPorts = map[int]string{ |
| 20 | 11434: "Ollama model API", |
| 21 | 1234: "LM Studio local server", |
| 22 | 7860: "Gradio AI web interface", |
| 23 | 8188: "ComfyUI image generation interface", |
| 24 | 8265: "Ray dashboard and jobs API", |
| 25 | 6333: "Qdrant vector database", |
| 26 | 19530: "Milvus vector database", |
| 27 | 6006: "TensorBoard", |
| 28 | } |
| 29 | |
| 30 | var jupyterPaths = []string{ |
| 31 | "/etc/jupyter/jupyter_server_config.py", "/etc/jupyter/jupyter_notebook_config.py", |
| 32 | "/usr/local/etc/jupyter/jupyter_server_config.py", "/usr/local/etc/jupyter/jupyter_notebook_config.py", |
| 33 | "/root/.jupyter/jupyter_server_config.py", "/root/.jupyter/jupyter_notebook_config.py", |
| 34 | "/home/*/.jupyter/jupyter_server_config.py", "/home/*/.jupyter/jupyter_notebook_config.py", |
| 35 | } |
| 36 | |
| 37 | // jupyterSetting matches one assignment such as c.ServerApp.token = ”. |
| 38 | var jupyterSetting = regexp.MustCompile(`^\s*c\.(ServerApp|NotebookApp|IdentityProvider|PasswordIdentityProvider)\.(token|password|hashed_password|disable_check_xsrf)\s*=\s*(.+?)\s*$`) |
| 39 | |
| 40 | // jupyterAuthOff reads Jupyter configuration text and reports, without |
| 41 | // copying any value, whether the token is blanked with no password set, or |
| 42 | // whether cross-site request forgery protection is turned off. |
| 43 | func jupyterAuthOff(text string) []string { |
| 44 | emptyToken, password, xsrfOff := false, false, false |
| 45 | for _, line := range strings.Split(text, "\n") { |
| 46 | m := jupyterSetting.FindStringSubmatch(line) |
| 47 | if m == nil { |
| 48 | continue |
| 49 | } |
| 50 | v := strings.TrimSpace(strings.SplitN(m[3], "#", 2)[0]) |
| 51 | blank := v == "''" || v == `""` || v == "u''" |
| 52 | switch m[2] { |
| 53 | case "token": |
| 54 | emptyToken = blank |
| 55 | case "password", "hashed_password": |
| 56 | password = !blank |
| 57 | case "disable_check_xsrf": |
| 58 | xsrfOff = v == "True" |
| 59 | } |
| 60 | } |
| 61 | var out []string |
| 62 | if emptyToken && !password { |
| 63 | out = append(out, "token is an empty string and no login password is set, so anyone who reaches the server can run code") |
| 64 | } |
| 65 | if xsrfOff { |
| 66 | out = append(out, "disable_check_xsrf = True: other websites can send requests to the server through a visitor's browser") |
| 67 | } |
| 68 | return out |
| 69 | } |
| 70 | |
| 71 | func init() { |
| 72 | check.Register(&check.Check{ |
| 73 | ID: "NET-AI-001", Title: "AI Model Server or Dashboard Reachable From the Network", Domain: check.DomainNetwork, |
| 74 | Severity: check.High, Category: check.CatNetwork, Effort: check.EffortLow, |
| 75 | CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L", |
| 76 | Summary: "Checks whether a self-hosted AI model server, AI web interface, AI cluster dashboard or vector database listens on a network-reachable address.", |
| 77 | Rationale: "Many self-hosted AI tools have no login enabled by default. Reached from the network, anyone can use the models and the computing power, read stored documents and embeddings, pull or delete models, and on some dashboards submit jobs that run code.", |
| 78 | Detection: "Matches network-reachable listening TCP sockets from /proc/net against the documented default ports of Ollama, LM Studio, Gradio, ComfyUI, the Ray dashboard, Qdrant, Milvus and TensorBoard, and records how each is bound. No request, prompt or model call is sent.", |
| 79 | Fix: "Bind each service to 127.0.0.1 (Ollama: OLLAMA_HOST=127.0.0.1:11434 in its service environment; Ray: --dashboard-host=127.0.0.1; Gradio: server_name=\"127.0.0.1\"), and if others need it, publish it only through a reverse proxy with authentication and TLS.", |
| 80 | Verify: "ss -tlnp shows these ports on 127.0.0.1 or ::1 only.", |
| 81 | Refs: []check.Ref{{Title: "Ollama FAQ, How do I configure Ollama server?", URL: "https://github.com/ollama/ollama/blob/main/docs/faq.md"}, |
| 82 | {Title: "Ray documentation, Security", URL: "https://docs.ray.io/en/latest/ray-security/index.html"}, |
| 83 | {Title: "OWASP Top 10 for Large Language Model Applications", URL: "https://genai.owasp.org/llm-top-10/"}}, |
| 84 | Standards: check.Standards{CWE: []string{"CWE-306", "CWE-1327"}, OWASP: []string{"A05:2021", "A07:2021"}, NIST: []string{"SC-7", "CM-7", "AC-3"}, CISTopic: "Expose applications only through a hardened front end"}, |
| 85 | Run: appPortRun(aiPorts, "AI services are network-reachable", "No AI model server, dashboard or vector database port is network-reachable"), |
| 86 | }) |
| 87 | |
| 88 | check.Register(&check.Check{ |
| 89 | ID: "NET-AI-002", Title: "Jupyter Configuration Weakens Authentication", Domain: check.DomainNetwork, |
| 90 | Severity: check.High, Category: check.CatIdentity, Effort: check.EffortLow, |
| 91 | CVSS: "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 92 | Summary: "Checks whether a Jupyter configuration blanks the login token without setting a password, or turns off cross-site request protection.", |
| 93 | Rationale: "Jupyter runs any code typed into it with the rights of the account that started it. With the token blanked and no password, anyone who can reach the port, or any local user, gets that account. With XSRF checks off, a web page a user visits can send it commands.", |
| 94 | Detection: "Reads Jupyter configuration files under /etc/jupyter, /usr/local/etc/jupyter, /root/.jupyter and /home/*/.jupyter and looks for token, password and disable_check_xsrf assignments. Only whether a value is empty is recorded; no token or password is copied.", |
| 95 | Fix: "Remove the empty token line so Jupyter generates a random token, or set a password with jupyter server password . Remove disable_check_xsrf = True. Bind to 127.0.0.1 and reach it through an SSH tunnel.", |
| 96 | Verify: "grep -nE 'token|password|disable_check_xsrf' <config file> shows no empty token and no disabled XSRF check, and the server log prints a URL with a token at start-up.", |
| 97 | Refs: []check.Ref{{Title: "Jupyter Server documentation, Security in the Jupyter Server", URL: "https://jupyter-server.readthedocs.io/en/latest/operators/security.html"}}, |
| 98 | Standards: check.Standards{CWE: []string{"CWE-306", "CWE-352"}, OWASP: []string{"A07:2021", "A05:2021"}, NIST: []string{"IA-2", "AC-3"}, CISTopic: "Require authentication for administrative and code execution interfaces"}, |
| 99 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 100 | var paths []string |
| 101 | for _, p := range jupyterPaths { |
| 102 | if strings.Contains(p, "*") { |
| 103 | m, _ := s.Glob(p) |
| 104 | paths = append(paths, m...) |
| 105 | } else { |
| 106 | paths = append(paths, p) |
| 107 | } |
| 108 | } |
| 109 | var res []check.Result |
| 110 | for _, p := range paths { |
| 111 | r, err := s.ReadFile(ctx, p) |
| 112 | if err != nil { |
| 113 | continue |
| 114 | } |
| 115 | if ev := jupyterAuthOff(string(r.Data)); len(ev) > 0 { |
| 116 | res = append(res, check.FailR(p, "Jupyter configuration weakens authentication or request forgery protection", ev...)) |
| 117 | } else if !r.Complete { |
| 118 | res = append(res, check.IncompleteR(p, "Configuration input was truncated")) |
| 119 | } else { |
| 120 | res = append(res, check.PassR(p, "Jupyter keeps token or password authentication and XSRF protection")) |
| 121 | } |
| 122 | } |
| 123 | if len(res) == 0 { |
| 124 | return []check.Result{check.SkipR("jupyter configuration", "No Jupyter configuration file was found")} |
| 125 | } |
| 126 | return res |
| 127 | }, |
| 128 | }) |
| 129 | } |
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-AI-001 AI Model Server or Dashboard Reachable From the Network (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/NET-AI-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_ai_001,
author = {{OpenVAPT}},
title = {{NET-AI-001 AI Model Server or Dashboard Reachable From the Network}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/NET-AI-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.