[GitHub Actions] .env와 Secrets 안전하게 관리하기

데이터베이스 비밀번호, API 키, 배포 토큰이 들어 있는 .env 파일은 Git 저장소에 커밋하면 안 된다. GitHub Actions에서는 민감한 값은 Secrets, 공개되어도 괜찮은 설정은 Variables, 클라우드 인증은 가능하면 OIDC로 나누어 관리하는 것이 기본 원칙이다.

이 글은 GitHub의 Secrets 사용 문서, Secrets 제한 문서, 안전한 워크플로우 작성 지침을 기준으로 검증했다.

결론부터: 무엇을 어디에 저장해야 할까?

값의 종류권장 저장 위치예시
비밀번호, API 키, 개인 키Repository SecretAPI_KEY, DB_PASSWORD
운영 배포용 자격 증명Environment SecretproductionDEPLOY_TOKEN
공개되어도 되는 설정Actions VariableNODE_ENV, 서버 이름, 리전
AWS·Azure·GCP 같은 클라우드 인증OIDC 우선 검토장기 액세스 키 대신 단기 토큰

대부분의 프로젝트에서는 민감한 값을 키별 Secret으로 등록하는 방식이 가장 관리하기 쉽다. .env 전체를 ENV_FILE이라는 하나의 Secret으로 저장하는 방식은 애플리케이션이 실제 파일을 요구할 때 사용할 수 있지만, 기본 선택으로 삼을 필요는 없다.

1. 먼저 .env가 Git에 들어가지 않게 막기

프로젝트 루트의 .gitignore에 다음 규칙을 추가한다.

.env
.env.*
!.env.example

.env.example에는 실제 값 대신 필요한 키 이름과 설명 가능한 기본값만 둔다.

DB_HOST=localhost
DB_PORT=5432
API_KEY=

현재 .env가 제대로 무시되는지 확인한다.

git check-ignore -v .env
git ls-files .env

두 번째 명령에서 .env가 출력되면 이미 추적 중인 파일이다. 로컬 파일은 남기고 Git 추적만 해제한다.

git rm --cached .env

중요한 점은 파일을 최신 커밋에서 지웠다고 과거 기록에서도 사라지는 것은 아니라는 사실이다. 실제 Secret이 한 번이라도 push되었다면 먼저 해당 자격 증명을 폐기하거나 교체한 뒤, 필요할 때 Git 기록 정리를 진행해야 한다. GitHub도 민감 정보 제거 안내에서 회전·폐기를 첫 단계로 권장한다.

2. Repository Secret 등록하기

GitHub 웹에서 대상 저장소를 연 뒤 다음 순서로 이동한다.

  1. Settings
  2. 왼쪽 사이드바의 Secrets and variables
  3. Actions
  4. Secrets
  5. New repository secret

Name에는 API_KEY처럼 의미가 분명한 대문자 이름을, Secret에는 실제 값을 입력한 뒤 Add secret을 누른다.

저장된 Secret은 목록에서 이름과 수정 시점은 확인할 수 있지만 값 자체를 다시 읽는 용도로 제공되지 않는다. 값을 잃어버렸거나 변경해야 한다면 새 값으로 업데이트한다.

GitHub CLI로 더 빠르게 등록하기

GitHub CLI를 사용하면 터미널에서도 안전하게 입력할 수 있다.

# 입력 프롬프트에서 값 붙여넣기
gh secret set API_KEY

# .env의 각 항목을 각각의 Repository Secret으로 가져오기
gh secret set -f .env

# 등록된 Secret 이름 확인
gh secret list

# production Environment에 등록
gh secret set --env production DEPLOY_TOKEN

gh secret set -f .env.env 전체를 하나의 값으로 저장하는 명령이 아니다. dotenv 파일의 각 키를 각각의 Secret으로 가져온다. 실행 전에 파일에 등록하지 않을 값이 섞여 있지 않은지 확인한다.

3. 키별 등록과 .env 통째 등록 중 무엇이 좋을까?

