Security fixes, ready-to-deploy image, and rewritten docs

Audit fixes:

- Wallet names were used unvalidated as file paths, so a name like
  "../<other-id>/x" reached another user's wallet directory or overwrote
  bot_settings.json. Names are now validated and every path is resolved and
  confirmed to sit inside the caller's own directory, which also blocks a
  planted symlink from escaping.
- Tor mode leaked: the mempool.space fee check opened a direct clear-net
  connection, tying the server's IP to its Bitcoin activity. It now goes
  through Tor's SOCKS proxy to mempool's onion service, DNS included.
- The xpub message was left in the chat forever. It reveals the wallet's whole
  address and balance history, so it is now deleted like the password is.
- Inline buttons stay live on old messages, so a user removed from the
  allow-list could keep using them. Callbacks re-check authorisation, and
  auth_check now works for callback queries instead of raising on a null
  message.
- Wallet names echoed back in callback_data are confirmed against the user's
  real wallet list.
- Settings are written via temp file + atomic rename; a crash mid-write used
  to truncate the file and lose every user's wallet selection.
- Replies were sent against messages that had just been deleted; deletion is
  now best-effort and cannot abort the flow.

Deployment:

- Allow-list can come from ALLOWED_USERS instead of a bind-mounted file, so a
  deployment needs no files on disk. Both sources merge when both are present.
- A missing bind-mounted allowed_users.txt made Docker create a directory in
  its place and silently lock everyone out; the entrypoint now detects that
  and stops with an explanation.
- Standard mode honours ELECTRUM_SERVER, which previously only worked in Tor
  mode despite being documented.
- Added VERSION, push.sh, docker-compose.deploy.yml and .env.deploy.example
  for a two-file deploy off the published image.

