# D-019 Wallet key security check

By DC Auditor, Third Window. Written 14:03 Bangkok, 7 Oct 2026. READ ONLY: no key, seed or secret value was printed, copied or changed. Names, modes, owners and hashes only. Every output went through a mask that blanks long hex and long strings.

All 5 boxes were reachable. Nothing is UNCHECKED. Extra box found and checked: picker-box 167.99.74.137. Mac checked too.

## Short answer

- 2 live wallet keys exist: SW live key on main, FW live key on firstmin.
- Both are mode 600, owner root, in 700 folders. Good.
- The big risk is not file modes. It is WHO CAN GET ROOT. One Mac ssh key (kookwatch-deploy) opens root on all 6 boxes. Main can root into firstmin. So one stolen Mac key, or one hacked bot on main, can drain both wallets.
- The FW live key has a second copy on the Mac.

## Root access map (who can log in as root)

| Box | ssh keys that can log in as root |
|---|---|
| main 159.223.88.168 | kookwatch-deploy (Mac) |
| firstmin 178.128.20.136 | kookwatch-deploy (Mac), pbsync@main, swbridge@main |
| research-box 152.42.241.219 | kookwatch-deploy, pbsync@main, root@rot-box, thirdwindow-ff |
| thirdwindow 168.144.142.0 | kookwatch-deploy |
| rot-box 178.128.50.12 | kookwatch-deploy, root@research-box |
| picker-box 167.99.74.137 | kookwatch-deploy, pbsync@main |

All boxes: root is the only shell user, password login off, key login only, PermitRootLogin yes.

Private ssh keys that hold those doors: Mac ~/kookwatch/id_kookwatch (600). Main /opt/pbsync/pbkey (600) and /root/.ssh/id_swbridge (600). research-box /root/.ssh/id_ed25519 (600). rot-box /root/.ssh/id_ed25519 (600). thirdwindow /root/.ssh/rbox_ff (600).

pbkey on main is used by 10 scripts: /opt/hist/t10/pdguard.py, /opt/hist/t10/sentinel.py, /opt/hist/sentinel.py, /opt/swbus/mc5pub.sh, a1health.sh, a1top.sh, pullpub.sh, kitbpub.sh, pp3pub.sh, wethpub.sh.

## main 159.223.88.168 (SW live)

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| M1 | SW live key /opt/w2coder/secret/live_key.json: 600 root, folder 700, one copy, loaded by /opt/w2coder/live.py via config "live_keyfile" (code checks mode 600). | OK | none | SW |
| M2 | Same box runs about 40 services, all as root, plus 3 open web ports (80 serve.py, 8088 /opt/xdeck/xdeck.py, 8092 /opt/casig/casig.py; ufw allows all 3 from Anywhere). A bug in any one root service can read the live key. | HIGH | Move live.py and live_key.json to its own small box, or at least run live.py as its own user (useradd -r swlive; chown swlive:swlive /opt/w2coder/secret -R; User=swlive in its unit). Close 8088 and 8092 to the world (ufw delete allow 8088/tcp; ufw delete allow 8092/tcp) or put them behind nginx with a password, unless someone needs them public. | SW + Ben |
| M3 | Main holds pbkey and id_swbridge, which give root on firstmin (FW live wallet), research-box and picker-box. Hack main, get both wallets. | HIGH | On firstmin, change pbsync@main and swbridge@main lines in authorized_keys to a forced command that only allows the sync job, e.g. prefix: command="/usr/local/bin/sync-only",no-pty,no-port-forwarding,from="159.223.88.168". Better: have firstmin push to main instead of main pulling from firstmin, then delete both lines. | FW + Ben |
| M4 | Old code copies with a literal long secret assigned in the code, mode 644: 6 files /opt/RETIRED-20260923/_retired-20260923-033605/pairexmon/pairexmon.py.*bak* and 3 files /opt/RETIRED-20260923/_removed/linkscan.RETIRED-20260918-151113/checks.py.*bak* (variable names KEY and TOKEN). Not wallet keys, but live API creds may sit in them. | MEDIUM | Check which API those KEY / TOKEN values belong to and rotate them. Then rm -rf the two RETIRED folders above (or chmod -R go-rwx /opt/RETIRED-20260923). | SW |
| M5 | 24 other .bak code files (trench, ubermsol, xmeter, showcfg.py.bak-v1, kookwatch.py.bak, .env.example) hold key/token style lines at 644. Scan found no literal long value in them outside M4. | LOW | Delete .bak files older than 7 days in /opt (find /opt -name '*bak*' -mtime +7 -type f, review, then remove). | SW |
| M6 | Other secrets, all 600 root: /opt/dbridge/key (raw 64-hex bridge key), /opt/runwatch/gmgn_key (runwatch is inactive), /opt/common/{gmgn_key, birdeye_key, fomoscan_key, twitterapi_key, github_token, rpc_keys.json, anthropic_key, dune.key}. | LOW | Ask what /opt/dbridge/key controls. If it is a wallet, treat it like M1. Delete /opt/runwatch/gmgn_key if runwatch is retired. | SW |
| M7 | /opt/robinhood-indices/delivery.json 640 root:robinhood-indices; the group has no members. | OK | none | - |
| M8 | Git repo /root/oc/clear/.git: only .env.template found, no key files. Public folder /opt/launchwatch/pub: no key files, path tricks return 404. | OK | none | - |
| M9 | 3 key/token named symlinks show as 777 (symlinks always do). | OK | none | - |

