GitHub Actions Secrets 관리: 유출 위험부터 OIDC 키리스 배포까지

2025년 3월의 어느 주말, GitHub 보안팀은 23,000개가 넘는 저장소의 소유자들에게 긴급 이메일을 보냈습니다.
그들의 secrets가 이미 유출됐을 가능성이 있다는 내용이었습니다.
원인은 tj-actions/changed-files라는 action이었습니다. 널리 사용되던 이 도구가 공격자에게 침해되어 workflow의 모든 환경 변수와 secrets를 몰래 빼냈습니다. ‘내 프로젝트도 이 action을 쓰는데, 나도 피해를 본 걸까?’라는 생각이 들 수 있습니다.
이 사건은 많은 사람이 간과하던 문제를 수면 위로 끌어올렸습니다. GitHub Actions의 secrets는 과연 어떻게 관리해야 할까요?
이 글에서는 세 가지를 살펴봅니다. secrets의 3계층 아키텍처를 선택하는 방법, 반드시 지켜야 할 8가지 보안 원칙, 그리고 OIDC로 정적 자격 증명 유출의 악몽에서 완전히 벗어나는 방법입니다.
1. GitHub Actions Secrets의 3계층 아키텍처
GitHub는 secrets를 저장할 수 있는 세 가지 계층, 즉 Repository, Environment, Organization을 제공합니다. 무엇을 선택해야 할까요? 답은 사용 환경에 달려 있습니다.
Repository Secrets: 개인 프로젝트의 첫 번째 선택
가장 단순한 계층입니다. secrets가 저장소 수준에 저장되고 모든 workflow에서 접근할 수 있습니다. 개인 프로젝트나 단일 저장소를 운영하고 다중 환경 배포가 필요하지 않다면 이것만으로 충분합니다.
유일한 단점은 staging과 production에서 같은 이름의 secret을 구분할 수 없다는 것입니다. 예를 들어 DATABASE_URL이 있고 staging과 production의 값이 다르다면 어떻게 해야 할까요? 이때 Environment Secrets가 필요합니다.
Environment Secrets: 다중 환경 배포의 필수 요소
Environment secrets는 환경별로 격리할 수 있고 승인 절차도 지원합니다. GitHub 저장소 설정에서 staging과 production 환경을 만들고 서로 다른 secrets를 각각 설정할 수 있습니다.
핵심 특징은 해당 환경을 참조하는 job만 그 환경의 secrets에 접근할 수 있다는 것입니다. 덕분에 보안 경계가 더 명확해집니다.
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # staging 환경 참조
steps:
- run: echo "Deploying to staging..."
- env:
API_KEY: ${{ secrets.API_KEY }} # staging 환경의 secret
deploy-production:
runs-on: ubuntu-latest
environment: production # production 환경 참조(승인 설정 가능)
steps:
- run: echo "Deploying to production..."
- env:
API_KEY: ${{ secrets.API_KEY }} # production 환경의 secret
위 설정에서 deploy-staging은 staging 환경의 secrets에만 접근할 수 있고, deploy-production은 production 환경의 secrets에만 접근할 수 있습니다. 두 환경은 서로 영향을 주지 않습니다.
또한 Environment는 ‘보호 규칙’도 지원합니다. 예를 들어 production 환경은 사람의 승인을 거쳐야 실행되도록 설정할 수 있습니다. 팀 협업에서 특히 유용한 기능입니다.
Organization Secrets: 팀 공유와 중앙 관리
팀에 수십 개의 저장소가 있고 각 저장소마다 같은 AWS_ACCESS_KEY를 설정해야 한다고 생각해 보세요. 수십 번 복사해 붙여 넣고, 업데이트할 때마다 다시 수십 번 수정해야 한다면 생각만 해도 골치 아픕니다.
Organization secrets는 바로 이 문제를 해결합니다. 조직 수준에서 한 번 설정하면 모든 저장소에서 사용할 수 있습니다. 접근 가능한 저장소도 모든 저장소 또는 지정한 저장소 목록으로 제한할 수 있습니다.
# 저장소 workflow에서 repository secrets와 완전히 같은 방식으로 사용
steps:
- env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
무엇을 선택해야 할까요?
간단히 정리하면 다음과 같습니다.
| 상황 | 권장 계층 |
|---|---|
| 개인 프로젝트, 단일 환경 | Repository Secrets |
| 다중 환경 배포(staging/production) | Environment Secrets |
| 팀의 여러 저장소에서 자격 증명 공유 | Organization Secrets |
직접 겪어 본 경험으로는 많은 프로젝트가 처음에는 Repository Secrets를 사용하다가 다중 환경이 필요해진 뒤 Environment Secrets로 이전합니다. 이전 비용이 크지는 않지만 미리 계획해 두는 편이 좋습니다.
2. Secrets 보안 모범 사례 — 8가지 원칙
앞에서는 secrets를 어디에 저장할지 살펴봤습니다. 이제 어떻게 사용할지 이야기해 보겠습니다. 실전에서 시행착오를 겪으며 정리한 8가지 원칙이며, 각각 실제 교훈이 담겨 있습니다.
1. 일관된 이름 규칙 사용
AWS_ACCESS_KEY_ID처럼 모두 대문자로 쓰고 단어는 밑줄로 구분하세요. 소문자나 camelCase는 사용하지 않는 편이 좋습니다. 한눈에 secret임을 알 수 있고 일반 변수와 혼동하지 않기 때문입니다.
2. JSON 전체를 하나의 secret에 저장하지 않기
흔히 빠지는 함정입니다. 설정 파일 전체를 하나의 secret에 넣는 경우가 있습니다.
{"api_key": "xxx", "db_url": "yyy", "token": "zzz"}
그런 다음 workflow에서 fromJson으로 파싱합니다. 문제는 이 secret 하나가 유출되면 모든 민감 정보가 함께 유출된다는 것입니다. 각 값을 별도의 secret으로 저장하는 것이 올바른 방법입니다.
3. 인라인으로 쓰지 말고 명시적으로 전달하기
# 잘못된 예 ❌
- run: my-cli --token ${{ secrets.MY_TOKEN }}
# 올바른 예 ✅
- env:
MY_TOKEN: ${{ secrets.MY_TOKEN }}
run: my-cli --token $MY_TOKEN
왜 그럴까요? GitGuardian의 연구에 따르면 명령줄 인자는 같은 컴퓨터의 다른 프로세스가 ps x -w를 통해 볼 수 있습니다. 환경 변수가 상대적으로 훨씬 안전합니다.
4. 정기적으로 교체하기
30~90일마다 한 번씩 교체하세요. 교체가 번거롭다는 사실은 잘 압니다. 하지만 유출된 뒤 수습하는 것에 비하면 이 정도 수고는 아무것도 아닙니다. Blacksmith 팀은 AWS나 GCP 같은 클라우드 서비스를 사용한다면 OIDC를 결합해 이 단계 자체를 완전히 없앨 수 있다고 권장합니다.
5. Actions를 SHA에 고정하기
공급망 공격을 막는 첫 번째 방어선입니다.
# 잘못된 예 ❌
- uses: tj-actions/changed-files@v45
# 올바른 예 ✅
- uses: tj-actions/changed-files@b827595e0a7e97537d7c7a2f458b5a8e6d5c8e39
버전 태그 대신 commit SHA를 사용하세요. 태그는 공격자가 변경할 수 있지만 SHA는 불변입니다.
6. GITHUB_TOKEN에는 최소 권한만 부여하기
GitHub는 각 workflow에 GITHUB_TOKEN을 자동으로 제공합니다. 기본 권한은 너무 높아서 코드를 쓰고 issue를 수정할 수도 있습니다. workflow 또는 저장소 설정에서 다음처럼 read-only로 변경하는 것을 권장합니다.
permissions:
contents: read
7. 로그에서 secrets가 올바르게 마스킹되는지 확인하기
GitHub는 로그에서 ${{ secrets.XXX }}를 자동으로 ***로 바꿉니다. 하지만 다음처럼 작성하면 문제가 생깁니다.
- run: echo "Token is $MY_TOKEN"
로그에 실제 token 값이 나타납니다. workflow를 테스트해 의도치 않은 노출이 없는지 확인하세요.
8. 파생된 민감 값 등록하기
workflow가 secret에서 새로운 민감 값, 예를 들어 API key를 이용한 JWT를 생성한다면 새 값도 secret으로 등록하세요. 메모리에서만 전달해서는 안 됩니다.
이 8가지 원칙은 탁상공론이 아닙니다. tj-actions 사건 이후 StepSecurity 팀이 수천 개의 공개 저장소를 감사한 결과, 상당수의 저장소가 이 원칙들을 위반하고 있었습니다. 고치기 어렵지는 않지만 항목별로 점검하려면 시간이 조금 필요합니다.
3. OIDC — 키리스 시대의 클라우드 배포 인증
앞의 원칙에서 ‘secrets를 정기적으로 교체하기’를 언급했습니다. 솔직히 교체 작업은 정말 번거롭습니다. 매번 AWS 콘솔에서 수동으로 바꾸고 GitHub secrets를 업데이트한 뒤 팀원에게 알려야 합니다.
OIDC(OpenID Connect)는 다른 길을 제시합니다. 아예 secrets를 저장하지 않는 것입니다.
OIDC는 어떻게 작동하나요?
기존 방식에서는 AWS에 IAM 사용자를 만들고 access key를 생성한 뒤 그 key를 GitHub secrets에 저장합니다. workflow가 실행될 때마다 이 정적 key로 AWS 리소스를 요청합니다.
OIDC 방식에서는 GitHub가 ID 공급자로서 ‘이 workflow는 eastondev/my-repo 저장소에서 왔다’는 사실을 AWS에 증명합니다. AWS가 이를 검증한 뒤 단기 JWT token을 발급하며, 유효 기간은 몇 분에서 몇 시간입니다. workflow는 이 임시 token으로 작업을 완료하고 token은 만료되면 자동으로 무효화됩니다.
장기 자격 증명을 저장할 필요도, 교체할 필요도 없으며 유출 위험도 없습니다.
AWS OIDC 설정 예시
설정은 두 부분으로 나뉩니다. AWS 측에서 신뢰 관계를 설정하고 GitHub 측에서 token을 요청합니다.
AWS 측(콘솔 작업):
- IAM Identity Provider를 생성하고 URL을
https://token.actions.githubusercontent.com으로 설정합니다. - IAM Role을 생성하고 신뢰 정책을 자신의 저장소로 제한합니다.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:eastondev/my-repo:ref:refs/heads/main"
}
}
}]
}
이 설정은 eastondev/my-repo 저장소의 main 브랜치만 이 role을 assume할 수 있다는 뜻입니다.
GitHub 측(workflow 설정):
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # 필수: OIDC token 요청
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket
여기에는 secrets.AWS_ACCESS_KEY가 전혀 없습니다. role로 직접 인증합니다.
GCP와 Azure
3대 클라우드 모두 OIDC를 지원하며 설정 방식도 대체로 비슷합니다.
| 클라우드 플랫폼 | GitHub Action | 공식 문서 검색어 |
|---|---|---|
| AWS | aws-actions/configure-aws-credentials | OIDC federation |
| GCP | google-github-actions/auth | Workload Identity Federation |
| Azure | azure/login | Federated Identity Credentials |
OIDC의 뜻밖의 장점
johal.in의 테스트에 따르면 OIDC 인증 지연은 기존 secrets 방식보다 약 87% 낮았습니다. 이유는 간단합니다. GitHub secrets API에서 자격 증명을 읽을 필요 없이 로컬 JWT를 임시 token으로 직접 교환하기 때문입니다.
개인적으로 사용해 본 경험으로는 OIDC가 클라우드 배포의 첫 번째 선택입니다. 설정이 조금 복잡하다는 진입 장벽은 있지만, 한 번 구성해 두면 이후에는 완전히 신경 쓰지 않아도 됩니다.
4. 공급망 공격 방어 — tj-actions 사건 되짚어 보기
글의 시작에서 언급한 tj-actions/changed-files 사건으로 돌아가 보겠습니다. 어떻게 발생했으며 여기에서 무엇을 배울 수 있을까요?
사건 경위
2025년 3월, 공격자는 tj-actions 저장소의 maintainer 권한을 획득했습니다. 구체적인 경로는 여전히 조사 중이며 자격 증명 유출이나 계정 침해가 원인이었을 가능성이 있습니다. 공격자는 v45 버전 코드에 악성 스크립트를 삽입했고, 이 스크립트는 workflow 실행 중 모든 환경 변수와 secrets를 몰래 읽어 공격자가 제어하는 서버로 전송했습니다.
Semgrep의 분석에 따르면 tj-actions/changed-files@v45를 사용하는 모든 workflow가 영향을 받았습니다. GitHub secrets를 사용하든 OIDC를 사용하든 이 action이 환경 변수에 접근할 수 있다면 탈취될 수 있었습니다.
Unit42 Palo Alto의 보고서는 23,000개가 넘는 저장소가 이 action을 사용했다고 밝혔습니다. 여기에는 일부 유명 프로젝트의 fork도 포함되어 있었습니다.
교훈
이 사건은 몇 가지 문제를 드러냈습니다.
- Action 버전 태그는 신뢰할 수 없습니다. 공격자가 태그를 악성 commit으로 변경할 수 있습니다.
- 서드파티 action은 secrets에 접근할 수 있습니다. 침해되면 모든 secrets가 노출됩니다.
- 과거 실행 로그에서 민감 정보가 유출될 수 있습니다. 지금 수정했더라도 이전 실행 기록에 흔적이 남아 있을 수 있습니다.
점검 목록
tj-actions/changed-files를 사용한 적이 있다면 다음 항목을 하나씩 점검하세요.
□ workflow 로그를 확인해 secrets가 출력에 유출되지 않았는지 확인
□ 노출됐을 가능성이 있는 모든 secrets(API keys, tokens 등) 교체
□ action을 버전 태그가 아닌 commit SHA에 고정
□ 다른 서드파티 action의 maintainer 출처 감사
□ Dependabot 또는 Renovate로 action 버전 점검 자동화 검토
아직 침해되지 않은 프로젝트에는 SHA 고정이 가장 중요한 방어 수단입니다. SHA는 버전 번호보다 읽기 어렵지만 태그 변조를 막을 수 있는 유일한 방법입니다.
또한 GitHub는 2026년 보안 로드맵에서 중요한 방향 하나를 언급했습니다. 바로 코드 기여 권한과 자격 증명 관리 권한의 분리입니다. 앞으로 더 세분화된 접근 제어를 도입해 서드파티 action이 모든 secrets가 아니라 꼭 필요한 secrets에만 접근하도록 할 가능성이 있습니다. 반가운 소식이지만 지금은 여전히 스스로 방어선을 지켜야 합니다.
결론
GitHub Actions Secrets 관리는 복잡한 기술 문제가 아니라 지속적으로 주의를 기울여야 하는 보안 실천입니다. 요약하면 다음과 같습니다.
3계층 아키텍처 선택: 개인 프로젝트는 Repository Secrets를 사용하고, 다중 환경 배포에는 Environment Secrets로 전환하며, 팀 공유에는 Organization Secrets를 사용합니다.
보안 원칙 준수: SHA 고정, 명시적 전달, 정기 교체가 가장 중요한 세 가지입니다.
클라우드 배포 인증: OIDC가 첫 번째 선택입니다. secrets 저장도, 교체의 번거로움도 없습니다.
공급망 공격 방어: 서드파티 action의 secrets 접근을 제한하고 maintainer 출처를 감사해야 합니다.
tj-actions 사건에서 교훈을 얻은 뒤 저는 모든 action을 SHA에 고정하고, 클라우드 배포에는 전부 OIDC를 사용하며, 매달 한 번 secrets 감사를 진행하고 있습니다. 이 절차를 구축하는 데 약 2주가 걸렸지만 이후 유지보수 비용은 매우 낮았습니다.
아직 점검을 시작하지 않았다면 오늘 이 글의 체크리스트를 한 번 실행해 보세요. 사고가 난 뒤 수습하는 것보다 미리 예방하는 비용이 훨씬 적습니다.
GitHub Actions OIDC 키리스 배포 설정
AWS 클라우드 서비스에 OIDC 인증을 설정해 정적 자격 증명을 저장하지 않고 안전하게 배포합니다.
⏱️ Estimated time: 30 min
- 1
Step 1: IAM Identity Provider 생성
AWS IAM 콘솔에서 ID 공급자를 생성합니다:
• Provider URL: https://token.actions.githubusercontent.com
• Audience: sts.amazonaws.com
• 생성된 Provider ARN 기록 - 2
Step 2: IAM Role 생성 및 신뢰 정책 설정
IAM Role을 생성하고 신뢰 정책을 자신의 GitHub 저장소로 제한합니다:
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:ref:refs/heads/main"
}
}
}]
}
```
• ACCOUNT_ID를 자신의 AWS 계정 ID로 교체
• OWNER/REPO를 자신의 GitHub 저장소로 교체
• 모든 브랜치를 허용하려면 :ref:refs/heads/main 제거 가능 - 3
Step 3: Workflow에서 OIDC 사용 설정
GitHub Actions workflow에서 OIDC token을 요청합니다:
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # 필수: OIDC token 요청
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GitHubActionsRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket
```
• id-token: write는 필수 권한 선언
• secrets.AWS_ACCESS_KEY_ID를 설정할 필요 없음 - 4
Step 4: 테스트 및 검증
workflow를 실행해 OIDC 설정을 검증합니다:
• workflow 로그에서 인증 성공 여부 확인
• role-to-assume 값이 올바른지 확인
• 정적 자격 증명 없이 AWS 리소스에 접근할 수 있는지 확인
• 대상 작업(S3 동기화, ECR 푸시 등) 테스트
FAQ
Repository Secrets와 Environment Secrets의 차이는 무엇인가요?
workflow에서 secrets를 안전하게 사용하려면 어떻게 해야 하나요?
• 명시적 전달: ${{ secrets.XXX }}를 인라인으로 사용하지 말고 env 필드로 주입
• 최소 권한: 필요한 step에만 전달하고 GITHUB_TOKEN 사용 시 permissions: contents: read 설정
• 정기 교체: 30~90일마다 교체하는 것을 권장하며, 클라우드 배포에서는 OIDC를 사용해 교체 자체를 없앨 수 있음
OIDC란 무엇이며 기존 secrets보다 더 안전한 이유는 무엇인가요?
tj-actions 사건과 같은 공급망 공격을 방지하려면 어떻게 해야 하나요?
• Actions를 버전 태그가 아닌 commit SHA에 고정
• secrets를 신뢰할 수 있는 actions에만 전달
• 서드파티 action의 maintainer 출처 감사
• Dependabot 또는 Renovate를 정기적으로 실행해 버전 업데이트 확인
secrets가 로그에 유출될 수 있나요?
Organization Secrets는 어떤 상황에 적합한가요?
2분 읽기 · 게시일: 2026년 4월 18일 · 수정일: 2026년 9월 8일
GitHub Actions 완전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
GitHub Actions 배포 전략: VPS부터 클라우드 플랫폼까지의 CD 파이프라인
GitHub Actions의 세 가지 배포 전략인 VPS SSH 배포, 클라우드 플랫폼 호스팅(Vercel/Cloudflare/Netlify), 하이브리드 아키텍처를 완전한 workflow 설정과 자주 발생하는 문제 해결 방법과 함께 자세히 설명합니다.
8편 중 6편
다음
GitHub Actions 셀프 호스티드 Runner: 프라이빗 환경 배포 완벽 가이드
GitHub Actions 셀프 호스티드 Runner를 프라이빗 환경에 배포하는 완벽 가이드입니다. 2026년 요금 변경 분석, 세 가지 배포 방식 비교, 보안 모범 사례, 오픈 소스 관리 도구 Runner Fleet를 다룹니다.
8편 중 8편



댓글
GitHub로 로그인하여 댓글을 남기세요