Ghost 블로그 서버 명령어와 Mailgun 보안 체크리스트를 함께 점검하는 관리자 일러스트

Ghost·Mailgun 보안 점검 실전편: 서버 명령어와 대시보드

서버 운영 2026년 8월 19일

Ghost 관리자 로그인 메일이 EAUTH와 SMTP 535 Authentication failed로 실패하고 Mailgun에서 잠재적 침해 경고가 표시됐을 때, 서버와 Mailgun 관리 화면에서 실제로 무엇을 확인해야 할까?

이번 문제의 발견 과정과 복구 조치 전체는 Mailgun 잠재적 침해 경고 후 Ghost EAUTH 535 점검과 복구에서 먼저 정리했다. 이번 글에서는 같은 사건 흐름을 반복하지 않고, 서버에서 사용한 명령을 공개용으로 안전하게 다듬고 Mailgun 대시보드의 확인 위치를 단계별로 정리한다.

한 줄 결론: 서버에서는 원본 로그나 환경변수 전체를 덤프하지 말고 상태·오류 코드·개수만 확인한다. Mailgun에서는 상태 배너, 기존 API 키, SMTP 사용자, 사용자·세션, IP 허용 목록, 발송 로그를 서로 별개의 점검 항목으로 본다.

먼저 지켜야 할 공개 원칙

읽기 전용 명령이라고 해서 출력까지 안전한 것은 아니다. Ghost 로그와 Docker 설정에는 이메일 주소, API 키, SMTP 비밀번호, 내부 IP, 볼륨 경로, 관리자 인증 URL이 포함될 수 있다.

이 글의 명령은 다음 원칙으로 바꿨다.

  • 실제 도메인·IP·컨테이너명·경로·이메일 대신 환경변수를 사용한다.
  • 비밀값은 출력하지 않고 SET 또는 MISSING만 표시한다.
  • 로그는 원문 대신 오류 종류와 발생 횟수만 집계한다.
  • 파일 검색은 경로 대신 개수부터 확인한다.
  • HTTP 검사는 응답 본문이 아니라 상태 코드만 확인한다.
  • 상세 결과는 운영자 로컬 터미널에서만 보고, 화면 캡처와 전체 복사는 피한다.

다음 값은 각자 서버에서만 설정한다. 실제 값이 들어간 명령 기록이나 터미널 화면은 공개하지 않는다.

set +x

export GHOST_CONTAINER='<GHOST_CONTAINER>'
export GHOST_VERSION_FILE='<GHOST_VERSION_FILE>'
export PUBLIC_BASE_URL='https://<BLOG_DOMAIN>'
export GHOST_CONTENT_ROOT='<GHOST_CONTENT_ROOT>'
export BACKUP_ROOT='<BACKUP_ROOT>'
export SEARCH_ROOT='<SEARCH_ROOT>'
export DEPLOY_REPO='<DEPLOY_REPO>'
export SUSPICIOUS_STRING='<SUSPICIOUS_STRING>'

: "${GHOST_CONTAINER:?GHOST_CONTAINER를 설정하세요}"
: "${PUBLIC_BASE_URL:?PUBLIC_BASE_URL을 설정하세요}"

set +x는 이후 명령과 변수 값이 셸 디버그 로그에 그대로 기록되는 것을 막는다. /, 홈 디렉터리 전체처럼 지나치게 넓은 경로를 검색 대상으로 지정하지 않는다.

전체 점검 순서

  1. 서비스와 계정 상태 분리: 서버에서는 컨테이너 상태와 Ghost 버전을, Mailgun에서는 상단 경고 배너와 Support 티켓을 확인한다.
  2. 오류와 이벤트 대조: 서버에서 EAUTH, 535, ESOCKET을 집계하고 Reporting → Logs의 실제 이벤트와 비교한다.
  3. SMTP 구성 대조: 서버에서는 설정 항목의 존재 여부만 보고, SMTP Credentials에는 승인된 사용자만 남아 있는지 확인한다.
  4. 공개 경로 점검: 백업·설정 파일 후보가 외부에서 403 또는 404인지 확인한다.
  5. 변경 흔적 점검: 최근 변경과 의심 문자열을 검색하되, 결과 하나를 침해 증거로 단정하지 않는다.
  6. 키 노출 범위 점검: 저장소의 비밀 파일 추적 여부와 API Security·Sending Keys의 구 키 폐기 상태를 함께 확인한다.
  7. 재활성화 뒤 전달 확인: 새 테스트 뒤 서버 오류가 늘지 않고 Mailgun 이벤트가 Accepted에서 Delivered까지 이어지는지 확인한다.
  8. 접근 범위 점검: 포트·권한과 Users·Sessions·IP Access를 대조해 모르는 사용자·세션·IP가 없는지 확인한다.

