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

데이터베이스 비밀번호, API 키, 배포 토큰이 들어 있는 .env 파일은 Git 저장소에 커밋하면 안 된다. GitHub Actions에서는 민감한 값은 Secrets, 공개되어도 괜찮은 설정은 Variables, 클라우드 인증은 가능하면 OIDC로 나누어 관리하는 것이 기본 원칙이다.
이 글은 GitHub의 Secrets 사용 문서, Secrets 제한 문서, 안전한 워크플로우 작성 지침을 기준으로 검증했다.
결론부터: 무엇을 어디에 저장해야 할까?
| 값의 종류 | 권장 저장 위치 | 예시 |
|---|---|---|
| 비밀번호, API 키, 개인 키 | Repository Secret | API_KEY, DB_PASSWORD |
| 운영 배포용 자격 증명 | Environment Secret | production의 DEPLOY_TOKEN |
| 공개되어도 되는 설정 | Actions Variable | NODE_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 웹에서 대상 저장소를 연 뒤 다음 순서로 이동한다.
- Settings
- 왼쪽 사이드바의 Secrets and variables
- Actions
- Secrets 탭
- 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 Request | GITHUB_TOKEN을 제외한 Actions Secrets는 runner에 전달되지 않음 |
| Dependabot이 시작한 워크플로우 | 일반 Actions Secrets를 사용할 수 없음 |
| Reusable workflow | Secret이 자동 전달되지 않으므로 호출부에서 명시해야 함 |
| 등록되지 않은 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 보안의 범위다.

