운영·보안

Astro 배포 후 파일 권한이 707이었습니다, 755·644로 고친 과정

Astro 정적 사이트 배포 후 발견한 707·777·666 권한을 디렉터리 755, 파일 644로 수정하고 자동 배포에서 재발을 막은 실제 과정을 정리합니다.

흰색 파일 두 장이 담긴 차콜 배포 폴더를 오렌지색 자물쇠로 잠근 3D 클레이 이미지

minml은 Astro로 만든 정적 사이트를 Nginx 서버에 직접 배포합니다. 새 글을 올린 뒤 홈페이지와 글 주소가 모두 정상적으로 열렸기 때문에 한동안 파일 권한은 따로 의심하지 않았습니다. 하지만 서버 상태를 점검하면서 새 릴리스 안의 일부 디렉터리가 707, 이전 배포에는 777 디렉터리와 666 파일이 남아 있다는 사실을 발견했습니다.

화면만 보면 문제는 없었습니다. Nginx는 HTML과 이미지를 읽을 수 있었고 방문자는 평소처럼 사이트를 볼 수 있었습니다. 그렇지만 사이트가 열린다는 것과 필요한 사용자만 파일을 바꿀 수 있다는 것은 다른 문제였습니다.

점검 당시 minml 배포 영역에서 그룹이나 기타 사용자에게 쓰기 권한이 있는 일반 파일과 디렉터리는 모두 1,531개였습니다. 이번 글에서는 이 숫자를 0개로 줄인 방법과 다음 배포에서 같은 권한이 생기지 않도록 배포 순서를 바꾼 과정을 정리했습니다. 모든 웹서버에 755/644가 유일한 정답이라는 뜻은 아닙니다. 별도의 업로드 서비스나 여러 운영자가 같은 그룹으로 파일을 관리한다면 권한 설계가 달라질 수 있습니다.

707·777·666은 무엇을 허용할까

리눅스의 숫자 권한은 소유자, 그룹, 기타 사용자 순서로 읽기 4, 쓰기 2, 실행 1을 더해서 표시합니다. GNU 문서도 파일 모드를 이 세 사용자 범주와 읽기·쓰기·실행 권한으로 설명합니다.

권한 소유자 그룹 기타 사용자
707 읽기·쓰기·실행 권한 없음 읽기·쓰기·실행
777 읽기·쓰기·실행 읽기·쓰기·실행 읽기·쓰기·실행
666 읽기·쓰기 읽기·쓰기 읽기·쓰기
755 읽기·쓰기·실행 읽기·실행 읽기·실행
644 읽기·쓰기 읽기 읽기

파일에서 읽기와 쓰기는 이름 그대로 내용을 읽고 바꾸는 권한입니다. 디렉터리에서는 의미가 조금 다릅니다. 읽기 권한은 내부 이름을 나열하는 데, 실행 권한은 해당 디렉터리를 통과해 파일에 접근하는 데 필요합니다. 쓰기 권한까지 있으면 조건에 따라 내부 항목을 만들거나 삭제하고 이름을 바꿀 수 있습니다.

minml의 공개 결과물은 이미 빌드가 끝난 HTML, CSS, JavaScript와 이미지입니다. 웹 서버는 파일을 읽고 디렉터리를 통과할 수 있으면 되지만, 모든 로컬 사용자가 공개 파일을 수정할 필요는 없습니다. 그래서 현재 운영 구조에서는 디렉터리 755, 일반 파일 644를 기준으로 정했습니다.

사이트가 정상인데도 왜 수정했을까

707은 가운데 그룹에는 아무 권한도 주지 않으면서 기타 사용자에게는 모든 권한을 줍니다. 777666도 그룹과 기타 사용자의 쓰기를 허용합니다. 인터넷 방문자가 이 권한만으로 곧바로 서버 파일을 바꿀 수 있다는 뜻은 아닙니다. 먼저 서버에서 실행되는 계정이나 취약한 서비스처럼 로컬 파일에 접근할 수단이 있어야 합니다.

그렇더라도 불필요한 쓰기 권한은 문제가 발생했을 때 영향을 넓힙니다. 같은 서버의 다른 로컬 계정이나 서비스가 해당 경로에 접근한다면 공개 중인 HTML이나 스크립트를 교체할 여지가 생기기 때문입니다. 특히 디렉터리에 쓰기 권한이 있으면 내부 파일 자체가 읽기 전용이더라도 이름을 바꾸거나 다른 파일로 대체할 가능성을 따져야 합니다.

이번 점검에서 파일 변조나 침입 흔적을 발견한 것은 아닙니다. 따라서 “해킹당한 서버를 복구했다”고 표현할 근거는 없습니다. 확인한 사실은 정적 사이트 제공에 필요하지 않은 쓰기 권한이 넓게 열려 있었고, 이를 제거했다는 것입니다.

