엔지니어링 글

클라우드 Mac CI에서 iOS 파일 보호 등급을 검증하는 방법

클라우드 Mac CI에서 iOS 파일 보호 등급을 검증하는 방법

원격 빌드 노드에서 모든 테스트가 통과했더라도 앱이 디스크에 기록한 데이터가 의도한 수준으로 보호된다는 보장은 없습니다. 가장 흔한 문제는 개발자가 암호화 API 호출을 빠뜨리는 것이 아니라, 파일이 원자적으로 교체되거나 데이터베이스 마이그레이션 또는 캐시 재생성을 거친 뒤 기존에 설정한 보호 등급이 최종 파일에 적용되지 않는 것입니다. 이 검사를 클라우드 Mac의 지속적 통합 프로세스에 포함하면 특정 쓰기 코드 한 줄만 검토하는 대신, 병합할 때마다 실제 파일 속성을 검증할 수 있습니다.

보호할 데이터를 먼저 정의하기

iOS 파일 보호는 무조건 엄격할수록 좋은 것이 아닙니다. 먼저 기기가 잠긴 뒤에도 읽어야 하는지를 기준으로 데이터를 분류한 다음 보호 등급을 결정해야 합니다. 토큰, 오프라인 업무 데이터, 사용자가 내보낸 민감한 콘텐츠는 일반적으로 기기가 잠기면 접근할 수 없어야 합니다. 반면 백그라운드 전송 대기열, 필수 푸시 상태, 잠금 중에도 계속 실행되어야 하는 작업은 별도로 평가해야 합니다.

데이터 유형 권장 기준 검사 항목
세션 토큰 내보내기 파일 완전 보호 잠금 후 읽을 수 없어야 함
사용자의 민감한 문서 완전 보호 생성, 교체, 복원 후에도 속성이 동일해야 함
백그라운드 작업 상태 최초 잠금 해제 후 사용 가능 잠금 중 접근이 실제로 업무상 필요한지 확인
재생성 가능한 캐시 위험도에 따라 결정 토큰이나 개인 데이터가 섞이지 않아야 함
SQLite 데이터베이스 업무 데이터와 동일 WAL 및 SHM 파일도 함께 검사

Documents 또는 Library 디렉터리 전체를 같은 종류의 데이터로 취급해서는 안 됩니다. 게이트에서는 상대 경로, 예상 보호 등급, 파일을 생성하는 작업, 예외를 허용하는 이유를 기록한 명시적인 목록을 관리하는 것이 좋습니다.

검사 대상은 테스트 실행 후 실제로 존재하는 파일이어야 합니다. .completeFileProtection 문자열만 검색하면 데이터베이스 보조 파일, 교체 후 생성된 새 inode, 서드파티 구성 요소가 만든 데이터를 놓치게 됩니다.

XCTest로 최종 파일 속성 읽기

테스트에서는 먼저 정식 비즈니스 로직 진입점을 통해 데이터를 기록한 다음, 최종 URL에서 fileProtection을 읽어야 합니다. 아래 도우미 메서드는 속성 누락을 보고할 뿐 아니라 실제 값도 실패 메시지에 포함하므로 CI 로그에서 바로 문제를 찾을 수 있습니다.

import XCTest

final class FileProtectionTests: XCTestCase {
    private func assertProtection(
        _ url: URL,
        equals expected: URLFileProtection,
        file: StaticString = #filePath,
        line: UInt = #line
    ) throws {
        XCTAssertTrue(
            FileManager.default.fileExists(atPath: url.path),
            "Expected file does not exist: \(url.path)",
            file: file,
            line: line
        )

        let values = try url.resourceValues(forKeys: [.fileProtectionKey])
        XCTAssertEqual(
            values.fileProtection,
            expected,
            "Unexpected protection at \(url.path): \(String(describing: values.fileProtection))",
            file: file,
            line: line
        )
    }

    func testSensitiveExportUsesCompleteProtection() throws {
        let root = FileManager.default.temporaryDirectory
        let url = root.appendingPathComponent("private-export.json")
        let payload = Data(#"{"status":"ready"}"#.utf8)

        try payload.write(to: url, options: [.atomic, .completeFileProtection])
        try assertProtection(url, equals: .complete)
    }
}

테스트 데이터에는 실제 자격 증명을 포함하지 않아야 합니다. 경로도 테스트 컨테이너에서 동적으로 생성해 특정 노드의 사용자 이름이나 고정 작업 디렉터리에 의존하지 않도록 해야 합니다. 제품이 덮어쓰기를 지원한다면 테스트에서 연속으로 두 번 기록해야 합니다. 두 번째 쓰기는 일반적으로 임시 파일을 만든 뒤 이름을 변경하는 경로를 거치기 때문입니다.

예외를 명시적인 목록으로 관리하기

반드시 .completeUntilFirstUserAuthentication을 사용해야 하는 파일은 테스트에서 하나씩 명시해야 하며, “완전 보호만 아니면 통과”하도록 허용해서는 안 됩니다. 예외 목록에는 비즈니스 시나리오와 담당자를 함께 기록해야 합니다. 기능상 잠금 중 접근이 더 이상 필요하지 않게 되면 예외를 삭제하고 보호 등급을 높여야 합니다.

SQLite 보조 파일을 빠뜨리지 않기

SQLite는 WAL 모드에서 database.sqlite-waldatabase.sqlite-shm을 생성할 수 있습니다. 주 파일만 검사하면 잘못된 보안 결론에 도달하게 됩니다. 테스트에서는 먼저 실제 쓰기 트랜잭션을 한 번 실행해 보조 파일이 생성되었는지 확인한 다음, 세 파일 모두에 동일한 단언을 적용해야 합니다.

Core Data를 사용한다면 빈 데이터베이스를 수동으로 만드는 대신 영구 컨테이너를 통해 삽입과 저장을 수행해야 합니다. 일부 보조 파일은 연결이 닫힌 뒤 사라질 수 있으므로 쓰기 트랜잭션이 완료된 후, 저장소를 닫기 전에 검사해야 합니다. 다음 내용을 기록하는 것이 좋습니다.