1. 컨테이너 상태는 필요한 필드만 확인한다

docker inspect <컨테이너> 전체 출력에는 환경변수, 마운트 경로, 내부 네트워크 정보가 포함될 수 있다. 필요한 필드만 형식 지정해 조회한다.

docker inspect \
  --format 'state={{.State.Status}} restart_count={{.RestartCount}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}not-configured{{end}}' \
  "$GHOST_CONTAINER"

확인할 부분은 다음과 같다.

  • 상태가 예상대로 running인지
  • 재시작 횟수가 계속 증가하는지
  • 헬스체크가 있다면 healthy인지

정확한 시작 시각과 컨테이너 이름은 공개 글에 필요하지 않다.

외부 페이지는 본문을 내려받지 않고 상태 코드만 확인한다.

curl --proto '=https' \
  --max-time 10 \
  --silent \
  --output /dev/null \
  --write-out 'status=%{http_code}\n' \
  "$PUBLIC_BASE_URL"

200 응답은 사이트가 응답한다는 뜻이지, Ghost와 Mailgun의 모든 기능이 정상이라는 뜻은 아니다.

2. Ghost 버전은 값만 읽고 설치 경로는 숨긴다

Ghost 버전 파일 경로는 환경마다 다를 수 있다. 실제 경로는 GHOST_VERSION_FILE에만 넣고 결과값만 확인한다.

docker exec \
  --env VERSION_FILE="$GHOST_VERSION_FILE" \
  "$GHOST_CONTAINER" \
  node -e 'console.log(require(process.env.VERSION_FILE).version)'

확인한 버전은 Ghost 공식 업데이트 안내에 따라 지원 버전과 비교한다. 업데이트 전에는 데이터베이스와 콘텐츠를 백업하고, 메이저 버전 변경 시 마이그레이션과 테마 호환성을 별도로 검증한다.

보안 점검 글을 공개할 때는 패치가 끝난 뒤 작성하고, 내부 설치 경로나 이미지 태그 전체는 노출하지 않는 편이 안전하다.

3. EAUTH·535·ESOCKET은 원문 대신 집계한다

로그 원문에는 발신자·수신자 이메일, 메시지 ID, IP, 관리자 매직 링크가 섞일 수 있다. 먼저 필요한 오류 토큰만 추출해 집계한다.

docker logs --since 24h "$GHOST_CONTAINER" 2>&1 |
  rg -io 'sendAuthCodeToUser|EAUTH|535 Authentication failed|ESOCKET' |
  sort |
  uniq -c

rg가 없다면 같은 범위를 grep -Eio로 확인할 수 있다.

docker logs --since 24h "$GHOST_CONTAINER" 2>&1 |
  grep -Eio 'sendAuthCodeToUser|EAUTH|535 Authentication failed|ESOCKET' |
  sort |
  uniq -c

해석은 다음처럼 구분한다.

  • EAUTH·535: SMTP 사용자·비밀번호, Mailgun 계정 상태, IP Allowlist를 우선 확인한다.
  • ESOCKET: 호스트·포트·TLS·네트워크 연결을 먼저 확인한다.
  • sendAuthCodeToUser: Ghost의 관리자 로그인 인증 메일 흐름에서 발생했는지 좁히는 단서다.

문맥이 꼭 필요할 때만 운영자 로컬에서 제한된 줄을 확인한다.

docker logs --since 24h "$GHOST_CONTAINER" 2>&1 |
  rg -i 'EAUTH|ESOCKET|(^|[^0-9])535([^0-9]|$)' |
  head -n 20

