#!/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 -- 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."