  1. 주 데이터베이스, WAL, SHM의 절대 경로
  2. 각 파일의 실제 보호 등급
  3. 파일 생성을 유발한 테스트 단계
  4. 파일이 없을 때 예상된 정리 결과인지, 테스트에서 실제 쓰기를 실행하지 않은 것인지

다운로드한 데이터베이스를 가져오는 흐름에서는 “임시 파일 다운로드—검증—정식 디렉터리로 이동”의 전체 과정도 다뤄야 합니다. 대상 디렉터리에 올바른 속성이 설정되어 있더라도 이동된 파일이 같은 정책을 자동으로 상속한다고 보장할 수는 없습니다.

클라우드 Mac에서 빌드 게이트 연동하기

게이트는 독립적인 테스트 계획으로 실행해 매번 전체 UI 테스트를 수행하지 않도록 할 수 있습니다. 먼저 프로젝트, Scheme, 시뮬레이터 기기, 결과 번들 경로를 고정한 다음 xcodebuild의 종료 코드에 따라 파이프라인의 계속 진행 여부를 결정합니다.

set -euo pipefail

RESULT_DIR="${PWD}/artifacts"
RESULT_BUNDLE="${RESULT_DIR}/FileProtection.xcresult"

rm -rf "${RESULT_BUNDLE}"
mkdir -p "${RESULT_DIR}"

xcodebuild test \
  -project App.xcodeproj \
  -scheme SecurityRegression \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -resultBundlePath "${RESULT_BUNDLE}" \
  -only-testing:AppSecurityTests/FileProtectionTests

기기 이름은 해당 노드에 설치된 런타임을 기준으로 지정해야 하며, 모든 환경이 완전히 동일하다고 가정해서는 안 됩니다. 실행 전 xcrun simctl list devices available로 대상을 확인할 수 있습니다. 운영 파이프라인에서는 허용할 기기와 시스템 버전을 구성에 명시하고, 대상이 없으면 다른 환경으로 조용히 전환하지 말고 즉시 실패 처리해야 합니다.

실패 시 최소한 .xcresult, 테스트 표준 출력, 보호 등급 목록을 보존해야 합니다. 테스트 컨테이너의 민감한 샘플을 보관해서는 안 되며, 문제 해결을 쉽게 하려고 파일 본문을 출력해서도 안 됩니다. 증거는 비즈니스 데이터를 복사하는 대신 “어느 경로에서, 무엇을 예상했고, 실제 값은 무엇이며, 어떤 테스트 단계에서 생성되었는지”를 보여줘야 합니다.

시뮬레이터의 한계와 흔한 오판 처리하기

시뮬레이터는 코드가 최종 파일에 보호 속성을 기록했는지 검증하는 데 적합하지만, 물리 기기가 잠긴 뒤의 키 가용 상태를 완전히 재현할 수는 없습니다. 따라서 이 CI 게이트는 속성 회귀를 탐지하는 역할을 하며, 잠금, 재시작, 백그라운드 깨우기 상황에서의 실제 접근 동작은 별도로 통제된 기기 프로세스에서 검증해야 합니다.

또 다른 오판은 정리 순서에서 발생합니다. 테스트에서 데이터베이스를 닫거나 임시 디렉터리를 삭제한 다음 보조 파일을 검사하면 결과에 “파일 없음”이 표시되지만, 보안상 정상적인 정리인지 테스트 자체가 무효화된 것인지 구분할 수 없습니다. 단언은 수명 주기가 명확한 지점에 배치하고, 파일이 없을 때 경로를 포함한 실패가 발생하도록 해야 합니다.

마지막으로 네 가지 변경 유형을 확인해야 합니다. 쓰기 API가 교체되었는지, 파일이 원자적으로 교체되는지, 데이터베이스 모드가 변경되었는지, 서드파티 구성 요소가 새 영구 저장 경로를 추가했는지입니다. 이 중 하나라도 변경되면 보호 목록을 다시 검토해야 합니다. 이렇게 구축한 게이트는 사람의 기억에 의존하지 않으며, “과거에 올바르게 설정했다”는 사실을 “현재 디스크에 기록된 파일도 여전히 올바르다”는 의미로 잘못 받아들이지 않습니다.

자주 묻는 질문

소스 코드의 파일 보호 옵션만 확인하면 충분한가요?

아닙니다. 원자적 교체, 마이그레이션, 외부 라이브러리가 파일을 다시 만들 수 있으므로 최종 파일의 실제 보호 등급을 읽어야 합니다.

시뮬레이터 검사로 실제 기기 검증을 대체할 수 있나요?

아닙니다. 시뮬레이터는 속성 설정 확인에 적합하지만 잠금 이후의 접근 동작은 통제된 실제 기기에서 별도로 검증해야 합니다.

VMMini M4

전용 물리 노드에서 다음 작업 실행

노드와 결제 주기를 선택하고 고정된 M4, 16GB RAM 및 256GB SSD 구성으로 배포를 시작하세요. 실제 사용 가능 여부는 콘솔에 실시간으로 표시되는 결과를 기준으로 합니다.

지금 클라우드 Mac 대여