이 결과는 그대로 게시하지 않는다. 공유가 필요하면 [EMAIL], [IP], [URL], [MESSAGE_ID], [PATH]로 수동 마스킹한 뒤 다시 읽어 실제 값이 남지 않았는지 확인한다.

4. SMTP 설정은 값을 출력하지 않고 존재 여부만 본다

다음 명령은 Ghost 컨테이너 안에서 메일 설정 항목이 존재하는지만 확인한다.

docker exec "$GHOST_CONTAINER" sh -lc '
for name in \
  mail__transport \
  mail__options__host \
  mail__options__port \
  mail__options__auth__user \
  mail__options__auth__pass
do
  if printenv "$name" 2>/dev/null | grep -q .; then
    printf "%s=SET\n" "$name"
  else
    printf "%s=MISSING\n" "$name"
  fi
done
'

예상 출력은 이 정도다.

mail__options__auth__user=SET
mail__options__auth__pass=SET

SET은 값이 있다는 의미일 뿐, 현재 Mailgun에 등록된 올바른 자격증명이라는 뜻은 아니다. 실제 SMTP 사용자는 Mailgun의 SMTP Credentials 화면과 로컬 설정을 운영자만 대조한다.

Ghost의 대량 뉴스레터는 Mailgun API 키를 사용하고, 관리자·멤버 인증 메일은 표준 mail 설정을 사용한다. 따라서 API 키를 바꿨다고 SMTP 비밀번호까지 자동으로 바뀌는 것이 아니다. 두 자격증명을 별도로 교체하고 구 값을 폐기해야 한다. 자세한 구조는 Ghost 뉴스레터 공식 문서에서 확인할 수 있다.

5. 공개된 백업·설정 파일은 상태 코드만 확인한다

과거에 노출됐던 정확한 URL을 알고 있다면 응답 본문이나 파일명을 출력하지 않고 상태 코드만 확인한다.

check_http_status() {
  label="$1"
  target_url="$2"

  status="$(
    curl --proto '=https' \
      --max-time 10 \
      --silent \
      --output /dev/null \
      --write-out '%{http_code}' \
      "$target_url"
  )"

  printf '%s status=%s\n' "$label" "$status"
}

check_http_status 'public-site' "$PUBLIC_BASE_URL"
check_http_status 'backup-path' "$PUBLIC_BASE_URL/<PRIVATE_BACKUP_PATH>"
check_http_status 'environment-file' "$PUBLIC_BASE_URL/.env"
check_http_status 'git-metadata' "$PUBLIC_BASE_URL/.git/HEAD"
check_http_status 'ghost-config' "$PUBLIC_BASE_URL/config.production.json"

백업·설정·Git 메타데이터 후보는 일반적으로 403 또는 404가 기대된다. 민감 경로의 200은 즉시 추가 확인이 필요한 신호다. 다만 일부 서버는 존재하지 않는 페이지도 사용자 정의 오류 화면과 함께 200을 반환하므로, 상태 코드 하나만으로 안전을 확정하지 않는다.

실제 백업 파일명, 날짜 규칙, 저장소명과 직접 URL은 공개하지 않는다.

6. 서버 내부 파일은 경로보다 개수부터 본다

백업 후보 개수를 먼저 확인한다.

printf 'backup_candidates='
find "$BACKUP_ROOT" -xdev -type f \
  \( -iname '*.zip' \
     -o -iname '*.tar' \
     -o -iname '*.tar.gz' \
     -o -iname '*.sql' \
     -o -iname '*.sqlite*' \) \
  -printf '.' |
wc -c

최근 7일 동안 바뀐 콘텐츠 파일 수도 개수로 확인한다.

printf 'recent_files='
find "$GHOST_CONTENT_ROOT" \
  -xdev \
  -type f \
  -newermt '7 days ago' \
  -printf '.' |
wc -c

컨테이너 쓰기 계층의 변화는 다음처럼 확인할 수 있다.

docker diff "$GHOST_CONTAINER"

상세 결과에는 내부 디렉터리 구조와 파일명이 나타날 수 있으므로 원문을 공개하지 않는다. 업로드 이미지, 캐시, 로그 같은 정상 변경도 포함되므로 변경 파일이 있다는 사실만으로 침해라고 단정해서는 안 된다.

