ENTH
PART 5 · Maintenance

Security, upgrades and troubleshooting

Security and monitoring · upgrade runbook and troubleshooting

TL;DR — the 30-second summarytldr

The core of BOMLLM security in one sentence: everything binds to localhost or Tailscale, the only thing open to the world is the single Cloudflare Tunnel gate, every API has a key, every key has a budget, and no secret is ever committed to the repo

The final chapter is the "survivor guide" — every other chapter points back here when trouble hits. It has two parts: upgrade discipline that keeps the system from breaking during upgrades, and the fix recipe for when it breaks at 2 AM

When to usewhen to use

This chapter is not a "read once" — come back to it every time you (1) add a new channel, (2) open a new port/service, (3) before a big upgrade

Come back to this chapter whenever: you are about to upgrade any component, you hit an odd symptom, or you read the words "if you get stuck anywhere" in another chapter

How-tohow to

The bind table of every service (actual policy from ss -tlnp):

Servicebinds towhy
LiteLLM :4000127.0.0.1 (+ Tailscale via a forward for probing)the public path is the tunnel only
Open WebUI :3000127.0.0.1sits behind the tunnel
SearXNG :8888127.0.0.1internal only
Qdrant :6333127.0.0.1no public vectors
Postgres / Valkeycontainer network onlyno host port opened at all
Ollama (Mac) :11434Tailscale IP onlymesh only + firewall open
SSHnon-standard port + key onlyufw defaults to DROP

Properties re-checked on every audit round:

# Basic checks you can run yourself (run on the VPS)
curl -o /dev/null -w "%{http_code}" -X POST https://llm.example.com/v1/chat/completions
# must return 401 — no key, no entry (checked every round)

curl -o /dev/null -w "%{http_code}" https://chat.example.com/api/models
# must return 401 — no session, no model list

ss -tlnp | grep -v 127.0.0.1   # see if anything outside localhost is listening that should not be

If any result differs from this = stop everything and fix it back to correct first

Secret hygiene (recap of chapter 4): the master key lives only in .env on the VPS, per-channel keys can rotate without touching the master, .env is chmod 600 + gitignored, logs/reports only ever say "key loaded, length N", and n8n keeps secrets in the credential store only

Monitoring: Uptime Kuma on the NAS probes every 5 minutes (firing GET /api/version through the mesh) and sends a Telegram alert when a status flips — monitoring status is summarized on an internal board

If you get stuck anywhere → Chapter 9

The 6 rules of upgrading (none negotiable):

  1. Pin the version of everything — latest is absolutely forbidden (we have been bitten by latest changing behavior under us)
  2. One component per time window — upgrade two things at once and break, and you learn nothing at all
  3. Back up before touching — dump Postgres + snapshot the config, and it must be "really restorable" (a backup never restored is a rumor)
  4. Write the rollback plan before typing the upgrade command
  5. Run the 6-probe check after every change — all six green or roll back
  6. Outside peak hours — after 23:00 every day, avoiding our own automatic restart window

The 6-probe check set (run after every change):

vps — the standard check set (from docs/upgrade.md, sanitized) · 6-probe smoke
# 1. LiteLLM alive + DB connectedcurl -sf https://llm.example.com/health/liveliness # 200curl -sf https://llm.example.com/health/readiness # 200, db=connected# 2. The no-key-no-entry gate still standsPOST /v1/chat/completions without a key attached # must return 401# 3. A real Thai chat fires through the whole pipelinecurl -sf .../v1/chat/completions -H "Authorization: $SMOKE_KEY" -d @smoke-payload.json# 4. WebUI 200 5. SearXNG local answers 6. Mac: models resident + swap not growingssh mac 'ollama ps && vm_stat | grep -i swap'

The full original lives in docs/upgrade.md in the repo — copy it into your own script freely

Common symptoms with hands-on fixes:

