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).
This commit is contained in:
+65
-17
@@ -142,36 +142,77 @@ if [[ "${TLS_ENABLED}" == "enabled" ]]; then
|
|||||||
CRT="${TLS_DIR}/stratum.crt"
|
CRT="${TLS_DIR}/stratum.crt"
|
||||||
KEY="${TLS_DIR}/stratum.key"
|
KEY="${TLS_DIR}/stratum.key"
|
||||||
CERT="${TLS_DIR}/stratum.pem"
|
CERT="${TLS_DIR}/stratum.pem"
|
||||||
|
MARKER="${TLS_DIR}/cert_version"
|
||||||
mkdir -p "${TLS_DIR}"
|
mkdir -p "${TLS_DIR}"
|
||||||
|
|
||||||
# Regenerate when the cert is missing, OR when an older cert lacks
|
# Bump TLS_CERT_VERSION any time the cert format/extensions change.
|
||||||
# a subjectAltName extension. Strict TLS clients (Go, Rust,
|
# The startup check regenerates whenever the marker file is missing
|
||||||
# mbedtls, most modern miner firmwares) reject CN-only certs with
|
# or doesn't match this version. This is more reliable than poking
|
||||||
# a "bad certificate" alert, which is what we saw in the logs on
|
# at the existing cert's extensions — we know *exactly* when a new
|
||||||
# older Kamado installs.
|
# shape is required and the upgrade self-heals on next boot.
|
||||||
|
TLS_CERT_VERSION=3
|
||||||
|
|
||||||
NEEDS_REGEN=false
|
NEEDS_REGEN=false
|
||||||
if [[ ! -f "${CERT}" || ! -f "${CRT}" || ! -f "${KEY}" ]]; then
|
if [[ ! -f "${CERT}" || ! -f "${CRT}" || ! -f "${KEY}" ]]; then
|
||||||
NEEDS_REGEN=true
|
NEEDS_REGEN=true
|
||||||
elif ! openssl x509 -in "${CRT}" -noout -ext subjectAltName 2>/dev/null \
|
elif [[ ! -f "${MARKER}" ]] \
|
||||||
| grep -qE "DNS:|IP:"; then
|
|| [[ "$(cat "${MARKER}" 2>/dev/null)" != "${TLS_CERT_VERSION}" ]]; then
|
||||||
echo "kamado-entrypoint: existing TLS cert lacks subjectAltName; regenerating"
|
echo "kamado-entrypoint: TLS cert is older format (want v${TLS_CERT_VERSION}); regenerating"
|
||||||
NEEDS_REGEN=true
|
NEEDS_REGEN=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${NEEDS_REGEN}" == "true" ]]; then
|
if [[ "${NEEDS_REGEN}" == "true" ]]; then
|
||||||
echo "kamado-entrypoint: generating self-signed stratum TLS cert"
|
echo "kamado-entrypoint: generating self-signed stratum TLS cert v${TLS_CERT_VERSION}"
|
||||||
openssl req -x509 -newkey rsa:2048 -sha256 -nodes \
|
# 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}" \
|
-keyout "${KEY}" \
|
||||||
-out "${CRT}" \
|
-out "${CRT}" \
|
||||||
-days 3650 \
|
-days 3650 \
|
||||||
-subj "/CN=kamado-pool-stratum" \
|
-config "${CONF}" \
|
||||||
-addext "subjectAltName = DNS:kamado-pool.embassy, DNS:localhost, IP:127.0.0.1" \
|
|
||||||
-addext "extendedKeyUsage = serverAuth" \
|
|
||||||
>/dev/null 2>&1
|
>/dev/null 2>&1
|
||||||
# stunnel happily reads cert+key in either order, but cert-first
|
rm -f "${CONF}"
|
||||||
# is the convention openssl and most tooling expect.
|
|
||||||
|
# stunnel reads cert+key in either order, but cert-first is the
|
||||||
|
# convention openssl and most tooling expect.
|
||||||
cat "${CRT}" "${KEY}" > "${CERT}"
|
cat "${CRT}" "${KEY}" > "${CERT}"
|
||||||
chmod 600 "${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
|
fi
|
||||||
|
|
||||||
FINGERPRINT=$(openssl x509 -in "${CRT}" -noout -fingerprint -sha256 | cut -d= -f2)
|
FINGERPRINT=$(openssl x509 -in "${CRT}" -noout -fingerprint -sha256 | cut -d= -f2)
|
||||||
@@ -185,13 +226,20 @@ foreground = yes
|
|||||||
pid =
|
pid =
|
||||||
output = /dev/stdout
|
output = /dev/stdout
|
||||||
debug = 4
|
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]
|
[stratum]
|
||||||
accept = 0.0.0.0:${TLS_PORT}
|
accept = 0.0.0.0:${TLS_PORT}
|
||||||
connect = 127.0.0.1:${STRATUM_PORT}
|
connect = 127.0.0.1:${STRATUM_PORT}
|
||||||
cert = ${CERT}
|
cert = ${CERT}
|
||||||
# No client-cert auth — stratum over TLS is opportunistic encryption,
|
# No client-cert auth — stratum over TLS is opportunistic encryption;
|
||||||
# the stratum layer handles miner auth via username.
|
# the stratum protocol layer handles miner auth via username.
|
||||||
verify = 0
|
verify = 0
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user