7. 의심 문자열은 일치한 파일 수만 센다

게시글 변조나 Code Injection에서 본 의심 문자열을 검색할 때는 실제 문자열과 파일 내용을 글에 넣지 않는다.

: "${SUSPICIOUS_STRING:?SUSPICIOUS_STRING을 설정하세요}"

printf 'matching_files='
rg \
  --fixed-strings \
  --files-with-matches \
  --no-messages \
  "$SUSPICIOUS_STRING" \
  "$SEARCH_ROOT" |
wc -l

Ghost 로그의 출현 횟수도 원문 없이 셀 수 있다.

docker logs --since 7d "$GHOST_CONTAINER" 2>&1 |
  grep -F -- "$SUSPICIOUS_STRING" |
  wc -l

검색 루트는 테마나 검토 대상 콘텐츠 디렉터리로 제한한다. / 전체를 재귀 검색하면 시스템 부하와 과도한 정보 노출이 생길 수 있다.

검색 결과가 0이라는 것은 지정한 문자열이 현재 범위에서 발견되지 않았다는 뜻일 뿐, 과거 침해나 다른 변조가 전혀 없었다는 증명은 아니다. 게시글별 Code Injection은 데이터베이스에 저장될 수 있으므로 Ghost 관리자에서 의심 게시글의 Head·Footer Code Injection도 따로 확인한다.

8. 저장소에서 비밀 파일이 추적됐는지 확인한다

배포 저장소를 사용한다면 .env와 Ghost 설정 파일이 현재 또는 과거 Git 이력에 들어갔는지 확인한다. 출력은 YES/NO만 남긴다.

: "${DEPLOY_REPO:?DEPLOY_REPO를 설정하세요}"

for path in .env config.production.json
do
  if git -C "$DEPLOY_REPO" check-ignore -q -- "$path"; then
    printf '%s ignored=YES\n' "$path"
  else
    printf '%s ignored=NO\n' "$path"
  fi

  if git -C "$DEPLOY_REPO" ls-files --error-unmatch "$path" >/dev/null 2>&1; then
    printf '%s tracked_now=YES\n' "$path"
  else
    printf '%s tracked_now=NO\n' "$path"
  fi

  if git -C "$DEPLOY_REPO" log --all --format=%H -- "$path" | head -n 1 | grep -q .; then
    printf '%s found_in_history=YES\n' "$path"
  else
    printf '%s found_in_history=NO\n' "$path"
  fi
done

Git 이력에서 비밀 파일이 발견되면 단순히 현재 파일을 삭제하는 것으로 끝나지 않는다. 노출됐을 가능성이 있는 API 키·SMTP·DB 자격증명을 먼저 교체하고 구 값을 폐기한 뒤, 저장소 정리 범위는 별도로 결정한다.

9. 권한과 리스닝 포트는 요약값부터 본다

그룹 또는 기타 사용자에게 쓰기 권한이 열린 항목 수를 확인한다.

printf 'group_or_world_writable='
find "$GHOST_CONTENT_ROOT" \
  -xdev \
  \( -type f -o -type d \) \
  -perm /022 \
  -printf '.' |
wc -c

컨테이너 볼륨에서는 그룹 쓰기 권한이 정상 구성일 수 있으므로 숫자가 0보다 크다고 바로 취약점으로 판단하지 않는다. 확인 없이 chmod -R 777이나 chown -R을 실행하면 보안을 약화하거나 Ghost를 중단시킬 수 있다.

포트는 번호 대신 현재 리스너 개수부터 본다.

ss -H -lnt |
  awk 'END { print "tcp_listeners=" NR+0 }'

ss -H -lnu |
  awk 'END { print "udp_listeners=" NR+0 }'

상세 확인용 ss -H -lntupdocker port "$GHOST_CONTAINER" 출력에는 주소·포트·프로세스·매핑이 나타난다. 로컬에서만 검토하고 블로그에는 원문을 싣지 않는다. 리스닝 중이라는 사실과 실제 인터넷에서 접근 가능하다는 사실도 같지 않다. 방화벽과 클라우드 보안 규칙을 함께 확인해야 한다.