SymptomCommon causeFix
Bot answers blankthink enabled + low num_predictpin think:false on the route (chapter 4)
Text streams slowly, in burststwo models resident until swap kicks inevict the secondary model (keep_alive 0), then see chapter 2
Cannot log into the chat site, rapid 401skey/session expiredcheck the master key in .env, then up -d again
Thai answers start showing Chinese characterssystem prompt lost / the new route is missing Config Ggo back and check the route + chapter 6
Today's cloud bill abnormally highthe Mac was down long enough that Tier 3 worked hardcheck the hourly spend log + find the cause on the Mac
Updated and brokenfloating version / params silently droppedroll back to the old tag first, find the cause afterwards

The 2 AM recipe (broken at night): (1) do not debug deep while sleepy — roll back to the old tag first (2) save the last 30 minutes of logs into an incident folder (3) sleep (4) write a one-page post-mortem within 48 hours naming the root cause + which rule, had it been followed, would have prevented this incident, then add that rule to the manual

Config exampleconfig example

Threats vs countermeasures table (abridged from docs/security.md):

ThreatWhat the system does
Leaked key (git/image/log)gitignore + pre-push checks + a per-key budget limits the damage
Internet scannersexposed through the tunnel only + 401 without a key
VPS breachedno models/heavy data on the VPS; the Mac sits in the mesh; off-site backups every night
Prompt injection via web resultssearch results are context, not commands + the system prompt pins behavior + bots have post-processing
Runaway costsdaily budget per key + $5 total cap + alerts
Supply (image) changed under youevery dependency pinned to a version tag, latest forbidden

The popular upgrade steps in short (full version in docs/upgrade.md):

# --- Ollama on the Mac ---
brew upgrade ollama          # then restart the service
ollama --version             # client == server must match
ollama ps                    # the main model must be resident again
# the first warm-up chat must be < 5 s to count as done

# --- LiteLLM / WebUI on the VPS ---
docker compose pull litellm          # the pinned tag
docker compose up -d litellm         # recreate just this one
docker compose logs -f litellm       # watch boot: parse config + DB migrate
# after upgrade: diff /model/info against the pre-upgrade snapshot + run the 6-probe smoke

The top risk = database migrations, so dump Postgres every time before pulling

Pro tip — field-tested advicepro tip

★ PROD LESSON
A weak spot we hit ourselves: Netdata — in one audit round we found Netdata :19999 floating on 0.0.0.0 without anyone intending it — since that day "check the bind of everything newly installed" has been a permanent item on the audit checklist. Always assume there is a service you forgot, then go find it with ss -tlnp
★ PRO TIP
Where does the data live? Answer that at any moment — chats/users live in Postgres (VPS), RAG vectors in Qdrant (VPS), nightly backups go to the NAS and are kept 14 days — if you cannot answer this, you do not know your own system well enough to protect it
★ PROD LESSON
A backup never restored = nonexistent — we test a real restore at least once a year by loading the dump onto a test machine — the first time you try, you will certainly find what is missing from your own backup script (everyone does)
★ PRO TIP
Write down every upgrade — a simple table: date | what | from->to | smoke result | rollback needed — three months later, when someone asks "when did it start breaking?", this table is the cheapest correct answer

FAQfaq

Q: Does any data leave the house? — No, except when the Tier 3 cloud is pulled in (it can be disabled entirely in chapter 4) and web searches that go online to fetch results (the search engine runs locally, but the target website sees the VPS IP as usual)

Q: What if I want it locked down as tight as possible? — close signup on the WebUI + wrap every admin page in Cloudflare Access + turn off Tier 3 + set ufw to DROP all inbound except SSH/Tailscale — the system goes completely silent to the outside world

For other questions see the FAQ

Q: How often should I update? — an urgent CVE = immediately but by the 6 rules; normal = once a month in a night window; macOS = only in a quiet week

Q: So broken you do not know what to do? — roll the whole stack back to its previous tags (possible because everything is version-pinned + you have dumps), then hunt the cause on a copy, not on the live system customers are using

See alsosee also

Figuresfigures

The Cloudflare Access gate in front of every admin page
A protective layer before the real app's login page — in real production use
The 3-tier fallback ladder — why the system tolerates upgrades
While one component is being upgraded, the other two tiers temporarily take over the work

Real screen recording for this lesson · click play to hear the summary · not investment advice

▶ Watch the manual video: https://youtu.be/O4hM39cgDCY