Deployment & Production · BraivIQ AI Engineering Playbook
Self-Hosted LLM Security In A Bank: A Worked Example After The 28 October vLLM Advisories
Self-hosted LLM security inside a bank depends on the boring settings: version, CORS, network binding, API keys and which input types are switched on. On 28 October 2026 vLLM published ten advisories fixed in 0.32.0, including permissive default CORS origins, Ghostscript code execution from untrusted EPS inputs and forged shared-memory handles reaching pickle.loads. This worked example follows a composite bank team putting an open-weight model on its internal network for confirmation work. It covers legacy data, change control, evaluation, latency, approvals and the audit trail, with a deployment linter and a rollout gate in tested Python.
Published · Updated · 13 min read · By BraivIQ Engineering
Key takeaways
- vLLM published ten advisories on 28 October 2026, all fixed in 0.32.0, including CVE-2026-115915 (Ghostscript RCE from untrusted EPS inputs, CVSS 8.1) and CVE-2026-115912 (forged shared-memory handles reaching pickle.loads, CVSS 7.5).
- CVE-2026-115917 covers permissive default CORS origins. List your internal UI origin explicitly.
- Lint the deployment record before rollout: version, CORS, binding, API key and unneeded multimodal inputs.
- Gate the rollout on a passed evaluation, a p95 latency budget and a named model risk approver, all in a hash-chained change log.
- This is a composite worked example, not a client story.
10 - vLLM advisories published on 28 October 2026, all fixed in 0.32.0 · 8.1 - CVSS score of CVE-2026-115915, Ghostscript RCE from untrusted EPS inputs · 3.4 - CVSS score of CVE-2026-115917, permissive default CORS origins
Self-hosted LLM security is mostly configuration discipline. A bank that runs an open-weight model on its own network avoids sending data to a provider, but takes on the server's vulnerabilities and defaults. The 28 October vLLM advisories are a useful checklist: upgrade, restrict origins, bind to an internal address behind an authenticating gateway, require a key and switch off input types you do not use.
What is the worked example?
A composite bank operations team wants a model to summarise confirmation mismatches from a legacy matching system for the settlements desk. Data must stay on the internal network, so the team plans to serve an open-weight model with vLLM. Model risk will approve only a pinned version with evaluation evidence, a latency budget the desk accepts and a named approver. Every booking change stays with a desk supervisor.
What did the vLLM advisories of 28 October cover?
| Advisory | Severity | Issue |
|---|---|---|
| CVE-2026-115915 | High, 8.1 | Untrusted EPS inputs can trigger denial of service or Ghostscript remote code execution |
| CVE-2026-115912 | High, 7.5 | Public multimodal shared-memory handle forgery reaches worker-side pickle.loads() |
| CVE-2026-115917 | Low, 3.4 | Permissive default CORS origins allow cross-origin requests to the HTTP server |
| Seven more | Medium | Denial of service and validation issues, including unbounded chat template rendering |
What does self-hosted LLM security look like end to end?
How do you lint the deployment before it goes live?
model_server_lint.py"""Check a self-hosted model server's deployment record before it goes live inside the bank.
vLLM published advisories on 28 October 2026, fixed in 0.32.0, including permissive default CORS origins
(CVE-2026-115917), Ghostscript RCE from untrusted EPS inputs (CVE-2026-115915) and forged multimodal
shared-memory handles reaching pickle.loads (CVE-2026-115912). The record fields below are our own
deployment record, not vLLM option names. Python 3.14 standard library only.
"""
from __future__ import annotations
MIN_VERSION = (0, 32, 0)
def version(v: str) -> tuple[int, ...]:
return tuple(int(p) for p in v.split(".")[:3])
def lint(rec: dict[str, object]) -> list[str]:
issues = []
if version(str(rec["version"])) < MIN_VERSION:
issues.append(f"version {rec['version']} is below 0.32.0, which fixes the 28 October advisories")
if "*" in list(rec.get("cors_origins", ["*"])): # type: ignore[arg-type]
issues.append("CORS allows any origin: list the internal UI origin explicitly")
if rec.get("bind") == "0.0.0.0" and not rec.get("behind_gateway"):
issues.append("listening on all interfaces without an authenticating gateway")
if not rec.get("api_key_required"):
issues.append("no API key required on the model server")
if rec.get("multimodal_inputs") and not rec.get("multimodal_needed"):
issues.append("multimodal inputs enabled but not needed: switch them off")
return issues
if __name__ == "__main__":
pilot = {"version": "0.31.2", "cors_origins": ["*"], "bind": "0.0.0.0", "behind_gateway": False,
"api_key_required": False, "multimodal_inputs": True, "multimodal_needed": False}
fixed = {"version": "0.32.0", "cors_origins": ["https://ops-ui.internal"], "bind": "10.20.0.15",
"behind_gateway": True, "api_key_required": True, "multimodal_inputs": False}
for name, rec in (("pilot", pilot), ("release candidate", fixed)):
found = lint(rec)
print(f"{name}: {'PASS' if not found else 'FAIL'}")
for i in found:
print(" -", i)Python 3.14.4, standard library only. Run once as shown. The record fields are our own deployment record, not vLLM option names.
The pilot record fails five checks: an old version, CORS open to any origin, binding to all interfaces without a gateway, no API key and multimodal inputs enabled for a text-only job. The release candidate passes. Keep the deployment record in version control and map each field to the server options in your own configuration.
How do evaluation, latency and approval fit together?
rollout_gate.py"""Rollout gate for a model change inside a bank: evaluation passed, latency within budget, and a named
approver, all written to a hash-chained change log before the new model serves the desk.
Python 3.14 standard library only. Sample timings and results only.
"""
from __future__ import annotations
import hashlib
import json
import statistics
LOG: list[dict[str, object]] = []
def record(**entry: object) -> None:
prev = str(LOG[-1]["hash"]) if LOG else "0" * 64
body = {**entry, "prev": prev}
LOG.append({**body, "hash": hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()})
def p95(ms: list[float]) -> float:
return statistics.quantiles(ms, n=20)[-1]
def gate(change: str, eval_passed: bool, timings_ms: list[float], budget_ms: float, approver: str | None) -> bool:
latency = round(p95(timings_ms), 1)
reasons = [r for r, bad in [("evaluation not passed", not eval_passed),
(f"p95 {latency} ms over {budget_ms} ms budget", latency > budget_ms),
("no named approver", approver is None)] if bad]
record(change=change, eval_passed=eval_passed, p95_ms=latency, approver=approver,
decision="refused" if reasons else "released", reasons=reasons)
return not reasons
if __name__ == "__main__":
fast = [420, 455, 470, 480, 510, 515, 530, 545, 560, 590, 610, 640, 650, 700, 720, 760, 780, 800, 820, 905]
slow = [t * 1.6 for t in fast]
print("CHG-3101:", gate("CHG-3101", True, slow, budget_ms=1200, approver="model.risk@example.co.uk"))
print("CHG-3102:", gate("CHG-3102", True, fast, budget_ms=1200, approver=None))
print("CHG-3103:", gate("CHG-3103", True, fast, budget_ms=1200, approver="model.risk@example.co.uk"))
for e in LOG:
print(f" {e['change']} {e['decision']:<9} p95={e['p95_ms']} {e['reasons']} {str(e['hash'])[:10]}")Python 3.14.4, standard library only. Run once as shown. Sample timings and results.
CHG-3101 is refused because its p95 latency of 1,441 ms breaks the 1,200 ms budget. CHG-3102 is refused because nobody approved it. CHG-3103 is released. Each decision is chained into the change log with its reasons, which gives model risk and audit one place to look.
What does the dev team have to learn?
- **Legacy data.** The matching system exports overnight files. Read them through read-only views, check the layout on arrival and refuse files that drift, as in our reconciliation code samples.
- **Change control.** Model weights, server version and prompts change together under one change record.
- **Evaluation.** Golden cases from real mismatches, re-run on every change. Our LLM evaluation playbook shows the gate.
- **Latency.** Measure p95 on the bank's own hardware with realistic prompts, then agree the budget with the desk.
- **Restricted network.** No outbound route from the model host, as in our restricted network walkthrough.
What breaks in production?
- **Defaults that change.** Re-run the linter after every server upgrade.
- **Features switched on for a demo.** Multimodal inputs enabled once stay enabled until someone checks.
- **Gateways bypassed.** Test that the model port is unreachable except through the gateway.
- **Latency drift.** Longer prompts and more users raise p95. Measure monthly.
What are the trade-offs between self-hosting and a hosted model?
| Option | Strength | Trade-off |
|---|---|---|
| Self-hosted open-weight model | Data stays on your network, full version control | You patch the server and own capacity and evaluation |
| Hosted model through a private endpoint | Stronger models, provider patches the stack | Depends on the provider and region |
For where data goes with hosted models, see our blog on sovereign cloud and AI data residency. Our case studies include systems for a global investment bank, and our Workflow Automation London team can run this checklist with you in a 14-day Proof Run.
Frequently asked questions
Which vLLM version fixes the 28 October 2026 advisories?
vLLM 0.32.0, according to the ten GitHub Advisory Database entries published on 28 October 2026.
Is a self-hosted LLM more secure than a hosted one?
Not automatically. It keeps data on your network, but you take on patching, configuration and capacity. The vLLM advisories show how defaults such as CORS origins matter when you run the server yourself.
What should model risk approve for a self-hosted model?
A pinned model and server version, evaluation results on real cases, a measured latency against an agreed budget and the deployment settings, with a named approver and a change record.
References
- GitHub Advisory Database, "vLLM: untrusted EPS inputs can trigger denial of service or Ghostscript RCE (CVE-2026-115915)", 28 October 2026. https://github.com/advisories/GHSA-h3rc-j82p-2c3h
- GitHub Advisory Database, "vLLM: public multimodal SHM handle forgery reaches worker-side pickle.loads() (CVE-2026-115912)", 28 October 2026. https://github.com/advisories/GHSA-6757-hggm-hp2w
- GitHub Advisory Database, "vLLM: permissive default CORS origins allow cross-origin requests (CVE-2026-115917)", 28 October 2026. https://github.com/advisories/GHSA-vvpg-rrww-wpmh