ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
313
scripts/trust-internal-ca.sh
Executable file
313
scripts/trust-internal-ca.sh
Executable file
|
|
@ -0,0 +1,313 @@
|
|||
#!/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."
|
||||
Loading…
Reference in a new issue