터미널이나 iTerm2에서 스크립트를 실행하거나 ~/Library, ~/Desktop, 외장 저장장치 경로의 파일에 접근할 때 Operation not permitted 오류가 발생하면 흔히 sudo를 붙여 재시도합니다. 그러나 macOS에서는 최고 관리자 권한인 root로 명령을 실행해도 동일하게 접근이 거부됩니다.
이는 유닉스 표준 POSIX 퍼미션(777, chown) 문제가 아니라, 운영체제 커널의 SIP(System Integrity Protection)와 앱 권한을 통제하는 TCC(Transparency, Consent, and Control) 프레임워크가 프로세스의 시스템 호출을 가로채 차단하기 때문입니다.
시스템 설정의 '전체 디스크 접근 권한(Full Disk Access)'에 터미널을 등록해 두었더라도, 앱 업데이트나 바이너리 서명 해시 변경으로 TCC SQLite 데이터베이스 내부 레코드가 꼬이면 권한 인가가 누락되는 현상이 발생합니다.
macOS 다중 보안 계층 구조와 차단 원인
macOS의 파일 접근 제어는 전통적인 리눅스 환경과 달리 네 가지 계층이 중첩되어 동작합니다.
보안 계층별 제어 범위와 우회 한계
어떤 계층에서 명령이 차단되었는지 구분하지 못하면 불필요하게 chmod나 chown을 남발하여 시스템 보안 설정만 왜곡하게 됩니다.
| 보안 계층 | 통제 주체 | 차단 대상 리소스 | root(sudo) 우회 가능 여부 |
상태 확인 및 제어 도구 |
|---|---|---|---|---|
| POSIX 권한 | 파일 시스템 메타데이터 | 사용자/그룹별 읽기·쓰기·실행 | 가능 (root는 무조건 통과) |
ls -l, chmod, chown |
| 격리 속성 (Quarantine) | 파일 확장 속성(xattr) | 웹이나 외부에서 내려받은 바이너리 | 가능 (속성 제거 시 즉시 실행) | xattr -l, xattr -d |
| TCC 프레임워크 | tccd 시스템 데몬 |
개인 폴더(Desktop, Documents), 디스크 전체 | 불가능 (root 권한도 강제 차단) |
tccutil reset, 시스템 설정 UI |
| SIP (시스템 무결성 보호) | 커널 (XNU) | /System, /usr, 보호 프로세스 메모리 |
불가능 (복구 모드에서만 제어) | csrutil status |
TCC와 SIP는 프로세스의 코드 서명(Code Signature)과 권한 목록(Entitlements)을 기준으로 커널 및 전용 데몬(tccd)에서 접근을 직접 검증합니다. 따라서 UID 0(root) 상태이더라도 인가 목록에 없는 프로세스는 시스템 호출 단계에서 즉시 거절당합니다.
격리 플래그(Quarantine) 및 SIP 상태 점검
오류가 발생했을 때 가장 먼저 점검해야 할 요소는 다운로드 파일에 자동 부여되는 격리 플래그와 SIP 상태입니다.
파일 시스템 확장 속성 확인
외부에서 스크립트나 바이너리를 내려받아 실행할 때 Operation not permitted가 출력된다면 게이트키퍼(Gatekeeper) 격리 속성이 원인일 수 있습니다:
# 파일에 부여된 확장 속성(Extended Attributes) 목록 확인
xattr -l /path/to/target_script.sh
# com.apple.quarantine 속성이 존재한다면 즉시 제거
xattr -d com.apple.quarantine /path/to/target_script.sh
디렉터리 내부의 모든 파일에 대해 격리 플래그를 일괄 해제하려면 -r 옵션을 함께 사용합니다:
# 디렉터리 내 모든 파일의 격리 플래그 재귀 제거
xattr -dr com.apple.quarantine ./bin/
시스템 무결성 보호(SIP) 상태 확인
대상 경로가 /System 또는 /usr/bin 등 시스템 보호 디렉터리인 경우 SIP가 원인입니다:
# SIP 활성화 상태 확인
csrutil status
출력 결과가 System Integrity Protection status: enabled.로 표시된다면 해당 영역은 운영체제가 완전히 잠근 상태이므로 로컬 설정이나 심볼릭 링크를 /usr/local 또는 /opt/homebrew 경로로 우회해야 합니다.
tccutil 명령어를 통한 TCC 데이터베이스 초기화
사용자 홈 디렉터리 내부(~/Library, ~/Desktop)나 외장 볼륨 접근 시 발생하는 에러는 TCC 권한 데이터베이스 손상이 주원인입니다.
꼬인 권한 캐시 리셋
macOS는 ~/Library/Application Support/com.apple.TCC/TCC.db 경로에 각 앱의 접근 권한 상태를 SQLite로 기록합니다. 앱을 새로 빌드하거나 업데이트했을 때 코드 서명이 변경되면 기존 권한 레코드와 충돌을 일으킵니다.
tccutil CLI 도구를 사용하여 관련 권한 캐시를 초기화하고 시스템이 새로운 권한 승인 창을 띄우도록 유도합니다:
# 전체 디스크 접근 권한(SystemPolicyAllFiles) 캐시 전체 초기화
tccutil reset SystemPolicyAllFiles
# 특정 터미널 클라이언트(iTerm2) 권한만 선택적으로 초기화
tccutil reset SystemPolicyAllFiles com.googlecode.iterm2
# macOS 기본 터미널(Terminal.app) 권한 초기화
tccutil reset SystemPolicyAllFiles com.apple.Terminal
💡 Tip: VS Code 내장 터미널이나 Cursor 에디터에서 동일한 권한 거부가 지속된다면 해당 번들 ID(
com.microsoft.VSCode또는com.todesktop.230313mzl4w4u92)를 지정하여tccutil reset을 수행한 후, 애플리케이션을 완전히 종료(Cmd + Q)하고 다시 실행해야 새로운 권한 인가 대화상자가 정상 호출됩니다.
시스템 설정 UI에서 수동 재인가
캐시를 초기화한 후 시스템 설정 > 개인정보 보호 및 보안 > 전체 디스크 접근 권한 메뉴로 이동합니다.
터미널 항목의 토글을 껐다가 다시 켜거나, 목록에서 - 버튼으로 제거한 뒤 터미널 앱을 다시 드래그하여 등록합니다. 변경 사항을 적용하려면 터미널 프로세스를 재시작해야 합니다.
launchd 및 백그라운드 자동화 스크립트 권한 처리
터미널 대화형 셸에서는 정상 동작하던 스크립트가 launchd 데몬이나 cron 환경에서 실행될 때 권한 오류를 일으키는 경우가 많습니다.
데몬 실행 주체에 따른 권한 격리
백그라운드로 구동되는 작업은 부모 프로세스인 터미널 앱의 TCC 권한을 상속받지 못하고 독립적인 세션으로 분리됩니다.
# 백그라운드 스크립트를 구동할 셸 인터프리터 경로 확인
which zsh
which bash
자동화 작업을 /Library/LaunchDaemons에 등록하는 경우 시스템 레벨에서 실행되므로 일반 사용자 영역의 TCC 보호 디렉터리에 접근할 수 없습니다. 따라서 사용자 데이터에 접근하는 자동화 스크립트는 반드시 /Users/<사용자명>/Library/LaunchAgents 경로에 등록하여 사용자 세션 권한 내에서 실행되도록 구성해야 합니다.
추가로 스크립트 내에서 외부 바이너리(예: rsync, rclone)를 직접 호출하는 경우, 해당 바이너리 자체를 전체 디스크 접근 권한에 명시적으로 추가해주어야 백그라운드 환경에서 중단 없이 실행됩니다.
정상 권한 복구 확인
모든 설정을 완료한 후 보호된 시스템 디렉터리 목록 조회를 통해 권한 복구 여부를 검증합니다:
# 보호 영역 디렉터리 접근 테스트
ls -la "$HOME/Library/Application Support/com.apple.TCC"
출력 결과에 TCC.db 파일이 정상적으로 표시되고 에러가 반환되지 않는다면 터미널 프로세스에 전체 디스크 접근 권한이 정상적으로 재부여된 것입니다.
터미널 작업 중 권한 에러를 마주했을 때 무조건 sudo나 chmod에 의존하기보다, TCC 프레임워크와 확장 속성의 차단 원인을 먼저 점검하는 접근이 불필요한 설정 왜곡 없이 시스템을 안정적으로 유지하는 가장 정확한 방법입니다.