방식적합한 경우장점주의점
키별 Secret일반적인 CI/CD권한·교체·감사가 쉽고 필요한 값만 전달 가능키가 많으면 등록 작업이 늘어남
.env 전체를 ENV_FILE로 저장도구가 실제 .env 파일을 요구함파일을 한 번에 복원하기 편함일부 값만 교체하기 어렵고 구조화된 값의 로그 마스킹이 불리함

GitHub는 로그에서 Secret을 마스킹할 때 정확한 값의 일치를 활용하므로, Secret 값에 JSON이나 여러 줄 설정처럼 구조화된 데이터를 넣지 않는 편이 좋다고 안내한다. 또한 Secret 하나의 크기는 48KB로 제한된다.

따라서 기본값은 키별 등록으로 두고, 다음 조건을 모두 만족할 때만 ENV_FILE 방식을 고려한다.

  • 빌드 도구가 환경변수보다 실제 .env 파일을 요구한다.
  • 파일 크기가 48KB보다 작다.
  • 파일 전체를 같은 주기로 교체해도 된다.
  • 워크플로우에서 파일 내용을 출력하거나 artifact·cache에 올리지 않는다.

4. 워크플로우에서 안전하게 .env 만들기

Secret 표현식을 run: 내부 문자열에 직접 삽입하기보다 env:를 통해 셸 환경변수로 전달한다. GitHub의 안전한 사용 지침도 인라인 스크립트에서는 중간 환경변수를 사용하고, 셸에서 값을 따옴표로 감싸는 방식을 권장한다.

방법 A: 키별 Secret으로 생성하기

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Create runtime .env
        shell: bash
        env:
          DB_HOST: ${{ secrets.DB_HOST }}
          DB_PORT: ${{ secrets.DB_PORT }}
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          set -euo pipefail
          : "${DB_HOST:?DB_HOST secret is missing}"
          : "${DB_PORT:?DB_PORT secret is missing}"
          : "${API_KEY:?API_KEY secret is missing}"

          umask 077
          {
            printf 'DB_HOST=%s\n' "$DB_HOST"
            printf 'DB_PORT=%s\n' "$DB_PORT"
            printf 'API_KEY=%s\n' "$API_KEY"
          } > .env

이 패턴의 핵심은 다음과 같다.

  • ${{ secrets.* }}env:에서 한 번만 평가한다.
  • Bash에서는 "$API_KEY"처럼 항상 따옴표로 감싼다.
  • printf를 사용해 특수문자가 명령으로 해석될 여지를 줄인다.
  • umask 077로 생성 파일을 현재 사용자만 읽고 쓸 수 있게 한다.
  • 값이 없으면 Secret 내용을 출력하지 않고 키 이름만 표시한 뒤 실패한다.

방법 B: ENV_FILE 하나로 복원하기

- name: Restore runtime .env
  shell: bash
  env:
    ENV_FILE: ${{ secrets.ENV_FILE }}
  run: |
    set -euo pipefail
    : "${ENV_FILE:?ENV_FILE secret is missing}"
    umask 077
    printf '%s' "$ENV_FILE" > .env

다음처럼 Secret 표현식을 셸 명령 안에 바로 넣는 방식은 피한다.

# 피해야 할 예시
- run: echo "${{ secrets.ENV_FILE }}" > .env

직접 삽입하면 GitHub가 먼저 표현식을 문자열로 치환한 뒤 셸이 스크립트를 해석한다. 값에 따옴표, 줄바꿈, $, 백틱 같은 문자가 있으면 예상치 못한 인용·파싱 문제가 생길 수 있다. env:printf 조합은 이 경계를 더 분명하게 만든다.

파일이 빌드 단계에서만 필요하다면 마지막에 정리한다.

- name: Remove runtime .env
  if: ${{ always() }}
  shell: bash
  run: rm -f .env

5. 운영용 Secret은 Environment로 분리하기

개발, 스테이징, 운영이 나뉘는 프로젝트라면 운영 자격 증명을 Repository Secret 하나로 공유하지 말고 production Environment에 둔다.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./scripts/deploy.sh