10. Mailgun 홈페이지에서 확인할 곳

2026년 8월 19일 실제 Control Panel과 공식 문서를 대조했다. 계정 유형과 UI 개편에 따라 UsersUser Management로, 도메인 진입 경로가 다르게 보일 수 있어 세부 버튼 위치보다 화면 이름을 기준으로 찾는 편이 안전하다.

  • 계정 제한: 로그인 직후 상단 경고 배너 → Support. 제한 사유, 지원 요청 상태와 재활성화 여부를 확인한다.
  • Account API Keys: 프로필 메뉴 → API Security. 모르는 키와 영향받은 구 키를 삭제하고 현재 키의 설명·생성일을 확인한다.
  • SMTP 사용자: Send 영역 → 대상 도메인 → Domain settings → SMTP Credentials. 승인된 사용자만 남았는지, 사용 중 계정 비밀번호를 재설정했는지 확인한다.
  • Domain Sending Keys: 같은 Domain settings → Sending Keys. Account API Key와 구분해 불필요한 도메인 키를 삭제한다.
  • 사용자·권한: 프로필 메뉴 → User Management 또는 Users → Users. 모르는 사용자, 과도한 역할과 MFA 상태를 확인한다.
  • 계정 세션: User Management 또는 Users → Sessions. 미인식 세션을 검토하고 기존 세션을 정리한다.
  • IP 허용 목록: 프로필 메뉴 → IP Access Management. 실제 고정 송신 공인 IP만 등록됐는지 확인한다.
  • 발송 이력: Reporting → Logs. 경고 전후의 이벤트·수신자·상태 메시지를 운영 기록과 대조한다.
  • 도메인 DNS: Send 영역 → 대상 도메인 → Domain settings → DNS Records. SPF·활성 DKIM·MX·추적 CNAME의 현재 상태를 확인한다.

상태 배너와 Support

계정 제한은 별도 Account status 메뉴를 찾기보다 로그인 직후 상단 배너의 정확한 문구와 Support 티켓을 확인한다. potential compromise는 침해가 확정됐다는 뜻이 아니라, 의심 정황에 따라 보호 조치가 적용됐다는 의미로 표현하는 편이 정확하다.

Mailgun의 침해 의심 계정 대응 안내는 지원팀 연락, 노출 경로 조사, 영향받은 구 API 키 삭제, 모든 도메인의 SMTP 비밀번호 재설정, 알 수 없는 SMTP 사용자 제거, 계정 비밀번호 변경, 전 사용자 2FA 적용을 권고한다.

API Security: 새 키 발급만으로 끝내지 않는다

API Security에서는 키의 설명과 생성일을 기준으로 용도를 식별한다. 새 키를 만들었다면 Ghost 설정을 교체한 뒤 영향받은 기존 키를 삭제해야 회전이 완료된다.

새 API 키의 비밀값은 생성 직후 한 번만 표시될 수 있다. 키 목록, 생성 창, Webhook Signing Key, 2FA 복구 코드가 보이는 화면은 캡처하지 않는다.

SMTP Credentials: 사용자와 비밀번호를 따로 본다

먼저 대상 도메인이 맞는지 확인한 뒤 SMTP 사용자 전체를 검토한다. 사용 중인 계정만 남기고 모르는 사용자와 오래된 중복 사용자는 제거한다. 비밀번호를 재설정한 뒤에는 Ghost 환경변수를 새 값으로 갱신하고, 이전 값이 더 이상 작동하지 않는지 확인한다.

Account API Key, Domain Sending Key, SMTP 비밀번호는 서로 다른 비밀값이다. 하나를 바꿨다고 나머지가 자동 폐기되지 않는다.

User Management: Users와 Sessions를 모두 본다

Users 탭에서는 사용자와 역할, MFA 상태를 확인한다. Sessions 탭에서는 현재·이전 세션의 생성 시각, 만료 시각, 브라우저·운영체제 정보를 보고 미인식 세션을 정리한다.

세션 종료는 웹 로그인 세션을 정리하는 조치다. API 키와 SMTP 비밀번호까지 폐기하는 것은 아니므로 자격증명 회전과 별도로 수행한다.

