평문 .env 파일 대신 시크릿 저장소(1Password나 macOS Keychain)에 보관하고, 실행 시점에 환경 변수로 주입해 디스크에 남기지 않는 간단한 패턴을 소개한다.
업데이트(2026년 2월 28일): 이 글은 Hacker News와 Lobsters에서 많은 논의를 불러일으켰습니다. 짚고 넘어갈 만한 점이 두 가지 있습니다.
첫째, 여러 사람이 제가 아직 써보지 못했던 비교적 최신 기능인 1Password Environments를 지적했습니다. 이 기능은 UNIX 네임드 파이프(FIFO)를 기반으로 한 .env 파일을 마운트해서 기존 dotenv 라이브러리들이 그대로 동작하게 해주지만, 평문이 디스크에 기록되지는 않습니다. 1Password가 잠길 때까지 인증이 유지되므로 op run에서 겪을 수 있는 반복적인 Touch ID 프롬프트 문제도 해결됩니다. 이미 1Password를 쓰고 있다면, 현재로서는 아마 이것이 가장 좋은 선택일 것입니다.
둘째, 타당한 비판입니다. 시크릿을 환경 변수로 주입한다고 해서 그것이 보이지 않게 되는 것은 아닙니다. Linux에서는 같은 사용자로 실행 중인 어떤 프로세스든 /proc/self/environ(또는 내가 소유한 다른 프로세스의 경우 /proc/<pid>/environ)을 읽을 수 있습니다. macOS에는 /proc 파일시스템이 없지만, 다른 방법으로 환경 변수에 접근하는 것은 여전히 가능합니다. 런타임 주입은 디스크에 평문 파일을 남겨두는 것에 비해 의미 있는 개선이지만, 완전한 보안 경계는 아닙니다. 더 높은 보안이 필요한 환경에서는 수명이 짧은 자격증명, tmpfs를 통한 마운트형 시크릿, 하드웨어 기반 키스토어 등을 사용하는 도구를 고려하세요.
몇 주 전, 제 친구 Harrison(@hktouw)과 저는 매년 하던 대로 베이 에어리어를 도는 Tesla FSD 크루즈를 했습니다. 차가 운전하는 동안(무려 7시간) 떠오르는 대로 이야기를 나누는 자리죠. 올해는 처음으로 운전대를 잡아야 할 일이 한 번도 없었고, 덕분에 대화할 시간이 더 많았습니다. 우리는 AI 도입, 투자 이야기 등을 하다가, 제가 한동안 계속 신경 쓰이던 주제로 흘러갔습니다. 도대체 왜 우리는 아직도 자격증명을 평문 .env 파일에 저장하고 있을까요?
저는 한동안 1Password에 개별 시크릿을 저장해 두고 CLI로 하나씩 꺼내 쓰고 있었습니다. Harrison은 한 단계 더 나아갔습니다. “.env 파일에 들어갈 시크릿 전체를 하나의 1Password 아이템 안에 필드로 저장하면 안 돼?”라고 말했죠. 간단했습니다. 돌이켜보면 너무 당연했고, 그 말은 제가 모든 프로젝트에서 시크릿을 다루는 방식을 재고하게 만드는 계기가 됐습니다.
그 결과, 지난 한 달 동안 제가 써온 패턴을 공유하고 싶습니다. 복잡하지 않습니다. 엔터프라이즈 툴링도 필요 없습니다. 여러분이 이미 가지고 있을 법한 도구로, 오늘 당장 쓸 수 있습니다.
우리는 모두 .env 파일이 gitignore 대상이어야 한다는 것을 알고 있습니다. 그리고 보통은 실제로 그렇게 합니다. 하지만 git에 실수로 올라갈 위험 외에도, 자격증명을 평문으로 저장해 두는 건 찜찜합니다. 카페에서 노트북을 잠깐 잠금 해제해 둔 채 자리를 비우거나 누군가가 여러분의 머신에 접근하게 되면, 그 .env 파일들은 바로 거기에 있습니다. 보호 장치 하나 없는 고가치 타깃이죠.
.env 파일은 현실에서 이렇게 됩니다.
.env 파일을 보내달라고 합니다.12-factor app은 설정을 환경 변수에 넣으라고 말했습니다. 좋은 조언입니다. 하지만 .env 파일은 그 원칙을 새는 방식으로 구현한 것입니다. 환경 변수인 척하는 평문 파일이죠.
패턴은 간단합니다. 파일에서 시크릿을 로드하는 대신, 래퍼 스크립트를 사용해 안전한 저장소에서 시크릿을 가져오고 실행할 프로세스에 환경 변수로 주입합니다.
# 이렇게 하지 말고:
source .env && node server.js
# 이렇게 하세요:
./with-1password.sh node server.js
# 또는
./with-keychain.sh node server.js
애플리케이션은 전혀 바뀌지 않습니다. 여전히 예전처럼 process.env.API_KEY나 $DATABASE_URL을 읽기만 하면 됩니다. 달라지는 건 값이 어디서 오느냐뿐입니다.
저는 1Password CLI용 구현과 macOS Keychain용 구현, 두 가지가 모두 들어 있는 데모 레포를 만들었습니다. 클론해서 5분 정도면 둘 다 시험해 볼 수 있습니다.
이건 Harrison과 제가 처음에 이야기하던 접근이고, 제가 가장 자주 쓰는 방법입니다. 이미 1Password를 쓰고 있다면 CLI(op) 덕분에 거의 마찰 없이 적용할 수 있습니다.
핵심 인사이트는 1Password가 **시크릿 레퍼런스(secret references)**를 지원한다는 점입니다. op://Development/myapp/api-key처럼 볼트의 특정 필드를 가리키는 URI죠. 이 레퍼런스들을 시크릿이 전혀 없는 파일에 넣어 커밋해도 안전합니다.
# .env.1password — 커밋해도 안전, 시크릿을 포함하지 않음
API_KEY=op://Development/secure-env-demo/api-key
DATABASE_URL=op://Development/secure-env-demo/database-url
WEBHOOK_SECRET=op://Development/secure-env-demo/webhook-secret
그다음 래퍼 스크립트는 거의 놀라울 정도로 단순합니다.
#!/usr/bin/env bash
exec op run --env-file=".env.1password" -- "$@"
이게 전부입니다. op run은 레퍼런스를 읽고, 볼트에서 각 시크릿을 가져온 뒤(Touch ID나 마스터 패스워드로 인증), 환경 변수로 주입하고, 여러분의 커맨드를 실행합니다. 시크릿은 평문으로 디스크에 닿지 않습니다. 덤으로 op run은 시크릿 값이 실수로 stdout에 찍히더라도 자동으로 마스킹해 줍니다.
설정은 한 번만 하면 됩니다. 시크릿이 들어 있는 볼트 아이템을 만들고(데모 레포에는 이를 위한 설정 스크립트가 포함되어 있습니다), .env.1password 안의 레퍼런스를 커스터마이징하면 끝입니다. 팀의 모든 개발자는 버전 컨트롤에 있는 동일한 .env.1password 파일을 공유하면서, 각자의 1Password 계정에 대해 이를 해석(resolve)해 사용할 수 있습니다.
모든 사람이 1Password를 쓰는 건 아니고, 괜찮습니다. Mac을 쓰고 있다면 OS에 이미 시크릿 매니저가 내장되어 있습니다. security 명령은 로그인 키체인에 읽기/쓰기를 할 수 있고, macOS는 암호나 Touch ID로 접근을 통제합니다.
래퍼 스크립트는 Keychain에서 각 시크릿을 읽어 export합니다.
#!/usr/bin/env bash
declare -A SECRETS=(
[API_KEY]="secure-env-demo/api-key"
[DATABASE_URL]="secure-env-demo/database-url"
[WEBHOOK_SECRET]="secure-env-demo/webhook-secret"
)
for var in "${!SECRETS[@]}"; do
service="${SECRETS[$var]}"
value=$(security find-generic-password -a "$USER" -s "$service" -w)
export "$var=$value"
done
exec "$@"
시크릿을 저장하는 건 명령 한 줄이면 됩니다.
security add-generic-password -a "$USER" -s "secure-env-demo/api-key" -w "sk-your-key" -U
1Password 접근보다 약간 더 수동적입니다(레퍼런스 파일 대신 스크립트에서 매핑을 유지해야 하니까요). 하지만 어떤 서드파티 의존성 없이도 동작합니다.
즉각적인 이점은 분명합니다. 디스크에 평문 시크릿이 더 이상 남지 않습니다. 하지만 덜 눈에 띄는 장점도 몇 가지 있습니다.
단일 진실 공급원(One source of truth). 자격증명이 로테이션되면, 한 곳만 업데이트하면 됩니다. 그 시크릿을 참조하는 모든 프로젝트가 자동으로 변경을 반영합니다.
온보딩이 더 쉬워집니다. “여기 .env 파일이야, 잃어버리지 마” 대신 “1Password 설정하고 셋업 스크립트 실행해”라고 말하면 됩니다. 시크릿은 적절한 접근 제어가 있는 볼트에 들어 있습니다.
감사(Auditing). 1Password와 Keychain은 둘 다 접근 로그를 유지합니다. 누가 어떤 시크릿에 언제 접근했는지 볼 수 있죠. .env 파일은 그런 걸 전혀 제공하지 않습니다.
무엇이든 동작합니다. 이 래퍼 패턴은 언어와 프레임워크에 구애받지 않습니다. ./with-1password.sh docker compose up은 ./with-1password.sh pytest만큼이나 잘 동작합니다.
시크릿 관리 도구 생태계는 굉장히 다양합니다. Doppler, Infisical, HashiCorp Vault, SOPS, dotenvx 같은 것들이 있죠. 모두 훌륭하고, 50명 이상의 엔지니어 팀을 운영한다면 아마 평가해볼 만합니다.
하지만 개인 프로젝트를 하거나 소규모 팀인 대부분의 개발자에게는 1Password나 Keychain 접근이 균형점이 좋습니다. 설정은 최소, 관리할 인프라는 없고, 아마 이미 그 도구에 비용을 지불하고 있을 가능성도 큽니다.
중요한 건 어떤 도구를 고르느냐가 아닙니다. 패턴입니다. 시크릿은 볼트에 저장하고, 실행 시점에 주입하고, 평문을 디스크에 절대 쓰지 마세요.
secure-env-demo 레포에는 두 접근 모두를 시험하는 데 필요한 모든 것이 들어 있습니다. 클론한 뒤, 여러분의 환경에 맞는 쪽을 골라 데모 앱을 실행해 보세요.
git clone https://github.com/jonmagic/secure-env-demo.git
cd secure-env-demo
# 1Password 경로:
./setup-1password.sh
./with-1password.sh ./app.sh
# Keychain 경로:
./setup-keychain.sh
./with-keychain.sh ./app.sh
작업 방식에 작은 변화이지만, 한 번 해보면 되돌아가기 어렵습니다. 이제 .env 파일을 볼 때마다 저는 Tesla에서 나눴던 그 대화를 떠올리며, 왜 몇 년 전에 이걸 하지 않았을까 생각하게 됩니다.
응원하거나 더 알아볼 수 있는 쉬운 방법 몇 가지입니다.
읽어주셔서 감사합니다 — 정말 큰 의미가 있습니다.
© Jonathan Hoyt — 손으로 LLM으로 만들었습니다