원인을 하나로 단정하지 않고 배포 결과를 고쳤습니다

기존 방식은 Windows에서 Astro 빌드를 마친 뒤 scp로 새 릴리스를 올리고, 현재 릴리스를 가리키는 링크를 전환하는 구조였습니다. 새 폴더를 따로 만든다는 점은 좋았지만 업로드가 끝난 뒤 서버에서 최종 권한을 정규화하고 검사하는 단계가 없었습니다.

새로 업로드한 릴리스에서 707이 확인됐지만 scp 하나만 원인이라고 단정하지는 않았습니다. 원격 디렉터리를 만드는 방식, 전송 도구와 서버의 기본 모드도 함께 영향을 줄 수 있기 때문입니다. 더 분명한 문제는 어느 도구를 사용하든 배포 결과의 권한을 확인하지 않은 채 공개 전환했다는 것이었습니다.

그래서 원인을 추측해 특정 전송 방식에만 기대기보다, 서버에 올라온 최종 결과를 항상 같은 권한으로 맞추는 쪽을 선택했습니다.

정확한 릴리스 경로만 755·644로 바꿨습니다

권한 수정은 서버 전체나 /var/www 전체가 아니라 확인을 마친 minml 릴리스 경로로 한정했습니다. 아래 명령은 구조를 보여주기 위한 예시이며 실제 서버 주소와 릴리스 이름은 바꿨습니다.

release="/var/www/example/releases/20260827T000000Z"

test "$(readlink -f "$release")" = "$release"
find "$release" -xdev -type d -exec chmod 755 {} +
find "$release" -xdev -type f -exec chmod 644 {} +

첫 번째 test는 변수에 적은 경로가 예상한 실제 경로인지 확인합니다. find -xdev는 다른 파일시스템으로 넘어가지 않게 하고, 디렉터리와 일반 파일을 나눠 서로 다른 권한을 적용합니다. 이 명령을 서버 루트나 사용자 업로드 디렉터리에 그대로 실행하면 안 됩니다. 실행 파일, 비밀키, 공동 편집 디렉터리처럼 다른 권한이 필요한 파일까지 바뀔 수 있기 때문입니다.

수정 후에는 그룹 또는 기타 사용자의 쓰기 비트가 하나라도 남았는지 별도로 검사했습니다.

find "$release" -xdev \( -type d -o -type f \) -perm /0022 -print

0022는 그룹 쓰기 0020과 기타 사용자 쓰기 0002를 함께 확인하는 값입니다. 출력이 비어 있어야 이번 기준을 통과합니다. 명령이 성공했다는 사실보다 검사 결과가 비어 있는지를 확인하는 것이 중요했습니다.

current 링크의 777 표시는 같은 문제가 아니었습니다

권한 목록을 보면 현재 릴리스를 가리키는 심볼릭 링크가 lrwxrwxrwx, 즉 777처럼 표시될 수 있습니다. 처음에는 이것도 바꿔야 하는지 확인했습니다. 하지만 일반적인 리눅스 시스템에서는 심볼릭 링크 자체의 권한을 접근 판단에 사용하지 않고, 링크가 가리키는 대상 파일과 디렉터리의 권한을 적용합니다. GNU chmod 문서에서도 심볼릭 링크의 모드는 대부분의 시스템에서 변경하지 않는다고 설명합니다.

따라서 이번에 확인할 대상은 current라는 링크 표시가 아니라 다음 두 가지였습니다.

  1. current가 예상한 릴리스 디렉터리를 가리키는지
  2. 링크 대상의 디렉터리와 일반 파일이 755/644인지

겉으로 같은 777이라는 숫자가 보이더라도 일반 디렉터리와 심볼릭 링크를 구분해야 했습니다.

한 번의 chmod보다 다음 배포를 바꾸는 것이 중요했습니다

기존 파일만 수정하면 다음 업로드에서 707 같은 권한이 다시 만들어질 수 있습니다. 그래서 minml의 자동 배포 스크립트에 권한 정규화와 실패 조건을 넣었습니다. 현재 배포 순서는 다음과 같습니다.

  1. Git 작업 폴더에 커밋하지 않은 변경이 없는지 확인
  2. Astro 빌드와 콘텐츠 검사 실행
  3. 서버에 새 릴리스 디렉터리 생성
  4. 완성된 dist 결과만 새 릴리스로 업로드
  5. 디렉터리 755, 일반 파일 644 적용
  6. 그룹·기타 사용자에게 쓰기 가능한 경로가 남으면 배포 중단
  7. 필수 HTML과 사이트맵 파일 확인
  8. current 링크를 새 릴리스로 전환
  9. 공개 주소와 Nginx 상태 검사
  10. 검사 실패 시 직전 릴리스로 복구

