diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh index 5555bc6..d02681b 100755 --- a/docker_entrypoint.sh +++ b/docker_entrypoint.sh @@ -142,36 +142,77 @@ if [[ "${TLS_ENABLED}" == "enabled" ]]; then CRT="${TLS_DIR}/stratum.crt" KEY="${TLS_DIR}/stratum.key" CERT="${TLS_DIR}/stratum.pem" + MARKER="${TLS_DIR}/cert_version" 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. + # 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 ! openssl x509 -in "${CRT}" -noout -ext subjectAltName 2>/dev/null \ - | grep -qE "DNS:|IP:"; then - echo "kamado-entrypoint: existing TLS cert lacks subjectAltName; regenerating" + 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" - openssl req -x509 -newkey rsa:2048 -sha256 -nodes \ + 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 \ - -subj "/CN=kamado-pool-stratum" \ - -addext "subjectAltName = DNS:kamado-pool.embassy, DNS:localhost, IP:127.0.0.1" \ - -addext "extendedKeyUsage = serverAuth" \ + -config "${CONF}" \ >/dev/null 2>&1 - # stunnel happily reads cert+key in either order, but cert-first - # is the convention openssl and most tooling expect. + 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) @@ -185,13 +226,20 @@ 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 layer handles miner auth via username. +# No client-cert auth — stratum over TLS is opportunistic encryption; +# the stratum protocol layer handles miner auth via username. verify = 0 EOF