Git: fatal: detected dubious ownership in repository
안녕하세요, BlueByte입니다. 어제까지 잘 되던 저장소에서 — 컨테이너 안에서, sudo로, 혹은 새로 마운트한 디스크에서 — git status를 실행했더니 fatal: detected dubious ownership in repository로 멈추는 경우가 있습니다. 망가진 것은 없습니다. 커밋은 그대로이고, Git이 명령을 실행한 사용자와 다른 사용자가 소유한 저장소를 읽지 않겠다고 거절하는 것뿐입니다. 오늘은 이 메시지가 뜻하는 것, Git이 이렇게 동작하는 이유, 어떤 불일치인지 확인하는 방법, 원인별 해결, 그리고 예방까지 하나씩 짚어보겠습니다.
메시지의 모양과 예전 문구
Linux와 macOS에서는 fatal 한 줄과 제안 명령이 출력되고, Git은 종료 코드 128로 끝납니다:
fatal: detected dubious ownership in repository at '/srv/app'
To add an exception for this directory, call:
git config --global --add safe.directory /srv/appGit for Windows는 경로의 소유자와 현재 사용자도 함께 보여주며, 모양은 다음과 같습니다:
'C:/work/app' is owned by:
BUILTIN/Administrators (S-1-5-32-544)
but the current user is:
WORKPC/alex (S-1-5-21-...)Git 2.35.2부터 2.37까지는 같은 검사가 예전 문구 unsafe repository ('/srv/app' is owned by someone else)로 출력됩니다. detected dubious ownership 문구는 2.38부터입니다. 검사도 해결책도 같습니다.
Git이 자기 소유가 아닌 저장소를 거절하는 이유
이 검사는 2022년 4월 Git 2.35.2에서 수정된 CVE-2022-24765에서 나왔습니다. 저장소 설정은 Git이 프로그램(hook, core.fsmonitor, pager)을 실행하게 만들 수 있어서, 공유 머신에서 다른 사용자가 심어 둔 .git 디렉터리가 근처에서 git status를 치는 순간 내 권한으로 코드를 실행할 수 있었습니다. 그 릴리스부터 Git은 작업 트리와 .git 디렉터리의 소유자를 명령을 실행하는 사용자와 비교하고, 다르면 멈춥니다.
Linux와 macOS에서는 숫자 사용자 ID로 비교합니다. root만 예외입니다. root 소유 저장소는 통과하고, sudo로 실행하면 sudo가 SUDO_UID에 저장한 UID도 인정합니다. 예외 목록은 safe.directory에 두며, Git은 이 설정을 보호된 설정 — system, global, 명령줄 — 에서만 읽습니다. 저장소 자신의 .git/config는 읽지 않는데, 바로 그 파일을 아직 신뢰하지 않기 때문입니다.
소유자와 사용자가 어긋나는 흔한 경로
- 컨테이너나 CI 작업이 root 또는 고정 UID로 실행되는데, 바인드 마운트한 체크아웃은 호스트 사용자 소유인 경우.
sudo git clone으로 받은 저장소를 일반 사용자로 쓰거나, 그 반대인 경우.- 서비스 계정(
www-data,jenkins)이 관리자가 만든 디렉터리에서 Git을 실행하는 경우. - Windows에서 외장·네트워크 드라이브의 저장소, 또는 관리자 권한 프롬프트로 만들어 Administrators 소유가 된 폴더.
먼저 소유자와 Git을 실행하는 사용자를 비교하기
파일시스템과 Git에 물어봅니다:
stat -c '%U %u %n' /srv/app /srv/app/.git
id -un; id -u
git config --show-scope --get-all safe.directory직접 재현해 보니, ubuntu(UID 1001) 소유 저장소에서 Git을 nobody로 실행했을 때 stat은 두 경로 모두 ubuntu 1001을, id -u는 65534를 출력했습니다 — 명백한 불일치입니다. 마지막 명령의 결과가 비어 있으면 Git이 신뢰하는 어느 범위에도 예외가 설정되지 않았다는 뜻입니다.
원인별 해결: 저장소를 소유하거나, 의도적으로 신뢰하거나
파일이 내 것이어야 한다면 검사를 끄지 말고 소유권을 바로잡습니다:
sudo chown -R "$(id -u):$(id -g)" /srv/app
git -C /srv/app status
# On branch main불일치가 의도된 것이라면(공유 저장소, CI 컨테이너), Git을 실행하는 사용자의 global 설정에 정확한 경로를 추가합니다:
git config --global --add safe.directory /srv/app
git config --show-scope --get-all safe.directory
# global /srv/app명령 한 번만이라면 명령줄 범위도 됩니다: git -c safe.directory=/srv/app status. /srv/app/.git/config에 적는 것은 소용없습니다 — 직접 해 보니 문서에 적힌 대로 Git이 거기 값은 무시했습니다.
한 디렉터리 아래 모든 저장소를 신뢰하려면 Git 2.46 이상에서 safe.directory=/srv/*처럼 끝에 /*를 붙일 수 있습니다. 이전 버전은 이를 아무것도 맞지 않는 문자 그대로의 경로로 취급합니다 — 제 2.43 환경에서는 오류가 그대로였습니다 — 그러니 먼저 git --version을 확인해 보세요. 값 * 하나는 검사를 완전히 끕니다. 다른 누구도 디스크에 쓸 수 없는 일회용 컨테이너에서만 쓰십시오.
따라 해 보기: 같은 저장소, sudo 유무에 따라
ubuntu 소유 저장소를 sudo를 통해 root로 실행합니다:
sudo git -C /srv/app status
# On branch main
sudo env -u SUDO_UID git -C /srv/app status
# fatal: detected dubious ownership in repository at '/srv/app'첫 번째는 Git이 SUDO_UID를 읽어 소유자 UID를 찾았기 때문에 됩니다. 이 변수를 빼면 — root로 도는 컨테이너 안이나 su - 이후가 사실상 이 상태입니다 — 같은 명령이 실패합니다. CI에서 보고되는 사례 대부분이 이 패턴입니다. 작업의 Git이 체크아웃 소유자와 한 번도 맞은 적 없는 사용자로 실행되는 것입니다. 컨테이너를 체크아웃의 UID로 실행하거나, 설정 단계에 git config --global --add safe.directory "$PWD"를 추가하세요.
끝까지 확인하고 그 상태를 유지하기
git status 다음에 echo $?를 실행합니다. 브랜치 정보와 0이 나오면 끝입니다. 유지하려면 클론은 그것을 쓰는 계정이 소유하게 두고, 일상 작업에 sudo git을 피하고, safe.directory 줄을 정확한 체크아웃 경로와 함께 컨테이너 이미지나 CI 설정에 넣고, *에 기대기보다 경로를 하나씩 나열하세요.
권한 오류, "not a git repository"와 다른 점
error: insufficient permission for adding an object to repository database .git/objects는 쓰기 실패입니다. Git은 저장소를 신뢰했고, 운영체제가 쓰기를 거부한 것입니다. fatal: not a git repository (or any of the parent directories): .git은 Git이 .git을 아예 찾지 못했다는 뜻입니다. dubious ownership은 그 사이에 있습니다. Git이 저장소를 찾았지만 일부러 읽기를 거절한 것입니다.
다음에 이 오류를 만나면 이 순서를 거꾸로 따라가 보세요. stat과 id -u를 비교하고, 소유자와 사용자 중 어느 쪽이 틀렸는지 판단한 뒤, 그다음에야 정확한 경로로 safe.directory를 꺼내 드세요.
관련 질문
safe.directory를 *로 설정하고 넘어가도 안전한가요?
혼자 쓰는 머신이나 다른 누구도 쓸 수 없는 일회용 컨테이너라면 위험은 작습니다. 공유 서버에서는 보호가 완전히 사라집니다. 다른 사용자가 내 작업 디렉터리 위쪽에 만든 .git 디렉터리를 다시 내 권한으로 읽게 됩니다. 정확한 경로를 나열하거나, Git 2.46 이상에서 끝에 /*를 붙이면 예외를 좁게 유지할 수 있습니다.
경로를 추가했는데도 오류가 계속 납니다.
세 가지를 확인하세요. 값은 Git이 메시지에 출력한 경로와 맞아야 하니 다시 타이핑하지 말고 거기서 복사하세요. Git이 신뢰하는 범위에 있어야 합니다 — git config --show-scope --get-all safe.directory가 global, system, command 중 하나로 보여야 합니다. 그리고 실제로 Git을 실행하는 사용자의 설정이어야 합니다. 명령이 sudo나 서비스 계정으로 돈다면 내 ~/.gitconfig는 읽히지 않습니다.
Git이나 Git for Windows를 업그레이드한 직후부터 이 오류가 납니다.
소유권 검사는 2.35.2부터 시작된 2022년 4월 보안 릴리스에 들어갔고, 이후 모든 버전이 유지합니다. 오래된 Git에서 업그레이드하면 검사가 켜지므로, 원래부터 다른 계정 소유였던 저장소가 새 버전으로 처음 건드리는 순간 실패하기 시작합니다.
chown을 할까요, safe.directory를 추가할까요?
파일이 정말 내 것이어야 한다면 소유권을 바꾸세요 — 실수로 sudo로 클론한 경우가 전형적입니다. 팀이 공유하는 저장소나, 다른 UID로 돌아야 하는 컨테이너에 바인드 마운트한 체크아웃처럼 분리가 의도된 것이라면 safe.directory를 추가하세요. 앞쪽은 원인을 고치고, 뒤쪽은 결정을 기록합니다.
Windows 드라이브에서도 똑같이 적용되나요?
네, 메시지에 한 가지 차이가 있습니다. Git for Windows는 소유 계정과 내 계정을 SID와 함께 출력해서 원인이 바로 보입니다. 외장·네트워크 드라이브의 저장소나, 관리자 권한 프롬프트로 만들어 Administrators 소유가 된 폴더가 흔한 계기입니다. 메시지에 출력된 git config --global --add safe.directory 줄을 그대로 복사하세요. Git이 비교하는 경로 형식이 이미 들어 있습니다.
참고 자료
- Git documentation — git-config (safe.directory: multi-valued list of repositories trusted despite a different owner; honoured only in protected configuration; * opts out; a trailing /* allows every repository under a directory; root under sudo also accepts SUDO_UID)
- The GitHub Blog — Git security vulnerability announced (CVE-2022-24765: a .git directory planted above the working directory on shared machines; fixed in Git 2.35.2 by stopping discovery at an ownership change; safe.directory introduced for exceptions)
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.
curl: (60) SSL certificate problem: unable to get local issuer certificate
curl이 서버가 보낸 인증서 체인을 따라가다가, 자기가 읽는 CA 저장소에 발급자가 없는 인증서에 닿아 종료 코드 60으로 연결을 거부한 것입니다. 발급자가 없는 이유는 넷 중 하나입니다. 서버가 중간 인증서 없이 리프만 보내거나(브라우저는 스스로 받아 와서 가려 줍니다), TLS 검사 프록시가 컨테이너·러너가 신뢰하지 않는 회사 CA로 사이트를 다시 서명했거나, curl이 생각과 다른 CA 번들(CURL_CA_BUNDLE, SSL_CERT_FILE, 벤더 curl)을 읽고 있거나, ca-certificates 패키지가 너무 오래된 경우입니다. curl -v로 어느 저장소를 썼는지, openssl s_client로 서버가 무엇을 보냈는지 보고 고리가 빠진 쪽을 고친 뒤 -w '%{ssl_verify_result}'로 확인합니다.