goyban b7a0a86be2 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>
2026-09-01 16:43:00 +00:00

Telegtrum

Telegram + Electrum — a self-hosted Telegram bot for managing Bitcoin wallets with Electrum.

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.


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.


No source checkout, no build. Two files and a docker compose up.

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:

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

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
  • A Telegram bot token — @BotFather
  • Your Telegram user ID — @userinfobot

Building from source

Only needed if you want to modify the bot.

git clone https://git.goyban.com/goyban/telegtrum_bot.git
cd telegtrum_bot
cp .env.example .env && nano .env
docker compose up -d --build

The source compose files mount bot.py read-only, so you can edit it and docker compose restart without rebuilding.

Allow-list: file or environment

Either works; if you use both, the two lists are merged.

Environment (simplest — nothing to mount):

ALLOWED_USERS=123456789,987654321

File — allowed_users.txt, one user per line, id, Name:

cp allowed_users.txt.example allowed_users.txt
123456789, Alice

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.

Publishing a new image

./push.sh            # tag comes from the VERSION file
./push.sh 1.0.1      # or pass one explicitly

Connection modes

Standard — public Electrum servers

The default. Electrum picks public peers. Pin it to one server instead:

ELECTRUM_SERVER=192.168.1.100:50001:t

Local / Umbrel — your own node

cp .env.example .env    # set TELEGRAM_BOT_TOKEN and UMBREL_IP
docker compose -f docker-compose.local.yml up -d --build

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 pinning that server. An existing config is left alone, so manual edits survive restarts.

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.

Tor

cp .env.tor.example .env
docker compose -f docker-compose.tor.yml up -d --build

Either point it at your own onion node:

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).

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.

Using the bot

A persistent keyboard sits at the bottom of the chat.

Button What it does
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.

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.

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

Backup

Everything — wallets, config, blockchain headers — is in one volume.

# Quick deploy
docker run --rm -v telegtrum-data:/data -v "$PWD":/backup debian:bookworm-slim \
  tar czf /backup/telegtrum-backup.tar.gz -C /data .
Mode Volume
Quick deploy telegtrum-data
Build from source electrum-data
Tor electrum-tor-data
Local / Umbrel ./electrum-data (bind mount)

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 — docker compose logs -f. Check the token, and that the container isn't restarting.

"You are not authorized" — your Telegram ID isn't in ALLOWED_USERS or allowed_users.txt. Add it and restart.

Everyone is refused — the container logs no allowed users configured at startup when both sources are empty.

"Wallet not loaded" — press Load first; Balance, Receive and Send all need an active wallet.

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, it's 60 lines.

S
Description
Telegram + Electrum — a self-hosted Telegram bot for managing Bitcoin wallets with Electrum.
Readme 74 KiB
Languages
Python 85%
Shell 13.5%
Dockerfile 1.5%