계정 유형에 따라 개인 2FA는 Manage my Sinch ID → Sign-in & Security에서 관리될 수 있다. 메뉴 이름이 다르더라도 계정 전체 강제 여부와 각 사용자의 MFA 상태를 모두 확인한다.

IP Access Management: 고정 IP일 때만 적용한다

서버의 현재 송신 공인 IPv4는 운영자 로컬에서만 확인한다.

curl -4fsS https://api.ipify.org
printf '\n'

이 출력은 서버 식별 정보이므로 게시글과 스크린샷에 넣지 않는다.

Mailgun IP Allowlist 공식 안내에 따르면 2024년 4월부터 한 IP를 등록하면 HTTP API와 SMTP 연결에 함께 적용되며 둘을 분리해 허용할 수 없다. Ghost 서버의 실제 고정 송신 IP가 빠져 있으면 비밀번호가 맞아도 SMTP 535 Authentication failed 또는 API 401 Forbidden이 발생할 수 있다.

동적 IP 환경이라면 정상 발송이 차단될 수 있으므로 무조건 등록하지 않는다. 등록된 IP·CIDR과 설명에 모르는 항목이 없는지도 확인한다.

Reporting → Logs: Accepted와 Delivered를 구분한다

Logs에서는 경고 전후 기간을 선택하고 다음을 운영 기록과 대조한다.

  • 내가 보내지 않은 Accepted 또는 Delivered 이벤트
  • 예상하지 못한 수신자·제목·시간대
  • Rejected·Failed의 갑작스러운 증가
  • 낯선 발신 도메인과 상태 메시지

Accepted는 Mailgun이 API 또는 SMTP 요청을 받아 큐에 넣었다는 의미다. 최종 전달 확인은 수신 서버가 받아들인 Delivered까지 봐야 한다. 로그 보존기간은 계정과 요금제에 따라 다르므로, 화면에 남아 있는 범위를 넘어 “전체 과거 이력에 이상이 없다”고 단정하지 않는다.

DNS Records: 정상 발송의 기반이지 침해 여부의 증거는 아니다

SPF, 현재 사용하는 활성 DKIM, MX, 추적 CNAME 상태를 확인하고 필요하면 Check status로 다시 검증한다. DNS 전파에는 시간이 걸릴 수 있다.

DNS가 Verified라는 사실은 발송 도메인이 구성됐다는 의미다. 계정이나 서버가 침해되지 않았다는 증거는 아니다. 반대로 과거·보조 DKIM 키가 Unverified라고 해서 현재 활성 키까지 무조건 잘못됐다고 단정하지 않는다.

11. 재활성화 뒤 최종 검증

Mailgun 계정이 제한 상태라면 Ghost 설정이 올바르더라도 메일은 계속 실패할 수 있다. 지원팀의 재활성화 확인 뒤 다음 순서로 검증한다.

  1. Mailgun 상단 경고 배너와 Support 티켓 상태를 확인한다.
  2. 새 자격증명으로 테스트 메일을 한 번 보낸다.
  3. Reporting → Logs에서 예상 요청의 AcceptedDelivered를 확인한다.
  4. 새로운 브라우저 세션에서 Ghost 관리자 로그인 인증 메일을 요청한다.
  5. 서버 집계 명령을 다시 실행해 새 EAUTH·535가 추가되지 않았는지 확인한다.

관리자 화면에 들어갈 수 있다는 사실만으로 SMTP와 뉴스레터까지 모두 복구됐다고 판단하지 않는다. Ghost 관리자·멤버 인증 메일과 뉴스레터 발송 경로를 각각 테스트한다.

그대로 출력하거나 실행하지 말아야 할 것