A .env file containing database passwords, API keys, or deployment tokens should never be committed to a Git repository. In GitHub Actions, the basic rule is to store sensitive values in Secrets, non-sensitive settings in Variables, and use OIDC for cloud authentication whenever possible.
This guide was verified against GitHub's official documentation for using secrets, secret limits, and secure workflow use.
The short answer: where should each value go?
| Value type | Recommended location | Example |
|---|---|---|
| Password, API key, private key | Repository Secret | API_KEY, DB_PASSWORD |
| Production deployment credential | Environment Secret | DEPLOY_TOKEN in production |
| Non-sensitive configuration | Actions Variable | NODE_ENV, server name, region |
| Cloud authentication for AWS, Azure, or GCP | Prefer OIDC | Short-lived token instead of a long-lived access key |
For most projects, registering one Secret per sensitive key is the easiest approach to maintain. Storing the whole .env file as one ENV_FILE Secret can work when an application requires a physical file, but it should not be the default.
1. Prevent .env from entering Git
Add these rules to the root .gitignore:
.env
.env.*
!.env.example
Keep only required key names and safe defaults in .env.example.
DB_HOST=localhost
DB_PORT=5432
API_KEY=
Check that Git ignores the real file:
git check-ignore -v .env
git ls-files .env
If the second command prints .env, the file is already tracked. Keep the local file but remove it from Git tracking:
git rm --cached .env
Deleting a file from the latest commit does not remove it from earlier history. If a real credential was ever pushed, revoke or rotate it first, then clean the Git history if necessary. GitHub recommends the same order in its guide to removing sensitive data.
2. Create a Repository Secret
Open the target repository on GitHub and follow this path:
- Settings
- Secrets and variables in the left sidebar
- Actions
- Secrets tab
- New repository secret
Enter a clear uppercase name such as API_KEY, paste the value in Secret, and select Add secret.
The list shows the Secret name and update time, not a reusable view of the stored value. If the value is lost or changes, update it with a new value.
Register Secrets faster with GitHub CLI
GitHub CLI also supports secure terminal entry:
# Paste the value at the prompt
gh secret set API_KEY
# Import each .env entry as a separate Repository Secret
gh secret set -f .env
# List registered Secret names
gh secret list
# Add a Secret to the production Environment
gh secret set --env production DEPLOY_TOKEN
gh secret set -f .env does not save the whole file as one value. It imports each dotenv key as a separate Secret. Review the file first so that you do not register unrelated values.
3. Separate Secrets or one complete .env?
| Method | Best for | Benefit | Caution |
|---|---|---|---|
| One Secret per key | Normal CI/CD | Easier access control, rotation, audit, and limited delivery | More registration work when many keys exist |
Whole .env in ENV_FILE |
A tool requires a real .env file |
Convenient one-step restoration | Harder partial rotation and weaker redaction behavior for structured data |
GitHub advises against storing structured data such as JSON or multi-line configuration in one Secret because log redaction relies on matching the exact value. A Secret is also limited to 48 KB.
Use separate keys by default. Consider ENV_FILE only when all of these are true:
- The build tool requires a physical
.envfile. - The file is smaller than 48 KB.
- Every value can be rotated on the same schedule.
- The workflow never prints the file or uploads it to an artifact or cache.
4. Build .env safely in a workflow
Pass Secret expressions through env: instead of interpolating them directly into a run: script. GitHub's secure-use guidance also recommends intermediate environment variables and correct shell quoting for inline scripts.
Method A: build it from separate Secrets
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
The important details are:
- Evaluate
${{ secrets.* }}only inenv:. - Always quote Bash variables, for example
"$API_KEY". - Use
printfso special characters are treated as data rather than commands. - Use
umask 077so only the current user can read and write the file. - Fail with the missing key's name without printing its value.
Method B: restore one 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
Avoid putting the Secret expression directly inside the shell command:
# Avoid this pattern
- run: echo "${{ secrets.ENV_FILE }}" > .env
With direct interpolation, GitHub replaces the expression before the shell parses the script. Quotes, newlines, $, or backticks inside the value can cause unexpected quoting or parsing behavior. The env: plus printf pattern creates a clearer boundary.
Remove the file after the build if it is no longer needed:
- name: Remove runtime .env
if: ${{ always() }}
shell: bash
run: rm -f .env
5. Isolate production Secrets with Environments
If development, staging, and production are separate, put production credentials in a production Environment instead of sharing one Repository Secret.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./scripts/deploy.sh
An Environment Secret is only available to jobs that reference that Environment. The approval, allowed-branch, and protection rules described in Deployments and environments can gate access to production credentials.
6. Common cases where Secrets are unavailable
| Situation | Behavior |
|---|---|
| Pull request from a fork | Actions Secrets are not sent to the runner, except GITHUB_TOKEN |
| Workflow started by Dependabot | Normal Actions Secrets are unavailable |
| Reusable workflow | Secrets are not passed automatically; the caller must declare them |
| Reference to an unset Secret | The expression evaluates to an empty string |
Pass only the required Secrets to a reusable workflow:
jobs:
deploy:
uses: ./.github/workflows/deploy.yml
secrets:
API_KEY: ${{ secrets.API_KEY }}
If fork PR tests must run without Secrets, split unit tests and real external-service integration tests into separate jobs or workflows. Running untrusted PR code in an environment that has production Secrets is not a safe workaround.
7. Prevent leaks through logs and artifacts
GitHub masks registered Secrets in logs, but masking is not a complete leak-prevention system. Transformed or structured values may not be redacted reliably.
- Do not print
cat .env,printenv, orenv. - Do not enable
set -xin a step that handles Secrets. - Do not include
.envin artifacts or caches. - Pass a Secret only to the step that needs it.
- Use
::add-mask::for generated sensitive values that are not GitHub Secrets. - Remove unused Secrets and rotate them regularly.
- Limit
GITHUB_TOKENwith the workflow'spermissions:setting.
8. Prefer OIDC for cloud deployment credentials
For AWS, Azure, Google Cloud, and other services that support OIDC, consider exchanging a GitHub OIDC identity for a short-lived token instead of storing a long-lived access key.
permissions:
contents: read
id-token: write
id-token: write allows the workflow to request a GitHub OIDC token; it does not directly grant write access to cloud resources. Restrict actual access through the cloud provider's trust policy. See GitHub's OIDC documentation for provider-specific setup.
Practical checklist
-
.envand environment-specific.env.*files are ignored. -
.env.examplecontains no real credentials. - Sensitive values use Secrets; non-sensitive settings use Variables.
- Secrets are registered separately in normal cases.
-
${{ secrets.* }}is passed throughenv:instead of directrun:interpolation. - Logs, artifacts, and caches contain no Secret or
.envdata. - Production Secrets are protected with an Environment and approval rules.
- Fork PR and Dependabot tests assume Secrets are unavailable.
- Long-lived cloud keys have been evaluated for OIDC replacement.
- A leaked Secret is revoked or rotated before Git history cleanup.
GitHub Secrets are the starting point for moving .env out of the repository. Real CI/CD security also means passing only the required value, to the required job and step, for the shortest possible time.

