클라우드 Mac CI 프로세스 환경 변수 유출 감사와 비밀 격리

클라우드 Mac CI 프로세스 환경 변수 유출 감사와 비밀 격리

한 대의 클라우드 Mac이 코드 가져오기, 빌드, 테스트, 배포를 모두 담당할 때 가장 놓치기 쉬운 경계는 디렉터리가 아니라 프로세스 환경입니다. 파이프라인이 토큰을 환경 변수에 기록하면 xcodebuild, 스크립트, 테스트 호스트, 백그라운드 자식 프로세스가 기본적으로 이를 상속합니다. 주 작업이 이미 끝났더라도 종료되지 않은 보조 프로세스에는 전체 변수가 남아 있을 수 있습니다. 올바른 대응은 환경 변수 사용을 금지하는 것이 아니라, 비밀이 노출되는 시간과 이를 볼 수 있는 프로세스의 범위를 최소화하는 것입니다.

먼저 변수 전파 경로 그리기

runner에서 시작되는 모든 프로세스는 빌드, 테스트, 배포, 보조 도구의 네 범주 중 하나로 분류해야 합니다. 빌드와 테스트에는 일반적으로 배포 토큰이 필요하지 않으며, 보조 도구 역시 이를 상속해서는 안 됩니다. 실제 값은 기록하지 말고 변수 이름, 주입 단계, 사용하는 명령, 예상 제거 시점을 먼저 기록합니다.

점검 대상 일반적인 위험 검증 방법
runner 부모 프로세스 시작할 때 모든 비밀을 전달받음 변수 이름의 존재 여부만 확인
빌드 자식 프로세스 부모 환경을 무조건 상속 무작위 표식으로 상속 테스트 수행
launchd 도메인 setenv 실행 후 여러 작업에 걸쳐 잔류 작업 전후에 각각 점검하고 제거
임시 파일 권한이 지나치게 넓거나 비정상 종료 후 잔류 권한을 600으로 설정하고 trap으로 삭제
로그 Shell 추적 기능이 명령 인수를 출력 비밀을 다루는 단계에서 set -x 비활성화

감사 스크립트는 “특정 표식이 보이는가”에만 답하면 됩니다. 환경 스냅샷을 빌드 첨부 파일로 저장하지 마십시오. 전체 env, export, ps eww 출력은 그 자체로 새로운 유출원이 될 수 있습니다.

무작위 표식으로 상속 검증하기

실제 토큰으로 실험하지 마십시오. 일회용 표식을 생성하고 수명이 짧은 자식 프로세스를 시작한 다음, 해당 프로세스에서 표식이 보이는지 출력 없이 판정합니다. 아래 검사는 표식 내용을 로그에 출력하지 않지만, 종료 코드를 통해 파이프라인에서 상속이 발생했는지 알 수 있습니다.

set -euo pipefail

marker="$(/usr/bin/uuidgen)"
export CI_SECRET_PROBE="$marker"

/bin/sleep 20 &
child_pid=$!

if /bin/ps eww -p "$child_pid" -o command= | /usr/bin/grep -q "$marker"; then
  result=1
else
  result=0
fi

kill "$child_pid" 2>/dev/null || true
wait "$child_pid" 2>/dev/null || true
unset CI_SECRET_PROBE

test "$result" -eq 1

이 테스트는 기본 상속이 실제로 존재한다는 점을 입증하지만, 운영 로그에서 모든 프로세스를 반복해서 스캔해야 한다는 뜻은 아닙니다. 정식 검증에서는 고정된 변수 이름과 무작위 값을 함께 사용하고 “통과” 또는 “실패”만 기록할 수 있습니다.

launchd 잔류 확인하기

작업 비밀을 주입할 때 launchctl setenv를 사용해서는 안 됩니다. 수명이 단일 Shell보다 길어질 수 있기 때문입니다. 기존 스크립트에서 이 방식을 사용한 적이 있다면 값을 출력하지 않은 채 상태를 먼저 확인한 후 다음을 실행합니다.

launchctl unsetenv CI_RELEASE_TOKEN
test -z "$(launchctl getenv CI_RELEASE_TOKEN)"

점검은 작업 시작 단계와 정리 단계에서 각각 한 번씩 수행해야 합니다. runner가 별도 사용자로 실행된다면 현재 원격 세션만 확인하지 말고, 해당 사용자에 대응하는 launchd 도메인에서 실행해야 합니다.

빌드를 위한 최소 환경 구성하기

env -i를 사용하면 빈 환경에서 명령을 시작할 수 있지만, 모든 항목을 무작정 제거해서는 안 됩니다. Xcode 도구 체인에는 일반적으로 HOME, PATH, TMPDIR, 로캘 설정, 명시적인 개발자 디렉터리가 필요합니다. 먼저 버전 조회로 환경을 검증한 다음 전체 빌드를 실행합니다.

set -euo pipefail

job_tmp="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/mac-ci.XXXXXX")"
trap '/bin/rm -rf "$job_tmp"' EXIT HUP INT TERM

/usr/bin/env -i \
  HOME="$HOME" \
  PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
  TMPDIR="$job_tmp/" \
  LANG="en_US.UTF-8" \
  DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" \
  /usr/bin/xcrun xcodebuild -version

