Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Thor Tigress Cub

Operations: recovery and hardening

This chapter is the runbook for the public Thor Tigress Cub: what is public, where every setting lives, what starts at boot, what to do when something leaks or breaks, and the gaps that are known and not yet closed. It changes no code; each fix it describes is a set of commands or clicks.

This chapter covers

  • which names and addresses are public, and why that is not the protection
  • every file, service and record that makes the chat work, and how to change each
  • what happens when the Thor boots, and how to check it
  • step-by-step recovery: a leaked key, abuse, a new Tailscale address, DNS changes, a dead service
  • the known gaps, each with its risk and the fix not yet made

What is public, and what protects the Thor

ItemPublic?SinceWhy
voltforge.techyeswhen boughtdomains are public by design
arpanpathak.taildb9a39.ts.netyes2026-10-05 07:15 UTCFunnel’s HTTPS certificate for it is in the public Certificate Transparency logs, which list every certificate ever issued
the same name in this book, the README, about.html, the voltforge.tech repositoryyes2026-10-05written there on purpose, so people can use the API
the personal keysnoin the encrypted keyring on the Thor, and in each person’s own browser or agent
the keyring passphraseno~/.config/thor-chat/keyring-passphrase (mode 600) on the Thor, and in the agent’s memory
the service keyno~/.config/thor-chat/api-key (mode 600) on the Thor and on the machines you work from
the home IP addressnothe .ts.net name resolves to Tailscale’s Funnel relays, never to the home connection

Check the certificate record yourself:

curl -s "https://api.certspotter.com/v1/issuances?domain=arpanpathak.taildb9a39.ts.net&expand=dns_names"

It lists the certificate issued on 2026-10-05 at 07:15 UTC. Every Funnel address is public this way the moment Funnel is turned on, so keeping the name out of documents would not hide it. The key is the protection, plus the fact that only port 8080 is reachable and only a fixed set of paths answers (chapter “Security”).

What the public name does allow: anyone can load the page, see the invite screen, call /health, and try keys. A new name helps against a flood of unwanted traffic; it does nothing for a leaked key, which works on any name.

Everything that makes it work

On the Thor