包含数据库密码、API 密钥或部署令牌的 .env 文件绝不能提交到 Git 仓库。在 GitHub Actions 中,基本原则是:敏感值放入 Secrets,可以公开的配置放入 Variables,云端认证则尽可能使用 OIDC。
本文依据 GitHub 官方的 Secrets 使用文档、Secrets 限制及安全使用工作流指南进行核实。
先说结论:不同数据该存在哪里?
| 数据类型 | 推荐位置 | 示例 |
|---|---|---|
| 密码、API 密钥、私钥 | Repository Secret | API_KEY, DB_PASSWORD |
| 生产部署凭据 | Environment Secret | production 中的 DEPLOY_TOKEN |
| 非敏感配置 | Actions Variable | NODE_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=
确认真实文件已被忽略:
git check-ignore -v .env
git ls-files .env
如果第二条命令输出 .env,说明文件已被跟踪。保留本地文件,仅取消 Git 跟踪:
git rm --cached .env
从最新提交删除文件,不会让它从历史记录中消失。如果真实凭据曾经被 push,先撤销或轮换该凭据,再按需清理 Git 历史。GitHub 的敏感数据移除指南也建议采用这个顺序。
2. 注册 Repository Secret
打开目标 GitHub 仓库,依次进入:
- Settings
- 左侧的 Secrets and variables
- Actions
- Secrets 标签
- 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 并不是把整个文件保存为一个值,而是把 dotenv 的每个键导入为独立 Secret。执行前请检查文件中是否混入不应注册的内容。
3. 分开注册,还是把整个 .env 放在一起?
| 方式 | 适用场景 | 优点 | 注意事项 |
|---|---|---|---|
| 每个键一个 Secret | 常规 CI/CD | 便于授权、轮换、审计,并只传递必要值 | 键很多时注册工作增加 |
整个 .env 存入 ENV_FILE |
工具必须读取真实 .env 文件 |
一次即可恢复文件 | 难以局部轮换,结构化数据的日志脱敏效果较弱 |
GitHub 建议不要把 JSON 或多行配置等结构化数据放进一个 Secret,因为日志脱敏依赖精确值匹配。此外,每个 Secret 的大小上限是 48KB。
默认应使用独立键。仅在以下条件全部满足时考虑 ENV_FILE:
- 构建工具必须使用实体
.env文件。 - 文件小于 48KB。
- 所有值可以按同一周期轮换。
- 工作流不会打印文件,也不会把它上传到 artifact 或 cache。
4. 在工作流中安全生成 .env
不要把 Secret 表达式直接插入 run: 字符串,而应通过 env: 传递给 Shell。GitHub 的安全指南同样建议内联脚本使用中间环境变量,并正确引用变量。
方法 A:由独立 Secrets 生成
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
关键点如下:
- 只在
env:中求值${{ secrets.* }}。 - 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 表达式直接写进 Shell 命令:
# 避免这种写法
- run: echo "${{ secrets.ENV_FILE }}" > .env
直接插值时,GitHub 会先替换表达式,再由 Shell 解析脚本。如果值包含引号、换行、$ 或反引号,可能产生意外的引用和解析问题。env: 配合 printf 能建立更清晰的边界。
构建结束后若不再需要该文件,请删除:
- name: Remove runtime .env
if: ${{ always() }}
shell: bash
run: rm -f .env
5. 用 Environment 隔离生产 Secrets
如果项目区分开发、预发布和生产,请把生产凭据放在 production Environment 中,不要共享一个 Repository Secret。
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 中的审批人、允许分支和保护规则,可以在访问生产凭据前增加审批门槛。
6. Secret 不会传递的常见情况
| 情况 | 行为 |
|---|---|
| 来自 fork 的 Pull Request | 除 GITHUB_TOKEN 外,Actions Secrets 不会发送给 runner |
| Dependabot 启动的工作流 | 常规 Actions Secrets 不可用 |
| Reusable workflow | Secret 不会自动传递,调用方必须显式声明 |
| 引用未注册的 Secret | 表达式结果为空字符串 |
只向 reusable workflow 传递必需的 Secret:
jobs:
deploy:
uses: ./.github/workflows/deploy.yml
secrets:
API_KEY: ${{ secrets.API_KEY }}
如果来自 fork 的 PR 必须在没有 Secret 的情况下测试,请把单元测试和真实外部服务集成测试拆成不同 job 或 workflow。让不可信的 PR 代码在持有生产 Secret 的环境中运行并不是安全的绕过方案。
7. 防止从日志和产物再次泄露
GitHub 会在日志中遮蔽已注册的 Secret,但不能把它视为完整的泄露防护。经过转换或结构化的值可能无法可靠脱敏。
- 不要输出
cat .env、printenv或env。 - 处理 Secret 的步骤不要启用
set -x。 - 不要把
.env放入 artifact 或 cache。 - 只向需要它的 step 传递 Secret。
- 对非 GitHub Secret 的动态敏感值使用
::add-mask::。 - 删除不用的 Secret,并定期轮换。
- 用
permissions:限制工作流的GITHUB_TOKEN权限。
8. 云部署凭据优先使用 OIDC
对于支持 OIDC 的 AWS、Azure、Google Cloud 等服务,应考虑用 GitHub OIDC 身份换取短期令牌,而不是保存长期访问密钥。
permissions:
contents: read
id-token: write
id-token: write 允许工作流请求 GitHub OIDC 令牌,并不会直接授予云资源写权限。实际访问范围必须由云服务商的信任策略限制。各提供商的配置请参考 GitHub 的 OIDC 文档。
实用检查清单
-
.env和各环境的.env.*已被忽略。 -
.env.example不含真实凭据。 - 敏感值放入 Secrets,非敏感设置放入 Variables。
- 常规情况下,每个 Secret 单独注册。
-
${{ secrets.* }}通过env:传递,而不是直接插入run:。 - 日志、artifact、cache 中没有 Secret 或
.env数据。 - 生产 Secrets 由 Environment 和审批规则保护。
- fork PR 与 Dependabot 测试按“无 Secret”设计。
- 已评估能否用 OIDC 替换长期云密钥。
- Secret 泄露时,先撤销或轮换,再清理 Git 历史。
GitHub Secrets 只是把 .env 移出仓库的起点。真正的 CI/CD 安全还包括:只把必要的值传给必要的 job 和 step,并把可用时间缩到最短。