실제 프로젝트가 Ruby, Node.js 또는 패키지 관리자에 의존한다면 각 실행 파일 디렉터리를 PATH에 하나씩 추가해야 합니다. 로그인 Shell 환경 전체를 복원해서는 안 됩니다. 변수를 추가할 때마다 어떤 명령에 필요한지 명시하십시오.

빌드와 배포를 두 개의 권한 도메인으로 분리하기

컴파일 단계에서는 아카이브와 검증 파일만 생성하고 배포 자격 증명에는 접근하지 않습니다. 배포 단계는 검증을 마친 산출물을 읽고, 업로드 명령에 필요한 변수만 주입합니다. 작업 시작 시 export CI_RELEASE_TOKEN을 실행하지 마십시오. 이후 실행되는 모든 스크립트가 이를 상속하기 때문입니다.

set +x
CI_RELEASE_TOKEN="$release_token" \
  /usr/bin/env \
  RELEASE_ARTIFACT="$artifact_path" \
  ./ci/publish.sh
unset release_token
set -x

더 안전한 방법은 publish.sh가 업로드 프로세스 하나만 시작하고, 해당 프로세스가 종료되기를 기다린 뒤 즉시 끝나도록 구성하는 것입니다. 도구가 자격 증명 파일을 요구한다면 작업 전용 임시 디렉터리를 사용하고, 파일을 만들기 전에 umask 077을 설정한 뒤 쓰기가 끝나면 권한이 600인지 확인합니다. 파일을 저장소, 사용자 데스크톱 또는 장기 캐시 디렉터리에 두지 마십시오.

실패 처리기 제한하기

실패 후크는 환경, 프로세스, 작업 공간 정보를 수집하는 경우가 많습니다. 필터링하지 않은 env를 실행하거나 전체 명령줄을 패키징해서는 안 됩니다. 종료 코드, 도구 버전, 산출물 경로, 남은 디스크 공간, 허용 목록으로 필터링한 변수 이름은 수집할 수 있습니다. 비밀을 확인해야 할 때는 해시나 무작위 프로브의 출현 여부만 비교하고 원래 값은 저장하지 않습니다.

정리 결과를 작업 게이트로 설정하기

작업 성공이 정리 성공을 의미하지는 않습니다. 종료 단계에서는 최소한 배포 변수에 unset이 적용되었는지, launchd에 해당 이름이 남아 있지 않은지, 임시 디렉터리가 사라졌는지, 테스트 및 업로드 자식 프로세스가 종료되었는지, 로그에서 명령 추적이 활성화되지 않았는지를 확인해야 합니다. 하나라도 실패하면 현재 runner를 점검 필요 상태로 표시하고, 비밀이 포함된 배포 작업을 더 이상 받지 않도록 해야 합니다.

작업마다 독립적인 TMPDIR을 생성하고 프로세스 그룹 단위로 정리하는 것이 좋습니다. 주 스크립트만 종료하면 시뮬레이터 보조 프로세스, 업로더 또는 사용자 정의 데몬이 남을 수 있습니다. 정리를 마친 후 실제 값이 들어 있지 않은 무작위 프로브를 다시 실행하여, 새 자식 프로세스에서 이전 단계의 표식을 볼 수 없는지 확인합니다.

최종 목표는 파이프라인에서 환경 변수를 전혀 사용하지 않는 것이 아니라 명확한 경계를 세우는 것입니다. 빌드 단계에는 배포 비밀이 없어야 하고, 배포 단계에서는 꼭 필요한 프로세스 하나만 비밀을 볼 수 있어야 하며, 작업이 끝난 뒤에는 프로세스, launchd, 디스크 어디에도 비밀이 남아 있지 않아야 합니다. 이 점검 절차를 runner 초기화 및 종료 스크립트에 포함하면 이후 프로젝트는 작업자의 기억에 의존하지 않아도 됩니다.

자주 묻는 질문

CI 비밀 값을 환경 변수에만 저장하면 안전한가요?

충분하지 않습니다. 환경 변수는 기본적으로 자식 프로세스에 상속되며 진단 명령, 디버그 출력, 장기 실행 서비스가 읽을 수 있습니다. 필요한 단일 명령에만 제한적으로 주입해야 합니다.

작업 종료 후 환경 변수 잔류를 어떻게 확인하나요?

runner 부모 프로세스, launchd 도메인, 살아 있는 자식 프로세스에서 변수 이름을 검사하고 작업용 임시 디렉터리가 삭제됐는지 확인합니다. 실제 비밀 값은 로그에 출력하지 않습니다.

env -i를 사용하면 xcodebuild가 실패할 수 있나요?

기본 환경이 제거되므로 HOME, PATH, TMPDIR, LANG, DEVELOPER_DIR를 명시해야 합니다. 먼저 버전 확인과 작은 빌드로 허용 목록을 검증한 뒤 전체 파이프라인에 적용합니다.

전용 물리 노드

다음 빌드를 클라우드 Mac mini에서 실행하세요

구성, 결제 주기와 노드 지역을 선택하고 전용 물리 컴퓨터에서 개발, 빌드와 자동화 작업을 실행하세요.

Mac mini 대여