인증서 만료 알림 체계 직접 만들기 — 스크립트 + 슬랙 + systemd
앞 글에서 스크립트 골격은 나왔다. 이 글은 그걸 운영 체계로 만드는 나머지 절반이다 — 알림 기준을 어떻게 잡고, 스케줄러를 뭘로 돌리고, 감시 자체가 죽은 건 어떻게 아는가.
1. 판정 기준부터 설계
코드보다 기준이 먼저다. 나쁜 기준이 알림 피로를 만들고, 알림 피로가 진짜 알림을 죽인다.
| 상태 | 기준 | 행동 |
|---|---|---|
| 🔴 CRITICAL | 14일 이내 만료 · 접속 불가 · 검증 실패 | 즉시 조치 |
| 🟡 WARNING | 30일 이내 만료 | 이번 주 안에 확인 |
| ⚪ OK | 그 외 | 통보 안 함 |
30일인 이유: Let’s Encrypt 계열 자동갱신은 만료 30일 전에 갱신한다. 즉 30일 이내인데 아직 옛 인증서라면 자동갱신이 이미 실패하고 있다는 뜻이다 — “만료 임박”이 아니라 “갱신 체계 고장”의 신호로 읽는 것이 정확하다.
OK 는 침묵: 매일 “전부 정상 ✅” 메시지를 보내고 싶어지는데, 2주면 아무도 안 읽는다. 정상은 조용히, 이상만 시끄럽게 — 대신 감시 생존 확인은 5번에서 따로 푼다.
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)로 바꾼다. 판정과 통보를 분리해둔
이유는 테스트 때문이다 — 이 스크립트만 돌리면 슬랙 없이 결과를 볼 수 있다.
3. 슬랙 통보
#!/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. 저장소에 커밋하는 순간 시크릿 유출이다.
4. 스케줄 — 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 은 그냥 건너뛴다.
5. 감시의 감시 — 데드맨 스위치
여기까지 만들고 대부분 멈추는데, 마지막 구멍이 남아 있다. 감시 서버가 죽으면 알림도 없다. 그리고 알림 없음은 정상과 구별되지 않는다.
해법은 방향을 뒤집는 것이다 — “이상 있으면 알림”에 더해 “살아 있다는 신호가 끊기면 알림”. 헬스체크 핑 서비스(healthchecks.io 셀프호스트 가능, Uptime Kuma 등)에 매 실행 끝에 핑을 보낸다:
# notify.sh 마지막 줄에
curl -fsS -m 10 "https://hc-ping.com/여러분의-uuid" >/dev/null
핑이 하루 이상 끊기면 그쪽에서 알림이 온다. 이제 감시 체인의 모든 고리가 “침묵 = 이상”으로 정렬됐다.
이 체계의 한계 — 그리고 언제 갈아탈까
직접 만든 체계로 도메인 10~20개까지는 충분히 간다. 남는 한계는 앞 글의 3단계와 같다 — 도메인 등록 만료는 RDAP 파편화 때문에 스크립트가 금방 지저분해지고, 지역별 차이는 감시 서버 한 대로는 안 보이고, 고객사별 이력·보고는 결국 DB 가 필요해진다.
그 지점을 넘는 도구를 만드는 중이다 — 인증서 + 도메인 만료 + 지역별 확인을 에이전트 설치 없이. 진단 툴에서 미리 받아보기로 등록할 수 있다.
직접 만들든 기다리든, 오늘 domains.txt 와 30일 기준 하나는 만들어두자. 이 글 전체에서 그게 제일 중요한 한 줄이다.