핵심은 5번과 6번이 공개 전환보다 앞에 있다는 점입니다. 잘못된 권한을 발견하고도 사이트부터 전환한 뒤 나중에 고치는 방식이 아닙니다. 권한이 기준에 맞지 않으면 새 릴리스는 준비된 상태로 남고 기존 사이트가 계속 공개됩니다.

실제 자동화에서는 수정 명령 뒤에 다음과 같은 실패 조건을 사용합니다.

test -z "$(find "$release" -xdev \
  \( -type d -o -type f \) -perm /0022 -print -quit)"

-print -quit는 문제가 있는 첫 경로만 찾아도 검사를 끝냅니다. 출력이 하나라도 있으면 test -z가 실패하고 이후 릴리스 전환으로 진행하지 않습니다.

수정 뒤에는 권한과 공개 사이트를 함께 확인했습니다

권한을 고친 뒤 웹 서버가 파일을 읽지 못한다면 보안 설정만 맞고 사이트는 망가진 셈입니다. 그래서 파일 모드와 실제 HTTP 응답을 함께 확인했습니다.

확인 항목 수정 전 수정 후
그룹·기타 사용자에게 쓰기 가능한 경로 1,531개 0개
현재 릴리스 디렉터리 일부 707/777 755
현재 릴리스 일반 파일 일부 666 644
Nginx 상태 실행 중 실행 중
minml 홈페이지 200 200
별도 운영 사이트 200 200

여기에 새 글 주소와 대표 이미지, 사이트맵의 200, 존재하지 않는 시험 주소의 404, 기존 통합 글의 301도 배포 검사에 포함했습니다. 파일 권한만 맞는지 보는 것과 방문자가 실제 페이지를 받을 수 있는지는 서로 다른 검사이기 때문입니다.

자동 배포 명령도 새 글 주소를 직접 넘기도록 바꿨습니다.

npm run deploy -- --check-path=/새-글-주소/

이제 배포 스크립트는 빌드, 파일 업로드, 권한 정리, 릴리스 전환과 외부 응답 검사를 순서대로 실행합니다. 이전처럼 수동으로 배포할 수는 있지만, 권한 확인을 빠뜨리지 않기 위해 새 글 발행에는 이 경로를 기본으로 사용합니다.

정적 사이트 배포에서 제가 확인할 권한 체크리스트

이번 경험 뒤에는 배포를 마칠 때 다음 항목을 확인합니다.

  • 웹 서버가 실제로 바라보는 현재 릴리스는 어디인가
  • 릴리스 디렉터리는 필요한 사용자만 쓸 수 있는가
  • 일반 파일과 디렉터리를 나눠 권한을 적용했는가
  • 그룹 또는 기타 사용자 쓰기 권한이 남아 있지 않은가
  • 777처럼 보이는 항목이 일반 파일인지 심볼릭 링크인지 구분했는가
  • 권한 수정이 서버 전체가 아닌 정확한 배포 경로에만 적용됐는가
  • 다음 배포에서도 같은 검사가 자동으로 실행되는가
  • 수정 후 주요 페이지, 이미지, 404와 리디렉션이 정상인가

AI에게 SSH 서버 작업을 맡길 때 확인하는 7가지에서는 대상 서버 확인과 작업 범위를 먼저 나누는 이유를 설명했습니다. 이번 권한 수정에서도 가장 도움이 된 것은 서버 전체를 한꺼번에 정리하지 않고 minml 배포 영역을 정확히 확인한 일이었습니다.

글 작성부터 새 릴리스 배포까지의 전체 흐름은 Astro 블로그를 발행하는 과정에 정리했습니다. 당시에는 빌드와 공개 응답 확인에 집중했지만, 이제 그 사이에 서버 파일 권한 정규화와 쓰기 가능 경로 검사가 추가됐습니다.

사이트가 열린다는 결과만으로 배포를 끝내지 않습니다

이번 문제는 화면 오류가 아니라서 평소 방문만으로는 발견하기 어려웠습니다. 707755로, 666644로 바꾸는 명령 자체는 짧았지만 더 중요한 변화는 배포 성공의 기준이 달라진 것입니다.

이전에는 새 페이지가 열리면 배포가 끝났다고 생각했습니다. 지금은 새 릴리스의 권한이 기준에 맞고, 쓰기 가능한 경로가 없으며, 공개 페이지와 기존 사이트까지 정상일 때 배포가 끝난 것으로 봅니다. 한 번 안전하게 만든 현재 상태보다 다음 배포에서도 같은 검사가 빠지지 않는 과정이 더 오래 남는 개선이라고 생각합니다.

참고한 공식 문서