Docs rewritten in English and Persian around the name Telegtrum, leading with
the security model and what it does not protect against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goyban
2026-09-01 16:34:26 +00:00
parent 468111a52a
commit b7a0a86be2
10 changed files with 615 additions and 438 deletions
+148 -221
View File
@@ -1,299 +1,226 @@
# Online Wallet Bot
# Telegtrum
A self-hosted Telegram bot for managing Bitcoin wallets using Electrum. Designed for watch-only (xpub) wallets with air-gapped signing support.
**Telegram + Electrum** — a self-hosted Telegram bot for managing Bitcoin wallets with Electrum.
## Features
Telegtrum is built for **watch-only (xpub) wallets with air-gapped signing**. Your private keys never touch the server. The bot can see balances, hand out receiving addresses and build *unsigned* transactions; only your offline device can sign them.
- Per-user isolated wallets
- Watch-only wallet restore from xpub
- Balance display (Confirmed / Unconfirmed)
- Receive address generation
- Unsigned transaction (PSBT) creation with fee options
- Unit switching: BTC / mBTC / sats
- Persian (Farsi) and English language support
- Persistent bottom keyboard
- Optional Tor support (connect via .onion to a public server or your own node)
---
## Security model — read this first
| | |
|---|---|
| **The server never holds private keys** | You import an **xpub**. It can watch, not spend. |
| **Spending needs your offline device** | The bot produces an unsigned transaction; you sign it elsewhere and paste it back to broadcast. |
| **Access is deny-by-default** | Only Telegram user IDs you list may use the bot. An empty list refuses everyone. |
| **Secrets are scrubbed from chat** | Wallet passwords and your xpub are deleted from the conversation right after they're read. |
| **Wallets are isolated per user** | Each Telegram user gets their own directory; names are validated so one user cannot reach another's files. |
**What this does not protect against.** Anyone who controls your Telegram account can see your balances and addresses, and can build transactions (they still cannot sign them). Anyone with root on the server can read wallet files and see wallet passwords in the process list while an Electrum command runs — so treat the server as trusted infrastructure and keep the allow-list short. Your xpub is a privacy secret: whoever has it can see your entire transaction history forever.
---
## Quick deploy (recommended)
No source checkout, no build. Two files and a `docker compose up`.
```bash
mkdir telegtrum && cd telegtrum
curl -O https://git.goyban.com/goyban/telegtrum_bot/raw/branch/main/docker-compose.deploy.yml
curl -o .env https://git.goyban.com/goyban/telegtrum_bot/raw/branch/main/.env.deploy.example
nano .env # add your bot token and your Telegram user ID
docker compose -f docker-compose.deploy.yml up -d
docker compose -f docker-compose.deploy.yml logs -f
```
Your `.env` needs two things:
```dotenv
TELEGRAM_BOT_TOKEN=123456:ABC... # from @BotFather
ALLOWED_USERS=123456789 # from @userinfobot, comma-separated for several
```
Compose refuses to start if `ALLOWED_USERS` is unset, rather than launching a bot nobody can use.
You should see `Electrum daemon started. Launching bot...`. Message your bot on Telegram and press **Add Wallet**.
### Updating
```bash
docker compose -f docker-compose.deploy.yml pull
docker compose -f docker-compose.deploy.yml up -d
```
Wallet data lives in the `telegtrum-data` volume and survives updates.
---
## Prerequisites
- A Linux server with Docker and Docker Compose installed
- A Telegram bot token (from [@BotFather](https://t.me/BotFather))
- Your Telegram user ID (from [@userinfobot](https://t.me/userinfobot))
- A Linux server with Docker and Docker Compose
- A Telegram bot token — [@BotFather](https://t.me/BotFather)
- Your Telegram user ID — [@userinfobot](https://t.me/userinfobot)
---
## Server Setup (Step by Step)
## Building from source
### 1. Clone the repository
Only needed if you want to modify the bot.
```bash
git clone https://git.goyban.com/goyban/telegtrum_bot.git
cd telegtrum_bot
cp .env.example .env && nano .env
docker compose up -d --build
```
### 2. Create your `.env` file
The source compose files mount `bot.py` read-only, so you can edit it and `docker compose restart` without rebuilding.
```bash
cp .env.example .env
nano .env
### Allow-list: file or environment
Either works; if you use both, the two lists are merged.
**Environment** (simplest — nothing to mount):
```dotenv
ALLOWED_USERS=123456789,987654321
```
Fill in:
```
ELECTRUM_VERSION=4.7.2
TELEGRAM_BOT_TOKEN=your_bot_token_here
```
### 3. Add your Telegram user ID to the allowed users list
**File** — `allowed_users.txt`, one user per line, `id, Name`:
```bash
cp allowed_users.txt.example allowed_users.txt
nano allowed_users.txt
```
Add a line in the format `userid, Name`:
```
123456789, Alice
```
You can find your user ID by messaging [@userinfobot](https://t.me/userinfobot) on Telegram.
> If you bind-mount `allowed_users.txt` and the file doesn't exist, Docker silently creates a **directory** in its place and nobody can log in. The container now detects this and stops with an explanation instead of starting unusable.
### 4. Build and start the container
### Publishing a new image
```bash
docker compose up -d --build
./push.sh # tag comes from the VERSION file
./push.sh 1.0.1 # or pass one explicitly
```
This will:
- Build the Docker image with Electrum installed
- Start the Electrum daemon
- Launch the Telegram bot
### 5. Check logs
```bash
docker compose logs -f
```
You should see `Electrum daemon started. Launching bot...` followed by the bot starting up.
---
## Local / Umbrel Mode
## Connection modes
Use this mode to connect to your own Electrum server running on Umbrel (or any local node) on your home network.
### Standard — public Electrum servers
> **Note:** `umbrel.local` does not resolve inside Docker containers — mDNS is host-only. Use the actual **IP address** of your Umbrel device (e.g. `192.168.1.100`).
The default. Electrum picks public peers. Pin it to one server instead:
### Setup
```bash
cp .env.example .env
nano .env
```
Fill in `TELEGRAM_BOT_TOKEN` and set your Umbrel's IP:
```
UMBREL_IP=192.168.1.100
```
```bash
cp allowed_users.txt.example allowed_users.txt
nano allowed_users.txt
```
### Start
```dotenv
ELECTRUM_SERVER=192.168.1.100:50001:t
```
### Local / Umbrel — your own node
```bash
cp .env.example .env # set TELEGRAM_BOT_TOKEN and UMBREL_IP
docker compose -f docker-compose.local.yml up -d --build
```
### How it works
> `umbrel.local` will **not** resolve inside a container — mDNS is host-only. Use the actual IP, e.g. `192.168.1.100`.
On first start, the entrypoint writes `./electrum-data/config` with:
On first start the entrypoint writes `./electrum-data/config` pinning that server. An existing config is left alone, so manual edits survive restarts.
```json
{
"auto_connect": false,
"enable_proxy": false,
"oneserver": true,
"server": "192.168.1.100:50001:t"
}
```
> If it won't connect, check that the server string in `./electrum-data/config` still ends in `:t` (TCP). Electrum sometimes rewrites it to `:s` (SSL). Stop the container, change it back, restart.
If the config file already exists it is left untouched, so manual edits survive restarts.
> **If the connection doesn't work:** check `./electrum-data/config` after the first run and make sure the server string still ends in `:t` (TCP). Electrum may rewrite it to `:s` (SSL) on first connect. If it did, stop the container, change it back to `:t`, and restart.
### Check logs
```bash
docker compose -f docker-compose.local.yml logs -f
```
---
## Tor Mode
Tor mode runs Electrum through the Tor network. It uses a separate Docker Compose file and Dockerfile so the standard setup is not affected.
### Setup
### Tor
```bash
cp .env.tor.example .env
nano .env
docker compose -f docker-compose.tor.yml up -d --build
```
Fill in `TELEGRAM_BOT_TOKEN` and choose a connection mode:
**Option A — your own Electrum server over Tor:**
```
Either point it at your own onion node:
```dotenv
ELECTRUM_SERVER=yournode123.onion:50001:t
```
…or leave it unset to use a public onion server (`electrums3lojbuj.onion:50001:t`, override with `ELECTRUM_ONION_SERVER`).
**Option B — public .onion server (default, no extra config needed):**
Leave `ELECTRUM_SERVER` unset. The bot will connect to `electrums3lojbuj.onion:50001:t` by default. Override with `ELECTRUM_ONION_SERVER` if needed.
### Start
```bash
docker compose -f docker-compose.tor.yml up -d --build
```
### Notes
- Tor runs inside the same container — no sidecar needed.
- The container waits for the Tor SOCKS proxy to be ready before starting Electrum.
- Wallet data is stored in a separate volume (`electrum-tor-data`) so it does not mix with the standard setup.
- `t` in the server string means TCP — encryption is handled by the Tor layer, not SSL.
Notes:
- Tor runs inside the same container; the entrypoint waits for the SOCKS proxy before starting Electrum.
- **The fee lookup goes through Tor too.** It uses mempool.space's onion service, so checking fees cannot tie your server's IP to your Bitcoin activity.
- Wallet data uses its own volume (`electrum-tor-data`), kept separate from standard mode.
- `:t` means TCP — Tor provides the encryption, so SSL is not needed.
---
## Updating the Bot
## Using the bot
The `bot.py` file is mounted as a volume, so you can update it without rebuilding the image:
A persistent keyboard sits at the bottom of the chat.
```bash
# Edit bot.py, then:
docker compose restart
# or for Tor mode:
docker compose -f docker-compose.tor.yml restart
```
To update Electrum itself or system packages:
```bash
docker compose up -d --build
# or for Tor mode:
docker compose -f docker-compose.tor.yml up -d --build
```
---
## Adding or Removing Allowed Users
Edit `allowed_users.txt` and restart:
```bash
nano allowed_users.txt
docker compose restart
```
---
## Data Persistence
Wallet data is stored in a named Docker volume at `/root/.electrum` inside the container. This persists across restarts and rebuilds.
| Mode | Volume name |
| Button | What it does |
|---|---|
| Standard | `electrum-data` |
| Tor | `electrum-tor-data` |
| **Add Wallet** | Import a watch-only wallet: name → xpub → password. Your xpub message is deleted once read. |
| **List** | Your wallets. |
| **Load** | Activate a wallet (asks for its password). Needed before Balance / Receive / Send. |
| **Balance** | Confirmed and unconfirmed balance. Inline buttons switch BTC / mBTC / sats. |
| **Receive** | A fresh unused receiving address. |
| **Send** | Builds an *unsigned* transaction — see below. |
| **Close** | Unload the active wallet from the daemon. |
| **🌐 Language** | English ⇄ Persian. |
To back up wallet data (standard mode):
Wallet names may use letters, numbers, spaces, dot, dash and underscore (1–64 characters) and must start with a letter or number. Passwords need at least 8 characters with an uppercase letter, a lowercase letter and a digit.
```bash
docker run --rm -v electrum-data:/data -v $(pwd):/backup debian:bookworm-slim \
tar czf /backup/electrum-backup.tar.gz -C /data .
```
### Sending
1. **Send** → choose a fee source: **Electrum** (its own estimate) or **mempool.space** (live)
2. Enter the fee rate in sat/vB
3. Enter the wallet password *(message deleted immediately)*
4. Enter the recipient address
5. Enter the amount, or tap **Send Max**
6. You get the unsigned transaction as both a code block and a `.txt` file
7. Sign it offline — Electrum Android: *Send → Paste from clipboard*; Electrum desktop: *Send → tools icon → Read text from file*
8. **Send → Broadcast**, paste the signed transaction
---
## Bot Usage Guide
## Backup
### Starting the bot
Everything — wallets, config, blockchain headers — is in one volume.
Open the bot in Telegram and send any message. You'll see a persistent keyboard at the bottom with all available actions.
```bash
# Quick deploy
docker run --rm -v telegtrum-data:/data -v "$PWD":/backup debian:bookworm-slim \
tar czf /backup/telegtrum-backup.tar.gz -C /data .
```
### Add Wallet
| Mode | Volume |
|---|---|
| Quick deploy | `telegtrum-data` |
| Build from source | `electrum-data` |
| Tor | `electrum-tor-data` |
| Local / Umbrel | `./electrum-data` (bind mount) |
Import a watch-only wallet from an xpub (extended public key):
1. Press **Add Wallet**
2. Enter a wallet name
3. Paste your xpub
4. Set a password (min 8 chars, must include upper, lower, and digit)
### List
Shows all your wallets.
### Load
Load a wallet to make it active:
1. Press **Load**
2. Select a wallet (auto-selects if you only have one)
3. Enter the wallet password
### Balance
Shows the confirmed and unconfirmed balance of the loaded wallet.
Use the inline buttons below the balance to switch between **BTC**, **mBTC**, and **sats**.
### Receive
Shows a fresh receiving address for the loaded wallet.
### Send
Creates an unsigned transaction (PSBT) for air-gapped signing:
1. Press **Send**
2. Choose a fee source: **Electrum** (recommended) or **mempool.space** (live)
3. Enter fee rate in sat/vB
4. Enter the wallet password
5. Enter the recipient address
6. Enter the amount (in your selected unit)
7. Receive the unsigned TX as both a code block and a `.txt` file for offline signing
8. Sign it with your offline wallet, then use **Broadcast**
### Close
Unloads the active wallet from the Electrum daemon.
### Language
Press **🌐 Language** to switch between English and Persian (Farsi).
A backup is not a substitute for your seed phrase. These are watch-only wallets — the seed on your offline device is what actually recovers your coins.
---
## Troubleshooting
**Bot not responding:** Check logs with `docker compose logs -f`. Make sure your bot token is correct and the container is running.
**Bot not responding** — `docker compose logs -f`. Check the token, and that the container isn't restarting.
**"You are not authorized":** Your Telegram user ID is not in `allowed_users.txt`. Add it and restart.
**"You are not authorized"** — your Telegram ID isn't in `ALLOWED_USERS` or `allowed_users.txt`. Add it and restart.
**Wallet not loaded error:** Use **Load** before checking balance, receiving, or sending.
**Everyone is refused** — the container logs `no allowed users configured` at startup when both sources are empty.
**Daemon not starting:** The container removes the lockfile on startup automatically. If issues persist, try `docker compose restart`.
**"Wallet not loaded"** — press **Load** first; Balance, Receive and Send all need an active wallet.
**Tor mode — Electrum not connecting:** Tor bootstrap can take up to ~30 seconds on first run. Check logs with `docker compose -f docker-compose.tor.yml logs -f`.
**Invalid wallet name** — stick to letters, numbers, spaces, dot, dash, underscore.
**Daemon won't start** — the entrypoint clears the stale lockfile each boot. If it persists, `docker compose restart`.
**Tor: not connecting** — bootstrap can take ~30s on first run.
---
## Verifying what you run
The image builds Electrum from the official source tarball and **verifies its GPG signature against pinned release-key fingerprints** (ThomasV, sombernight, Emzy). If the signature is missing or the fingerprint doesn't match, the build fails rather than shipping an unverified wallet.
Reproduce it yourself with `./push.sh` — or read [`install.sh`](install.sh), it's 60 lines.