Files
KamadoPool-StartOS-0351/docker_entrypoint.sh
T
satoshi f47699d7b0 Harden TLS cert generation with a version marker and openssl config
The previous fix relied on inspecting the existing cert for a
subjectAltName extension to decide whether to regenerate. That
works but is brittle — it depends on openssl text output format
and on the assumption that SAN is the only thing that could go
wrong. If a future client rejects us for some *other* missing
extension, we'd be stuck on a bad cert again.

Switch to an explicit cert version marker (TLS_CERT_VERSION). Any
time we change the cert shape, we bump the version; the startup
check regenerates whenever the marker file is absent or out of
date. Upgrades self-heal on next boot with no introspection.

Write the full extension set via an openssl config file instead of
-addext flags. -addext is subtly different across openssl versions
(in some builds the extension lands in the CSR rather than the
cert). The config-file path is the documented, portable way to pin
basicConstraints, keyUsage, extendedKeyUsage, subjectKeyIdentifier,
and subjectAltName together.

Also log the resulting extensions at startup so operators can
verify cert sanity from the service logs without exec'ing into the
container, and pin a modern TLS floor in stunnel.conf (no SSL3,
no TLSv1, no TLSv1.1).
2026-04-24 00:45:29 +03:00

266 lines
9.8 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"
MARKER="${TLS_DIR}/cert_version"
mkdir -p "${TLS_DIR}"
# Bump TLS_CERT_VERSION any time the cert format/extensions change.
# The startup check regenerates whenever the marker file is missing
# or doesn't match this version. This is more reliable than poking
# at the existing cert's extensions — we know *exactly* when a new
# shape is required and the upgrade self-heals on next boot.
TLS_CERT_VERSION=3
NEEDS_REGEN=false
if [[ ! -f "${CERT}" || ! -f "${CRT}" || ! -f "${KEY}" ]]; then
NEEDS_REGEN=true
elif [[ ! -f "${MARKER}" ]] \
|| [[ "$(cat "${MARKER}" 2>/dev/null)" != "${TLS_CERT_VERSION}" ]]; then
echo "kamado-entrypoint: TLS cert is older format (want v${TLS_CERT_VERSION}); regenerating"
NEEDS_REGEN=true
fi
if [[ "${NEEDS_REGEN}" == "true" ]]; then
echo "kamado-entrypoint: generating self-signed stratum TLS cert v${TLS_CERT_VERSION}"
# Write the extensions to a config file rather than rely on
# `-addext`: some openssl builds emit them into unpredictable
# locations (e.g. CSR instead of the cert), and this is the
# documented, cross-version way to pin the full extension set.
CONF=$(mktemp)
cat > "${CONF}" <<'OPENSSL_CONF'
[ req ]
default_bits = 2048
default_md = sha256
prompt = no
distinguished_name = req_dn
x509_extensions = v3_cert
[ req_dn ]
CN = kamado-pool
[ v3_cert ]
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = kamado-pool.embassy
DNS.2 = kamado-pool
DNS.3 = localhost
IP.1 = 127.0.0.1
OPENSSL_CONF
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "${KEY}" \
-out "${CRT}" \
-days 3650 \
-config "${CONF}" \
>/dev/null 2>&1
rm -f "${CONF}"
# stunnel 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}"
printf '%s\n' "${TLS_CERT_VERSION}" > "${MARKER}"
# Log the extensions so operators can verify the cert is sane
# from the service logs without needing to exec into the
# container.
echo "kamado-entrypoint: cert extensions:"
openssl x509 -in "${CRT}" -noout -ext subjectAltName,extendedKeyUsage,keyUsage 2>&1 \
| sed 's/^/ /'
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
# Pin a modern TLS floor. Any miner firmware younger than ~2018
# speaks TLS 1.2, and TLS 1.0/1.1 are deprecated anyway.
sslVersion = all
options = NO_SSLv2
options = NO_SSLv3
options = NO_TLSv1
options = NO_TLSv1_1
[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 protocol 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}"