200 lines
8 KiB
Bash
Executable file
200 lines
8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# vault-tls-check
|
||
# ----------------------------------------------------------------------
|
||
# Features
|
||
# - Live TLS (and mTLS) handshake check against Vault (or any TLS server)
|
||
# - SNI support
|
||
# - CA file inspection (--show) to print Subject/Issuer/Validity chain
|
||
# - Smart defaults from current user’s HOME (override with flags or env)
|
||
# - Helpful hints & non-zero exit when verification fails
|
||
#
|
||
# Defaults (overridable by flags or env):
|
||
# CA: ${VAULTTLS_CAFILE:-$HOME/vault/ca/ca.pem}
|
||
# mTLS cert: ${VAULTTLS_CERT:-$HOME/vault/mtls/agent.crt}
|
||
# mTLS key: ${VAULTTLS_KEY:-$HOME/vault/mtls/agent.key}
|
||
#
|
||
# Examples (one-liners):
|
||
# - Live verify via Public IP with SNI using current user's defaults:
|
||
# vault-tls-check --addr 109.199.99.183:22300 --sni vault.test.privsec.ch
|
||
# - Live verify via loopback (server cert has 127.0.0.1 SAN):
|
||
# vault-tls-check --addr 127.0.0.1:22300 --sni vault.test.privsec.ch
|
||
# - Show what’s inside your CA file (no network):
|
||
# vault-tls-check --show
|
||
# - Live verify with explicit paths (override defaults):
|
||
# vault-tls-check --addr 109.199.99.183:22300 --sni vault.test.privsec.ch \
|
||
# --cafile /some/ca/chain.pem --mtls-cert /path/agent.crt --mtls-key /path/agent.key
|
||
#
|
||
# Exit codes: 0 OK, 1 verify/connect error, 2 usage error
|
||
# ----------------------------------------------------------------------
|
||
|
||
set -Eeuo pipefail
|
||
|
||
# ---------- styling ----------
|
||
if [[ -t 1 ]]; then
|
||
B=$'\e[1m'; R=$'\e[0m'; BL=$'\e[34m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'
|
||
else B= R= BL= G= Y= E=; fi
|
||
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
|
||
info(){ echo -e "[$(ts)] ${BL}${B}INFO${R} $*"; }
|
||
ok(){ echo -e "[$(ts)] ${G}${B}OK${R} $*"; }
|
||
warn(){ echo -e "[$(ts)] ${Y}${B}WARN${R} $*"; }
|
||
err(){ echo -e "[$(ts)] ${E}${B}ERR${R} $*" >&2; }
|
||
|
||
usage(){
|
||
sed -n '1,120p' "$0" | sed -n '1,80p'
|
||
cat <<USAGE
|
||
|
||
Usage:
|
||
vault-tls-check --addr host:port [--sni name] [--cafile file] [--mtls-cert file] [--mtls-key file] [--timeout N]
|
||
vault-tls-check --show [--cafile file]
|
||
|
||
Options:
|
||
--addr host:port Target address (required for live handshake)
|
||
--sni name SNI / servername to verify CN/SAN properly
|
||
--cafile file CA bundle / chain (defaults to \$HOME/vault/ca/ca.pem)
|
||
--mtls-cert file Client cert for mTLS (defaults to \$HOME/vault/mtls/agent.crt if readable)
|
||
--mtls-key file Client key for mTLS (defaults to \$HOME/vault/mtls/agent.key if readable)
|
||
--timeout N s_client -connect timeout (default: 5 seconds)
|
||
--show Only inspect the CA file (no network)
|
||
-h|--help This help
|
||
|
||
Env overrides (same meaning as flags):
|
||
VAULTTLS_CAFILE, VAULTTLS_CERT, VAULTTLS_KEY
|
||
|
||
Examples:
|
||
vault-tls-check --addr 109.199.99.183:22300 --sni vault.test.privsec.ch
|
||
vault-tls-check --addr 127.0.0.1:22300 --sni vault.test.privsec.ch
|
||
vault-tls-check --show
|
||
vault-tls-check --addr 109.199.99.183:22300 --sni vault.test.privsec.ch --cafile /home/vault/tls-test/ca_chain.pem
|
||
USAGE
|
||
}
|
||
|
||
need(){ command -v "$1" >/dev/null 2>&1 || { err "missing: $1"; exit 2; }; }
|
||
need openssl
|
||
need awk
|
||
need sed
|
||
|
||
ADDR=""
|
||
SNI=""
|
||
SHOW=0
|
||
TIMEOUT=5
|
||
|
||
# Defaults from env → HOME
|
||
CAF_DEFAULT="${VAULTTLS_CAFILE:-${HOME}/vault/ca/ca.pem}"
|
||
CRT_DEFAULT="${VAULTTLS_CERT:-${HOME}/vault/mtls/agent.crt}"
|
||
KEY_DEFAULT="${VAULTTLS_KEY:-${HOME}/vault/mtls/agent.key}"
|
||
|
||
CAF="$CAF_DEFAULT"
|
||
MTLS_CERT=""
|
||
MTLS_KEY=""
|
||
|
||
# ---------- parse args ----------
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--addr) ADDR="$2"; shift 2;;
|
||
--sni) SNI="$2"; shift 2;;
|
||
--cafile) CAF="$2"; shift 2;;
|
||
--mtls-cert) MTLS_CERT="$2"; shift 2;;
|
||
--mtls-key) MTLS_KEY="$2"; shift 2;;
|
||
--timeout) TIMEOUT="$2"; shift 2;;
|
||
--show) SHOW=1; shift;;
|
||
-h|--help) usage; exit 0;;
|
||
*) err "Unknown option: $1"; usage; exit 2;;
|
||
esac
|
||
done
|
||
|
||
# ---------- resolve defaults for mTLS if not provided ----------
|
||
# Only set these if user didn’t pass flags and files exist/readable
|
||
if [[ -z "${MTLS_CERT}" && -r "${CRT_DEFAULT}" ]]; then MTLS_CERT="${CRT_DEFAULT}"; fi
|
||
if [[ -z "${MTLS_KEY}" && -r "${KEY_DEFAULT}" ]]; then MTLS_KEY="${KEY_DEFAULT}"; fi
|
||
|
||
# ---------- CA inspection only ----------
|
||
if (( SHOW == 1 )); then
|
||
if [[ -z "${CAF}" ]]; then CAF="${CAF_DEFAULT}"; fi
|
||
[[ -r "${CAF}" ]] || { err "CA file not readable: ${CAF}"; exit 1; }
|
||
info "Inspecting CA file: ${CAF}"
|
||
# Try to print multiple certs if a bundle
|
||
# shellcheck disable=SC2005
|
||
echo "$(openssl crl2pkcs7 -nocrl -certfile "${CAF}" 2>/dev/null \
|
||
| openssl pkcs7 -print_certs -noout -text 2>/dev/null \
|
||
| sed -n 's/^ *Subject: */Subject: /p; s/^ *Issuer: */Issuer: /p; s/^ *Not Before: */NotBefore: /p; s/^ *Not After : */NotAfter: /p')" \
|
||
|| warn "Could not parse bundle (is it a single PEM?). Falling back to x509 view…"
|
||
if ! openssl x509 -in "${CAF}" -noout -subject -issuer -dates >/dev/null 2>&1; then
|
||
exit 0
|
||
fi
|
||
openssl x509 -in "${CAF}" -noout -subject -issuer -dates
|
||
exit 0
|
||
fi
|
||
|
||
# ---------- live verify mode ----------
|
||
[[ -n "${ADDR}" ]] || { err "--addr is required (or use --show)"; usage; exit 2; }
|
||
[[ -r "${CAF}" ]] || { err "CA file not readable: ${CAF}"; exit 1; }
|
||
|
||
info "Live verify -> ${ADDR} SNI: ${SNI:-<none>}"
|
||
|
||
# Build s_client arguments
|
||
SC_ARGS=( -connect "${ADDR}" -CAfile "${CAF}" -verify_return_error -servername "${SNI:-}" -showcerts -brief -timeout "${TIMEOUT}" )
|
||
# Remove empty -servername if not set
|
||
if [[ -z "${SNI}" ]]; then SC_ARGS=( -connect "${ADDR}" -CAfile "${CAF}" -verify_return_error -showcerts -brief -timeout "${TIMEOUT}" ); fi
|
||
# Add mTLS if provided (or discovered)
|
||
if [[ -n "${MTLS_CERT}" || -n "${MTLS_KEY}" ]]; then
|
||
if [[ ! -r "${MTLS_CERT}" ]]; then err "mtls cert not readable: ${MTLS_CERT:-<unset>}"; exit 1; fi
|
||
if [[ ! -r "${MTLS_KEY}" ]]; then err "mtls key not readable: ${MTLS_KEY:-<unset>}"; exit 1; fi
|
||
SC_ARGS+=( -cert "${MTLS_CERT}" -key "${MTLS_KEY}" )
|
||
fi
|
||
|
||
TMPDIR="$(mktemp -d)"
|
||
trap 'rm -rf "${TMPDIR}"' EXIT
|
||
LEAF="${TMPDIR}/leaf.pem"
|
||
LOGF="${TMPDIR}/sclient.out"
|
||
|
||
# We feed empty stdin so s_client exits after handshake
|
||
if ! printf '' | openssl s_client "${SC_ARGS[@]}" 2>&1 | tee "${LOGF}" >/dev/null; then
|
||
err "openssl s_client failed to connect/verify"
|
||
# Print a concise hint if mTLS likely required
|
||
if grep -qi 'certificate required' "${LOGF}"; then
|
||
warn "mTLS required by server; provide --mtls-cert/--mtls-key (or place them in \$HOME/vault/mtls/)."
|
||
fi
|
||
exit 1
|
||
fi
|
||
|
||
# Extract first PEM cert from output (the leaf)
|
||
awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{exit}' "${LOGF}" > "${LEAF}" || true
|
||
if [[ -s "${LEAF}" ]]; then
|
||
echo "----- Leaf summary -----"
|
||
openssl x509 -in "${LEAF}" -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null || \
|
||
openssl x509 -in "${LEAF}" -noout -subject -issuer -dates 2>/dev/null || true
|
||
echo "------------------------"
|
||
fi
|
||
|
||
# Determine verification outcome
|
||
# Prefer "Verification: OK" (OpenSSL >= 3 brief), else parse "Verify return code"
|
||
if grep -q 'Verification: OK' "${LOGF}"; then
|
||
ok "TLS verification OK"
|
||
exit 0
|
||
fi
|
||
|
||
VERIFY_CODE="$(grep -Eo 'Verify return code: [0-9]+' "${LOGF}" | awk '{print $4}' | tail -n1 || true)"
|
||
if [[ -n "${VERIFY_CODE}" && "${VERIFY_CODE}" != "0" ]]; then
|
||
err "TLS verification FAILED (verify code: ${VERIFY_CODE})"
|
||
# Hints
|
||
if grep -qi 'unable to get issuer certificate' "${LOGF}"; then
|
||
warn "Check CA chain (--cafile). Your CA must contain the issuer(s) of the server cert."
|
||
fi
|
||
if grep -qi 'certificate is valid for 127.0.0.1' "${LOGF}"; then
|
||
warn "Add --sni vault.test.privsec.ch (or include public IP in the cert’s SANs)."
|
||
fi
|
||
if grep -qi 'certificate required' "${LOGF}"; then
|
||
warn "mTLS required by server; supply --mtls-cert/--mtls-key (defaults from \$HOME/vault/mtls/ are auto-used if readable)."
|
||
end
|
||
exit 1
|
||
fi
|
||
|
||
# Fallback: if neither pattern found, assume success when no obvious errors present
|
||
if grep -qiE 'verify error|handshake failure|certificate required|unable to get local issuer|unable to verify' "${LOGF}"; then
|
||
err "TLS verification FAILED (see output above)"
|
||
exit 1
|
||
fi
|
||
|
||
ok "TLS verification OK"
|
||
exit 0
|
||
|