データベースのパスワード、APIキー、デプロイトークンを含む .env ファイルは、Gitリポジトリにコミットしてはいけません。GitHub Actionsでは、機密情報を Secrets、公開されても問題ない設定を Variables、クラウド認証を可能な限り OIDC で管理するのが基本です。
この記事は、GitHub公式の Secretsの使用方法、Secretsの制限、安全なワークフロー利用ガイドをもとに検証しています。
先に結論:何をどこに保存するか
| 値の種類 | 推奨する保存先 | 例 |
|---|---|---|
| パスワード、APIキー、秘密鍵 | Repository Secret | API_KEY, DB_PASSWORD |
| 本番デプロイ用の認証情報 | Environment Secret | production の DEPLOY_TOKEN |
| 機密ではない設定 | Actions Variable | NODE_ENV、サーバー名、リージョン |
| AWS・Azure・GCPなどのクラウド認証 | OIDCを優先 | 長期アクセスキーではなく短期トークン |
ほとんどのプロジェクトでは、機密キーごとにSecretを登録する方法が最も管理しやすい選択です。アプリケーションが実ファイルを必要とする場合は、.env 全体を1つの ENV_FILE Secretとして保存できますが、標準の方法にする必要はありません。
1. まず .env がGitに入らないようにする
プロジェクトルートの .gitignore に次を追加します。
.env
.env.*
!.env.example
.env.example には、必要なキー名と安全な初期値だけを残します。
DB_HOST=localhost
DB_PORT=5432
API_KEY=
実際のファイルが無視されているか確認します。
git check-ignore -v .env
git ls-files .env
2つ目のコマンドで .env が表示された場合、そのファイルはすでに追跡されています。ローカルファイルを残したまま、Gitの追跡だけを解除します。
git rm --cached .env
最新コミットからファイルを削除しても、過去の履歴からは消えません。実際の認証情報を一度でもpushした場合は、まずその認証情報を失効またはローテーションし、その後必要に応じてGit履歴を整理します。GitHubも機密データの削除ガイドでこの順序を推奨しています。
2. Repository Secretを登録する
GitHubで対象リポジトリを開き、次の順に進みます。
- Settings
- 左サイドバーの Secrets and variables
- Actions
- Secrets タブ
- 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 はファイル全体を1つの値として保存するコマンドではありません。dotenvの各キーを個別のSecretとして取り込みます。不要な値が混ざっていないか、実行前に確認してください。
3. キー別登録と .env 全体登録のどちらを選ぶか
| 方法 | 適した用途 | メリット | 注意点 |
|---|---|---|---|
| キーごとのSecret | 一般的なCI/CD | 権限、ローテーション、監査がしやすく、必要な値だけ渡せる | キーが多いと登録作業が増える |
.env 全体を ENV_FILE に保存 |
ツールが実際の .env ファイルを要求する |
ファイルを一度に復元しやすい | 一部だけの更新が難しく、構造化データのログマスキングに不利 |
GitHubはログでSecretをマスクする際に完全一致を利用するため、JSONや複数行設定のような構造化データを1つのSecretにしないよう案内しています。また、1つの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:1つの 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で分離する
開発、ステージング、本番を分けている場合、本番認証情報を1つの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で説明されている承認者、許可ブランチ、保護ルールを使えば、本番認証情報へアクセスする前に承認を要求できます。
6. Secretが渡されない代表的なケース
| 状況 | 動作 |
|---|---|
| forkからのPull Request | GITHUB_TOKEN を除くActions Secretsはrunnerへ渡されない |
| Dependabotが開始したワークフロー | 通常のActions Secretsは利用できない |
| Reusable workflow | Secretは自動で渡らないため、呼び出し側で明示する |
| 未登録のSecretを参照 | 式の結果は空文字列になる |
Reusable workflowには必要なSecretだけを渡します。
jobs:
deploy:
uses: ./.github/workflows/deploy.yml
secrets:
API_KEY: ${{ secrets.API_KEY }}
fork PRのテストをSecretなしで実行する必要があるなら、unit testと実サービスを使うintegration testを別のjobまたはworkflowに分けます。信頼できないPRコードを本番Secretのある環境で実行するのは安全な回避策ではありません。
7. ログと成果物からの再流出を防ぐ
GitHubは登録済みSecretをログでマスクしますが、完全な漏えい防止機能ではありません。変換された値や構造化された値は確実にマスクされない場合があります。
cat .env、printenv、envで内容を出力しない。- Secretを扱うstepで
set -xを有効にしない。 .envをartifactやcacheに含めない。- Secretは必要なstepにだけ渡す。
- GitHub Secretではない動的な機密値には
::add-mask::を使う。 - 不要なSecretを削除し、定期的にローテーションする。
permissions:でワークフローのGITHUB_TOKEN権限を最小化する。
8. クラウドデプロイ資格情報にはOIDCを優先する
AWS、Azure、Google CloudなどOIDCをサポートするサービスでは、長期アクセスキーを保存せず、GitHub OIDC IDと短期トークンを交換する方法を検討します。
permissions:
contents: read
id-token: write
id-token: write はGitHub OIDCトークンを要求できるようにする設定で、クラウドリソースへの書き込み権限を直接与えるものではありません。実際のアクセス範囲はクラウド側の信頼ポリシーで制限します。プロバイダー別の設定はGitHubの OIDCドキュメントを確認してください。
実践チェックリスト
-
.envと環境別の.env.*が無視されている。 -
.env.exampleに実際の認証情報がない。 - 機密値はSecrets、非機密設定はVariablesに保存した。
- 通常はSecretをキーごとに登録した。
-
${{ secrets.* }}をrun:に直接入れず、env:で渡した。 - ログ、artifact、cacheにSecretや
.envが残らない。 - 本番SecretをEnvironmentと承認ルールで保護した。
- fork PRとDependabotのテストはSecretなしを前提にした。
- 長期クラウドキーをOIDCへ置き換えられるか確認した。
- 漏えいしたSecretはGit履歴整理より先に失効・ローテーションした。
GitHub Secretsは、.env をリポジトリの外へ移す出発点です。実際のCI/CDセキュリティには、必要な値だけを、必要なjobとstepへ、最短の時間だけ渡すことまで含まれます。

