클라우드 GitHub Actions 시크릿 유출 트러블슈팅 - 100-hours-a-week/16-Hot6-wiki GitHub Wiki

GitHub Actions 시크릿 유출 트러블슈팅

사고 개요

본 문서는 아래 두 건의 GitHub Actions 실행에서 발생한 시크릿 유출 사고를 트러블슈팅한 결과입니다.

FE CI/CD GitHub Actions 워크플로우에서 민감한 시크릿이 GitHub Actions 로그에 출력되어 두 차례 유출 사고가 발생했습니다. 두 경우 모두 환경 변수 설정 후 외부 명령 실행 중 발생한 env: 블럭 로그 출력이 원인이며, 코드 내부에서 echo를 통해 출력한 것은 아니었습니다.

첫 번째 유출

설정 코드

- name: Load secrets from GCP and export as env
  run: |
    gcloud secrets list --filter="labels.env=frontend_prod" --format="value(name)" | while read SECRET_NAME; do
      SECRET_VALUE=$(gcloud secrets versions access latest --secret="$SECRET_NAME")
      IFS='-' read -r SERVICE KEY ENV <<< "$SECRET_NAME"
      echo "export $KEY='$SECRET_VALUE'" >> $GITHUB_ENV
    done

문제 발생 지점

- name: Deploy to S3
  run: |
    aws s3 sync ott/dist/ s3://onthe-top/frontend/prod/blue --delete

이때 AWS 설정을 위한 환경 변수가 다음과 같이 GITHUB_ENV에 설정되어 있었고, 워크플로우 로그에서 자동으로 다음처럼 노출되었습니다.

env:
  S3_ACCESS_KEY: AKIAXPYLOAYRVANCGQS7
  S3_SECRET_KEY: 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

즉, echo 명령이 문제라기보다는 aws 명령 실행 전 자동으로 출력되는 env: 블럭이 민감정보를 포함하고 있었던 것이 유출의 직접적인 원인이었습니다.

추가 정황

배포 명령이 실패하며 fatal error: AuthorizationHeaderMalformed 등의 에러 메시지와 함께 env: 블럭 전체가 출력되었고, 여기에 모든 키 값이 포함되었습니다.

두 번째 유출

설정 코드

이전 사고 이후 echo 대신 printf를 사용하여 key=value 형태로 GITHUB_ENV에 기록하도록 개선하였습니다.

- name: Load secrets from GCP
  run: |
    printf "%s=%s\n" "$KEY" "$SECRET_VALUE" >> $GITHUB_ENV

문제 발생 지점

- name: Invalidate CloudFront
  run: |
    aws cloudfront create-invalidation \
      --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" \
      --paths "/*"

이 명령도 실패하면서 GitHub Actions가 자동으로 모든 env: 값을 출력했고, 다시 한 번 다음과 같이 민감 정보가 포함된 상태로 노출되었습니다.

env:
  AWS_ACCESS_KEY_ID: AKIAXPYLOAYRRHLZLVHR
  AWS_SECRET_ACCESS_KEY: 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

백엔드에서는 유출이 없었던 이유

  • 백엔드의 경우 시크릿을 환경 변수로 설정하지 않고 secrets.properties 파일로 생성하여 서버에 scp로 복사하는 구조였습니다.
  • aws, gcloud CLI 명령을 실행할 필요 없이 시스템 내부에서 Java 애플리케이션이 로컬 파일 기반으로 시크릿을 참조하였습니다.
  • GitHub Actions의 env: 블럭에 민감 정보가 포함될 일이 없었습니다.

재발 방지 방안

1. 민감 정보는 환경변수로 전달하지 않고 파일로 전달

echo "$KEY=$SECRET_VALUE" >> ./secrets/secrets.properties
scp secrets.properties ...

이 방식을 사용하면 GitHub Actions가 자동 출력하는 env: 블럭에 민감 정보가 포함되지 않으므로 안전합니다.

2. AWS CLI 실행 시 credentials 파일 또는 --profile 사용

aws configure set aws_access_key_id "$KEY" --profile deploy
aws configure set aws_secret_access_key "$SECRET" --profile deploy

aws s3 sync ... --profile deploy

또는 다음과 같이도 설정할 수 있습니다:

mkdir -p ~/.aws
cat <<EOF > ~/.aws/credentials
[default]
aws_access_key_id=$KEY
aws_secret_access_key=$SECRET
EOF

이 방식은 환경변수 없이도 AWS CLI가 인증에 성공할 수 있게 해줍니다. 따라서 민감 정보가 env: 블럭을 통해 유출되지 않습니다.

3. $GITHUB_ENV에 export 명령어 절대 사용 금지

# 잘못된 예시 (유출 발생 가능)
echo "export AWS_ACCESS_KEY_ID=..." >> $GITHUB_ENV

# 올바른 예시
echo "AWS_ACCESS_KEY_ID=..." >> $GITHUB_ENV

GitHub Actions는 GITHUB_ENV 파일에 export KEY=... 형식으로 기록된 값을 실행 시점에 출력하지 않지만, 이후 외부 명령 실패 시 전체 env: 블럭을 노출하며 유출로 이어질 수 있습니다.

4. GitHub Actions 시크릿 기능으로 전환

GCP Secret Manager를 사용하는 구조 대신, 아래처럼 GitHub Actions의 Secrets 기능을 사용해 간단하게 해결할 수 있습니다.

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}

이 방식은 설정이 간편하며, GitHub이 자체적으로 시크릿 값을 로그에 노출하지 않도록 보호해줍니다.
단점은 시크릿 수가 많거나 복잡한 계층 구조가 필요한 경우 관리가 어렵다는 점입니다.

결론

  • 두 차례 유출 모두 명시적으로 출력하지 않았음에도, GitHub Actions의 env: 블럭 자동 출력 정책으로 인해 발생하였습니다.
  • 민감 정보는 환경변수 기반 전달이 아닌 파일 기반 전달이 가장 안전합니다.
  • 또는 GitHub Actions 시크릿 기능을 직접 사용하여 출력 억제를 GitHub에 위임하는 것도 현실적인 대안이 될 수 있습니다.
  • AWS CLI 사용 시에는 --profile 기반 인증 또는 명시적 credentials 파일 사용을 고려해야 합니다.
⚠️ **GitHub.com Fallback** ⚠️