| Server IP : 119.195.102.159 / Your IP : 216.73.217.134 Web Server : nginx/1.18.0 System : Linux picell 5.15.0-181-generic #191-Ubuntu SMP Fri May 22 19:09:02 UTC 2026 x86_64 User : altablue ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/altablue/ |
Upload File : |
#!/usr/bin/env bash
set -Eeuo pipefail
# ============================================================
# Let's Encrypt / Certbot HTTP-01 자동발급 통합 진단 스크립트
#
# 사용법:
# sudo bash certbot-http01-diagnose.sh DOMAIN WEBROOT
#
# 예:
# sudo bash certbot-http01-diagnose.sh \
# hyh.picell.biz \
# /home2/haeyulhome
#
# Certbot staging 실제 검증까지 실행:
# sudo RUN_CERTBOT=1 bash certbot-http01-diagnose.sh \
# hyh.picell.biz \
# /home2/haeyulhome
#
# tcpdump 패킷 캡처까지 실행:
# sudo RUN_CERTBOT=1 CAPTURE_TCP=1 bash certbot-http01-diagnose.sh \
# hyh.picell.biz \
# /home2/haeyulhome
#
# 주의:
# - RUN_CERTBOT=1은 실제 Let’s Encrypt staging 검증을 실행합니다.
# - nginx는 중단하지 않습니다. webroot 방식으로 검사합니다.
# - 테스트 challenge 파일은 종료 시 자동 삭제합니다.
# ============================================================
DOMAIN="${1:-}"
WEBROOT="${2:-}"
RUN_CERTBOT="${RUN_CERTBOT:-0}"
CAPTURE_TCP="${CAPTURE_TCP:-0}"
CERTBOT_EMAIL="${CERTBOT_EMAIL:-}"
REPORT_DIR="${REPORT_DIR:-/root/certbot-diagnose}"
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
REPORT_FILE="${REPORT_DIR}/${DOMAIN:-unknown}-${TIMESTAMP}.log"
PCAP_FILE="${REPORT_DIR}/${DOMAIN:-unknown}-${TIMESTAMP}.pcap"
TEST_TOKEN="certbot-diagnose-${TIMESTAMP}-$$"
CHALLENGE_REL=".well-known/acme-challenge"
CHALLENGE_DIR=""
TEST_FILE=""
RED=$'\033[31m'
GREEN=$'\033[32m'
YELLOW=$'\033[33m'
CYAN=$'\033[36m'
RESET=$'\033[0m'
usage() {
cat <<'EOF'
사용법:
sudo bash certbot-http01-diagnose.sh DOMAIN WEBROOT
예:
sudo bash certbot-http01-diagnose.sh hyh.picell.biz /home2/haeyulhome
환경변수:
RUN_CERTBOT=1 Certbot staging HTTP-01 실제 검증 실행
CAPTURE_TCP=1 Certbot 실행 중 TCP 80 패킷 캡처
CERTBOT_EMAIL= Certbot 계정 이메일. 비어 있으면 임시 등록 옵션 사용
REPORT_DIR= 보고서 저장 디렉터리
EOF
}
if [[ -z "$DOMAIN" || -z "$WEBROOT" ]]; then
usage
exit 2
fi
if [[ "${EUID}" -ne 0 ]]; then
echo "이 스크립트는 root 권한으로 실행해야 합니다."
echo "sudo bash $0 $DOMAIN $WEBROOT"
exit 1
fi
mkdir -p "$REPORT_DIR"
touch "$REPORT_FILE"
exec > >(tee -a "$REPORT_FILE") 2>&1
cleanup() {
if [[ -n "${TEST_FILE:-}" && -f "$TEST_FILE" ]]; then
rm -f "$TEST_FILE" || true
fi
}
trap cleanup EXIT
section() {
echo
echo "${CYAN}============================================================${RESET}"
echo "${CYAN}$*${RESET}"
echo "${CYAN}============================================================${RESET}"
}
ok() {
echo "${GREEN}[정상]${RESET} $*"
}
warn() {
echo "${YELLOW}[주의]${RESET} $*"
}
fail() {
echo "${RED}[오류]${RESET} $*"
}
run() {
echo
echo "+ $*"
"$@" || true
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
get_primary_interface() {
ip route get 1.1.1.1 2>/dev/null |
awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
}
get_public_ipv4() {
local ip=""
if command_exists curl; then
ip="$(curl -4fsS --max-time 10 https://api.ipify.org 2>/dev/null || true)"
fi
printf '%s' "$ip"
}
get_dns_a() {
if command_exists dig; then
dig +short A "$DOMAIN" | sed '/^$/d'
elif command_exists getent; then
getent ahostsv4 "$DOMAIN" | awk '{print $1}' | sort -u
fi
}
get_dns_aaaa() {
if command_exists dig; then
dig +short AAAA "$DOMAIN" | sed '/^$/d'
fi
}
HTTP_CODE_LOCAL=""
HTTP_CODE_DOMAIN=""
HTTP_BODY_LOCAL=""
HTTP_BODY_DOMAIN=""
section "1. 기본 정보"
echo "실행 시각: $(date -Is)"
echo "호스트명: $(hostname -f 2>/dev/null || hostname)"
echo "도메인: $DOMAIN"
echo "웹루트: $WEBROOT"
echo "보고서: $REPORT_FILE"
echo "Certbot 실행: $RUN_CERTBOT"
echo "패킷 캡처: $CAPTURE_TCP"
if [[ -d "$WEBROOT" ]]; then
ok "웹루트 디렉터리가 존재합니다."
else
fail "웹루트 디렉터리가 없습니다: $WEBROOT"
exit 1
fi
section "2. 시스템 및 네트워크"
run uname -a
run ip -4 -br addr
run ip rule show
run ip route show table all
PRIMARY_IFACE="$(get_primary_interface || true)"
PUBLIC_IPV4="$(get_public_ipv4 || true)"
DNS_A="$(get_dns_a || true)"
DNS_AAAA="$(get_dns_aaaa || true)"
echo
echo "주 인터페이스: ${PRIMARY_IFACE:-확인 실패}"
echo "공인 IPv4: ${PUBLIC_IPV4:-확인 실패}"
echo "DNS A:"
printf '%s\n' "${DNS_A:-없음}"
echo "DNS AAAA:"
printf '%s\n' "${DNS_AAAA:-없음}"
if [[ -n "$PUBLIC_IPV4" && -n "$DNS_A" ]]; then
if grep -Fxq "$PUBLIC_IPV4" <<<"$DNS_A"; then
ok "DNS A 레코드가 현재 공인 IPv4와 일치합니다."
else
fail "DNS A 레코드가 현재 공인 IPv4와 일치하지 않습니다."
fi
fi
if [[ -n "$DNS_AAAA" ]]; then
warn "AAAA 레코드가 존재합니다. IPv6 nginx 리스닝과 IPv6 방화벽도 반드시 정상이어야 합니다."
fi
section "3. rp_filter 및 커널 네트워크 설정"
for key in \
net.ipv4.conf.all.rp_filter \
net.ipv4.conf.default.rp_filter \
"net.ipv4.conf.${PRIMARY_IFACE:-all}.rp_filter" \
net.ipv4.ip_forward; do
sysctl "$key" 2>/dev/null || true
done
section "4. TCP 80/443 리스닝 상태"
run ss -lntp
echo
ss -lntp 2>/dev/null | grep -E '(:80[[:space:]]|:443[[:space:]])' || true
if ss -lnt 2>/dev/null | grep -qE '(:80[[:space:]])'; then
ok "TCP 80 포트가 리스닝 중입니다."
else
fail "TCP 80 포트가 리스닝 중이 아닙니다."
fi
section "5. nginx 상태와 설정"
if command_exists nginx; then
run nginx -v
run nginx -t
if systemctl is-active --quiet nginx; then
ok "nginx 서비스가 실행 중입니다."
else
fail "nginx 서비스가 실행 중이 아닙니다."
fi
echo
echo "--- 도메인 관련 nginx -T 발췌 ---"
nginx -T 2>&1 |
awk -v domain="$DOMAIN" '
BEGIN {show=0; count=0}
$0 ~ domain {show=1; count=0}
show {
print
count++
if (count >= 80) show=0
}
' || true
echo
echo "--- ACME location 전체 검색 ---"
nginx -T 2>&1 |
grep -nE 'server_name|root |alias |\.well-known/acme-challenge|listen 80|listen \[::\]:80' |
tail -n 300 || true
else
fail "nginx 명령이 없습니다."
fi
section "6. 웹루트 권한과 파일 구조"
run namei -l "$WEBROOT"
run ls -ld "$WEBROOT"
run find "$WEBROOT" -maxdepth 2 -type f \
\( -name index.php -o -name index.html -o -name wp-config.php \) \
-print
CHALLENGE_DIR="${WEBROOT}/${CHALLENGE_REL}"
TEST_FILE="${CHALLENGE_DIR}/${TEST_TOKEN}"
mkdir -p "$CHALLENGE_DIR"
printf '%s\n' "$TEST_TOKEN" > "$TEST_FILE"
chmod 755 "$WEBROOT" 2>/dev/null || true
chmod 755 "${WEBROOT}/.well-known" "$CHALLENGE_DIR" 2>/dev/null || true
chmod 644 "$TEST_FILE"
echo
echo "테스트 파일: $TEST_FILE"
run namei -l "$TEST_FILE"
section "7. ACME challenge 로컬 Host 헤더 테스트"
LOCAL_TMP="$(mktemp)"
DOMAIN_TMP="$(mktemp)"
trap 'cleanup; rm -f "$LOCAL_TMP" "$DOMAIN_TMP"' EXIT
HTTP_CODE_LOCAL="$(
curl -sS \
--max-time 15 \
-o "$LOCAL_TMP" \
-w '%{http_code}' \
-H "Host: $DOMAIN" \
"http://127.0.0.1/${CHALLENGE_REL}/${TEST_TOKEN}" \
2>/dev/null || true
)"
HTTP_BODY_LOCAL="$(cat "$LOCAL_TMP" 2>/dev/null || true)"
echo "로컬 HTTP 코드: $HTTP_CODE_LOCAL"
echo "로컬 응답 본문: $HTTP_BODY_LOCAL"
if [[ "$HTTP_CODE_LOCAL" == "200" && "$HTTP_BODY_LOCAL" == "$TEST_TOKEN" ]]; then
ok "nginx가 Host 헤더 기준 ACME 파일을 정상 제공합니다."
else
fail "로컬 Host 헤더 ACME 테스트가 실패했습니다."
fi
section "8. 도메인 경유 HTTP 테스트"
HTTP_CODE_DOMAIN="$(
curl -4sS \
--max-time 20 \
--connect-timeout 10 \
-o "$DOMAIN_TMP" \
-w '%{http_code}' \
"http://${DOMAIN}/${CHALLENGE_REL}/${TEST_TOKEN}" \
2>/dev/null || true
)"
HTTP_BODY_DOMAIN="$(cat "$DOMAIN_TMP" 2>/dev/null || true)"
echo "도메인 HTTP 코드: $HTTP_CODE_DOMAIN"
echo "도메인 응답 본문: $HTTP_BODY_DOMAIN"
if [[ "$HTTP_CODE_DOMAIN" == "200" && "$HTTP_BODY_DOMAIN" == "$TEST_TOKEN" ]]; then
ok "서버 자체에서 도메인 경유 ACME 파일 조회가 정상입니다."
else
warn "서버 자체의 도메인 경유 조회가 실패했습니다. NAT loopback 또는 외부 경로 문제일 수 있습니다."
fi
echo
echo "--- 상세 curl ---"
curl -4v \
--max-time 20 \
"http://${DOMAIN}/${CHALLENGE_REL}/${TEST_TOKEN}" \
-o /dev/null || true
section "9. DNS 외부 리졸버 확인"
if command_exists dig; then
for resolver in 1.1.1.1 8.8.8.8 9.9.9.9; do
echo
echo "--- Resolver ${resolver} ---"
dig @"$resolver" +time=3 +tries=1 A "$DOMAIN" +short || true
dig @"$resolver" +time=3 +tries=1 AAAA "$DOMAIN" +short || true
done
else
warn "dig 명령이 없어 외부 리졸버 확인을 생략합니다."
fi
section "10. UFW / iptables / nftables"
if command_exists ufw; then
run ufw status verbose
run ufw status numbered
else
warn "ufw 명령이 없습니다."
fi
if command_exists iptables; then
echo
echo "--- iptables INPUT ---"
iptables -L INPUT -n -v --line-numbers || true
echo
echo "--- UFW 사용자 입력 체인 ---"
iptables -L ufw-user-input -n -v --line-numbers 2>/dev/null || true
echo
echo "--- DROP/REJECT/ipset/geo 관련 규칙 ---"
iptables -S |
grep -Ei 'DROP|REJECT|ipset|geo|country|dport 80|dports 80' || true
fi
if command_exists nft; then
echo
echo "--- nftables 관련 규칙 ---"
nft -a list ruleset 2>/dev/null |
grep -Ei 'drop|reject|dport (80|443)|ip saddr|set ' |
head -n 500 || true
fi
section "11. Fail2ban 상태"
if command_exists fail2ban-client; then
run fail2ban-client status
echo
fail2ban-client status 2>/dev/null |
sed -n 's/.*Jail list:[[:space:]]*//p' |
tr ',' '\n' |
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' |
while read -r jail; do
[[ -n "$jail" ]] || continue
echo "--- Jail: $jail ---"
fail2ban-client status "$jail" || true
done
else
warn "fail2ban-client 명령이 없습니다."
fi
section "12. 최근 커널 차단 로그"
journalctl -k --since "30 minutes ago" 2>/dev/null |
grep -Ei 'DROP|REJECT|BLOCK|martian|reverse path|DPT=80|DPT=443' |
tail -n 300 || true
section "13. Certbot 설치 및 인증서 상태"
if command_exists certbot; then
run certbot --version
run certbot plugins
run certbot certificates
else
fail "certbot 명령이 없습니다."
fi
section "14. 도메인 nginx 로그의 ACME 요청"
ACCESS_LOG="/var/log/nginx/${DOMAIN}.access.log"
ERROR_LOG="/var/log/nginx/${DOMAIN}.error.log"
if [[ -f "$ACCESS_LOG" ]]; then
echo "--- $ACCESS_LOG ---"
grep '/.well-known/acme-challenge/' "$ACCESS_LOG" |
tail -n 100 || true
else
warn "도메인별 access log가 없습니다: $ACCESS_LOG"
echo "nginx 전체 access 로그를 검색합니다."
grep -Rhs '/.well-known/acme-challenge/' /var/log/nginx/*access*.log 2>/dev/null |
tail -n 100 || true
fi
if [[ -f "$ERROR_LOG" ]]; then
echo
echo "--- $ERROR_LOG ---"
tail -n 100 "$ERROR_LOG" || true
fi
section "15. Certbot staging HTTP-01 실제 검증"
if [[ "$RUN_CERTBOT" == "1" ]]; then
if ! command_exists certbot; then
fail "Certbot이 없어 실제 검증을 실행할 수 없습니다."
else
TCPDUMP_PID=""
if [[ "$CAPTURE_TCP" == "1" ]]; then
if command_exists tcpdump && [[ -n "$PRIMARY_IFACE" ]]; then
echo "TCP 80 패킷 캡처 시작: $PCAP_FILE"
tcpdump -ni "$PRIMARY_IFACE" -nn -s0 \
-w "$PCAP_FILE" 'tcp port 80' >/dev/null 2>&1 &
TCPDUMP_PID="$!"
sleep 2
else
warn "tcpdump 또는 주 인터페이스를 확인할 수 없어 캡처를 생략합니다."
fi
fi
CERTBOT_ARGS=(
certonly
--webroot
-w "$WEBROOT"
-d "$DOMAIN"
--staging
--preferred-challenges http
--agree-tos
--non-interactive
--no-eff-email
--verbose
)
if [[ -n "$CERTBOT_EMAIL" ]]; then
CERTBOT_ARGS+=(--email "$CERTBOT_EMAIL")
else
CERTBOT_ARGS+=(--register-unsafely-without-email)
fi
echo
echo "+ certbot ${CERTBOT_ARGS[*]}"
set +e
certbot "${CERTBOT_ARGS[@]}"
CERTBOT_RC=$?
set -e
if [[ -n "$TCPDUMP_PID" ]]; then
kill "$TCPDUMP_PID" 2>/dev/null || true
wait "$TCPDUMP_PID" 2>/dev/null || true
echo "패킷 캡처 저장: $PCAP_FILE"
fi
if [[ "$CERTBOT_RC" -eq 0 ]]; then
ok "Certbot staging HTTP-01 검증이 성공했습니다."
else
fail "Certbot staging HTTP-01 검증이 실패했습니다. 종료 코드: $CERTBOT_RC"
fi
echo
echo "--- 최근 Certbot 핵심 로그 ---"
grep -nE \
'secondary validation|Timeout during connect|challenge|validation|unauthorized|connection' \
/var/log/letsencrypt/letsencrypt.log 2>/dev/null |
tail -n 250 || true
if [[ -f "$PCAP_FILE" && -s "$PCAP_FILE" ]]; then
echo
echo "--- 캡처된 TCP 80 SYN/SYN-ACK 요약 ---"
tcpdump -nn -tttt -r "$PCAP_FILE" \
'tcp[tcpflags] & tcp-syn != 0' 2>/dev/null |
tail -n 300 || true
fi
fi
else
warn "RUN_CERTBOT=0이므로 실제 Certbot staging 검증은 생략했습니다."
echo "실제 검증 실행:"
echo " sudo RUN_CERTBOT=1 CAPTURE_TCP=1 bash $0 $DOMAIN $WEBROOT"
fi
section "16. 자동 판정"
echo "DNS A: ${DNS_A:-없음}"
echo "공인 IPv4: ${PUBLIC_IPV4:-확인 실패}"
echo "로컬 ACME HTTP: ${HTTP_CODE_LOCAL:-확인 실패}"
echo "도메인 ACME HTTP: ${HTTP_CODE_DOMAIN:-확인 실패}"
echo
if [[ -n "$PUBLIC_IPV4" && -n "$DNS_A" ]] &&
! grep -Fxq "$PUBLIC_IPV4" <<<"$DNS_A"; then
fail "DNS A 불일치: 먼저 DNS 레코드를 수정해야 합니다."
elif [[ "$HTTP_CODE_LOCAL" != "200" || "$HTTP_BODY_LOCAL" != "$TEST_TOKEN" ]]; then
fail "nginx/webroot 매핑 문제입니다. server_name, root, ACME location을 확인하십시오."
elif [[ "$HTTP_CODE_DOMAIN" != "200" || "$HTTP_BODY_DOMAIN" != "$TEST_TOKEN" ]]; then
warn "도메인 경유 ACME 접근이 실패했습니다. DNS, IPv6, 상위 네트워크 또는 loopback 경로를 확인하십시오."
else
ok "DNS, nginx, webroot, 로컬 HTTP-01 경로는 정상으로 보입니다."
echo
echo "이 상태에서 Certbot이 'During secondary validation: Timeout during connect'로 실패하면,"
echo "Certbot 또는 challenge 디렉터리 문제가 아니라 일부 Let’s Encrypt 검증 노드와"
echo "서버 사이의 상위 네트워크/해외 라우팅/DDoS 필터 문제일 가능성이 큽니다."
fi
section "17. 완료"
echo "진단 보고서:"
echo " $REPORT_FILE"
if [[ -f "$PCAP_FILE" && -s "$PCAP_FILE" ]]; then
echo "패킷 캡처:"
echo " $PCAP_FILE"
fi
echo
echo "테스트 challenge 파일은 자동 삭제됩니다."