Un archivo .env que contiene contraseñas de bases de datos, claves API o tokens de despliegue nunca debe incluirse en un repositorio Git. En GitHub Actions, la regla básica es guardar los valores sensibles en Secrets, la configuración no sensible en Variables y usar OIDC para la autenticación en la nube siempre que sea posible.
Esta guía se verificó con la documentación oficial de GitHub sobre el uso de Secrets, sus límites y el uso seguro de workflows.
Respuesta rápida: ¿dónde se guarda cada valor?
| Tipo de valor | Ubicación recomendada | Ejemplo |
|---|---|---|
| Contraseña, clave API o clave privada | Repository Secret | API_KEY, DB_PASSWORD |
| Credencial de despliegue a producción | Environment Secret | DEPLOY_TOKEN en production |
| Configuración no sensible | Actions Variable | NODE_ENV, nombre de servidor, región |
| Autenticación en AWS, Azure o GCP | Priorizar OIDC | Token temporal en lugar de una clave de larga duración |
En la mayoría de los proyectos, registrar un Secret por cada clave sensible es lo más fácil de mantener. Guardar todo el archivo .env en un único Secret llamado ENV_FILE puede servir cuando la aplicación necesita un archivo físico, pero no debería ser la opción predeterminada.
1. Evita que .env entre en Git
Añade estas reglas al .gitignore de la raíz:
.env
.env.*
!.env.example
En .env.example, conserva solo los nombres necesarios y valores predeterminados seguros.
DB_HOST=localhost
DB_PORT=5432
API_KEY=
Comprueba que Git ignora el archivo real:
git check-ignore -v .env
git ls-files .env
Si el segundo comando muestra .env, el archivo ya está bajo seguimiento. Conserva el archivo local y elimina únicamente su seguimiento en Git:
git rm --cached .env
Eliminarlo del último commit no lo borra del historial. Si alguna credencial real llegó a subirse, revócala o rótala primero y limpia el historial después si es necesario. GitHub recomienda ese mismo orden en su guía para eliminar datos sensibles.
2. Crea un Repository Secret
Abre el repositorio en GitHub y sigue esta ruta:
- Settings
- Secrets and variables en la barra lateral
- Actions
- Pestaña Secrets
- New repository secret
Escribe un nombre claro en mayúsculas, como API_KEY, pega el valor real en Secret y selecciona Add secret.
La lista permite ver el nombre y la fecha de actualización, pero no volver a consultar el valor guardado. Si lo pierdes o cambia, actualízalo con uno nuevo.
Registra Secrets más rápido con GitHub CLI
GitHub CLI permite introducirlos de forma segura desde el terminal:
# Pegar el valor en el prompt
gh secret set API_KEY
# Importar cada entrada de .env como un Repository Secret separado
gh secret set -f .env
# Ver los nombres de los Secrets registrados
gh secret list
# Registrar un Secret en el Environment production
gh secret set --env production DEPLOY_TOKEN
gh secret set -f .env no guarda todo el archivo como un solo valor. Importa cada clave dotenv como un Secret independiente. Revisa el archivo antes para no registrar valores innecesarios.
3. ¿Secrets separados o un .env completo?
| Método | Cuándo usarlo | Ventaja | Precaución |
|---|---|---|---|
| Un Secret por clave | CI/CD habitual | Facilita permisos, rotación, auditoría y entrega mínima | Requiere más trabajo si hay muchas claves |
Todo .env en ENV_FILE |
Una herramienta exige un .env real |
Permite restaurar el archivo de una vez | Dificulta la rotación parcial y el enmascarado de datos estructurados |
GitHub recomienda evitar datos estructurados, como JSON o configuración multilínea, dentro de un solo Secret porque la ocultación en logs depende de coincidencias exactas. Además, cada Secret tiene un límite de 48 KB.
Usa claves separadas por defecto. Considera ENV_FILE solo si se cumplen todas estas condiciones:
- La herramienta de compilación exige un archivo
.envfísico. - El archivo ocupa menos de 48 KB.
- Todos los valores pueden rotarse con la misma frecuencia.
- El workflow nunca imprime el archivo ni lo sube a artifacts o caches.
4. Crea .env de forma segura en el workflow
Pasa las expresiones de Secrets mediante env: en lugar de interpolarlas directamente en un script run:. La guía de seguridad de GitHub también recomienda variables de entorno intermedias y un entrecomillado correcto en scripts inline.
Método A: crear el archivo desde Secrets separados
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
Los detalles importantes son:
- Evaluar
${{ secrets.* }}únicamente enenv:. - Entrecomillar siempre las variables Bash, por ejemplo
"$API_KEY". - Usar
printfpara tratar los caracteres especiales como datos y no como comandos. - Aplicar
umask 077para que solo el usuario actual pueda leer y escribir el archivo. - Fallar mostrando el nombre de la clave ausente, sin imprimir su valor.
Método B: restaurar un único 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
Evita insertar la expresión directamente en el comando:
# Evita este patrón
- run: echo "${{ secrets.ENV_FILE }}" > .env
Con interpolación directa, GitHub sustituye primero la expresión y después el shell analiza el script. Las comillas, saltos de línea, $ o acentos graves del valor pueden provocar problemas inesperados. La combinación de env: y printf crea un límite más claro.
Elimina el archivo al terminar si ya no hace falta:
- name: Remove runtime .env
if: ${{ always() }}
shell: bash
run: rm -f .env
5. Aísla los Secrets de producción con Environments
Si separas desarrollo, staging y producción, guarda las credenciales de producción en un Environment production, en lugar de compartir un único Repository Secret.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./scripts/deploy.sh
Un Environment Secret solo está disponible para jobs que hacen referencia a ese Environment. Los revisores, ramas permitidas y reglas de protección descritos en Deployments and environments permiten exigir aprobación antes de acceder a credenciales de producción.
6. Casos habituales en los que no se entregan Secrets
| Situación | Comportamiento |
|---|---|
| Pull Request desde un fork | Los Actions Secrets no llegan al runner, salvo GITHUB_TOKEN |
| Workflow iniciado por Dependabot | Los Actions Secrets normales no están disponibles |
| Reusable workflow | Los Secrets no se transfieren automáticamente; el llamador debe declararlos |
| Referencia a un Secret no configurado | La expresión devuelve una cadena vacía |
Entrega solo los Secrets necesarios a un reusable workflow:
jobs:
deploy:
uses: ./.github/workflows/deploy.yml
secrets:
API_KEY: ${{ secrets.API_KEY }}
Si las pruebas de PR desde forks deben ejecutarse sin Secrets, separa los unit tests de las pruebas de integración con servicios externos en jobs o workflows distintos. Ejecutar código no confiable con acceso a Secrets de producción no es una alternativa segura.
7. Evita filtraciones en logs y artifacts
GitHub oculta los Secrets registrados en los logs, pero el enmascarado no es un sistema completo contra filtraciones. Los valores transformados o estructurados pueden no ocultarse de forma fiable.
- No imprimas
cat .env,printenvnienv. - No actives
set -xen pasos que usan Secrets. - No incluyas
.enven artifacts o caches. - Entrega cada Secret solo al step que lo necesita.
- Usa
::add-mask::para valores sensibles generados que no sean GitHub Secrets. - Elimina Secrets sin uso y rótalos periódicamente.
- Limita
GITHUB_TOKENmediantepermissions:.
8. Prioriza OIDC para credenciales de despliegue en la nube
Para AWS, Azure, Google Cloud y otros servicios compatibles con OIDC, considera intercambiar la identidad OIDC de GitHub por un token temporal, en lugar de guardar una clave de larga duración.
permissions:
contents: read
id-token: write
id-token: write permite solicitar un token OIDC de GitHub; no concede por sí mismo permisos de escritura sobre recursos cloud. El acceso real debe limitarse mediante la política de confianza del proveedor. Consulta la documentación de OIDC de GitHub para la configuración específica.
Lista de comprobación práctica
-
.envy los archivos.env.*de cada entorno están ignorados. -
.env.exampleno contiene credenciales reales. - Los valores sensibles usan Secrets y los no sensibles, Variables.
- En casos normales, cada Secret se registra por separado.
-
${{ secrets.* }}se pasa medianteenv:y no se interpola enrun:. - Logs, artifacts y caches no contienen Secrets ni
.env. - Los Secrets de producción están protegidos por un Environment y aprobaciones.
- Las pruebas de forks y Dependabot suponen que no hay Secrets disponibles.
- Se evaluó sustituir claves cloud de larga duración por OIDC.
- Un Secret filtrado se revoca o rota antes de limpiar el historial Git.
GitHub Secrets es el punto de partida para sacar .env del repositorio. La seguridad real de CI/CD también exige entregar solo el valor necesario, al job y step necesarios, durante el menor tiempo posible.