접속 키가 svn 이력에 들어갔다는 것을 알았다. 지워야 하는데 그 키로 지금 여러 장치가 붙어 있었다.
Table of contents
Open Table of contents
지우면 당장 멈춘다
OLD_KEY 를 무효화하면 그것을 쓰는 장치가 전부 못 붙는다.
수집 장치 40대
배치 3종
관리 화면 1곳
전부 NEW_KEY 로 바꿔야 하는데 장치는 현장에 있어서 원격으로 갱신해야 한다. 그동안 자료가 안 들어오면 그것도 사고이므로 위험을 없애려다 다른 사고를 만드는 셈이 된다.
위험을 없애는 것이 맞아도 순서가 있다.
순서를 정했다
1. 새 키를 발급한다 (옛 키는 아직 살림)
2. 서버가 두 키를 다 받아들이게 한다
3. 장치를 하나씩 새 키로 바꾼다
4. 전부 바뀐 것을 확인한다
5. 옛 키를 무효화한다
6. 이력에서 옛 키를 지운다
2번이 핵심이었고 OLD_KEY 와 NEW_KEY 를 동시에 받으면 전환 중에 안 끊긴다.
$valid = [OLD_KEY, NEW_KEY];
if (!in_array($given, $valid, true)) {
return unauthorized();
}
in_array 로 둘 다 통과시키면 어느 시점에도 멈추는 장치가 없다.
어느 것이 아직 옛 키인지 봤다
3번을 하려면 누가 아직 안 바뀌었는지 알아야 했다. 접속할 때 어느 키를 썼는지 log_message 로 남겼다.
log_message('info', "auth ok device={$id} key=" . ($given === OLD_KEY ? 'old' : 'new'));
auth_log 를 집계해서 봤다.
SELECT key_type, COUNT(DISTINCT device_id) FROM auth_log
WHERE reg_date >= DATE_SUB(NOW(), INTERVAL 1 DAY) GROUP BY key_type;
old 12
new 28
12대가 남았고 목록을 뽑아 하나씩 처리했다.
안 오는 장치의 확인
old 도 new 도 안 나오는 장치가 있었다. 하루 동안 접속을 안 한 것이다.
SELECT device_id FROM device
WHERE device_id NOT IN (SELECT DISTINCT device_id FROM auth_log WHERE reg_date >= DATE_SUB(NOW(), INTERVAL 1 DAY));
device 에만 있고 auth_log 에 없는 것이 3대였고 원래 꺼져 있는 것인지 확인이 필요했다. 둘 다 아님을 따로 세지 않으면 그 3대를 놓치고 40대 중 37대만 세고 있었다.
두 대는 현장에서 꺼 둔 것이었고 한 대는 통신이 끊긴 상태였다. 그 한 대를 고치고 나서 진행했다.
검증 — 무효화 뒤와 재발 방지
5번을 하고 나서 접속 실패가 있는지 봤다.
SELECT COUNT(*) FROM auth_log WHERE result='fail' AND reg_date >= '2016-10-10 14:00';
몇 건 나왔고 확인해 보니 시험용 PHP 스크립트가 OLD_KEY 를 들고 있었다. 목록에 없던 것인데 장치와 배치만 세고 개인 스크립트는 안 셌다.
쓰는 곳을 목록으로 만들 때 놓치는 것이 있어서 무효화 뒤에 실패를 감시해서 잡았다.
문제가 크면 원래 상태로 가야 하므로 옛 키를 바로 삭제하지 않고 비활성으로만 뒀다.
활성 NEW_KEY
비활성 OLD_KEY (2016-10-24 삭제 예정)
2주 두고 문제가 없으면 OLD_KEY 를 지운다. 다시 살리려면 비활성을 활성으로 바꾸면 되고 한 줄이다.
저장소 이력에서 값을 지우는 것은 이력을 다시 쓰는 일이라 받아 간 사본이 있으면 그쪽에는 남는다. 그래서 키를 무효화하는 것이 실질적 방어이고 이력에서 지우는 것은 흔적 정리다.
값을 못 쓰게 만드는 것이 먼저고 흔적을 지우는 것이 나중이다.
애초에 왜 들어갔는지도 봤는데 config/secret.php 가 무시 목록에 없었다.
config/secret.php
*.key
.env
무시 목록에 넣고 예시 파일만 남겼다. 커밋 전에 키처럼 보이는 문자열이 있는지 보는 검사도 넣었다.
# 커밋 전 훅에서 바뀐 파일을 훑는다
for f in $(svn status | awk '/^[AM]/ {print $2}'); do
if grep -qE '(api[_-]?key|secret|password)[ ]*[=:][ ]*.{16,}' "$f"; then
echo "자격증명으로 보이는 값이 포함돼 있습니다: $f"
exit 1
fi
done
완벽하지 않지만 명백한 것은 잡힌다.
정리
- 위험한 것을 없애는 것이 맞아도 그 순간 멈추면 안 된다
- 위험을 없애려다 다른 사고를 만들 수 있다
- 새 것을 만들고 둘 다 받아들이게 한 뒤 하나씩 옮기고 옛 것을 막는다
- 어느 것이 아직 안 바뀌었는지 기록으로 센다
- 둘 다 아님을 따로 세지 않으면 안 오는 대상을 놓친다
- 목록에 없던 것이 나오므로 무효화 뒤에 실패를 감시한다
- 옛 것을 바로 지우지 말고 비활성으로 두면 다시 살릴 수 있다
- 값을 못 쓰게 만드는 것이 먼저고 흔적을 지우는 것이 나중이다
- 왜 들어갔는지 보고 무시 목록과 커밋 전 검사를 넣는다