## firstmin 178.128.20.136 (FW live)

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| F1 | FW live key /root/.config/gmgn/keypair.pem and .env (holds GMGN_API_KEY and GMGN_PRIVATE_KEY): 600 root, folder 700. Used by fmmc5, fmtrader, fmlane1 and other services, all root. | OK on mode | Run the trader as its own user if time allows. | FW |
| F2 | Same key has a copy on the Mac (see Mac section, hashes match). | HIGH | See X2. | Ben |
| F3 | Root login allowed from main (pbsync, swbridge). | HIGH | See M3. | FW + Ben |
| F4 | Leftover /root/.ssh/authorized_keys.bak_swbridge (600). Old door list. | LOW | rm /root/.ssh/authorized_keys.bak_swbridge | FW |
| F5 | 9 old code copies at 644 (/opt/fm/fm_lane1*.bak, /opt/fm_pp/*.bak*) have key/token style lines; no literal long value found in them. | LOW | Delete after review. | FW |
| F6 | 2 npm cache files contain a PRIVATE KEY header (library test data). | OK | none | - |

## research-box 152.42.241.219

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| R1 | No wallet key. SW v3live and v3mc5 run here; no key file found for them on this box (check they sign through main, not here). | OK | none | SW |
| R2 | /root/.ssh/id_ed25519 (600) gives root on rot-box. Root reachable from 4 keys incl. thirdwindow-ff and root@rot-box. Firewall allows anything from 10.104.0.0/16 (private network). | MEDIUM | Confirm every box on 10.104.0.0/16 is ours, or narrow it to the exact IPs. | Ben |
| R3 | A cached json named affcf373d9be1131899b8a282f1825348b12d8c8.json contains the word "mnemonic" (likely cached library or token data, value not read). | LOW | Owner of that cache should confirm it is not a seed, then delete it. | Research |
| R4 | /opt/common/twitterapi_key and rpc_keys.json 600 root. | OK | none | - |

## thirdwindow 168.144.142.0

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| T1 | No wallet key yet (/opt/tw/live.key absent). twlive running. | OK | When a key is added: 600, own user, own folder 700. | Third Window |
| T2 | /root/.ssh/rbox_ff (600) gives root on research-box. nginx open on port 80. | LOW | Limit thirdwindow-ff on research-box with from="168.144.142.0". | Third Window |

## rot-box 178.128.50.12

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| O1 | No wallet key. Research jobs only. /root/.ssh/id_ed25519 (600) gives root on research-box. | OK | none | - |
| O2 | Firewall is off (ufw inactive). Only port 22 listens. | LOW | ufw allow OpenSSH; ufw enable | Ben |

## picker-box 167.99.74.137 (extra box, not on the list)

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| P1 | No wallet key. rpc_keys.json 600. Root from kookwatch-deploy and pbsync@main. Firewall off, only port 22 listens. | LOW | ufw allow OpenSSH; ufw enable | Ben |

## Mac (Ben)

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| X1 | ~/kookwatch/id_kookwatch (600) is kookwatch-deploy: root on all 6 boxes, both live wallets. No passphrase check done (read only). | HIGH | Add a passphrase (ssh-keygen -p -f ~/kookwatch/id_kookwatch) and load it in the macOS keychain, or move it to a hardware key. Use a separate key for the two live boxes (main, firstmin) that is not used by bots. | Ben |
| X2 | ~/.config/gmgn/keypair.pem and ~/.config/gmgn/.env (600) are the SAME FW live wallet key as firstmin (checked by hash, not printed). | HIGH | If no Mac bot needs it, move both files off the Mac (to an offline backup). If a Mac bot needs it, keep 600 and turn on FileVault. | Ben |
| X3 | ~/binance-script-v2/.env mode 644, holds BINANCE_API_KEY and BINANCE_API_SECRET. | MEDIUM | chmod 600 ~/binance-script-v2/.env ; in Binance, make sure the key has withdrawals OFF and an IP list. | Ben |
| X4 | ~/Downloads/Telegram Desktop/twitter-bot-ben/.env mode 664 (plus a duplicate copy). | MEDIUM | chmod 600 on both, or delete the Downloads copy. | Ben |
| X5 | ~/.dbridge.key (600). Local http.server on 127.0.0.1:8765 serves a Codex folder (local only). | LOW | Stop the http.server when not needed. | Ben |

## All boxes

| # | Finding | Severity | Exact fix | Owner |
|---|---|---|---|---|
| A1 | PermitRootLogin yes on every box (key only, so not open to passwords). | LOW | In /etc/ssh/sshd_config set PermitRootLogin prohibit-password, then systemctl reload ssh. | Ben |
| A2 | No raw key or seed found in logs, env dumps or process command lines on any box. | OK | none | - |

## Fix order

1. X1 passphrase on the Mac deploy key, and a separate key for the 2 live boxes.
2. M3 / F3 lock down main's root doors into firstmin.
3. X2 remove the FW live key copy from the Mac.
4. M2 run SW live as its own user and close 8088 / 8092 to the world.
5. M4 rotate the creds in the retired .bak files, then delete them.
6. X3, X4 chmod 600.
7. The LOW list.

Cost: about 30 read-only ssh calls from the Mac, under 10 min total box CPU at nice/timeout 50, no paid APIs, no changes on any box.