피해야 할 명령·행동 이유
env, printenv, docker exec ... env SMTP·DB·API 비밀값이 노출될 수 있음
형식 지정 없는 docker inspect <CONTAINER> 환경변수, 마운트, 내부 IP가 함께 나올 수 있음
docker compose config 변수 치환 뒤 실제 비밀값이 출력될 수 있음
cat .env, cat <GHOST_CONFIG>, cat <COMPOSE_FILE> 계정·비밀번호·내부 구성이 그대로 보임
전체 docker logs 출력 복사 이메일, 메시지 ID, IP, 인증 링크가 섞일 수 있음
set -x, history, ps e, /proc/<PID>/environ 명령 인수와 환경변수가 노출될 수 있음
curl -v, curl --trace, 인증 헤더 포함 명령 쿠키·Authorization 헤더·내부 주소가 남을 수 있음
grep -R ... /, find / ... 과도한 부하와 시스템 전체 정보 노출 가능
docker compose down -v, docker system prune, volume prune 서비스·볼륨·데이터 손실 가능
chmod -R 777, 검증 없는 chown -R 권한 약화 또는 Ghost 장애 가능
로그·설정 파일을 외부 파일 공유 서비스에 업로드 민감정보 유출 가능

이번 글은 읽기 전용 진단 범위다. 삭제·권한 변경·컨테이너 재생성 같은 작업은 백업과 영향 범위를 확인한 뒤 별도 절차로 진행한다.

최종 체크리스트

서버

  • [ ] 컨테이너 상태·재시작 횟수·헬스체크를 필요한 필드로만 확인했다.
  • [ ] Ghost 실제 버전을 지원 버전과 비교했다.
  • [ ] 로그 원문 대신 EAUTH·535·ESOCKET을 집계했다.
  • [ ] SMTP 변수는 값이 아니라 SET/MISSING으로만 확인했다.
  • [ ] 공개 백업·환경 파일·Git 메타데이터 후보가 403 또는 404인지 확인했다.
  • [ ] 최근 변경과 의심 문자열 검색 결과를 침해 확정으로 해석하지 않았다.
  • [ ] .env와 Ghost 설정 파일의 현재·과거 Git 추적 여부를 확인했다.
  • [ ] 포트·파일 경로·소유자 목록의 원문을 공개하지 않았다.

Mailgun

  • [ ] 상단 상태 배너와 Support 요청을 확인했다.
  • [ ] 영향받은 구 Account API Key와 불필요한 Sending Key를 삭제했다.
  • [ ] 승인된 SMTP 사용자만 남기고 비밀번호를 재설정했다.
  • [ ] 사용자·역할·MFA와 Sessions를 각각 확인했다.
  • [ ] 고정 송신 IP가 확실할 때만 IP Allowlist를 적용했다.
  • [ ] 경고 전후 Logs에서 비정상 수신자·이벤트·시간대를 확인했다.
  • [ ] SPF·활성 DKIM·MX·추적 CNAME 상태를 확인했다.
  • [ ] 재활성화 뒤 테스트가 Accepted에서 Delivered까지 이어지는지 확인했다.

공개 전

  • [ ] 이메일·도메인·IP·컨테이너명·내부 경로를 자리표시자로 바꿨다.
  • [ ] API 키·SMTP 비밀번호·2FA 복구 코드·세션 토큰이 없다.
  • [ ] Mailgun Key ID·메시지 ID·지원 티켓 번호를 제거했다.
  • [ ] 실제 터미널과 대시보드 캡처 대신 재작성한 예시를 사용했다.
  • [ ] “해킹 확정”, “특정 CVE 악용 확정”, “완전히 안전”이라고 표현하지 않았다.

마무리

이번 점검에서 가장 중요한 원칙은 현재 확인할 수 있는 사실과 추정을 분리하는 것이었다.

서버 명령으로 확인할 수 있는 것은 현재 컨테이너 상태, 특정 오류 발생 여부, 지정한 문자열의 존재, 공개 경로의 응답, 파일과 권한의 현재 상태다. Mailgun에서 확인할 수 있는 것도 현재 사용자·세션·키·SMTP 계정과 보존 중인 발송 기록이다. 이것만으로 과거 침해 여부나 정확한 유입 경로까지 확정할 수는 없다.

그래서 EAUTH 535를 단순 비밀번호 오류로만 보지 않되, 잠재적 침해 경고를 곧바로 해킹 확정으로 표현하지도 않았다. 구 자격증명을 폐기하고, 서버·공개 콘텐츠·계정 세션·발송 로그를 함께 점검한 뒤, 서비스 제공자의 재활성화 절차와 최종 테스트까지 이어가는 것이 가장 재현 가능한 대응이었다.

공식 참고 문서

태그