인증서 만료 알림 직접 만들기
앞 글에서 스크립트 골격은 만들었다. 이걸 매일 알아서 도는 감시로 바꾸기 전에 언제 사람을 부를지부터 정한다. 기준을 잘못 잡으면 알림이 너무 자주 오고, 그러다 보면 정작 중요한 알림을 아무도 안 본다.
알림 기준
| 상태 | 기준 | 행동 |
|---|---|---|
| 🔴 CRITICAL | 14일 이내 만료, 접속 불가 | 즉시 조치 |
| 🟡 WARNING | 30일 이내 만료 | 이번 주 안에 확인 |
| ⚪ OK | 그 외 | 통보 안 함 |
WARNING을 30일로 잡은 건 Let’s Encrypt 계열 자동갱신이 만료 30일 전에 갱신하기 때문이다. 30일 안쪽인데 아직 옛 인증서라면 자동갱신이 이미 실패하고 있다는 뜻이다. 만료가 다가온다는 신호보다 갱신이 고장 났다는 신호로 읽어야 맞다.
OK는 알리지 않는다. 매일 “전부 정상 ✅” 메시지를 보내고 싶어지지만 2주면 아무도 안 읽는다. 감시가 살아 있는지는 아래 데드맨 스위치로 따로 확인한다.
판정과 통보는 따로
판정 스크립트는 기준에 걸린 도메인만 표준출력으로 내보낸다.
#!/bin/bash
# cert-watch.sh — domains.txt 를 검사해 임계값 위반만 표준출력으로
set -u
CRIT_DAYS=14; WARN_DAYS=30
while read -r d; do
[ -z "$d" ] || [ "${d:0:1}" = "#" ] && continue
end=$(openssl s_client -connect "$d:443" -servername "$d" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$end" ]; then
echo "CRIT|$d|접속 불가 또는 인증서 읽기 실패"
continue
fi
days=$(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 ))
if [ "$days" -lt "$CRIT_DAYS" ]; then echo "CRIT|$d|${days}일 남음 ($end)"
elif [ "$days" -lt "$WARN_DAYS" ]; then echo "WARN|$d|${days}일 남음 ($end)"
fi
done < /opt/certwatch/domains.txt
macOS에는 date -d가 없으니 gdate(coreutils)로 바꾼다.
슬랙 통보는 다른 스크립트가 맡는다. 판정 결과가 있을 때만 보낸다.
#!/bin/bash
# notify.sh — cert-watch.sh 출력이 있을 때만 슬랙 발송
WEBHOOK="$(cat /opt/certwatch/.webhook)" # 코드에 URL 하드코딩 금지
out=$(/opt/certwatch/cert-watch.sh)
[ -z "$out" ] && exit 0
crit=$(echo "$out" | grep -c '^CRIT' || true)
warn=$(echo "$out" | grep -c '^WARN' || true)
body=$(echo "$out" | awk -F'|' '{printf "%s %s — %s\n", ($1=="CRIT"?"🔴":"🟡"), $2, $3}')
curl -s -X POST "$WEBHOOK" -H 'content-type: application/json' \
-d "$(python3 -c "import json,sys;print(json.dumps({'text':f'인증서 감시: CRIT {sys.argv[1]} / WARN {sys.argv[2]}\n'+sys.argv[3]}))" "$crit" "$warn" "$body")" >/dev/null
둘을 나눈 건 테스트 때문이다. 판정 스크립트만 돌리면 슬랙 없이 결과를 볼 수 있다.
웹훅 URL은 파일로 빼고 권한을 600으로 둔다. 저장소에 커밋하는 순간 시크릿 유출이다.
cron 대신 systemd 타이머
cron으로도 되지만 systemd 타이머가 낫다. 실행 이력이 journal에 남아 실패한 실행을 확인할 수 있고, 실행이 겹치지 않는다.
# /etc/systemd/system/certwatch.service
[Unit]
Description=certificate expiry watch
[Service]
Type=oneshot
ExecStart=/opt/certwatch/notify.sh
# /etc/systemd/system/certwatch.timer
[Unit]
Description=daily certificate watch
[Timer]
OnCalendar=*-*-* 09:00:00
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
systemctl enable --now certwatch.timer
systemctl list-timers certwatch.timer # 다음 실행 확인
journalctl -u certwatch.service -n 20 # 지난 실행 로그
Persistent=true를 넣어 두면 09:00에 서버가 꺼져 있었어도 부팅한 뒤 밀린 실행을 돌린다. cron은
그냥 건너뛴다.
데드맨 스위치
여기까지 만들고 멈추는 경우가 많은데, 구멍이 하나 남아 있다. 감시 서버가 죽으면 알림도 안 온다. 알림이 없는 상태는 정상과 구별되지 않는다.
그래서 방향을 뒤집은 알림을 하나 더 건다. 이상이 있을 때 울리는 것과 별개로, 살아 있다는 신호가 끊기면 울리게 한다. healthchecks.io나 Uptime Kuma 같은 헬스체크 핑 서비스(healthchecks.io는 셀프호스트도 된다)에 매 실행 끝마다 핑을 보낸다.
# notify.sh 의 out=$(...) 다음 줄, [ -z "$out" ] && exit 0 앞에
curl -fsS -m 10 "https://hc-ping.com/여러분의-uuid" >/dev/null
마지막 줄에 두면 모든 도메인이 정상인 날에는 앞의 exit 0에서 끝나 핑이 나가지 않는다. 그러면 정상인
날마다 헛알림이 온다.
핑이 하루 이상 끊기면 그쪽에서 알림이 온다.
어디까지 버티나
직접 만든 체계로 도메인 10~20개까지는 충분히 간다. 그 뒤에 남는 한계는 앞 글의 3단계에서 본 것과 같다. 도메인 등록 만료는 RDAP가 파편화돼 있어 스크립트가 금방 지저분해진다. 지역별 차이는 감시 서버 한 대로는 안 보인다. 고객사별 이력과 보고까지 하려면 결국 DB가 필요하다.
도메인 5개까지는 만료 감시 구독이 이 일을 대신한다. 인증서 만료, 도메인 만료, 측정 지점별 확인을 에이전트 설치 없이 매일 돌리고, 문제가 있을 때만 메일을 보낸다.