Environment Secret은 해당 Environment를 참조하는 job에서만 사용할 수 있다. Deployments and environments 문서에 설명된 승인자, 허용 브랜치, 보호 규칙을 함께 적용하면 운영 Secret에 접근하기 전 승인 단계를 둘 수 있다.

6. Secret이 전달되지 않는 대표적인 경우

상황동작
Fork 저장소에서 올라온 Pull RequestGITHUB_TOKEN을 제외한 Actions Secrets는 runner에 전달되지 않음
Dependabot이 시작한 워크플로우일반 Actions Secrets를 사용할 수 없음
Reusable workflowSecret이 자동 전달되지 않으므로 호출부에서 명시해야 함
등록되지 않은 Secret 참조표현식 결과가 빈 문자열이 됨

Reusable workflow에는 필요한 Secret만 전달한다.

jobs:
  deploy:
    uses: ./.github/workflows/deploy.yml
    secrets:
      API_KEY: ${{ secrets.API_KEY }}

Fork PR의 테스트가 Secret 없이도 실행되어야 한다면 unit test와 실제 외부 서비스 연동 테스트를 job 또는 workflow로 분리한다. 신뢰할 수 없는 PR 코드를 운영 Secret이 있는 환경에서 실행하도록 우회하는 것은 해결책이 아니다.

7. 로그와 산출물에서 다시 새지 않게 하기

GitHub가 등록된 Secret을 로그에서 마스킹하더라도 이를 완전한 유출 방지 장치로 간주하면 안 된다. 값이 변형되거나 구조화되어 있으면 자동 마스킹이 보장되지 않는다.

  • cat .env, printenv, env로 내용을 출력하지 않는다.
  • Secret을 다루는 단계에서 set -x를 켜지 않는다.
  • .env를 artifact나 cache에 포함하지 않는다.
  • Secret은 필요한 step에만 env:로 전달한다.
  • GitHub Secret이 아닌 동적 민감 값은 로그에 쓰기 전에 ::add-mask::로 마스킹한다.
  • 사용하지 않는 Secret은 삭제하고, 정기적으로 교체한다.
  • 워크플로우의 GITHUB_TOKEN 권한도 permissions:로 최소화한다.

8. 클라우드 배포 키라면 OIDC를 먼저 검토하기

AWS, Azure, Google Cloud처럼 OIDC를 지원하는 서비스라면 장기 액세스 키를 Repository Secret에 저장하는 것보다 워크플로우 실행 시 단기 토큰을 발급받는 방식을 우선 검토한다.

permissions:
  contents: read
  id-token: write

id-token: write는 클라우드 리소스에 직접 쓰기 권한을 주는 설정이 아니라 GitHub OIDC 토큰을 요청할 수 있게 하는 권한이다. 실제 접근 범위는 클라우드 제공자의 신뢰 정책으로 제한해야 한다. 자세한 구성은 GitHub OIDC 문서에서 제공자별 안내를 확인한다.

실전 체크리스트

  • .env와 환경별 .env.*.gitignore에 포함되어 있다.
  • .env.example에는 실제 자격 증명이 없다.
  • 민감한 값은 Secrets, 비민감 설정은 Variables에 저장했다.
  • 일반적인 경우 Secret을 키별로 등록했다.
  • ${{ secrets.* }}run:에 직접 삽입하지 않고 env:로 전달했다.
  • 로그, artifact, cache에 Secret이나 .env가 남지 않는다.
  • 운영 Secret은 Environment와 승인 규칙으로 보호한다.
  • Fork PR과 Dependabot에서는 Secret이 없다는 전제로 테스트를 설계했다.
  • 클라우드 장기 키를 OIDC로 대체할 수 있는지 확인했다.
  • 유출된 Secret은 Git 기록 정리보다 먼저 폐기·교체한다.

GitHub Secrets는 .env를 저장소 밖으로 옮기는 출발점이다. 하지만 안전성은 저장 위치만으로 완성되지 않는다. 필요한 값만, 필요한 job과 step에, 가장 짧은 시간 동안 전달하는 것까지가 실제 CI/CD 보안의 범위다.