Docker: automatic SSL with a self-signed certificate, reused if present
docker-entrypoint.sh uses the given SSL_CERTFILE/SSL_KEYFILE, or the certificate in the electrumx-ssl volume, and generates a self-signed one (10 years) only when none exists, never overwriting it, as clients pin it. It also sets SERVICES (tcp 50001 + ssl 50002). SSL is on by default. Documented in README.md and DEPLOY.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e5d005efe
commit
c2dcff5cc8
7 files changed
+173
-29
No files matched your search
@@ -5,3 +5,12 @@ PLM_RPC_PASSWORD=change-me
|
||||
#CACHE_MB=1200
|
||||
#REPORT_SERVICES=tcp://electrum.example.com:50001
|
||||
#ELECTRUMX_TCP_PORT=50001
|
||||
|
||||
# SSL (port 50002): on by default, self-signed certificate generated once and kept in the electrumx-ssl volume.
|
||||
#SSL_ENABLED=true
|
||||
# name (or IP) of the server, written in the generated certificate
|
||||
#SSL_HOSTNAME=electrum.example.com
|
||||
#ELECTRUMX_SSL_PORT=50002
|
||||
# your own certificate (paths inside the container; mount the files, see DEPLOY.md):
|
||||
#SSL_CERTFILE=/certs/fullchain.pem
|
||||
#SSL_KEYFILE=/certs/privkey.pem
|
||||
@@ -22,7 +22,7 @@ DB_ENGINE=rocksdb DB_DIRECTORY=... DAEMON_URL=http://user:pass@host:port/ electr
|
||||
electrumx_rpc getinfo # talk to a running server via local RPC
|
||||
```
|
||||
|
||||
Docker: `docker compose up -d --build` (root `Dockerfile`, `docker-compose.yml`, `.env` from `.env.example`) runs the server on the external `palladium` network of the Palladium node container, daemon at `palladiumd:2332`.
|
||||
Docker: `docker compose up -d --build` (root `Dockerfile`, `docker-compose.yml`, `.env` from `.env.example`) runs the server on the external `palladium` network of the Palladium node container, daemon at `palladiumd:2332`. `docker-entrypoint.sh` sets up SSL: it uses `SSL_CERTFILE`/`SSL_KEYFILE` if given, else the self-signed certificate in the `electrumx-ssl` volume, generating it only if missing (never overwrite it: clients pin it), and sets `SERVICES`.
|
||||
|
||||
Configuration is entirely via environment variables, parsed in `src/electrumx/server/env.py` (documented in `docs/environment.rst`). `DB_ENGINE` is mandatory in 2.0 (`leveldb` or `rocksdb`). Python >=3.10; free-threaded 3.14t is experimental.
|
||||
|
||||
|
||||
@@ -47,12 +47,12 @@ Keep `.env` private (`chmod 600 .env`); it is ignored by git.
|
||||
| Port | What | Open to | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| **50001/tcp** | Electrum protocol, plain | clients (internet, if the server is public) | published by `docker-compose.yml` |
|
||||
| **50002/tcp** | Electrum protocol over SSL | clients | optional, needs a certificate (see below); not published by default |
|
||||
| **50002/tcp** | Electrum protocol over SSL | clients | on by default, certificate generated automatically (see "SSL"); published by `docker-compose.yml` |
|
||||
| **2333/tcp** | Palladium P2P (the node) | the internet, inbound | published by the node's compose file |
|
||||
| 2332/tcp | node RPC | **only the `palladium` network** | never publish or forward it: it can spend wallet funds |
|
||||
| 28332, 28334, 28335/tcp | node ZMQ | only the `palladium` network | never publish or forward them |
|
||||
|
||||
Changing the published port: `ELECTRUMX_TCP_PORT` in `.env` (host side only; the container always listens on 50001).
|
||||
Changing the published ports: `ELECTRUMX_TCP_PORT` / `ELECTRUMX_SSL_PORT` in `.env` (host side only; the container always listens on 50001).
|
||||
|
||||
A server only for yourself (or for other containers) needs no open port: connect to `127.0.0.1:50001`, or join the
|
||||
`palladium` network and use `electrumx:50001`.
|
||||
@@ -79,31 +79,85 @@ Never forward 2332 or 28332/28334/28335. A server on a VPS with a public IP open
|
||||
|
||||
## SSL (port 50002)
|
||||
|
||||
Most Electrum wallets prefer SSL. You need a certificate and key for the host name of the server.
|
||||
SSL is **on by default** and needs nothing to be set up: Electrum wallets prefer it, and plain TCP (50001) sends
|
||||
addresses and balances unencrypted.
|
||||
|
||||
1. Get a certificate, for example with Let's Encrypt (`certbot certonly --standalone -d electrum.example.com`),
|
||||
or a self-signed one for tests:
|
||||
```bash
|
||||
openssl req -x509 -newkey rsa:4096 -nodes -days 3650 -subj "/CN=electrum.example.com" \
|
||||
-keyout privkey.pem -out fullchain.pem
|
||||
```
|
||||
2. Create `docker-compose.override.yml` next to `docker-compose.yml` (compose reads it automatically):
|
||||
### What happens at start
|
||||
|
||||
`docker-entrypoint.sh` (the entrypoint of the image) does this at every start:
|
||||
|
||||
1. If you gave your own certificate (`SSL_CERTFILE` and `SSL_KEYFILE`, see below), it uses that.
|
||||
2. Otherwise it looks for `/var/lib/electrumx-ssl/server.crt` and `server.key`, in the `electrumx-ssl` volume.
|
||||
- **They exist: they are used as they are, never overwritten.**
|
||||
- **They do not exist (first start): a self-signed certificate valid 10 years is generated** for `SSL_HOSTNAME`
|
||||
and saved there. If only one of the two files exists, the container stops with an error instead of overwriting it.
|
||||
3. It prints the certificate subject, expiry date and SHA-256 fingerprint in the log
|
||||
(`docker compose logs electrumx | grep entrypoint`), and warns if it expires in less than 30 days.
|
||||
4. If `SERVICES` is not set, it sets it to `tcp://:50001,ssl://:50002`.
|
||||
|
||||
The compose file publishes 50002 (`ELECTRUMX_SSL_PORT`). Settings, all in `.env`:
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `SSL_ENABLED` | `true` | `false`: no SSL, only `tcp://:50001` |
|
||||
| `SSL_HOSTNAME` | `electrumx` | name (or IP) written in a generated certificate; set it to the name of the server (it matters only when the certificate is generated) |
|
||||
| `SSL_CERTFILE`, `SSL_KEYFILE` | unset | your own certificate and key (paths inside the container); both or neither |
|
||||
| `ELECTRUMX_SSL_PORT` | `50002` | host port published for SSL |
|
||||
|
||||
### Self-signed certificate: what clients see
|
||||
|
||||
A generated certificate is signed by nobody. Electrum wallets handle it by *trust on first use*: at the first connection
|
||||
the wallet saves the certificate, and from then on it refuses a different one (as SSH does with host keys), so a
|
||||
man-in-the-middle cannot take over later. The fields of the certificate (name, country, ...) are not checked by the
|
||||
client, only the expiry date. Consequences:
|
||||
|
||||
- **Keep the certificate.** It is in the `electrumx-ssl` volume, separate from the database on purpose: the database
|
||||
can be deleted and rebuilt at any time (`docker volume rm <project>_electrumx-db`), the certificate should not be.
|
||||
`docker compose down -v` deletes **both** volumes, and after that a new certificate is generated and every client
|
||||
that saved the old one refuses to connect.
|
||||
- **Back it up** while the container runs:
|
||||
```bash
|
||||
docker compose cp electrumx:/var/lib/electrumx-ssl ./ssl-backup # contains the private key: keep it private
|
||||
```
|
||||
To restore it, put the two files somewhere on the host and use them as "your own certificate" below.
|
||||
- If it expires or is lost, clients that saved it must be told to forget it, or the server must get a new name.
|
||||
Generated certificates last 10 years, so expiry is rarely the problem.
|
||||
- Whether a wallet accepts a self-signed certificate depends on the wallet. Electrum does (trust on first use);
|
||||
check the wallet you intend to use.
|
||||
|
||||
### Your own certificate (for example Let's Encrypt)
|
||||
|
||||
With a certificate signed by a CA the client verifies it by itself, with no trust on first use, but it needs a domain
|
||||
and must be renewed (about every 90 days).
|
||||
|
||||
1. Get the certificate and key (`certbot certonly --standalone -d electrum.example.com`).
|
||||
Copy the files out of `/etc/letsencrypt/live/...` (they are symlinks into `archive/`) to a folder, and copy them
|
||||
again after each renewal. The container runs as uid 1000, so the key must be readable by it
|
||||
(`chown 1000 privkey.pem && chmod 600 privkey.pem`).
|
||||
2. Mount the folder with a `docker-compose.override.yml` next to `docker-compose.yml` (compose reads it automatically):
|
||||
```yaml
|
||||
services:
|
||||
electrumx:
|
||||
environment:
|
||||
SERVICES: tcp://:50001,ssl://:50002
|
||||
SSL_CERTFILE: /certs/fullchain.pem
|
||||
SSL_KEYFILE: /certs/privkey.pem
|
||||
ports:
|
||||
- "50002:50002"
|
||||
volumes:
|
||||
- /path/to/certs:/certs:ro
|
||||
```
|
||||
The container runs as uid 1000: the key must be readable by it (`chown 1000 privkey.pem; chmod 600 privkey.pem`).
|
||||
With Let's Encrypt, copy the files out of `/etc/letsencrypt/live/...` (they are symlinks into `archive/`), and
|
||||
copy them again after each renewal.
|
||||
3. `docker compose up -d`, and open 50002 as in "Ports".
|
||||
3. In `.env`:
|
||||
```ini
|
||||
SSL_CERTFILE=/certs/fullchain.pem
|
||||
SSL_KEYFILE=/certs/privkey.pem
|
||||
```
|
||||
4. `docker compose up -d`. After a renewal, `docker compose restart electrumx` loads the new files.
|
||||
|
||||
If the files cannot be read, the container stops with a message that says so (see `docker compose logs electrumx`).
|
||||
|
||||
### Check it
|
||||
|
||||
```bash
|
||||
echo | openssl s_client -connect 127.0.0.1:50002 2>/dev/null | openssl x509 -noout -subject -enddate -fingerprint -sha256
|
||||
printf '{"id":1,"method":"server.version","params":["test","1.4"]}\n' | openssl s_client -quiet -connect 127.0.0.1:50002
|
||||
```
|
||||
|
||||
The fingerprint must be the one printed in the log. Open 50002 as in "Ports" for a public server.
|
||||
|
||||
## Announcing a public server
|
||||
|
||||
@@ -160,3 +214,5 @@ From another machine, `nc -zv <public IP> 50001` must succeed if the server is m
|
||||
| `blockchain.outpoint.subscribe` or package broadcast return a daemon error | expected: Palladium Core is based on Bitcoin Core 0.19 and does not have these RPCs |
|
||||
| the server stays at an old height | the node is not synced; ElectrumX only follows the node |
|
||||
| clients cannot connect from outside | port not published/forwarded, firewall, or `SERVICES` does not list that protocol |
|
||||
| container restarts at once, log says `ERROR: ... SSL_CERTFILE` | your own certificate is not mounted or not readable by uid 1000, only one of `SSL_CERTFILE`/`SSL_KEYFILE` is set, or only one of the generated files exists in the `electrumx-ssl` volume |
|
||||
| a wallet refuses the server after it worked | the certificate changed (for example after `docker compose down -v`); the wallet saved the old one |
|
||||
+11
-7
@@ -24,14 +24,14 @@ RUN python -m venv venv \
|
||||
|
||||
FROM python:3.14-slim-trixie
|
||||
|
||||
# the libs needed by rocksdb (no development headers or statics)
|
||||
RUN apt-get update && apt-get -y --no-install-recommends install librocksdb9.10 && rm -rf /var/lib/apt/lists/*
|
||||
# the libs needed by rocksdb (no development headers or statics), and openssl to generate the SSL certificate
|
||||
RUN apt-get update && apt-get -y --no-install-recommends install librocksdb9.10 openssl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV COIN=Palladium
|
||||
ENV NET=mainnet
|
||||
ENV DB_ENGINE=rocksdb
|
||||
ENV DB_DIRECTORY=/var/lib/electrumx
|
||||
ENV SERVICES="tcp://:50001"
|
||||
# SERVICES is set by docker-entrypoint.sh (tcp://:50001 and, with SSL, ssl://:50002), see there for the SSL settings.
|
||||
# set at run time, see docker-compose.yml:
|
||||
#ENV DAEMON_URL="http://user:password@palladiumd:2332/"
|
||||
#ENV CACHE_MB=2000
|
||||
@@ -39,14 +39,18 @@ ENV SERVICES="tcp://:50001"
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=builder /usr/src/app/venv ./venv
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# the data directory is created here so that an empty volume is owned by the electrumx user
|
||||
# the data directories are created here so that empty volumes are owned by the electrumx user
|
||||
RUN useradd --create-home --uid 1000 electrumx \
|
||||
&& mkdir -p "$DB_DIRECTORY" && chown electrumx:electrumx "$DB_DIRECTORY"
|
||||
&& mkdir -p "$DB_DIRECTORY" /var/lib/electrumx-ssl \
|
||||
&& chown electrumx:electrumx "$DB_DIRECTORY" /var/lib/electrumx-ssl
|
||||
USER electrumx
|
||||
VOLUME /var/lib/electrumx
|
||||
VOLUME /var/lib/electrumx-ssl
|
||||
|
||||
# 50001: Electrum protocol over TCP.
|
||||
EXPOSE 50001
|
||||
# 50001: Electrum protocol over TCP, 50002: over SSL.
|
||||
EXPOSE 50001 50002
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["/usr/src/app/venv/bin/electrumx_server"]
|
||||
@@ -28,7 +28,9 @@ as `palladiumd:2332`. RPC and ZMQ of the node are not published on the host, so
|
||||
|
||||
The first start builds the database from the node from genesis, which takes a while; follow it with
|
||||
`docker compose logs -f electrumx`. The database is the `electrumx-db` volume. Electrum clients connect to
|
||||
TCP port 50001 (`ELECTRUMX_TCP_PORT` in `.env`). To announce a public server to the network, set
|
||||
TCP port 50001 (`ELECTRUMX_TCP_PORT` in `.env`) and SSL port 50002 (`ELECTRUMX_SSL_PORT`). SSL is on by default:
|
||||
a self-signed certificate is generated on the first start, kept in the `electrumx-ssl` volume and reused on every
|
||||
later start (or use your own, see [DEPLOY.md](DEPLOY.md)). To announce a public server to the network, set
|
||||
`REPORT_SERVICES` in `.env`. Settings are in `.env` and `docker-compose.yml`; the image defaults are in the
|
||||
`Dockerfile`. Ports, firewall, SSL, RPC credentials and troubleshooting are in [DEPLOY.md](DEPLOY.md).
|
||||
|
||||
|
||||
@@ -11,15 +11,24 @@ services:
|
||||
CACHE_MB: ${CACHE_MB:-1200}
|
||||
# address announced to other servers and clients, if this server is public (tcp://host:50001)
|
||||
REPORT_SERVICES: ${REPORT_SERVICES:-}
|
||||
# SSL: on by default, with a self-signed certificate generated on the first start (docker-entrypoint.sh)
|
||||
SSL_ENABLED: ${SSL_ENABLED:-true}
|
||||
SSL_HOSTNAME: ${SSL_HOSTNAME:-electrumx}
|
||||
# your own certificate instead (paths inside the container, mount the files with an override file)
|
||||
SSL_CERTFILE: ${SSL_CERTFILE:-}
|
||||
SSL_KEYFILE: ${SSL_KEYFILE:-}
|
||||
ports:
|
||||
- "${ELECTRUMX_TCP_PORT:-50001}:50001"
|
||||
- "${ELECTRUMX_SSL_PORT:-50002}:50002"
|
||||
volumes:
|
||||
- electrumx-db:/var/lib/electrumx
|
||||
- electrumx-ssl:/var/lib/electrumx-ssl # the certificate: keep it, clients pin it
|
||||
stop_grace_period: 2m # lets the server flush the db on shutdown
|
||||
networks: [palladium]
|
||||
|
||||
volumes:
|
||||
electrumx-db:
|
||||
electrumx-ssl:
|
||||
|
||||
networks:
|
||||
palladium:
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/sh
|
||||
# Entrypoint of the plm-electrumx image: sets up SSL, then runs the server (the command given to the container).
|
||||
#
|
||||
# SSL_ENABLED true (default) / false. If true, the server also listens on ssl://:50002.
|
||||
# SSL_HOSTNAME name (or IP) written in a generated certificate. Default: electrumx.
|
||||
# SSL_CERTFILE, SSL_KEYFILE
|
||||
# paths of your own certificate and key (mounted in the container). If unset, the files
|
||||
# /var/lib/electrumx-ssl/server.crt and server.key are used; they are generated on the first
|
||||
# start (self-signed, 10 years) and reused on every later start, never overwritten.
|
||||
# SERVICES if unset: tcp://:50001, plus ssl://:50002 when SSL is enabled. If set, it is left as it is.
|
||||
set -eu
|
||||
|
||||
SSL_DIR=/var/lib/electrumx-ssl
|
||||
SSL_ENABLED="${SSL_ENABLED:-true}"
|
||||
SSL_HOSTNAME="${SSL_HOSTNAME:-electrumx}"
|
||||
|
||||
log() { echo "entrypoint: $*"; }
|
||||
|
||||
case "$SSL_ENABLED" in
|
||||
true|TRUE|True|1|yes|on)
|
||||
if [ -n "${SSL_CERTFILE:-}" ] || [ -n "${SSL_KEYFILE:-}" ]; then
|
||||
if [ -z "${SSL_CERTFILE:-}" ] || [ -z "${SSL_KEYFILE:-}" ]; then
|
||||
log "ERROR: set both SSL_CERTFILE and SSL_KEYFILE, or neither" >&2; exit 1
|
||||
fi
|
||||
if [ ! -r "$SSL_CERTFILE" ] || [ ! -r "$SSL_KEYFILE" ]; then
|
||||
log "ERROR: cannot read $SSL_CERTFILE / $SSL_KEYFILE (is it mounted, and readable by uid $(id -u)?)" >&2; exit 1
|
||||
fi
|
||||
log "using the SSL certificate $SSL_CERTFILE"
|
||||
else
|
||||
export SSL_CERTFILE="$SSL_DIR/server.crt"
|
||||
export SSL_KEYFILE="$SSL_DIR/server.key"
|
||||
if [ -e "$SSL_CERTFILE" ] && [ -e "$SSL_KEYFILE" ]; then
|
||||
log "using the existing SSL certificate $SSL_CERTFILE"
|
||||
elif [ -e "$SSL_CERTFILE" ] || [ -e "$SSL_KEYFILE" ]; then
|
||||
log "ERROR: only one of $SSL_CERTFILE and $SSL_KEYFILE exists; not overwriting it. Fix or remove it." >&2; exit 1
|
||||
else
|
||||
case "$SSL_HOSTNAME" in
|
||||
*[!0-9.]*) san="DNS:$SSL_HOSTNAME" ;;
|
||||
*) san="IP:$SSL_HOSTNAME" ;;
|
||||
esac
|
||||
log "generating a self-signed SSL certificate for $SSL_HOSTNAME (valid 10 years) in $SSL_DIR"
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||
-subj "/CN=$SSL_HOSTNAME" -addext "subjectAltName=$san" \
|
||||
-keyout "$SSL_KEYFILE" -out "$SSL_CERTFILE" 2>/dev/null
|
||||
chmod 600 "$SSL_KEYFILE"
|
||||
fi
|
||||
fi
|
||||
log "certificate: $(openssl x509 -in "$SSL_CERTFILE" -noout -subject -enddate | tr '\n' ' ')"
|
||||
log "$(openssl x509 -in "$SSL_CERTFILE" -noout -fingerprint -sha256)"
|
||||
if ! openssl x509 -in "$SSL_CERTFILE" -noout -checkend 2592000 >/dev/null; then
|
||||
log "WARNING: the SSL certificate expires in less than 30 days or is already expired"
|
||||
fi
|
||||
export SERVICES="${SERVICES:-tcp://:50001,ssl://:50002}"
|
||||
;;
|
||||
*)
|
||||
# empty values would be read by the server as "set"
|
||||
unset SSL_CERTFILE SSL_KEYFILE 2>/dev/null || true
|
||||
export SERVICES="${SERVICES:-tcp://:50001}"
|
||||
log "SSL disabled"
|
||||
;;
|
||||
esac
|
||||
|
||||
log "SERVICES=$SERVICES"
|
||||
exec "$@"
|
||||
Reference in new issue
Block a user