Files
KamadoPool-StartOS-0351/docker_entrypoint.sh
T
satoshi 1fccebef01 Stratum TLS cert: add subjectAltName, auto-regen on upgrade
Old cert was /CN=kamado-pool-stratum with no SAN extension. Strict
TLS clients (Go, Rust, mbedtls, most modern miner firmwares) treat
missing SAN as a hard validation failure and send TLS alert 42
(bad_certificate), which is exactly what we saw in the stunnel
logs: "SSL_accept: ... error:0A000412: ... sslv3 alert bad
certificate".

Generate the cert with subjectAltName covering kamado-pool.embassy,
localhost, and 127.0.0.1, and an explicit
extendedKeyUsage=serverAuth. Swap the combined PEM so the cert
comes before the key (convention). Upgrades auto-heal: if an
existing cert lacks SAN, the entrypoint regenerates it on next
start — the fingerprint changes and the user needs to re-pin on
their miner, but the previous cert couldn't complete a handshake
anyway so there's nothing to lose.

Also make stunnel's intent explicit with verify=0 so anyone reading
the config knows we're doing opportunistic TLS, not client-cert
auth; miner auth is the stratum layer's job.
2026-04-23 02:12:44 +03:00

218 lines
8.4 KiB
Bash
Executable File

#!/bin/bash
# Kamado Pool StartOS entrypoint.
#
# Reads /root/.kamado/start9/config.yaml via yq, resolves the chosen
# bitcoind variant (mainnet vs testnet4), exports the env vars the
# upstream ckpool entrypoint expects, renders ckpool.conf from the
# template shipped alongside this script, and then supervises
# ckpool-solo + kamado-api as a pair.
set -euo pipefail
CONFIG_FILE="/root/.kamado/start9/config.yaml"
if [[ ! -f "${CONFIG_FILE}" ]]; then
echo "kamado-entrypoint: config file missing: ${CONFIG_FILE}" >&2
exit 1
fi
q() { yq -r "$1" "${CONFIG_FILE}"; }
BITCOIND_VARIANT=$(q '.bitcoind.type')
case "${BITCOIND_VARIANT}" in
bitcoind)
export BITCOIN_RPC_HOST="bitcoind.embassy"
export BITCOIN_RPC_PORT=8332
# Satoshi's genesis block coinbase address. Used only for
# ckpool's startup coinbase-builder self-test; never credited
# a satoshi since solo mode pays the worker's stratum address.
SELFTEST_ADDRESS="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
;;
bitcoind-testnet)
export BITCOIN_RPC_HOST="bitcoind-testnet.embassy"
export BITCOIN_RPC_PORT=48332
# Testnet genesis coinbase address — valid P2PKH on testnet4.
SELFTEST_ADDRESS="mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn"
;;
*)
echo "kamado-entrypoint: unknown bitcoind variant: ${BITCOIND_VARIANT}" >&2
exit 1
;;
esac
export BITCOIN_RPC_USER=$(q '.bitcoind.user')
export BITCOIN_RPC_PASSWORD=$(q '.bitcoind.password')
export STRATUM_PORT=$(q '.stratum-port // 3333')
export POOL_BTCSIG=$(q '.advanced.pool-identifier // "/Kamado/"')
export STARTDIFF=$(q '.advanced.startdiff // 16384')
export MINDIFF=$(q '.advanced.mindiff // 1000')
export MAXDIFF=$(q '.advanced.maxdiff // 0')
export DROPIDLE=$(q '.advanced.dropidle // 0')
LOG_LEVEL=$(q '.advanced.log-level // "info"')
ZMQ_ENABLED=$(q '.advanced.zmq-enabled // true')
TLS_ENABLED=$(q '.tls.enabled // "disabled"')
TLS_PORT=$(q '.tls.port // 3334')
# CKPool-solo uses the worker's stratum username as the payout
# address and refuses to authenticate workers whose username is not
# a valid address on the active network. The conf `btcaddress` is
# only consulted once at startup, for ckpool's coinbase-builder
# self-test: it builds and validates a sample coinbase transaction
# against bitcoind before accepting any workers. Since no worker has
# connected yet at that point, we hand it the genesis block coinbase
# address for the active network — it's always valid, and solo mode
# never credits it a satoshi.
export POOL_BTCADDRESS="${SELFTEST_ADDRESS}"
# LOGDIR lives on the ckpool data volume so ckpool's own state files
# (users/, workers/, pool/pool.status, daily log files) survive restart.
# SOCKET_DIR is ephemeral — sockets are re-created on each start.
export LOGDIR=/root/.ckpool/logs
export SOCKET_DIR=/run/ckpool
# ckpool has TWO independent new-block detection paths. Wire up both
# so we're never blind to a tip change (every second of stale work
# in solo mode is hashrate burned on a dead block).
#
# 1. Blockpoll thread: polls bitcoind's getbestblockhash every
# BLOCKPOLL_MS. This only runs when notify=false — with
# notify=true the thread sleeps 5s and returns immediately.
# So keep notify=false.
# 2. ZMQ hashblock subscriber: instant push notifications from
# bitcoind. ckpool defaults zmqblock to tcp://127.0.0.1:28332
# which is useless in this container, so point it at the real
# endpoint explicitly.
export BITCOIN_NOTIFY=false
export ZMQ_BLOCK="tcp://${BITCOIN_RPC_HOST}:28332"
export BLOCKPOLL_MS=100
export UPDATE_INTERVAL_S=30
mkdir -p "${LOGDIR}" "${SOCKET_DIR}" /etc/ckpool
# Render ckpool.conf using the same sed approach as the upstream
# KamadoPool ckpool entrypoint — the template is bundled into the
# image at build time.
TEMPLATE=/etc/ckpool/ckpool.conf.template
CONF=/etc/ckpool/ckpool.conf
sed \
-e "s|\${BITCOIN_RPC_HOST}|${BITCOIN_RPC_HOST}|g" \
-e "s|\${BITCOIN_RPC_PORT}|${BITCOIN_RPC_PORT}|g" \
-e "s|\${BITCOIN_RPC_USER}|${BITCOIN_RPC_USER}|g" \
-e "s|\${BITCOIN_RPC_PASSWORD}|${BITCOIN_RPC_PASSWORD}|g" \
-e "s|\${BITCOIN_NOTIFY}|${BITCOIN_NOTIFY}|g" \
-e "s|\${POOL_BTCADDRESS}|${POOL_BTCADDRESS}|g" \
-e "s|\${POOL_BTCSIG}|${POOL_BTCSIG}|g" \
-e "s|\${BLOCKPOLL_MS}|${BLOCKPOLL_MS}|g" \
-e "s|\${UPDATE_INTERVAL_S}|${UPDATE_INTERVAL_S}|g" \
-e "s|\${STRATUM_PORT}|${STRATUM_PORT}|g" \
-e "s|\${MINDIFF}|${MINDIFF}|g" \
-e "s|\${STARTDIFF}|${STARTDIFF}|g" \
-e "s|\${MAXDIFF}|${MAXDIFF}|g" \
-e "s|\${DROPIDLE}|${DROPIDLE}|g" \
-e "s|\${LOGDIR}|${LOGDIR}|g" \
-e "s|\${ZMQ_BLOCK}|${ZMQ_BLOCK}|g" \
"${TEMPLATE}" > "${CONF}"
echo "kamado-entrypoint: starting ckpool (solo, ${BITCOIND_VARIANT}) on port ${STRATUM_PORT}"
/usr/local/bin/ckpool --btcsolo --config "${CONF}" --sockdir "${SOCKET_DIR}" --log-shares &
CKPOOL_PID=$!
# DB_PATH must live on the persisted main volume; the default
# /var/lib/kamado/kamado.db is ephemeral container storage.
KAMADO_DATA_DIR=/root/.kamado/data
mkdir -p "${KAMADO_DATA_DIR}"
export LISTEN_ADDR=":8080"
export CKPOOL_SOCKDIR="${SOCKET_DIR}"
export CKPOOL_LOGFILE="${LOGDIR}/ckpool.log"
export DB_PATH="${KAMADO_DATA_DIR}/kamado.db"
export BITCOIN_RPC_URL="http://${BITCOIN_RPC_HOST}:${BITCOIN_RPC_PORT}"
export POLL_INTERVAL=5s
export KAMADO_LOG_LEVEL="${LOG_LEVEL}"
if [[ "${ZMQ_ENABLED}" == "true" ]]; then
export BITCOIN_ZMQ_BLOCK="tcp://${BITCOIN_RPC_HOST}:28332"
else
export BITCOIN_ZMQ_BLOCK=""
fi
echo "kamado-entrypoint: starting kamado-api"
/usr/local/bin/kamado-api &
API_PID=$!
# Optional TLS stratum via stunnel sidecar.
STUNNEL_PID=""
if [[ "${TLS_ENABLED}" == "enabled" ]]; then
TLS_DIR=/root/.kamado/tls
CRT="${TLS_DIR}/stratum.crt"
KEY="${TLS_DIR}/stratum.key"
CERT="${TLS_DIR}/stratum.pem"
mkdir -p "${TLS_DIR}"
# Regenerate when the cert is missing, OR when an older cert lacks
# a subjectAltName extension. Strict TLS clients (Go, Rust,
# mbedtls, most modern miner firmwares) reject CN-only certs with
# a "bad certificate" alert, which is what we saw in the logs on
# older Kamado installs.
NEEDS_REGEN=false
if [[ ! -f "${CERT}" || ! -f "${CRT}" || ! -f "${KEY}" ]]; then
NEEDS_REGEN=true
elif ! openssl x509 -in "${CRT}" -noout -ext subjectAltName 2>/dev/null \
| grep -qE "DNS:|IP:"; then
echo "kamado-entrypoint: existing TLS cert lacks subjectAltName; regenerating"
NEEDS_REGEN=true
fi
if [[ "${NEEDS_REGEN}" == "true" ]]; then
echo "kamado-entrypoint: generating self-signed stratum TLS cert"
openssl req -x509 -newkey rsa:2048 -sha256 -nodes \
-keyout "${KEY}" \
-out "${CRT}" \
-days 3650 \
-subj "/CN=kamado-pool-stratum" \
-addext "subjectAltName = DNS:kamado-pool.embassy, DNS:localhost, IP:127.0.0.1" \
-addext "extendedKeyUsage = serverAuth" \
>/dev/null 2>&1
# stunnel happily reads cert+key in either order, but cert-first
# is the convention openssl and most tooling expect.
cat "${CRT}" "${KEY}" > "${CERT}"
chmod 600 "${KEY}" "${CERT}"
fi
FINGERPRINT=$(openssl x509 -in "${CRT}" -noout -fingerprint -sha256 | cut -d= -f2)
printf '%s\n' "${FINGERPRINT}" > "${TLS_DIR}/fingerprint.txt"
echo "kamado-entrypoint: stratum TLS SHA256 fingerprint: ${FINGERPRINT}"
STUNNEL_CONF=/etc/stunnel/stratum.conf
mkdir -p /etc/stunnel
cat > "${STUNNEL_CONF}" <<EOF
foreground = yes
pid =
output = /dev/stdout
debug = 4
[stratum]
accept = 0.0.0.0:${TLS_PORT}
connect = 127.0.0.1:${STRATUM_PORT}
cert = ${CERT}
# No client-cert auth — stratum over TLS is opportunistic encryption,
# the stratum layer handles miner auth via username.
verify = 0
EOF
echo "kamado-entrypoint: starting stunnel on :${TLS_PORT} -> :${STRATUM_PORT}"
/usr/bin/stunnel4 "${STUNNEL_CONF}" &
STUNNEL_PID=$!
fi
term() {
echo "kamado-entrypoint: SIGTERM — shutting down"
kill -TERM "${API_PID}" "${CKPOOL_PID}" ${STUNNEL_PID:-} 2>/dev/null || true
wait "${API_PID}" "${CKPOOL_PID}" ${STUNNEL_PID:-} 2>/dev/null || true
exit 0
}
trap term TERM INT
# shellcheck disable=SC2086
wait -n ${CKPOOL_PID} ${API_PID} ${STUNNEL_PID:-}
EXIT_CODE=$?
echo "kamado-entrypoint: a supervised process exited (${EXIT_CODE}), stopping the rest"
kill -TERM "${API_PID}" "${CKPOOL_PID}" ${STUNNEL_PID:-} 2>/dev/null || true
wait || true
exit "${EXIT_CODE}"