WhatWhereChange it with
keyring: who may chat, one key each~/.config/thor-chat/keyring, mode 600; its passphrase in keyring-passphrasethor-tigress-serve keyring requests, approve EMAIL, revoke EMAIL, revoke-all; no restart
service key (agent ↔ llama-server)~/.config/thor-chat/api-key, mode 600thor-tigress-serve key, then restart both services
service settings (MODEL, USERS, CONTEXT, MODELS_MAX, MIN_FREE_GB, ports)~/.config/thor-chat/env; empty now, so defaults applyedit, then thor-tigress-serve install
the model command~/.local/bin/thor-tigress-serve → jetson-thor/model-serving/thor-tigress-servewritten by thor-tigress-serve install
llama-server service~/.config/systemd/user/thor-chat.service → jetson-thor/model-serving/thor-tigress-serve runwritten by thor-tigress-serve install
page and API service~/.config/systemd/user/thor-tigress-agent.service → jetson-thor/model-serving/thor-tigress-serve agentwritten by thor-tigress-serve install
web search service~/.config/systemd/user/searxng.service, settings ~/.config/searxng/settings.ymledit, then systemctl --user restart searxng
thor-tigress-agent binary~/.cargo/bin/thor-tigress-agentcargo install --path crates/thor-tigress-agent --locked
llama-server binary~/.local/src/llama.cpp/build/bin/llama-server (built from commit 8216c84, 2026-10-05)pull and rebuild llama.cpp
the model~/models/gguf/Nemotron-3-Nano-30B-A3B/NVIDIA-Nemotron-3-Nano-30B-A3B-Q8_0.ggufMODEL= in the env file
other models~/models/gguf/<name>/*.gguf; Nemotron 3.5 Lightning (35.0 GB) is there, not loadedthor-tigress-serve: load, unload; list-latest: download
the router’s model list~/.config/thor-chat/models.ini and models.local.iniwritten by thor-tigress-serve; don’t edit
the page, About page, artjetson-thor/web/ in this repositoryedit in your clone; thor-sync copies it; no restart
user services at boot without a loginsystemd “linger” for the user: Linger=yesloginctl enable-linger
Tailscalesystem service tailscaled (version 1.102.4)sudo tailscale up …
Funnelkept by tailscaled across reboots: / → http://127.0.0.1:8080sudo tailscale funnel --bg 8080 / … off

On each machine you work from

Optional; what the guides in this book set up. Paths are the same on Linux and macOS.

WhatWhere
copy of the key~/.config/thor-chat/api-key, mode 600
SSH tunnel to the Thor (8079, 8888, 8080)Linux: ~/.config/systemd/user/thor-model-tunnel.service; any OS: ssh -fN -L … (chapter “Local models”); uses 127.0.0.1, never the public name
claude-thor, claude-thor-thinkaliases in ~/.bashrc or ~/.zshrc
openbatrangs~/.cargo/bin/openbatrangs

Domain and GitHub

WhatWhereValue now
nameserversNamecheap → Domain List → Manage → NameserversNamecheap BasicDNS
A records for @Namecheap → Advanced DNSGitHub Pages: 185.199.108.153 … 185.199.111.153
CNAME for wwwNamecheap → Advanced DNSarpanpathak.github.io.
TXT for email forwardingNamecheap → Advanced DNSv=spf1 include:spf.efwd.registrar-servers.com ~all (leave it)
forwarding pagerepository arpanpathak/voltforge.tech: index.html, thor-tigress-cub.html, 404.htmlsends to https://arpanpathak.taildb9a39.ts.net/
HTTPS for voltforge.techthat repository → Settings → PagesLet’s Encrypt, approved 2026-10-05, valid to 2027-01-04, renewed by GitHub; Enforce HTTPS on

Where the Tailscale name is written

PlaceOccurrencesUpdate needed after a rename
arpanpathak/voltforge.tech: the three HTML files3 per fileyes, or voltforge.tech sends people to a dead address
jetson-thor/web/about.html3yes (agent examples)
book/src/ch16-bring-your-own-agent.md7yes
book/src/ch17-bring-your-own-domain.md12yes
jetson-thor/README.md2yes
invited people: agent configs, --openai-url, Claude Code aliasesunknownthey have to update
invited people’s browserskey and history are stored per addressthey paste the key again on the new address
git history of the platform repository7 commitscannot be removed; harmless once the old name is dead

jetson-thor/web/index.html, chat.css and chat.js (the chat itself) use relative paths and never contain the name, so the chat needs no change.

Boot: what starts, in what order

  1. tailscaled (system service) starts, joins the tailnet and restores Funnel for arpanpathak.taildb9a39.ts.net → 127.0.0.1:8080.
  2. Because of linger, the user’s systemd starts without anyone logging in: thor-chat (llama-server, loads the Nano), searxng, and thor-tigress-agent (ordered after thor-chat).
  3. Each service restarts itself 5 seconds after a crash (Restart=on-failure).
At power on, tailscaled starts as a system service, joins the tailnet and restores Funnel. Because of linger, the user's systemd then starts with nobody logged in and brings up thor-chat, one thor-edge-llm unit per Edge-LLM model, searxng, and thor-tigress-agent after the model servers. Each service restarts itself five seconds after a crash.
What starts at boot. Since 2026-10-08 each TensorRT Edge-LLM model also has a unit of its own, started the same way.

Measured: the last start of thor-chat (2026-10-05 05:17:55) loaded the model and listened 6 seconds later, with the model file already in memory cache. Not measured yet: the time from power-on to the first answer, when the two model files (33 GB and 35 GB) have to come from disk. With both files in the memory cache, a restart on 2026-10-06 had both models loaded in 32 seconds.

After a reboot, check from the Thor:

systemctl is-active tailscaled
systemctl --user is-active thor-chat thor-tigress-agent searxng
tailscale funnel status                              # Funnel on, / → 127.0.0.1:8080
curl -s 127.0.0.1:8080/health                         # {"status":"ok"}
curl -s https://arpanpathak.taildb9a39.ts.net/health  # the same, from outside

Runbook

The key leaked

Signs: someone uninvited is chatting, or a personal key was pasted somewhere public. Whose key it is decides the command.

ssh thor
thor-tigress-serve keyring keys          # who is active; the keys are masked
thor-tigress-serve keyring revoke KEY    # one leaked key: end that person
thor-tigress-serve keyring revoke-all    # every active key, at once

The agent re-reads the file when the keyring changes, so the next request is refused: no restart, and a reply in progress is not cut off. The records stay, so the people who should keep access are approved again with approve EMAIL, and each is sent a new key.

If what leaked was the service key, the one the agent sends to llama-server, rotate it and restart both services:

thor-tigress-serve key
systemctl --user restart thor-chat thor-tigress-agent
cat ~/.config/thor-chat/api-key

Then copy the new service key to each machine you work from: (umask 077; ssh thor cat .config/thor-chat/api-key > ~/.config/thor-chat/api-key). Personal keys are unaffected by that rotation.

Abuse: stop everything now

ssh thor 'sudo tailscale funnel --bg 8080 off'

The public address stops answering within seconds; the Thor, the model and the tailnet keep working, so your own devices still reach it over Tailscale or the SSH tunnel. To turn it back on: sudo tailscale funnel --bg 8080. Rotate the key before turning it back on if the abuse came from a key holder.

Move to a new Tailscale address

Use this when the name itself attracts unwanted traffic. It does not replace rotating the key.

1. Choose the new name. Either:

  • New machine name (the part before the first dot): Tailscale admin console → Machines → the Thor → ⋯ → Edit machine name → untick “Auto-generate from OS hostname” → type the new name → Update name. The address becomes <new-name>.taildb9a39.ts.net.
  • New tailnet name (the taildb9a39 part, for every device): admin console → DNS → Tailnet DNS name → Rename tailnet… → pick one of the offered names.

2. Bring Funnel up on the new name, on the Thor:

tailscale status --self --json | python3 -c "import sys,json;print(json.load(sys.stdin)['Self']['DNSName'])"
sudo tailscale funnel --bg 8080      # re-applies Funnel for the current name
tailscale funnel status
curl -s https://<new-address>/health

The first HTTPS request may take a few seconds while Tailscale gets the new certificate. The new name enters the public certificate logs at that moment.

3. Update every place that names it. On any machine with the two repositories cloned (paths below assume ~/Projects):

OLD=arpanpathak.taildb9a39.ts.net
NEW=<new-address>

gh repo clone arpanpathak/voltforge.tech ~/Projects/voltforge.tech 2>/dev/null || git -C ~/Projects/voltforge.tech pull
cd ~/Projects/voltforge.tech
grep -rl "$OLD" --include='*.html' --include='*.md' . | xargs sed -i "s/$OLD/$NEW/g"
git commit -am "Forward to $NEW" && git push

cd ~/Projects/thor-thunder-tigress-platform
grep -rl "$OLD" book/src jetson-thor | xargs sed -i "s/$OLD/$NEW/g"
git commit -am "New Tailscale address" && git push

(On macOS, sed -i '' instead of sed -i.) GitHub Pages republishes the forwarding page within a minute or two; the DNS at Namecheap does not change, because voltforge.tech points at GitHub, not at the Thor. This is the main benefit of the forwarding page.

4. Tell people. Their saved key and history belong to the old address; on the new one they paste the key again. Agents need the new --openai-url or ANTHROPIC_BASE_URL.

5. Check.

curl -s https://voltforge.tech/thor-tigress-cub | grep -o 'url=[^"]*'   # the new address
curl -s https://arpanpathak.taildb9a39.ts.net/health                      # old name: no answer

Change a DNS record

All records live at Namecheap → Domain List → Manage next to voltforge.tech → Advanced DNS → Host Records.

  • Edit: click the value, change it, click the green tick.
  • Add: Add New Record, choose the type, fill Host and Value, green tick.
  • Delete: the trash icon at the end of the row.
ToRecord
point the bare domain somewhere elsechange the four A records for @ (an A record holds an IP address)
point www somewhere elsechange the CNAME for www (a CNAME holds another name, ending in a dot)
add chat.voltforge.tech for another GitHub Pages siteCNAME, Host chat, Value arpanpathak.github.io., plus a CNAME file containing chat.voltforge.tech in that site’s repository
prove the domain to GitHubTXT, Host _github-pages-challenge-arpanpathak, the value GitHub shows

Records have a time to live of about 30 minutes, so old answers can linger that long. Check what the world sees, not your own machine’s cache:

curl -s "https://dns.google/resolve?name=voltforge.tech&type=A" | python3 -m json.tool | grep data
curl -s "https://dns.google/resolve?name=www.voltforge.tech&type=CNAME" | python3 -m json.tool | grep data

If the A records stop pointing at GitHub, GitHub Pages loses the certificate for voltforge.tech and will request it again once they are back.

A service is down

SymptomCheckFix
voltforge.tech loads, the Thor’s address doesn’tcurl https://arpanpathak.taildb9a39.ts.net/healthon the Thor: tailscale funnel status; sudo tailscale funnel --bg 8080
the page loads, red dotsystemctl --user status thor-tigress-agent thor-chatsystemctl --user restart thor-chat thor-tigress-agent
replies never startcurl 127.0.0.1:8079/health on the Thorjournalctl --user -u thor-chat -n 50; memory: free -g
Web finds nothingsystemctl --user status searxngsystemctl --user restart searxng
voltforge.tech itself failsthe DNS checks above; the repository’s Pages settingssee chapter “Bring your own domain”, “When a step fails”

Keep it updated

WhatHowHow often
Tailscalesudo apt update && sudo apt upgrade tailscalemonthly
llama.cppcd ~/.local/src/llama.cpp && git pull && cmake --build build -j then restart thor-chatmonthly, or for a security fix
SearXNGcd ~/.local/src/searxng && git pull, reinstall into its venv, restartmonthly
JetPackNVIDIA’s release notesper release

Known gaps

Not fixed yet; each needs a decision or work.

#GapRiskNowFix
1One shared keyfixed on 2026-10-07: thor-tigress-keyring keeps one key per person, and revoke / revoke-all take access back one at a time or all at once
2No limits per personone key holder can keep all four slots busynoneper-person token allowance and a fair queue (planned server)
3No record of who used whatabuse can’t be traced to a personthe keyring says who may, not who did; journalctl shows requests, not peopleper-person accounting (planned server)
4Domain not verified on GitHubif the voltforge.tech repository or its Pages site is removed while DNS still points at GitHub, someone else could claim the domain on GitHub PagesDNS points at GitHub; repository existsadd the TXT record from “Bring your own domain”, step 4
5The Tailscale name is written in five placesa rename needs the update in the runbook abovedocumented sed commandsone config value that the page, the About page and the forwarding page are built from
6The service key shown in a chat earlier (2026-10-05) may still be the current api-keyit may be in that chat’s history, and the agent still accepts it as the operator keynot rotatedrotate the service key (“The key leaked”); personal keys are separate
7No monitoringnobody is told when the Thor or Funnel goes downpeople noticean external check of /health every few minutes that sends a message on failure
8Cold-boot time not measuredunknown downtime after a power cutreboot once, time power-on to /health
9Accounts behind the domainwhoever gets into the Namecheap or GitHub account can redirect visitorsdepends on those accountstwo-factor authentication on both, checked
10When the Thor is down, voltforge.tech still forwardsvisitors land on a browser error instead of a messagethe forwarding page checks /health first and shows “the Thor is offline”
11Updates are manualold llama.cpp or Tailscale with known bugsthe table abovea monthly reminder, or unattended upgrades for Tailscale
12The key is compared with a plain string comparisonfixed on 2026-10-06: thor-tigress-agent compares keys in constant time (config::same_secret)
13The invite form (POST /request) is open and unmeteredanyone can fill the waiting list, and each request costs an Argon2id runapproval is by hand, so the list is the only damagea rate limit on that one route