
enject: .env 시크릿을 prAIng의 눈에서 숨깁니다: 시크릿은 (프로젝트별) 로컬 암호화 저장소에 저장되며 런타임에 앱에 직접 주입되어 디스크에 평문으로 저장되지 않습니다.
.env 비밀번호를 prAIng의 눈으로부터 숨깁니다.
(참고: 이 프로젝트는 이전에 enveil이라고 불렸으며, 현재 enject로 이름이 변경되었습니다.)
Claude Code, Copilot, Cursor 및 기타 AI 코딩 도구는 프로젝트 디렉토리의 파일을 읽을 수 있으며, 이는 일반 텍스트 .env 파일이 우발적인 비밀 덤프가 될 수 있음을 의미합니다. 이는 이론적인 문제가 아닙니다. 그것은 저에게 여러 번 발생한 알려진 문제입니다(Claude Code의 settings.json 파일에서 엿보지 말라고 명시적으로 지시한 후에도 말이죠). enject는 일반 텍스트 비밀번호가 절대 디스크에 존재하지 않도록 하여 이 문제를 해결합니다. .env 파일에는 기호 참조만 포함됩니다. 실제 값은 암호화된 로컬 저장소에 저장되며, 시작 시 하위 프로세스에 직접 주입됩니다.
이 프로젝트는 Filip Hric의 솔루션/블로그 게시물에서 영감을 받았으며, 1Password를 활용하는 유사한 개념을 사용합니다. 저는 타사 서비스에 의존하지 않는 자체 포함 솔루션을 원했고, 이 솔루션이 탄생했습니다. 그리고 네, 이 프로젝트는 수동 검증과 테스트를 포함하여 거의 전적으로 Claude Code로 구축되었습니다.
이 프로젝트는 주로 AI/LLM 도구가 프로젝트의 .env 비밀번호를 실수로 읽는 알려진 문제를 완화하기 위해 설계되었습니다. 추가 이점으로는 .env 파일이 실수로 리포지토리에 커밋되는 경우 비밀 누출을 방지하고, 일반 텍스트 비밀번호 대신 참조가 포함된 .env 파일을 공유할 수 있으며, 암호화된 저장소 자체를 공유할 수 있는 옵션도 있습니다.
이 프로젝트는 AI 에이전트가 비밀번호를 얻는 것을 막는 만병통치약이 아닙니다. 예를 들어, 에이전트는 여전히 (실수로 또는 프롬프트 인젝션을 통해) 런타임에 비밀번호를 터미널 출력이나 파일로 유출하는 코드를 작성할 수 있습니다. 이 도구나 일반적인 .env 파일에 프로덕션 비밀번호를 저장하는 데 의존하지 않는 것이 좋습니다.
.env 파일은 다음과 같습니다:
DATABASE_URL=en://database_url
STRIPE_KEY=en://stripe_key
PORT=3000
기술적으로는 커밋해도 안전합니다(하지만 그렇게 하지 않는 것이 좋습니다). 더 중요한 것은: AI 도구가 실수로 (또는 어쩌면 의도적으로) 이를 엿보더라도 안전합니다.
enject run -- npm start를 실행하면 다음이 수행됩니다:
en:// 참조를 확인합니다저장소 파일은 이진 blob입니다. 마스터 비밀번호 없이는 무작위 노이즈와 구별할 수 없습니다. nonce는 쓰기할 때마다 새로 생성되므로 AES-GCM nonce 재사용이 불가능합니다. 암호문에 대한 수정(단일 비트 뒤집기라도)은 인증 실패를 초래하고 복호화가 거부됩니다.
이 릴리스는 아직 알파 단계이므로 cargo install 호출 시 최신 버전을 추가해야 합니다.
cargo install enject --version 0.2.0-alpha
Rust 1.70+가 필요합니다.
git clone https://github.com/greatscott/enject
cd enject
cargo build --release
컴파일된 바이너리는 target/release/enject에 있습니다. PATH에 있는 위치에 한 번 설치하면 모든 프로젝트에서 실행할 수 있습니다:
macOS / Linux (bash 또는 zsh)
# Option A: ~/.local/bin (no sudo required, common on Linux)
mkdir -p ~/.local/bin
cp target/release/enject ~/.local/bin/
# Option B: /usr/local/bin (requires sudo, available system-wide)
sudo cp target/release/enject /usr/local/bin/
# Option C: ~/.cargo/bin (already on PATH if you used rustup)
cp target/release/enject ~/.cargo/bin/
옵션 A를 사용했고 ~/.local/bin이 이미 PATH에 없다면, 셸 설정 파일(~/.zshrc, ~/.bashrc 또는 ~/.bash_profile)에 다음을 추가하세요:
export PATH="$HOME/.local/bin:$PATH"
그런 다음 다시 로드하세요:
source ~/.zshrc # or ~/.bashrc
작동하는지 확인하세요:
enject --version
바이너리는 전역으로 설치됩니다 — 다시 설치할 필요가 없습니다. 그러나 각 프로젝트는 자체 암호화 저장소를 가집니다:
cd your-project
enject init
이렇게 하면 현재 디렉토리에 프로젝트의 설정과 암호화된 저장소가 포함된 .enject/가 생성됩니다. .gitignore에 추가하세요 — 절대 커밋되어서는 안 됩니다.
프로젝트 루트에서 프로젝트당 한 번 실행하세요:
enject init
이렇게 하면 임의의 32바이트 솔트가 생성되고, .enject/config.toml이 작성되며, .enject/store에 빈 암호화 저장소가 생성되고, 마스터 비밀번호를 설정하라는 메시지가 표시됩니다. .gitignore에 .enject/를 추가하세요 — 저장소는 절대 커밋되어서는 안 됩니다.
enject set some_database_url
# prompts: Value for 'database_url': (hidden)
enject set some_api_key
값은 항상 대화식으로 입력됩니다. 명령줄 인수로 값을 전달할 방법이 없습니다 — 이렇게 하면 셸 기록이나 ps 출력에 비밀번호가 나타나는 것을 방지합니다.
.env에서 비밀 참조DATABASE_URL=en://some_database_url
MY_API_KEY=en://stripe_key
PORT=3000
일반 KEY=VALUE 줄은 변경되지 않고 전달됩니다. en:// 참조만 확인됩니다.
enject run -- npm start
enject run -- python manage.py runserver
enject run -- cargo run
-- 이후의 모든 것은 OS에 그대로 전달됩니다. 하위 프로세스는 전체 셸 환경(PATH, HOME 등이 있음)을 상속받으며, 여기에 .env 값이 추가됩니다.
enject list # print stored key names (never values)
enject delete <key> # remove a secret
enject import <file> # encrypt all values in a plaintext .env, rewrite it as en:// template
enject rotate # re-encrypt the store with a new master password
get 및 export 명령은 없습니다. 비밀 값을 stdout으로 출력하면 AI가 읽을 수 있는 누출 경로가 생성됩니다 — enject의 핵심은 값을 디스크와 읽을 수 있는 출력 스트림에서 멀리 유지하는 것입니다.
모든 보안 불변성에는 자동화된 테스트와 수동 검증 경로가 있습니다.
cargo test
31개의 테스트가 아래의 주장을 모두 포함합니다.
자동화: store::password::tests::test_encrypt_decrypt_roundtrip
비밀을 저장하고, 저장소를 유지하며, 디스크에서 다시 로드하고, 복호화한 다음 값이 올바르게 왕복되는지 확인합니다. 디스크의 바이트가 유효한 암호문인 경우에만 통과합니다 — 일반 텍스트는 복호화에 실패합니다.
cargo test store::password::tests::test_encrypt_decrypt_roundtrip
수동 검증:
enject init # password: test123
enject set mykey # value: my-super-secret
xxd .enject/store | head -5
strings .enject/store
xxd는 이진 데이터를 표시합니다. strings는 아무것도 반환하지 않습니다 — 추출할 ASCII 시퀀스가 없습니다. 처음 12바이트는 임의의 nonce이고, 이후의 모든 것은 16바이트 인증 태그가 추가된 AES-GCM 암호문입니다.
자동화: store::password::tests::test_nonce_changes_on_each_save
저장소를 연속으로 두 번 저장하고, 파일의 처음 12바이트를 각각 읽어 서로 다른지 확인합니다.
cargo test store::password::tests::test_nonce_changes_on_each_save
수동 검증:
xxd .enject/store | head -1 # note the first 12 bytes
enject set anotherkey # any write rotates the nonce
xxd .enject/store | head -1 # first 12 bytes are now different
자동화: store::password::tests::test_wrong_password_returns_err
하나의 비밀번호로 저장소를 만든 다음, 다른 비밀번호로 잠금 해제를 시도하고 Err가 반환되는지 확인합니다.
cargo test store::password::tests::test_wrong_password_returns_err
수동:
enject list # enter the wrong password
# output: "Wrong master password or corrupted store."
# exit code: 1
AES-GCM은 암호문에 대한 16바이트 인증 태그를 생성합니다. 단일 비트 뒤집기와 같은 모든 수정은 복호화가 진행되기 전에 검증을 실패하게 합니다. 일반 텍스트는 절대 노출되지 않습니다.
자동화: store::password::tests::test_tampered_ciphertext_returns_err
저장소 파일의 암호문 영역(12바이트 nonce 이후)에서 1바이트를 뒤집은 후, 복호화를 시도하고 Err를 반환하는지 확인합니다.
cargo test store::password::tests::test_tampered_ciphertext_returns_err
수동:
# Flip byte 20 (inside ciphertext, past the nonce)
python3 -c "
data = open('.enject/store', 'rb').read()
bad = data[:20] + bytes([data[20] ^ 0xFF]) + data[21:]
open('.enject/store', 'wb').write(bad)
"
enject list
# output: "Wrong master password or corrupted store."
en:// 참조 시 하드 오류.env의 참조가 저장소에 일치하는 키가 없으면 enject run이 즉시 0이 아닌 코드로 종료됩니다. 하위 프로세스는 실행되지 않습니다.
자동화: env_template::tests::test_unknown_ev_ref_returns_err
resolve()를 일치하는 항목이 없는 참조로 호출하고 Err를 반환하는지 확인합니다.
cargo test env_template::tests::test_unknown_ev_ref_returns_err
수동:
echo "DB=en://nonexistent_key" > .env
enject run -- env
# output: Secret 'nonexistent_key' not found in store. Add it with: enject set nonexistent_key
# exit code: 1 (the `env` subprocess never ran)
여러 프로젝트에서 사용되는 비밀을 더 쉽게 관리할 수 있도록 선택적/추가 시스템 전체 저장소를 구현합니다.
업데이트할 때마다 저장소 비밀번호를 수동으로 입력해야 하는 필요성을 줄입니다.