quorate/scripts/trust-internal-ca.sh

313 lines
12 KiB
Shell
Raw Permalink Normal View History

#!/usr/bin/env bash
# Fetch and system-trust the internal root CA from Vault.
#
# Usage: ./trust-internal-ca.sh [--expected-sha256 AA:BB:...] [--kube-context NAME]
# [--non-interactive] [--no-browsers] [--no-java]
set -euo pipefail
# ── Config ────────────────────────────────────────────────────────────────────
CERT_URL="https://vault.quorate.internal/v1/pki/ca/pem"
CERT_NAME="internal-root-ca"
EXPECTED_CN="Internal Root CA"
KUBE_CONTEXT="${KUBE_CONTEXT:-quorate}"
# ── Flags ─────────────────────────────────────────────────────────────────────
EXPECTED_FP=""
NON_INTERACTIVE=false
SKIP_BROWSERS=false
SKIP_JAVA=false
FETCHED_INSECURELY=false
while [[ $# -gt 0 ]]; do
case "$1" in
--expected-sha256) EXPECTED_FP="$2"; shift 2 ;;
--kube-context) KUBE_CONTEXT="$2"; shift 2 ;;
--non-interactive) NON_INTERACTIVE=true; shift ;;
--no-browsers) SKIP_BROWSERS=true; shift ;;
--no-java) SKIP_JAVA=true; shift ;;
*) echo "Unknown flag: $1" >&2; exit 1 ;;
esac
done
# ── Helpers ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
die() { echo -e "${RED}[✗]${NC} $*" >&2; exit 1; }
hr() { echo -e "${BOLD}────────────────────────────────────────────────────────${NC}"; }
require() { command -v "$1" &>/dev/null || die "Required tool not found: $1 — install it and retry"; }
confirm() {
local prompt="$1"
if $NON_INTERACTIVE; then
info "(non-interactive) auto-confirming: $prompt"
return 0
fi
read -r -p "$prompt [yes/N] " ans
[[ "${ans,,}" == "yes" ]] || die "Aborted by user."
}
require curl
require openssl
# ── Fetch ─────────────────────────────────────────────────────────────────────
TMPDIR_WORK=$(mktemp -d)
trap 'rm -rf "$TMPDIR_WORK"' EXIT
CERT_FILE="$TMPDIR_WORK/${CERT_NAME}.crt"
info "Fetching CA certificate from $CERT_URL ..."
if ! curl -fsSL --max-time 15 "$CERT_URL" -o "$CERT_FILE"; then
[[ -n "$EXPECTED_FP" ]] \
|| die "TLS verification failed. Retry with an out-of-band --expected-sha256 pin."
warn "TLS verification failed; retrying with the supplied fingerprint pin."
curl --insecure -fsSL --max-time 15 "$CERT_URL" -o "$CERT_FILE" \
|| die "Fetch failed — is vault.quorate.internal reachable?"
FETCHED_INSECURELY=true
fi
# ── Structural validation ─────────────────────────────────────────────────────
openssl x509 -in "$CERT_FILE" -noout 2>/dev/null \
|| die "Fetched content is not a valid PEM certificate"
IS_CA=$(openssl x509 -in "$CERT_FILE" -noout -text | grep -c "CA:TRUE" || true)
[[ "$IS_CA" -gt 0 ]] || die "Certificate does not have CA:TRUE — refusing to install a non-CA cert"
ACTUAL_CN=$(openssl x509 -in "$CERT_FILE" -noout -subject \
| sed -E 's/.*[Cc][Nn]\s*=\s*([^,\/]+).*/\1/')
if [[ "$ACTUAL_CN" != "$EXPECTED_CN" ]]; then
warn "Subject CN mismatch — expected '${EXPECTED_CN}', got '${ACTUAL_CN}'"
confirm "CN does not match. Continue anyway?"
fi
# ── Fingerprint display & verification ───────────────────────────────────────
SHA256=$(openssl x509 -in "$CERT_FILE" -noout -fingerprint -sha256 | cut -d= -f2)
SHA1=$(openssl x509 -in "$CERT_FILE" -noout -fingerprint -sha1 | cut -d= -f2)
hr
echo -e "${BOLD}Certificate details:${NC}"
openssl x509 -in "$CERT_FILE" -noout -subject -issuer -dates 2>/dev/null | sed 's/^/ /'
echo ""
echo -e " ${BOLD}SHA-256:${NC} $SHA256"
echo -e " ${BOLD}SHA-1: ${NC} $SHA1"
hr
# Pinned fingerprint check (if --expected-sha256 was passed)
if [[ -n "$EXPECTED_FP" ]]; then
EXPECTED_NORM="${EXPECTED_FP^^}" # normalise to upper
ACTUAL_NORM="${SHA256^^}"
if [[ "$EXPECTED_NORM" == "$ACTUAL_NORM" ]]; then
info "Fingerprint matches --expected-sha256."
else
die "Fingerprint MISMATCH — expected $EXPECTED_FP, got $SHA256"
fi
fi
# ── kubectl cross-verify (best-effort) ───────────────────────────────────────
if command -v kubectl &>/dev/null; then
info "kubectl detected — attempting cross-verify against cluster ..."
CLUSTER_FP=""
VAULT_POD=$(kubectl --context "$KUBE_CONTEXT" get pod -n vault \
-l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}' \
2>/dev/null || true)
if [[ -n "$VAULT_POD" ]]; then
CLUSTER_CA_PEM=$(kubectl --context "$KUBE_CONTEXT" exec -n vault "$VAULT_POD" -- \
vault read -address=https://vault-active-tls.vault.svc.cluster.local:443 \
-ca-cert=/vault/userconfig/vault-server-tls/ca.crt \
-tls-server-name=vault-active-direct.vault.svc.cluster.local \
-field=certificate pki/cert/ca 2>/dev/null || true)
if [[ -n "$CLUSTER_CA_PEM" ]]; then
CLUSTER_FP=$(echo "$CLUSTER_CA_PEM" \
| openssl x509 -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2 || true)
[[ -n "$CLUSTER_FP" ]] && info "Cross-verify source: vault pod PKI CA"
fi
fi
if [[ -n "$CLUSTER_FP" ]]; then
if [[ "${CLUSTER_FP^^}" == "${SHA256^^}" ]]; then
info "Cluster cross-verify PASSED — fingerprints match."
else
warn "Cluster cross-verify FAILED — fingerprint from cluster: $CLUSTER_FP"
warn "Fingerprint from HTTP: $SHA256"
confirm "Fingerprints differ. Possible MITM or stale cert. Continue anyway?"
fi
else
warn "Could not retrieve CA cert from cluster for cross-verify."
warn "Manually check with: kubectl --context $KUBE_CONTEXT exec -n vault <pod> -- vault read pki/cert/ca"
fi
fi
# ── User confirmation ─────────────────────────────────────────────────────────
echo ""
if $FETCHED_INSECURELY; then
warn "TLS verification was disabled for bootstrap; the supplied SHA-256 pin matched."
fi
echo ""
confirm "Install and trust this CA certificate system-wide?"
# ── OS trust store install ────────────────────────────────────────────────────
install_debian() {
local dest="/usr/local/share/ca-certificates/${CERT_NAME}.crt"
info "Installing to $dest (Debian/Ubuntu)"
sudo install -m 644 "$CERT_FILE" "$dest"
sudo update-ca-certificates
}
install_rhel() {
local dest="/etc/pki/ca-trust/source/anchors/${CERT_NAME}.crt"
info "Installing to $dest (RHEL/Fedora)"
sudo install -m 644 "$CERT_FILE" "$dest"
sudo update-ca-trust extract
}
install_arch() {
local dest="/etc/ca-certificates/trust-source/anchors/${CERT_NAME}.crt"
info "Installing to $dest (Arch Linux)"
sudo install -m 644 "$CERT_FILE" "$dest"
sudo trust extract-compat
}
install_suse() {
local dest="/etc/pki/trust/anchors/${CERT_NAME}.crt"
info "Installing to $dest (openSUSE)"
sudo install -m 644 "$CERT_FILE" "$dest"
sudo update-ca-certificates
}
install_macos() {
info "Installing to macOS System Keychain (covers Safari and Chrome)..."
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain "$CERT_FILE"
}
install_linux() {
# shellcheck source=/dev/null
. /etc/os-release
local id="${ID:-unknown}" like="${ID_LIKE:-}"
if [[ "$id" == "debian" || "$id" == "ubuntu" || "$like" == *"debian"* ]]; then install_debian
elif [[ "$id" == "fedora" || "$like" == *"fedora"* || "$like" == *"rhel"* || "$id" == "rhel" || "$id" == "centos" ]]; then install_rhel
elif [[ "$id" == "arch" || "$like" == *"arch"* ]]; then install_arch
elif [[ "$id" == "opensuse"* || "$like" == *"suse"* ]]; then install_suse
else
warn "Unknown distro '$id' — attempting Debian-style install"
install_debian
fi
}
# ── NSS (Firefox / Chrome on Linux) ──────────────────────────────────────────
install_nss() {
if ! command -v certutil &>/dev/null; then
warn "certutil not found — skipping NSS databases."
warn " Debian/Ubuntu: sudo apt install libnss3-tools"
warn " RHEL/Fedora: sudo dnf install nss-tools"
return
fi
local added=0
# Collect NSS database directories from Firefox and Chrome/Chromium
local nss_dirs=()
# Firefox profiles (standard, snap, flatpak)
while IFS= read -r -d '' db_dir; do
nss_dirs+=("$db_dir")
done < <(find \
"$HOME/.mozilla/firefox" \
"$HOME/snap/firefox/common/.mozilla/firefox" \
"$HOME/.var/app/org.mozilla.firefox/.mozilla/firefox" \
2>/dev/null -maxdepth 2 -name "cert9.db" -printf '%h\0' || true)
# Chrome/Chromium shared NSS db
for chrome_db in \
"$HOME/.pki/nssdb" \
"$HOME/.config/google-chrome/Default" \
"$HOME/.config/chromium/Default" \
"$HOME/snap/chromium/current/.pki/nssdb"; do
[[ -d "$chrome_db" ]] && nss_dirs+=("$chrome_db")
done
if [[ ${#nss_dirs[@]} -eq 0 ]]; then
warn "No NSS databases found under \$HOME."
return
fi
for db_dir in "${nss_dirs[@]}"; do
info " NSS: $db_dir"
# Remove any stale entry first (ignore failure if not present)
certutil -D -n "$CERT_NAME" -d "sql:$db_dir" 2>/dev/null || true
if certutil -A -n "$CERT_NAME" -t "CT,," -i "$CERT_FILE" -d "sql:$db_dir" 2>/dev/null; then
((added++))
else
warn "Failed to add cert to $db_dir"
fi
done
info "Added to $added NSS database(s)."
info "Restart Firefox/Chrome for the change to take effect."
}
# ── Java truststore ───────────────────────────────────────────────────────────
install_java() {
local keytool
keytool=$(command -v keytool 2>/dev/null || true)
if [[ -z "$keytool" ]]; then
warn "keytool not found — skipping Java truststore."
return
fi
# Find JAVA_HOME cacerts (handles OpenJDK layout and SDKMAN)
local cacerts=""
for candidate in \
"${JAVA_HOME:-}/lib/security/cacerts" \
"${JAVA_HOME:-}/jre/lib/security/cacerts" \
"$(dirname "$(readlink -f "$(command -v java)")")/../lib/security/cacerts" \
"$(dirname "$(readlink -f "$(command -v java)")")/../jre/lib/security/cacerts"; do
[[ -f "$candidate" ]] && { cacerts="$candidate"; break; }
done
if [[ -z "$cacerts" ]]; then
warn "Could not locate Java cacerts file — skipping Java truststore."
return
fi
info "Adding to Java truststore: $cacerts"
sudo keytool -importcert -noprompt \
-alias "$CERT_NAME" \
-file "$CERT_FILE" \
-keystore "$cacerts" \
-storepass changeit \
&& info "Java truststore updated." \
|| warn "Java truststore update failed (may already be present)."
}
# ── Main ──────────────────────────────────────────────────────────────────────
OS=$(uname -s)
case "$OS" in
Linux) install_linux ;;
Darwin) install_macos ;;
*) die "Unsupported OS: $OS" ;;
esac
if [[ "$OS" == "Linux" ]] && ! $SKIP_BROWSERS; then
echo ""
if $NON_INTERACTIVE; then
install_nss
else
read -r -p "Also install into Firefox/Chrome NSS databases for $USER? [y/N] " ans
[[ "${ans,,}" == "y" ]] && install_nss || true
fi
fi
if ! $SKIP_JAVA; then
echo ""
if $NON_INTERACTIVE; then
install_java
else
read -r -p "Also install into Java truststore? [y/N] " ans
[[ "${ans,,}" == "y" ]] && install_java || true
fi
fi
hr
info "CA certificate installed. New processes and tools will trust it immediately."
info "Browser sessions and Java apps need a restart to pick up the change."