183 lines
6.8 KiB
Bash
183 lines
6.8 KiB
Bash
#!/bin/bash
|
||
# =============================================================================
|
||
# WOOO AIOps - 公開路由 / DNS / TLS 證據備份
|
||
# 2026-05-06 ogt + Codex: 補齊 external route reconstruction evidence。
|
||
#
|
||
# 安全原則:
|
||
# - 只做 read-only DNS/HTTP/TLS/nginx route map 匯出,不改 DNS。
|
||
# - 不需要 registrar/CDN token;若未設定 API token,只記錄缺口。
|
||
# - TLS private keys 不在此腳本輸出;private keys 由 encrypted configs 備份處理。
|
||
# =============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
source "$(dirname "$0")/common.sh"
|
||
|
||
SERVICE="public-routes"
|
||
LOCAL_REPO="${BACKUP_BASE}/public-routes"
|
||
DUMP_DIR="/tmp/public-routes-backup-$$"
|
||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8)
|
||
K8S_BACKUP_HOSTS="${K8S_BACKUP_HOSTS:-192.168.0.120 192.168.0.121 192.168.0.125}"
|
||
|
||
DOMAINS=(
|
||
"awoooi.wooo.work"
|
||
"mo.wooo.work"
|
||
"gitea.wooo.work"
|
||
"harbor.wooo.work"
|
||
"registry.wooo.work"
|
||
"sentry.wooo.work"
|
||
"signoz.wooo.work"
|
||
"stock.wooo.work"
|
||
"langfuse.wooo.work"
|
||
"bitan.wooo.work"
|
||
"aiops.wooo.work"
|
||
)
|
||
|
||
cleanup() {
|
||
rm -rf "${DUMP_DIR}"
|
||
}
|
||
|
||
low_priority() {
|
||
if command -v ionice >/dev/null 2>&1; then
|
||
ionice -c2 -n7 nice -n 10 "$@"
|
||
else
|
||
nice -n 10 "$@"
|
||
fi
|
||
}
|
||
|
||
capture_cmd() {
|
||
local label="$1"
|
||
shift
|
||
if "$@" > "${DUMP_DIR}/${label}.txt" 2>&1; then
|
||
log_success "Public routes 盤點完成: ${label}"
|
||
else
|
||
log_warn "Public routes 盤點失敗: ${label}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
capture_remote_cmd() {
|
||
local host="$1"
|
||
local label="$2"
|
||
local cmd="$3"
|
||
if ssh "${SSH_OPTS[@]}" "${host}" "${cmd}" > "${DUMP_DIR}/${label}.txt" 2>&1; then
|
||
log_success "Public routes 遠端盤點完成: ${label}"
|
||
else
|
||
log_warn "Public routes 遠端盤點失敗: ${label}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
capture_k8s_ingress_summary() {
|
||
local k8s_host
|
||
local cmd="sudo -n kubectl get ingress -A -o wide 2>/dev/null || kubectl get ingress -A -o wide"
|
||
for k8s_host in ${K8S_BACKUP_HOSTS}; do
|
||
if capture_remote_cmd "wooo@${k8s_host}" "cluster-k3s-ingress-summary" "${cmd}"; then
|
||
printf 'source_host=%s\n' "${k8s_host}" > "${DUMP_DIR}/cluster-k3s-ingress-summary.source"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
main() {
|
||
local start_time
|
||
local timestamp
|
||
local failed=0
|
||
start_time=$(date +%s)
|
||
timestamp=$(date "+%Y%m%d_%H%M%S")
|
||
|
||
trap cleanup EXIT
|
||
install -d -m 700 "${DUMP_DIR}"
|
||
|
||
log_info "========== 開始 Public routes 備份 (${timestamp}) =========="
|
||
|
||
{
|
||
echo "domain,record_type,answer"
|
||
for domain in "${DOMAINS[@]}"; do
|
||
if command -v dig >/dev/null 2>&1; then
|
||
for rrtype in A AAAA CNAME; do
|
||
dig +short "${rrtype}" "${domain}" | sed "s#^#${domain},${rrtype},#"
|
||
done
|
||
else
|
||
getent ahosts "${domain}" 2>/dev/null | awk -v d="${domain}" '{print d ",A_OR_AAAA," $1}' | sort -u
|
||
fi
|
||
done
|
||
} > "${DUMP_DIR}/dns-answers.csv"
|
||
log_success "Public routes DNS answers 匯出完成"
|
||
|
||
{
|
||
echo "domain,http_code,total_time,remote_ip"
|
||
for domain in "${DOMAINS[@]}"; do
|
||
curl -k -sS -o /dev/null \
|
||
--connect-timeout 5 \
|
||
--max-time 10 \
|
||
-w "${domain},%{http_code},%{time_total},%{remote_ip}\n" \
|
||
"https://${domain}/" || echo "${domain},000,0,unreachable"
|
||
done
|
||
} > "${DUMP_DIR}/https-status.csv"
|
||
log_success "Public routes HTTPS status 匯出完成"
|
||
|
||
{
|
||
echo "domain,not_before,not_after,issuer,subject"
|
||
for domain in "${DOMAINS[@]}"; do
|
||
cert_text=$(timeout 10 openssl s_client -servername "${domain}" -connect "${domain}:443" </dev/null 2>/dev/null | openssl x509 -noout -dates -issuer -subject 2>/dev/null || true)
|
||
not_before=$(printf "%s\n" "${cert_text}" | sed -n 's/^notBefore=//p')
|
||
not_after=$(printf "%s\n" "${cert_text}" | sed -n 's/^notAfter=//p')
|
||
issuer=$(printf "%s\n" "${cert_text}" | sed -n 's/^issuer=//p' | tr ',' ';')
|
||
subject=$(printf "%s\n" "${cert_text}" | sed -n 's/^subject=//p' | tr ',' ';')
|
||
echo "${domain},${not_before},${not_after},${issuer},${subject}"
|
||
done
|
||
} > "${DUMP_DIR}/tls-certificates.csv"
|
||
log_success "Public routes TLS certificate evidence 匯出完成"
|
||
|
||
capture_cmd "110-local-nginx-server-names" bash -lc "find /etc/nginx /home/wooo/monitoring /opt/harbor -maxdepth 4 -type f \\( -name '*.conf' -o -name '*.yml' -o -name '*.yaml' \\) -print0 2>/dev/null | xargs -0 grep -hoE 'server_name[[:space:]][^;]+' 2>/dev/null | sort -u" || true
|
||
capture_remote_cmd "ollama@192.168.0.188" "188-nginx-server-names" "find /etc/nginx /opt/n8n /opt/open-webui /opt/litellm /opt/signoz /opt/registry -maxdepth 4 -type f \\( -name '*.conf' -o -name '*.yml' -o -name '*.yaml' \\) -print0 2>/dev/null | xargs -0 grep -hoE 'server_name[[:space:]][^;]+' 2>/dev/null | sort -u" || true
|
||
capture_k8s_ingress_summary || true
|
||
|
||
cat > "${DUMP_DIR}/route-export-gap.txt" <<EOF
|
||
timestamp=${timestamp}
|
||
cloud_dns_zone_export=not_configured_without_provider_token
|
||
registrar_export=manual_escrow_required
|
||
cdn_or_tunnel_export=manual_escrow_required
|
||
private_keys_policy=not_exported_here_covered_by_encrypted_configs_backup
|
||
EOF
|
||
|
||
cat > "${DUMP_DIR}/backup-manifest.txt" <<EOF
|
||
service=public-routes
|
||
timestamp=${timestamp}
|
||
contains=dns_answers,https_status,tls_certificate_metadata,nginx_server_names,k8s_ingress_summary,route_export_gap
|
||
secret_policy=no_provider_tokens_no_tls_private_keys
|
||
failed_components=${failed}
|
||
EOF
|
||
|
||
if [ ! -d "${LOCAL_REPO}/data" ]; then
|
||
log_info "初始化 Restic 倉庫: ${LOCAL_REPO}"
|
||
low_priority restic -r "${LOCAL_REPO}" init --password-file "${RESTIC_PASSWORD_FILE}" 2>&1
|
||
fi
|
||
|
||
log_info "建立 Public routes Restic 備份..."
|
||
local tags
|
||
tags=$(build_tags "${SERVICE}")
|
||
low_priority restic -r "${LOCAL_REPO}" backup "${DUMP_DIR}" \
|
||
--password-file "${RESTIC_PASSWORD_FILE}" \
|
||
${tags} \
|
||
--tag "scope:public-routes" \
|
||
--tag "contains:dns-http-tls-route-evidence" 2>&1
|
||
|
||
local snapshot_id
|
||
snapshot_id=$(restic -r "${LOCAL_REPO}" snapshots --latest 1 --json \
|
||
--password-file "${RESTIC_PASSWORD_FILE}" 2>/dev/null | \
|
||
python3 -c 'import json,sys; rows=json.load(sys.stdin); print(rows[-1].get("short_id","unknown") if rows else "unknown")' 2>/dev/null || echo "unknown")
|
||
log_success "Public routes Restic 備份完成: ${snapshot_id}"
|
||
|
||
cleanup_old_backups "${LOCAL_REPO}"
|
||
|
||
local duration
|
||
duration=$(($(date +%s) - start_time))
|
||
log_success "========== Public routes 備份完成 (${duration}s) =========="
|
||
notify_clawbot "success" "${SERVICE}" "Public routes 備份完成" "${duration}"
|
||
}
|
||
|
||
main "$@"
|