서버 보안 점검 5편: Ghost·MySQL 백업 무결성
백업 파일이 존재한다는 사실과 복구할 수 있다는 사실은 다릅니다. 파일 크기가 그럴듯해 보여도 전송 중 손상됐을 수 있고, 압축 안에 위험한 경로나 누락된 파일이 있을 수 있으며, 데이터베이스와 미디어가 실제 애플리케이션에서 함께 동작하지 않을 수도 있습니다.
이번 점검은 운영 중인 Ghost와 MySQL을 건드리지 않는 읽기 전용 진단으로 진행했습니다. 알려진 체크섬 집합, 압축 아카이브의 멤버 목록, 매니페스트와의 순서 일치, 중복·비정상 경로를 확인했습니다. 다만 허용 범위 밖 경로 집계의 마지막 값과 실제 복원 성공 기록은 보존되지 않았으므로 완료로 과장하지 않습니다.
3줄 요약
- 점검 중 Ghost와 MySQL은 실행 상태였고, 아카이브를 실제로 풀지 않았습니다.
- 사전에 정의한 체크섬 항목이 모두
OK였으며, 아카이브와 매니페스트의 전체 항목이 순서까지 정확히 일치했습니다.- 중복·위험·비정규 경로는 0으로 기록됐지만, 허용 범위 밖 경로의 최종 값과 격리 복원 결과는 확인이 필요합니다.
자료 범위와 판정 원칙
당시 진단 스크립트의 전체 소스와 전체 터미널 로그는 현재 기록에 남아 있지 않습니다. 이 글은 값이 보존된 필드만 역사적 결과로 사용하고, 그 밖의 절차는 재현용 예시로 분리합니다.
특히 다음을 구분해야 합니다.
- 체크섬 일치: 백업 파일이 체크섬을 기록한 시점과 같은 바이트라는 뜻
- 아카이브 목록 일치: 예상한 멤버 목록과 실제 목록이 같다는 뜻
- 경로 안전성: 절대 경로나
..같은 명백한 이탈 경로가 발견되지 않았다는 뜻 - 복원 성공: 별도 환경에서 데이터베이스·미디어·Ghost가 실제로 함께 동작한다는 뜻
앞의 세 가지가 성공해도 마지막 복원 성공이 자동으로 증명되지는 않습니다.
점검 결과 요약
| 상태 | 점검 항목 | 보존된 결과 | 판단 |
|---|---|---|---|
| 성공 | 운영 서비스 영향 | Ghost와 MySQL이 실행 상태였고 압축 해제는 수행하지 않음 | 읽기 전용 점검으로 운영 변경 없음 |
| 성공 | 알려진 체크섬 집합 | 사전에 정의한 항목 모두 OK |
기록한 체크섬과 현재 파일 바이트가 일치 |
| 성공 | 아카이브·매니페스트 수 | 양측 전체 항목 수 일치 | 항목 수 일치 |
| 성공 | 목록의 정확한 일치 | 순서까지 정확히 일치 | 누락·추가·순서 차이가 기록상 없음 |
| 성공 | 중복 경로 | 원본·공백 정리·정규화 기준 모두 0 | 세 기준에서 중복 없음 |
| 성공 | 위험·비정규 경로 | 각각 0 | 보존된 진단 기준에서 이상 없음 |
| 확인 필요 | 허용 범위 밖 경로 | 마지막 출력값이 보존 과정에서 잘림 | 0이라고 추정하지 않음 |
| 보류 | 실제 복원 훈련 | 성공 기록이 보존되지 않음 | 복구 가능성은 아직 확정할 수 없음 |
| 실패 | 확정된 무결성 실패 | 보존된 기록 안에서는 없음 | 미확인 항목을 성공으로 바꾸지 않음 |
실제로 보존된 진단 결과
다음은 식별 가능한 파일명을 플레이스홀더로 바꾼 보존 결과입니다. 숫자와 참·거짓 값은 기록에 남아 있던 값만 사용했습니다.
===== READ-ONLY PRECHECK =====
ghost_status=running mysql_status=running
archive_extraction_performed=no
===== VERIFY EXACT KNOWN BACKUP CHECKSUM SET =====
<ARCHIVE_FILE_LIST>: OK
<BACKUP_INFO_FILE>: OK
<DATABASE_BACKUP_ARCHIVE>: OK
<MEDIA_BACKUP_ARCHIVE>: OK
verified_checksum_entry_count=<REDACTED_COUNT>
===== ARCHIVE LINK METADATA DIAGNOSTIC =====
archive_member_count=<REDACTED_COUNT>
manifest_member_count=<REDACTED_COUNT>
archive_manifest_ordered_exact_match=true
first_manifest_mismatch=null
duplicate_raw_member_count=0
duplicate_trimmed_member_count=0
duplicate_normalized_member_count=0
unsafe_member_path_count=0
noncanonical_member_path_count=0
outside_allowed_scope_count=<OUTPUT_NOT_PRESERVED>
마지막 outside_allowed_scope_count는 원문이 값 없이 끝난 채 보존됐습니다. 앞선 경로 검사들이 0이라고 해서 이 값도 0이었을 것이라고 추정하면 안 됩니다. 따라서 이 항목만 다시 실행해 결과를 확보해야 합니다.
1. 운영 서비스와 압축 해제 여부 확인
실제로 보존된 결과
- Ghost 상태:
running - MySQL 상태:
running - 아카이브 압축 해제: 수행하지 않음
이 결과는 점검 때문에 운영 컨테이너를 중단하거나 백업 파일을 덮어쓰지 않았다는 의미입니다. 그러나 백업을 생성하던 순간에도 데이터베이스가 일관된 방식으로 정지되거나 스냅샷됐다는 뜻은 아닙니다. 백업 생성 절차의 전체 기록이 없으므로 파일명에 cold 같은 단어가 있더라도 실제 생성 일관성을 별도로 확인해야 합니다.
재현용 점검 명령
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
컨테이너명과 이미지 태그는 공개 출력에서 가립니다. 읽기 전용으로 아카이브 목록만 보는 명령은 다음과 같습니다.
tar -tzf '<BACKUP_ARCHIVE>' >/dev/null
tar -t는 목록 조회이며 tar -x처럼 파일을 추출하지 않습니다. 성공 종료는 아카이브를 읽어 목록화할 수 있다는 뜻일 뿐, 애플리케이션 복원이 성공했다는 뜻은 아닙니다.
2. SHA-256 체크섬 검증
실제로 보존된 결과
미리 정의한 체크섬 집합의 모든 항목이 OK였고, 검증된 항목 수도 기대값과 일치했습니다. 정확한 개수와 파일명은 공개 원고에서 제외했습니다.
GNU Coreutils의 sha256sum --check는 체크섬 파일에 기록된 값과 현재 파일을 비교합니다. 백업 디렉터리에서 다음처럼 재현할 수 있습니다.
cd '<BACKUP_DIR>'
sha256sum --check '<CHECKSUM_FILE>'
검증 대상이 정확히 예상한 파일 집합인지도 확인해야 합니다.
awk 'NF {count++} END {print "checksum_entry_count=" count}' '<CHECKSUM_FILE>'
체크섬 파일 자체가 공격자에 의해 백업과 함께 바뀔 수 있다면 같은 위치에 둔 체크섬만으로는 충분하지 않습니다. 체크섬 또는 서명 정보를 별도 신뢰 저장소에 보관하고, 생성 시점과 백업 세대도 함께 기록해야 합니다.
3. 아카이브 목록과 매니페스트 비교
실제로 보존된 결과
- 아카이브 멤버 수: 비공개 기대값과 일치
- 매니페스트 멤버 수: 아카이브와 일치
- 순서를 포함한 정확한 일치:
true - 최초 불일치:
null
항목 수만 같다고 충분한 것은 아닙니다. 한 파일이 빠지고 다른 파일이 추가돼도 총개수는 같을 수 있습니다. 당시 진단은 목록 전체를 순서대로 비교했고 정확히 일치한 것으로 기록됐습니다.
재현용 점검 명령
아래 예시는 <ARCHIVE_MEMBER_MANIFEST>가 멤버 경로 한 줄당 하나인 형식일 때 사용할 수 있습니다. 매니페스트가 체크섬이나 크기 열을 포함한다면 그 형식에 맞게 경로 열을 먼저 추출해야 합니다.
CHECK_WORK_DIR="$(mktemp -d)"
trap 'rm -rf -- "$CHECK_WORK_DIR"' EXIT
LC_ALL=C tar -tzf '<BACKUP_ARCHIVE>' >"$CHECK_WORK_DIR/archive-members.txt"
LC_ALL=C cp '<ARCHIVE_MEMBER_MANIFEST>' "$CHECK_WORK_DIR/manifest-members.txt"
wc -l "$CHECK_WORK_DIR/archive-members.txt" \
"$CHECK_WORK_DIR/manifest-members.txt"
if cmp -s "$CHECK_WORK_DIR/archive-members.txt" \
"$CHECK_WORK_DIR/manifest-members.txt"; then
echo 'archive_manifest_ordered_exact_match=true'
else
echo 'archive_manifest_ordered_exact_match=false'
diff -u "$CHECK_WORK_DIR/manifest-members.txt" \
"$CHECK_WORK_DIR/archive-members.txt" | sed -n '1,80p'
fi
diff 결과에는 내부 경로, 고객명이나 다른 식별자가 포함될 수 있습니다. 공개 글에는 원문을 붙이지 말고 불일치 개수와 유형만 적습니다.
4. 중복·비정규·위험 경로 점검
실제로 보존된 결과
당시 기록에는 다음 다섯 값이 모두 0으로 남아 있습니다.
duplicate_raw_member_count=0
duplicate_trimmed_member_count=0
duplicate_normalized_member_count=0
unsafe_member_path_count=0
noncanonical_member_path_count=0
중복은 같은 경로가 여러 번 등장해 복원 순서에 따라 덮어쓰기 결과가 달라지는 문제를 찾는 데 도움이 됩니다. 정규화 검사는 ./, 반복 슬래시, 공백 차이처럼 눈으로는 놓치기 쉬운 변형을 확인합니다. 위험 경로 검사는 절대 경로와 상위 디렉터리 이동(..) 같은 아카이브 이탈 후보를 찾습니다.
재현용 점검 명령
먼저 목록을 만들고 원본 경로 기준 중복을 확인합니다.
CHECK_WORK_DIR="$(mktemp -d)"
trap 'rm -rf -- "$CHECK_WORK_DIR"' EXIT
LC_ALL=C tar -tzf '<BACKUP_ARCHIVE>' >"$CHECK_WORK_DIR/archive-members.txt"
LC_ALL=C sort "$CHECK_WORK_DIR/archive-members.txt" \
| uniq -d \
>"$CHECK_WORK_DIR/duplicate-raw.txt"
wc -l "$CHECK_WORK_DIR/duplicate-raw.txt"
절대 경로와 .. 구성요소는 다음처럼 1차 탐지할 수 있습니다.
awk '
/^\// || /(^|\/)\.\.($|\/)/ {
print
bad++
}
END {
print "unsafe_member_path_count=" (bad + 0) > "/dev/stderr"
exit(bad > 0)
}
' "$CHECK_WORK_DIR/archive-members.txt"
이 예시는 줄바꿈 문자가 들어간 특수 파일명을 완전하게 처리하는 범용 아카이브 감사 도구가 아닙니다. 운영 자동화에서는 사용 중인 tar 구현과 파일명 규칙을 고정하고, 별도 검증 스크립트에 테스트 케이스를 추가해야 합니다.
심볼릭 링크의 대상도 압축을 풀지 않고 목록에서 확인할 수 있습니다.
tar -tvzf '<BACKUP_ARCHIVE>' | awk '$1 ~ /^l/ {print}'
다만 tar -tv 출력 형식은 구현과 로캘에 따라 달라질 수 있습니다. 링크 대상이 허용 루트 밖으로 나가는지를 판정하는 자동화는 출력 형식에 의존하지 않는 별도 도구나 충분히 테스트된 스크립트를 사용하는 편이 안전합니다.
5. 확인이 끝나지 않은 허용 범위 검사
outside_allowed_scope_count의 값은 보존되지 않았습니다. 이 값은 “아카이브 경로가 문법적으로 안전한가”와 다른 질문에 답합니다.
예를 들어 절대 경로나 ..가 전혀 없어도 다음과 같은 파일이 백업 설계상 허용되지 않은 위치일 수 있습니다.
unexpected-service/config.json
temporary-debug-dump.sql
따라서 허용 루트 목록을 명시하고 그 밖의 최상위 경로를 다시 집계해야 합니다. 아래는 구조 예시이며 실제 허용 목록은 백업 설계 문서에 맞춰 바꿉니다.
awk '
{
path = $0
sub(/^\.\//, "", path)
split(path, part, "/")
root = part[1]
}
root != "<ALLOWED_DATABASE_ROOT>" &&
root != "<ALLOWED_MEDIA_ROOT>" &&
root != "<ALLOWED_METADATA_ROOT>" {
print path
outside++
}
END {
print "outside_allowed_scope_count=" (outside + 0) > "/dev/stderr"
exit(outside > 0)
}
' "$CHECK_WORK_DIR/archive-members.txt"
이 항목의 실제 값을 다시 확보하기 전까지 전체 아카이브 경로 검사를 “성공”으로 마감하면 안 됩니다.
6. 체크섬 성공 후에도 복원 테스트가 필요한 이유
체크섬과 목록 검증은 강한 신호지만 다음을 증명하지는 않습니다.
- MySQL 데이터 파일 또는 덤프가 엔진에서 정상적으로 읽히는지
- Ghost와 데이터베이스 스키마 버전이 호환되는지
- 이미지·파일·미디어 경로가 실제 게시물 URL과 연결되는지
- 관리자 로그인, 멤버 데이터, 테마와 라우팅 설정이 복원되는지
- 필요한 비밀값과 외부 서비스 설정을 안전하게 다시 주입할 수 있는지
Ghost 공식 문서는 self-hosted 설치의 백업에 콘텐츠, 멤버, 테마, 이미지·파일·미디어, 라우팅 설정 등 여러 구성요소가 필요함을 설명합니다. 데이터베이스와 파일 아카이브가 있다고 해서 이 모든 구성요소의 복원이 자동으로 확인되는 것은 아닙니다.
격리 복원 훈련의 최소 성공 기준
복원은 운영 서버가 아니라 외부 메일 발송과 공개 DNS가 차단된 격리 환경에서 수행합니다. 정확한 복원 명령은 백업 생성 방식과 Compose 구조에 따라 달라지므로, 보존되지 않은 값을 추정해 명령을 만들지 않습니다.
최소한 다음을 확인해야 합니다.
- 백업 파일의 체크섬이 다시
OK인지 확인 - 복원용 Compose 설정의 구문 검증
- MySQL 기동과 인증된
ping확인 - Ghost 컨테이너 기동과 로컬 HTTP 응답 확인
- 대표 게시물, 태그, 이미지·파일, 테마와 라우팅 확인
- 관리자 로그인과 필요한 경우 멤버 데이터 확인
- 외부 메일·결제·Webhook이 실제로 발송되지 않는지 확인
- 복원 시간과 수동 단계, 실패 원인을 기록
구문과 실행 상태를 확인하는 일반 명령은 다음과 같습니다.
docker compose -f '<RESTORE_COMPOSE_FILE>' config -q
docker compose -f '<RESTORE_COMPOSE_FILE>' up -d
docker compose -f '<RESTORE_COMPOSE_FILE>' ps
docker compose -f '<RESTORE_COMPOSE_FILE>' exec '<MYSQL_SERVICE>' \
mysqladmin --defaults-extra-file='<READ_ONLY_MYSQL_CLIENT_CNF>' ping
curl -fsS -o /dev/null \
-w 'http_status=%{http_code}\n' \
'http://127.0.0.1:<RESTORE_HTTP_PORT>/'
데이터베이스 비밀번호를 명령줄 인수로 직접 넣으면 셸 기록이나 프로세스 목록에 남을 수 있습니다. 읽기 권한이 제한된 클라이언트 설정 파일이나 비밀 관리 방식을 사용하고, 그 파일의 내용은 로그와 블로그에 공개하지 않습니다.
재점검 체크리스트
- [x] 점검 중 아카이브를 추출하지 않았다.
- [x] 사전에 정의한 체크섬 항목이 모두 일치했다.
- [x] 아카이브와 매니페스트의 전체 항목이 순서까지 일치했다.
- [x] 원본·정리·정규화 기준 중복이 0이었다.
- [x] 위험·비정규 경로가 0으로 기록됐다.
- [ ]
outside_allowed_scope_count를 다시 실행해 실제 값을 보존한다. - [ ] 백업 생성 시점의 MySQL 일관성 확보 절차를 확인한다.
- [ ] 체크섬 또는 서명을 백업과 분리된 신뢰 저장소에 보관한다.
- [ ] 격리 환경에서 데이터베이스와 Ghost를 실제로 복원한다.
- [ ] 게시물·미디어·로그인·외부 연동까지 복원 성공 기준을 통과한다.
- [ ] 복구 시간과 수동 절차를 운영 문서에 반영한다.
최종 판단
이번 기록에서 파일 무결성과 아카이브 목록 일치 검사는 성공했습니다. 특히 사전 정의한 체크섬 집합 전체의 일치, 목록의 순서 포함 정확한 일치, 중복·위험·비정규 경로 0은 유효한 근거입니다. 실제 항목 수와 파일명은 공개 원고에서 제거했습니다.
그러나 허용 범위 밖 경로의 최종 수치가 보존되지 않았고, 격리 환경에서 Ghost와 MySQL을 실제로 복원한 기록도 없습니다. 따라서 현재 상태는 무결성 점검 성공, 복구 가능성 검증 보류로 정리하는 것이 정확합니다.