@illya1123/privacy-sdk
v0.1.0
Published
Encrypted env.yaml SDK — AES-256-GCM per-value encryption, HMAC integrity, multi-stage config for React/Node apps
Maintainers
Readme
privacy-sdk
🇬🇧 English version (shown on npm): README.md
Mã hoá env.yaml nhiều môi trường (dev / stag / uat / prod) bằng AES-256-GCM
từng giá trị, có HMAC-SHA-256 bảo vệ toàn tài liệu. Tên biến vẫn đọc được nên file
mã hoá vẫn diff sạch trong git; giá trị thì không ai đọc được nếu không có khoá.
Build bằng tsup (~0.7s), lint bằng Biome (~40ms), 129 test.
env.yaml ──────▶ env.enc.yaml ──────▶ commit vào git
(bạn sửa file này) (artifact mã hoá)
gitignore khoá nằm ở CI secret / máy devMục lục
- Mô hình bảo mật — đọc trước khi dùng
- Cài đặt
- Bắt đầu nhanh — không cần cấu hình
- Ba khái niệm cốt lõi
- Định dạng
env.yaml - Tích hợp vào dự án
- Tham chiếu CLI đầy đủ
- Tham chiếu API Node
- Tham chiếu API runtime (browser)
- Quản lý khoá
- Quy trình làm việc nhóm
- CI/CD
- Docker — giữ secret ngoài image
- Chạy dự án bằng container
- Chi tiết mật mã và định dạng file
- Mã lỗi
- Xử lý sự cố
- Câu hỏi thường gặp
- Phát triển SDK này
Mô hình bảo mật — đọc trước khi dùng
Đây là phần quan trọng nhất, xin nói thẳng.
Với app chạy trên trình duyệt (React/Vite/CRA), không có cách nào giữ bí mật giá trị env sau khi đã build. Bất cứ thứ gì trình duyệt cần đọc được thì người dùng cũng mở DevTools đọc được. Bất kỳ thư viện nào hứa "giải mã trong browser" đều phải nhét luôn khoá vào bundle — nghĩa là không bảo mật gì cả.
Vì thế SDK này giải mã tại thời điểm build (Node), không giải mã trong browser:
| Nơi | Có gì | Ai đọc được |
|---|---|---|
| Git repo | env.enc.yaml — chỉ ciphertext | Không ai (không có khoá) |
| CI secret / .privacy-env.key | master key | Người có quyền CI / máy dev |
| RAM lúc build | plaintext | Tiến trình build |
| Bundle đã build | giá trị đã thay thế sẵn | Bất kỳ ai mở app |
Cái được bảo vệ tuyệt đối: secret nằm trong repo, trong lịch sử git, trong bản backup, trong máy của lập trình viên đã rời công ty, trong image Docker bạn đẩy lên registry. Đó chính là nơi rò rỉ thật sự hay xảy ra.
Cái không bảo vệ được: giá trị đã nằm trong bundle JavaScript gửi tới browser.
Nếu bạn có secret thật sự không được lộ (client secret, private key, DB password), đừng
đưa xuống browser dưới bất kỳ hình thức nào — để nó ở backend. SDK này dùng loadEnv()
phía Node cho trường hợp đó, và giá trị không bao giờ rời khỏi server.
Ba lớp bảo vệ mật mã
Phải hợp lệ cả ba thì mới giải mã được:
- AES-256-GCM bảo vệ từng giá trị — sửa 1 bit là fail.
- AAD cột ciphertext vào đúng
stage.KEY— copy giá trịprodsangdevsẽ fail xác thực, không phải âm thầm rò rỉ giữa các môi trường. - HMAC-SHA-256 toàn tài liệu bắt cái GCM không thấy: xoá biến, thêm biến mới, hay sửa header.
Che độ dài giá trị
AES-GCM là chế độ dòng: ciphertext dài đúng bằng plaintext. Không đệm thì ai cầm
env.enc.yaml cũng biết chính xác mỗi secret dài bao nhiêu byte — đủ để nhận dạng loại
token hoặc thấy password nào ngắn bất thường.
Mỗi giá trị được đệm lên bội số 32 byte, nên giá trị 1 ký tự và 16 ký tự cho ra ciphertext giống hệt nhau về kích thước.
Cài đặt
npm install @illya1123/privacy-sdk
# pnpm add @illya1123/privacy-sdk
# yarn add @illya1123/privacy-sdkYêu cầu Node ≥ 18.17. Chỉ có một dependency runtime (yaml).
Bắt đầu nhanh — không cần cấu hình
npm install @illya1123/privacy-sdk
# thả env.yaml của bạn vào thư mục gốc dự án
npm start # hoặc bất kỳ lệnh nào chạy qua privacy-env / pluginXong. Không có bước nào khác.
Nó tự làm gì, và làm lúc nào
| Thời điểm | Việc |
|---|---|
| npm install | Ghi rule vào .gitignore và .dockerignore, cài pre-commit guard |
| Lần đầu chạy app | Sinh khoá, in khoá ra màn hình, mã hoá env.yaml → env.enc.yaml |
| Mỗi lần sửa env.yaml rồi chạy lại | Tự đồng bộ lại env.enc.yaml |
| Lần chạy sau đó | Không làm gì (idempotent) |
Khoá chỉ được sinh ở nơi bạn nhìn thấy nó. npm install cố tình không sinh khoá:
npm ẩn output của lifecycle script, nên một khoá tạo ở đó là khoá không ai kịp lưu. Nó
chỉ được tạo khi bạn chạy app hoặc npx privacy-env protect — nơi nó được in ra.
Lưu khoá vào password manager và vào CI secret PRIVACY_ENV_KEY. Khoá cũng nằm ở
.privacy-env.key (đã gitignore, quyền owner-only).
Ranh giới an toàn của tự động
Tự động không bao giờ được phép gây thiệt hại, nên nó không:
- xoá
env.yamlcủa bạn (chỉ gitignore + guard chặn commit) - ghi đè
env.enc.yamlnếu khoá đang có không khớp với file đó — nếu không nó sẽ khoá cả nhóm ra ngoài - sinh khoá mới khi
env.enc.yamlđã tồn tại - chạy trong CI (biến
$CI), nơi chỉ có file mã hoá và khoá từ secret - đè lên pre-commit hook do người khác viết (chỉ cảnh báo)
Tắt hoàn toàn: privacyEnv({ autoProtect: false }) trong plugin, hoặc --no-protect
với privacy-env run.
Nếu bạn muốn tự tay từng bước
npx privacy-env init # tạo env.yaml mẫu + khoá + .gitignore + hook
npx privacy-env protect # bảo vệ ngay, in khoá nếu vừa sinh
npx privacy-env doctor # kiểm tra toàn bộ thế trận bảo mậtBa khái niệm cốt lõi
1. env.yaml — file nguồn
File bạn sửa hằng ngày. Local only, đã gitignore. Chứa giá trị thật ở dạng đọc được.
Mất nó không sao — bạn luôn khôi phục được bằng privacy-env decrypt.
2. env.enc.yaml — artifact
File commit vào git. Tên biến đọc được, mọi giá trị là ciphertext. Đây là thứ đồng
nghiệp và CI dùng. Tự động đồng bộ lại mỗi khi env.yaml mới hơn.
Đừng sửa tay file này — mọi lệnh đều từ chối nó, và pre-commit guard chặn commit.
3. Master key — 256 bit
Thứ duy nhất giải mã được. Không bao giờ vào git. Sống ở:
.privacy-env.keytrên máy dev (gitignore, quyền owner-only)- CI secret
PRIVACY_ENV_KEY - Docker secret mount
Mất khoá = mất toàn bộ giá trị. Lưu vào password manager ngay khi nó được in ra.
Stage (môi trường)
Mỗi khối cấp một trong env.yaml là một stage. Thứ tự SDK chọn stage:
--stage/options.stage(tường minh)$PRIVACY_ENV_STAGE$APP_ENV$REACT_APP_ENV$APP_STAGE$NODE_ENV- Stage đầu tiên trong file
Có bảng alias nên NODE_ENV=production tìm ra khối prod:
| Stage chuẩn | Alias được chấp nhận |
|---|---|
| dev | dev, develop, development, local |
| stag | stag, staging, stage, stg |
| uat | uat, qa, preprod |
| prod | prod, production, prd, live |
Định dạng env.yaml
Chính là định dạng bạn đang dùng — không cần đổi gì:
# Comment được giữ nguyên qua encrypt/decrypt
dev:
# Kể cả comment lồng trong stage
REACT_APP_DEFAULT_LANGUAGE: vi
REACT_APP_PORTAL_API_ENDPOINT: https://bvb-dev-api-int.atomsolution.vn/v1/portal
REACT_APP_IS_SHOW_SETTING_ECR: true # boolean giữ nguyên kiểu boolean
REACT_APP_TIMEOUT: 30000 # number giữ nguyên kiểu number
REACT_APP_OPTIONAL: null # null giữ nguyên null
REACT_APP_FEATURE_ENABLE: ["e-invoice", "digistore-id"] # mảng giữ nguyên mảng
stag:
...
uat:
...
prod:
...Kiểu dữ liệu
Kiểu YAML được bảo toàn tuyệt đối qua encrypt → decrypt:
| YAML | loaded.values (có kiểu) | loaded.env (chuỗi cho process.env) |
|---|---|---|
| vi | 'vi' | 'vi' |
| true | true | 'true' |
| 30000 | 30000 | '30000' |
| null | null | '' |
| ["a", "b"] | ['a', 'b'] | '["a","b"]' |
Chuỗi trông giống kiểu khác cũng an toàn: "true" (có nháy) vẫn quay về chuỗi 'true',
không biến thành boolean.
Tham chiếu biến
dev:
BASE: https://api.example.com
REACT_APP_PORTAL: ${BASE}/v1/portal # → https://api.example.com/v1/portal
REACT_APP_CDN: ${CDN_HOST:-https://cdn.default.com} # giá trị mặc địnhTìm trong cùng stage trước, rồi tới process.env. Tối đa 8 tầng lồng nhau, phát hiện
vòng lặp. Tắt bằng expand: false.
Những gì bị từ chối
- Anchor / alias YAML (
&x/*x) →E_UNSUPPORTED. Chúng phá liên kết giá-trị–đường-dẫn mà lớp AAD dựa vào. Hãy viết giá trị ra tường minh. - Key
_privacy_envở cấp gốc — dành riêng cho header mã hoá. - Root không phải mapping →
E_INVALID_DOCUMENT.
File sau khi mã hoá
dev:
REACT_APP_DEFAULT_LANGUAGE: 'ENC[2:AGCM:sP:FgXz-PKRu0XTd2ML:mwijHjXliYcCG...:Vtae3T...]'
REACT_APP_IS_SHOW_SETTING_ECR: 'ENC[2:AGCM:bP:...]'
REACT_APP_FEATURE_ENABLE: ['ENC[2:AGCM:sD:...]', 'ENC[2:AGCM:sD:...]']
# Encryption header written by privacy-sdk. Do not edit by hand.
# `mac` authenticates every value AND this header: any manual change is rejected.
_privacy_env:
version: 2
cipher: AES-256-GCM
kdf:
name: none
salt: A+sWzcf4kRj11yy6OHuA1fJS705ODQH67fJbGlDV43Y=
key_id: 1438f596dec55e0e
encrypted_at: 2026-08-07T06:22:52.379Z
created_by: [email protected]
mac: 1a99407908bc3970e2361c8092869b156758ef124a6df1c8bb09f7ad188cfe44decrypt trả lại đúng từng byte file gốc: kiểu dữ liệu, kiểu nháy, thứ tự khoá và
comment đều được khôi phục.
Tích hợp vào dự án
1. Cách phổ biến nhất — không đụng vào bundler
Hoạt động với CRA, Vite, Next, Remix, bất kỳ thứ gì đọc process.env:
// package.json của app
{
"scripts": {
"start": "privacy-env run -s dev -- react-scripts start",
"build:stag": "privacy-env run -s stag -- react-scripts build",
"build:uat": "privacy-env run -s uat -- react-scripts build",
"build:prod": "privacy-env run -s prod -- react-scripts build"
}
}run giải mã, nạp biến vào process.env của tiến trình con rồi chạy lệnh. CRA/Vite tự
nhặt các biến REACT_APP_* như bình thường. Không cần eject, không đổi code.
2. Vite plugin
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { privacyEnv } from '@illya1123/privacy-sdk/vite'
export default defineConfig({
plugins: [
react(),
privacyEnv({
prefix: 'REACT_APP_', // chỉ đưa các biến này vào bundle
requireEncrypted: true, // chặn build nếu ai đó bỏ env.yaml thô vào repo
required: ['REACT_APP_PORTAL_API_ENDPOINT'],
// stage mặc định lấy từ Vite `mode`; ép cứng nếu muốn:
// stage: 'prod',
}),
],
})Plugin thay thế process.env.X và import.meta.env.X lúc build, đồng thời cung cấp
virtual module:
import env, { stage, values } from 'virtual:privacy-env'Dev server tự restart khi env.enc.yaml đổi.
Tuỳ chọn plugin: mọi tuỳ chọn của loadEnv (xem bên dưới), cộng thêm:
| Tuỳ chọn | Mặc định | Việc |
|---|---|---|
| autoProtect | true | Mã hoá/đồng bộ + ghi ignore rule + cài hook trước khi load |
| define.processEnv | true | Thay thế process.env.X |
| define.importMetaEnv | true | Thay thế import.meta.env.X |
| injectProcessEnv | false | Nạp vào process.env của tiến trình dev server |
| watch | true | Restart dev server khi env file đổi |
| logging | true | In stage + số biến lúc khởi động |
3. Webpack / CRA + craco
// craco.config.js
const { PrivacyEnvWebpackPlugin } = require('@illya1123/privacy-sdk/webpack')
module.exports = {
webpack: {
plugins: {
add: [
new PrivacyEnvWebpackPlugin({
prefix: 'REACT_APP_',
requireEncrypted: true,
required: ['REACT_APP_PORTAL_API_ENDPOINT'],
}),
],
},
},
}Hoặc lấy thẳng map cho DefinePlugin tự cấu hình:
const { privacyEnvDefinitions } = require('@illya1123/privacy-sdk/webpack')
new webpack.DefinePlugin(privacyEnvDefinitions({ stage: 'prod' }))4. Node / Next.js / script backend
import { loadEnv, injectEnv } from 'privacy-sdk'
const config = loadEnv({
stage: 'prod',
required: ['REACT_APP_PORTAL_API_ENDPOINT'],
})
config.values.REACT_APP_IS_SHOW_SETTING_ECR // true (boolean thật)
config.values.REACT_APP_FEATURE_ENABLE // ['e-invoice'] (mảng thật)
config.env.REACT_APP_FEATURE_ENABLE // '["e-invoice"]' (chuỗi cho process.env)
injectEnv(config) // đổ vào process.envloadEnv là đồng bộ — dùng được ngay trong vite.config.ts / next.config.js. Cần
Promise thì dùng loadEnvAsync.
5. Đọc biến trong code React (có type, fail nhanh)
Sinh type từ file env:
npx privacy-env types -o src/env.generated.d.ts// src/env.ts
import { createEnvClient } from '@illya1123/privacy-sdk/runtime'
import type { PrivacyEnvVars } from './env.generated'
export const env = createEnvClient<PrivacyEnvVars>({
source: import.meta.env, // Vite. CRA/Next thì dùng process.env
prefix: 'REACT_APP_',
required: ['REACT_APP_PORTAL_API_ENDPOINT'], // ném lỗi ngay lúc khởi động
})
export const config = {
portal: env.url('REACT_APP_PORTAL_API_ENDPOINT'),
language: env.string('REACT_APP_DEFAULT_LANGUAGE', 'vi'),
showEcr: env.bool('REACT_APP_IS_SHOW_SETTING_ECR', false),
features: env.list('REACT_APP_FEATURE_ENABLE', []),
} as const@illya1123/privacy-sdk/runtime không chứa crypto và không import module Node nào — an toàn
cho bundle browser. File mẫu đầy đủ: example/env.ts.
Tham chiếu CLI đầy đủ
privacy-env <command> [options]Tuỳ chọn dùng chung
| Cờ | Viết tắt | Ý nghĩa |
|---|---|---|
| --file <path> | -f | Env file (mặc định: tìm ngược lên từ thư mục hiện tại) |
| --stage <name> | -s | Stage cần đọc |
| --key <key> | -k | Master key (base64:… / hex:… / chuỗi trần) |
| --key-file <path> | | Đọc khoá từ file |
| --passphrase <text> | -p | Passphrase (nên dùng --prompt để không vào lịch sử shell) |
| --prompt | | Hỏi passphrase tương tác (ẩn ký tự) |
| --cwd <dir> | | Thư mục làm việc |
| --output <path> | -o | Đích ghi, hoặc - để ra stdout |
| --force | | Ghi đè đích đã tồn tại |
| --help | -h | Trợ giúp |
| --version | -V | Phiên bản |
Mã thoát: 0 thành công, 1 lỗi. Riêng run trả về đúng mã thoát của lệnh con.
CLI sẽ cảnh báo nếu bạn truyền
--keyhoặc--passphrasetrên dòng lệnh: giá trị đó hiện trongps, trong Task Manager và trong lịch sử shell.
init
Dựng dự án từ đầu.
privacy-env init [--force] [--key-file <path>]Tạo env.yaml mẫu (4 stage), sinh .privacy-env.key, thêm rule vào .gitignore, cài
pre-commit guard. --force để ghi đè file đã có.
protect
Bảo vệ dự án ngay lập tức. Đây là lệnh mà auto-protect gọi phía sau.
privacy-env protect [--force]Làm bốn việc, mỗi việc chỉ làm nếu cần:
- Ghi rule thiếu vào
.gitignore - Ghi rule thiếu vào
.dockerignore(nếu có Dockerfile / compose file) - Cài pre-commit guard
- Mã hoá
env.yaml→env.enc.yaml, hoặc đồng bộ lại nếuenv.yamlmới hơn
Nếu vừa sinh khoá mới, in khoá ra stdout — lưu ngay.
keygen
privacy-env keygen [-o <path>] [--print] [--force]Sinh master key 256-bit ngẫu nhiên.
| Cờ | Việc |
|---|---|
| -o <path> | Ghi vào file này (mặc định .privacy-env.key) |
| --print | Chỉ in ra stdout, không ghi file |
| --force | Ghi đè file đã có |
privacy-env keygen --print # để copy vào CI secret
privacy-env keygen -o deploy.key # khoá riêng cho môi trường deployencrypt
privacy-env encrypt [file] [-o <path>] [--force] [--remove-source]env.yaml → env.enc.yaml. Mặc định tìm file plaintext (không bao giờ nhầm sang
chính output của nó).
| Cờ | Việc |
|---|---|
| -o <path> | Đích khác (mặc định chèn .enc vào tên) |
| --force | Ghi đè đích đã tồn tại |
| --remove-source | Ghi đè file nguồn bằng dữ liệu ngẫu nhiên rồi xoá |
privacy-env encrypt
privacy-env encrypt --remove-source
privacy-env encrypt config/env.yaml -o config/env.enc.yamldecrypt
privacy-env decrypt [file] [-o <path>] [--force]env.enc.yaml → env.yaml. Trả lại đúng từng byte file gốc. Ghi với quyền owner-only.
privacy-env decrypt # → env.yaml
privacy-env decrypt -o - # ra stdout, không chạm đĩaedit
privacy-env edit [file]Giải mã vào file tạm quyền 0600 trong thư mục temp, mở $VISUAL/$EDITOR, mã hoá lại
khi lưu, rồi ghi đè file tạm bằng dữ liệu ngẫu nhiên trước khi xoá.
Đây là cách sửa giá trị mà không bao giờ tạo plaintext trong repo.
EDITOR="code -w" privacy-env editEditor phải chặn tới khi đóng file (code -w, vim, nano). Nếu editor thoát với mã
lỗi, SDK không mã hoá lại gì — file cũ giữ nguyên.
view
privacy-env view [file] [-s <stage>] [--format <fmt>] [--reveal] [--all]In một stage. Giá trị trông giống secret bị che (khớp theo segment của tên, nên
API_KEY bị che còn KEYCLOAK_URL thì không).
| Cờ | Việc |
|---|---|
| -s <stage> | Stage cần xem |
| --format <fmt> | table (mặc định), dotenv, json, shell, yaml |
| --reveal | Không che giá trị nào |
| --all | Dump toàn bộ stage dạng JSON |
privacy-env view -s prod
privacy-env view -s prod --reveal
privacy-env view --all | jq '.prod.REACT_APP_SDK_MODE'run
privacy-env run [-s <stage>] [--keep-existing] [--no-protect] -- <command...>Chạy lệnh với stage đã nạp vào môi trường của tiến trình con. Trả về đúng mã thoát của lệnh con.
| Cờ | Việc |
|---|---|
| -s <stage> | Stage cần nạp |
| --keep-existing | Biến môi trường sẵn có thắng (mặc định file thắng) |
| --no-protect | Bỏ qua auto-protect |
privacy-env run -s dev -- npm start
privacy-env run -s prod -- npm run build
privacy-env run -s uat -- node scripts/migrate.jsTrên Windows, lệnh được resolve qua PATHEXT và chỉ đi qua cmd.exe khi thật sự cần
(.cmd/.bat), có escape tham số — nên tham số chứa dấu cách hay dấu nháy không bị hỏng.
export
privacy-env export [-s <stage>] [--format <fmt>] [-o <path>] [--force]Xuất một stage ra .env / JSON / shell / YAML.
privacy-env export -s prod # ra stdout
privacy-env export -s prod -o .env.production # ra file (quyền owner-only)
privacy-env export -s dev --format json | jq
eval "$(privacy-env export -s dev --format shell)"Từ chối ghi plaintext vào repo chưa gitignore — thêm vào .gitignore trước, hoặc
--force nếu bạn chắc chắn.
types
privacy-env types [-o <path>] [--interface <name>]Sinh file .d.ts mô tả mọi biến. Mặc định ghi privacy-env.d.ts; -o - để ra stdout.
Biến chỉ có ở một số stage được đánh dấu optional (?), nên TypeScript bắt bạn xử lý
trường hợp thiếu.
privacy-env types -o src/env.generated.d.ts
privacy-env types --interface AppEnvverify
privacy-env verify [--strict] [--require-encrypted]Kiểm tra toàn vẹn (MAC) và báo lệch biến giữa các stage.
file env.enc.yaml
encrypted ✔ yes
key id 1438f596dec55e0e
stages dev, stag, uat, prod
variables 25
integrity ✔ MAC verified
! 1 variable(s) are not defined in every stage:
REACT_APP_FEATURE_ENABLE missing in prodĐây chính là loại lỗi "chạy tốt ở dev, chết ở prod". --strict để CI fail khi lệch.
rotate
privacy-env rotate [--generate | --new-key <key> | --new-key-file <path> | --new-passphrase <text>]Mã hoá lại file bằng khoá mới. Mọi IV, salt và khoá dẫn xuất đều được sinh lại — khoá cũ hoàn toàn vô dụng với file mới.
privacy-env rotate --generate # sinh khoá mới, in ra
privacy-env rotate --new-key 'base64:…'
privacy-env rotate --new-passphrase 'chuỗi dài khó đoán'
privacy-env rotate # giữ khoá cũ, chỉ làm mới IV/saltNhớ cập nhật CI secret ngay sau khi rotate.
doctor
privacy-env doctor [--strict]Audit toàn bộ thế trận, không chỉ file. Thoát 1 nếu có lỗi; --strict thì cảnh báo
cũng tính là lỗi.
Nó kiểm những thứ mà chỉ mã hoá thôi không cứu được:
| Kiểm tra | Vì sao quan trọng |
|---|---|
| File có đang bị git theo dõi không | .gitignore không cứu file đã git add -f hoặc track từ trước |
| Secret có trong lịch sử git không | Xoá file không xoá được nội dung — phải rotate khoá |
| .gitignore phủ đủ chưa | So đúng danh sách mà guard chặn, không thể lệch nhau |
| .dockerignore phủ đủ chưa | .gitignore hoàn toàn vô nghĩa với docker build |
| Dockerfile có làm lộ khoá không | ARG / ENV / COPY / decrypt lúc build |
| Quyền file khoá | Trên Windows chmod vô tác dụng — kiểm ACL thật |
| Còn plaintext trên đĩa không | Và nó đã được gitignore chưa |
| Giá trị nào bị sửa tay thành plaintext | Chỉ đúng tên biến bị lộ |
| Độ mạnh KDF, phiên bản định dạng | Từ chối tham số scrypt bị hạ cấp |
| Lệch biến giữa các stage | Bug "chạy tốt ở dev" |
guard
privacy-env guardKiểm tra những gì đang được stage cho commit tới. Đây là lệnh mà pre-commit hook gọi; bạn hiếm khi chạy tay.
Đọc blob đã stage, không phải file trên đĩa — nên git add -p hay git add -f đều
không lách được. Không cần khoá, không cần mạng.
Chặn:
- file env plaintext (
env.yaml,env.yml) - key material (
.privacy-env.key,*.key,*.pem,*.p12,*.pfx) - file dotenv (
.env,.env.*) — trừ.env.example/.env.sample/.env.template - file bất kỳ có chứa master key hoặc
PRIVACY_ENV_KEY=… - file mã hoá bị sửa tay để lộ giá trị
install-hooks
privacy-env install-hooks [--force]Cài pre-commit guard vào repo. Idempotent. Không đè lên hook do người khác viết trừ
khi có --force.
Nếu dùng husky, lệnh sẽ báo và bạn thêm dòng này vào .husky/pre-commit:
npx --no-install privacy-env guardBỏ qua guard khi thật sự cần: git commit --no-verify.
scan
privacy-env scan --image <name>Kiểm chứng image Docker đã build. Xem chi tiết ở phần Docker.
Tham chiếu API Node
import { loadEnv /* … */ } from 'privacy-sdk'Đọc cấu hình
loadEnv(options?): LoadedEnv
Đọc và giải mã một stage. Đồng bộ.
interface LoadEnvOptions {
// Nguồn
cwd?: string // thư mục bắt đầu tìm (mặc định process.cwd())
file?: string // đường dẫn tường minh
stage?: string // stage cần đọc
requireEncrypted?: boolean // từ chối file plaintext
prefer?: 'any' | 'plain' // ưu tiên khi tìm file
// Khoá
key?: string | Buffer
keyFile?: string
passphrase?: string
env?: NodeJS.ProcessEnv // môi trường để đọc $PRIVACY_ENV_* (mặc định process.env)
scrypt?: ScryptParams // tham số KDF khi mã hoá bằng passphrase
quiet?: boolean // tắt cảnh báo quyền file
// Biến đổi
required?: string[] // thiếu là ném E_VAR_REQUIRED
prefix?: string | string[] // chỉ giữ biến có prefix này
expand?: boolean // giải ${VAR} (mặc định true)
cache?: boolean // dùng lại kết quả trong tiến trình (mặc định true)
}
interface LoadedEnv {
stage: string // stage đã chọn
file: string // file đã đọc
encrypted: boolean // file có mã hoá không
stages: string[] // mọi stage trong file
values: Record<string, EnvValue> // giữ nguyên kiểu YAML
env: Record<string, string> // đã stringify cho process.env
keyId: string | null // fingerprint khoá đã giải mã
}loadEnvAsync(options?): Promise<LoadedEnv>
Bản trả Promise của loadEnv.
readEnvFile(options?): EnvFileContents
Trả về mọi stage đã giải mã, kèm cả Document YAML (giữ comment).
injectEnv(loaded, options?): string[]
Đổ vào process.env. Trả về danh sách biến đã set.
injectEnv(config) // biến sẵn có thắng
injectEnv(config, { override: true }) // file thắng
injectEnv(config, { target: myEnvObject }) // đích khácTiện ích khác
locateEnvFile(options?): string // tìm file, không đọc
resolveStage(available, explicit?, env?): string
clearEnvCache(): void // xoá cache trong tiến trình
toEnvString(value): string // stringify một giá trị như loader làmThao tác file
encryptFile(options?): EncryptFileResult
decryptFile(options?): DecryptFileResult
rotateKey(options?): RotateKeyResult
verifyFile(options?): VerifyReport
encryptDocumentText(yaml, options?): { text, meta }
encryptedPathFor(path): string // env.yaml → env.enc.yaml
plainPathFor(path): string // env.enc.yaml → env.yamlBảo mật
autoProtect(options?): ProtectResult // gitignore + dockerignore + hook + encrypt
autoProtectOnce(options?, log?): ProtectResult | null // một lần mỗi tiến trình
auditSetup(options?): AuditReport // đằng sau `doctor`
guardStaged(cwd?): GuardReport // đằng sau `guard`
installPreCommitHook(cwd, force?): HookInstallResult
ensureGitignore(cwd): boolean
ensureDockerignore(cwd): boolean
analyseDockerfile(path, dockerignoreCovers): DockerFinding[]
scanDockerImage(image): Promise<DockerScanReport>
hasDockerContext(cwd): boolean
findDockerfiles(cwd): string[]ProtectOptions: gitignore, dockerignore, installHook, encrypt, generateKey,
force — tất cả đều boolean, mặc định true (riêng force mặc định false).
Xuất dữ liệu
formatEnv(loaded, format, options?): string // 'dotenv' | 'json' | 'shell' | 'yaml'
toDefineMap(loaded, options?): Record<string, string> // cho bundler define
generateTypes(document, options?): string // sinh .d.ts
maskValue(value): string
looksSecret(key): booleanKhoá
generateMasterKey(): Buffer
encodeMasterKey(key): string // → 'base64:…'
parseMasterKey(text): Buffer // nhận base64:/hex:/trần
keyFingerprint(key): string // 16 hex, không đảo ngược được
resolveKeyForEncrypt(options?): ResolvedKey
resolveKeyForDecrypt(meta, options?): ResolvedKey
clearKeyCache(): voidLỗi
import { PrivacyEnvError, isPrivacyEnvError } from 'privacy-sdk'
try {
loadEnv({ stage: 'prod' })
} catch (error) {
if (isPrivacyEnvError(error)) {
console.error(error.code) // mã ổn định, xem bảng bên dưới
console.error(error.hint) // gợi ý cách sửa
}
}Thông báo lỗi không bao giờ chứa key material, passphrase hay giá trị đã giải mã.
Tham chiếu API runtime (browser)
import { createEnvClient } from '@illya1123/privacy-sdk/runtime'Module này không chứa crypto và không import module Node nào.
createEnvClient<TSchema>(options?): EnvClient<TSchema>
interface EnvClientOptions {
source?: EnvSource | EnvSource[] // import.meta.env, process.env, hoặc object bất kỳ
prefix?: string | string[] // chỉ lộ biến có prefix này
required?: string[] // ném MissingEnvError ngay lúc tạo client
emptyIsMissing?: boolean // chuỗi rỗng coi như thiếu (mặc định true)
}Nhiều nguồn thì nguồn sau thắng:
createEnvClient({ source: [import.meta.env, window.__RUNTIME_CONFIG__] })Phương thức
| Phương thức | Trả về | Khi thiếu |
|---|---|---|
| raw | Readonly<Record<string, string>> | — |
| has(key) | boolean | false |
| get(key) | string | ném MissingEnvError |
| optional(key) | string \| undefined | undefined |
| string(key, fallback?) | string | fallback, hoặc ném |
| number(key, fallback?) | number | fallback, hoặc ném |
| bool(key, fallback?) | boolean | fallback, hoặc ném |
| list(key, fallback?) | string[] | fallback, hoặc ném |
| json<T>(key, fallback?) | T | fallback, hoặc ném |
| url(key, fallback?) | string | fallback, hoặc ném |
| assert(keys) | void | ném MissingEnvError |
Chi tiết chuyển kiểu:
bool:true/1/yes/y/on→true;false/0/no/n/off/''→false; khác → némInvalidEnvErrorlist: nhận cả JSON array (["a","b"]) lẫn danh sách phẩy (a, b, c)url: kiểm tra parse được, trả về đã bỏ dấu/cuốinumber: không phải số hữu hạn → némInvalidEnvError
Quản lý khoá
Thứ tự tìm khoá
Dừng ở cái đầu tiên tìm thấy:
--key/options.key$PRIVACY_ENV_KEY← dùng cái này trong CI$PRIVACY_ENV_KEY_FILEhoặc--key-file.privacy-env.key(tìm ngược lên từ thư mục hiện tại)~/.privacy-env/default.key$PRIVACY_ENV_PASSPHRASE- Hỏi tương tác (chỉ khi chạy trong terminal)
Khoá thô (mục 1–5) không qua KDF nên nhanh. Passphrase đi qua scrypt và được nhớ trong tiến trình để build lặp lại không phải trả giá lần nữa.
Biến môi trường
| Biến | Việc |
|---|---|
| PRIVACY_ENV_KEY | Master key (khuyến nghị cho CI) |
| PRIVACY_ENV_KEY_FILE | Đường dẫn tới file khoá (khuyến nghị cho Docker) |
| PRIVACY_ENV_PASSPHRASE | Passphrase |
| PRIVACY_ENV_FILE | Đường dẫn env file |
| PRIVACY_ENV_STAGE | Stage cần load |
| PRIVACY_ENV_DEBUG | In stack trace khi lỗi |
| CI | Khi có, auto-protect tự tắt |
| NO_COLOR / FORCE_COLOR | Điều khiển màu output |
Chế độ passphrase
Thay vì khoá ngẫu nhiên, bạn có thể dùng passphrase:
PRIVACY_ENV_PASSPHRASE='chuỗi dài khó đoán' privacy-env encrypt
privacy-env encrypt --prompt # hỏi tương tác, không vào lịch sử shellPassphrase đi qua scrypt N=2¹⁷, r=8, p=1 (~128 MiB và ~1s mỗi lần thử) — kẻ tấn công
brute-force phải trả giá đó cho từng lần đoán.
Đánh đổi: passphrase yếu vẫn là passphrase yếu. Khoá ngẫu nhiên 256-bit an toàn hơn hẳn; chỉ dùng passphrase khi bạn cần con người nhớ được nó.
Đổi khoá
privacy-env rotate --generateLàm ngay khi: nhân sự rời nhóm, khoá từng bị commit, khoá từng lọt vào Docker image, hoặc định kỳ theo chính sách công ty.
doctor sẽ nhắc bạn rotate nếu phát hiện khoá từng nằm trong lịch sử git.
Quy trình làm việc nhóm
Người khởi tạo dự án
npm install @illya1123/privacy-sdk
# thả env.yaml vào
npm start # tự mã hoá, in khoá ra
# → lưu khoá vào password manager + CI secret
git add env.enc.yaml .gitignore .dockerignore
git commit -m "chore: encrypted env config"Đồng nghiệp mới vào
git clone …
npm install # có env.enc.yaml, chưa có khoá
export PRIVACY_ENV_KEY='base64:…' # nhận qua kênh an toàn, KHÔNG qua chat/email
npm startKhông có env.yaml thì không có gì để mã hoá — SDK chỉ giải mã và chạy.
Muốn sửa giá trị? Hai cách:
privacy-env edit # không bao giờ tạo plaintext trong repo
# hoặc
privacy-env decrypt # tạo env.yaml để sửa thoải mái
# sửa xong chạy app là tự đồng bộ lạiThêm một biến mới
privacy-env edit # thêm biến vào TẤT CẢ các stage
privacy-env verify # xác nhận không lệch stage
git add env.enc.yaml && git commitHằng ngày
privacy-env view -s dev # xem đang có gì
privacy-env doctor # trước khi pushCI/CD
GitHub Actions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Kiểm tra thế trận bảo mật
run: npx privacy-env doctor --strict
env:
PRIVACY_ENV_KEY: ${{ secrets.PRIVACY_ENV_KEY }}
- name: Build
run: npx privacy-env run -s prod -- npm run build
env:
PRIVACY_ENV_KEY: ${{ secrets.PRIVACY_ENV_KEY }}GitLab CI
build:
image: node:22
variables:
PRIVACY_ENV_KEY: $PRIVACY_ENV_KEY # đặt ở Settings → CI/CD → Variables (Masked)
script:
- npm ci
- npx privacy-env doctor --strict
- npx privacy-env run -s prod -- npm run buildLưu ý cho mọi hệ CI
- Đặt secret là masked/protected để không lọt vào log.
- Đừng
echo $PRIVACY_ENV_KEYđể debug. - Auto-protect tự tắt khi thấy biến
$CI— CI chỉ đọc, không ghi. - Dùng
doctor --strictthay choverify --strictđể được cả kiểm tra git và Docker.
Docker — giữ secret ngoài image
Đây là nơi secret hay chết nhất.
Điểm quan trọng nhất: xoá file ở layer sau không xoá được nó. RUN rm secret chỉ
giấu file khỏi filesystem cuối; nó vẫn nằm nguyên trong layer trước và ai có image đều
lấy ra được bằng docker save.
Năm sai lầm và ai bắt
| Sai lầm | Vì sao chết | Ai bắt |
|---|---|---|
| COPY . . không có .dockerignore | env.yaml và khoá vào layer vĩnh viễn | protect tự ghi, doctor kiểm |
| ARG PRIVACY_ENV_KEY | Mọi --build-arg nằm trong docker history | doctor |
| ENV PRIVACY_ENV_KEY=… | docker inspect in ra cho bất kỳ ai | doctor |
| RUN privacy-env decrypt | Plaintext bị commit vào layer | doctor |
| docker run -e PRIVACY_ENV_KEY=… | Nằm trong container config | Dùng file mount |
SPA (React) — khoá đi qua BuildKit secret
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . . # .dockerignore đã chặn env.yaml + khoá
# Secret chỉ tồn tại trong đúng lệnh RUN này. Không thành layer,
# không hiện trong `docker history`.
RUN --mount=type=secret,id=privacy_env_key \
PRIVACY_ENV_KEY_FILE=/run/secrets/privacy_env_key \
npx privacy-env run -s prod -- npm run build
# Stage mới. Chỉ asset đã build đi theo, layer của builder không lọt vào image cuối.
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/htmldocker build --secret id=privacy_env_key,src=.privacy-env.key -t my-app .
# hoặc từ biến môi trường:
docker build --secret id=privacy_env_key,env=PRIVACY_ENV_KEY -t my-app .Node server — mạnh hơn: giải mã lúc chạy
Image chỉ chứa ciphertext, nên image lộ ra ngoài cũng không lộ gì:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . . # chỉ env.enc.yaml đi vào
CMD ["npx", "privacy-env", "run", "-s", "prod", "--", "node", "server.js"]docker run --rm \
-v /path/to/key:/run/secrets/privacy_env_key:ro \
-e PRIVACY_ENV_KEY_FILE=/run/secrets/privacy_env_key \
my-serverDùng PRIVACY_ENV_KEY_FILE (đường dẫn) chứ không dùng -e PRIVACY_ENV_KEY=… (giá
trị): biến môi trường nằm trong container config và docker inspect in ra hết.
docker compose
services:
api:
build:
context: .
dockerfile: Dockerfile.node-server
secrets: [privacy_env_key]
environment:
PRIVACY_ENV_KEY_FILE: /run/secrets/privacy_env_key
PRIVACY_ENV_STAGE: prod
secrets: [privacy_env_key]
secrets:
privacy_env_key:
file: ./.privacy-env.key
# production/swarm: external: trueFile mẫu đầy đủ: example/Dockerfile, example/Dockerfile.node-server,
example/docker-compose.yml.
Kiểm chứng image sau khi build
npx privacy-env scan --image my-app:latestKiểm ba thứ:
- image config (
docker inspect) — ENV/LABEL có chứa khoá không - build history (
docker history --no-trunc) — có build step nào chứa khoá không - filesystem — export toàn bộ image và quét từng file: tên file nhạy cảm
(
.privacy-env.key,env.yaml,.env*) và nội dung chứa khoá 256-bit
Thoát 1 nếu tìm thấy. Không có Docker trên máy thì bỏ qua êm, không làm hỏng CI.
Giới hạn, nói thẳng: scan đọc filesystem đã dẹp phẳng, nên file được thêm rồi
xoá ở layer sau sẽ không hiện ra ở đây. Đó chính là lý do phần phân tích Dockerfile của
doctor tồn tại — nó bắt trường hợp đó trước cả khi bạn build.
Chạy dự án bằng container
Phần trên nói về giữ secret ngoài image. Phần này nói về chạy dự án thật của bạn —
dự án đang dùng env.yaml với 4 stage — bên trong container.
Bước 0: quyết định mô hình
Câu hỏi duy nhất cần trả lời trước: giá trị được nạp lúc build hay lúc container khởi động? Mọi thứ khác suy ra từ đây.
| | Nạp lúc build | Nạp lúc chạy | |---|---|---| | Số image | Một image / stage | Một image cho mọi stage | | Đổi stage | Phải build lại | Đổi biến môi trường, restart | | Image cần | nginx thuần (nhẹ) | Node + privacy-sdk | | Image chứa gì | Giá trị đã thay sẵn | Chỉ ciphertext | | Image lộ ra ngoài | Lộ giá trị của stage đó | Không lộ gì | | Hợp với | SPA đơn giản, deploy theo tag | Nhiều môi trường, promote cùng artifact |
Với Node server thì luôn chọn "nạp lúc chạy" — không có lý do gì để nhúng.
Với SPA thì cả hai đều hợp lệ. Nhưng lưu ý: giá trị SPA cuối cùng vẫn tới browser trong cả hai trường hợp, nên "nạp lúc chạy" không làm nó bí mật hơn với người dùng cuối — cái nó cho bạn là một artifact duy nhất đi qua dev → stag → uat → prod, đúng thứ bạn muốn khi có 4 môi trường.
Mô hình A — SPA, một image mỗi stage
Đơn giản nhất. Runtime là nginx thuần, không cần Node.
File: example/Dockerfile
# Build cho từng môi trường
docker build --secret id=privacy_env_key,src=.privacy-env.key \
--build-arg STAGE=prod -t my-app:prod .Trong Dockerfile, đổi dòng build thành:
ARG STAGE=prod
RUN --mount=type=secret,id=privacy_env_key \
PRIVACY_ENV_KEY_FILE=/run/secrets/privacy_env_key \
npx privacy-env run -s "$STAGE" -- npm run buildARG STAGE an toàn — nó chỉ là tên môi trường, không phải secret. doctor không báo lỗi
vì tên không khớp mẫu secret.
docker run -p 8080:80 my-app:prodMô hình B — SPA, một image cho mọi stage
App đọc cấu hình từ window.__ENV__ thay vì từ biến nhúng lúc build. Entrypoint sinh
/config.js khi container khởi động.
Files: example/Dockerfile.spa-runtime, example/docker-entrypoint.sh
1. Sửa index.html — nạp config trước bundle:
<script src="/config.js"></script>
<script type="module" src="/src/main.tsx"></script>2. Sửa src/env.ts — đọc từ window.__ENV__:
import { createEnvClient } from '@illya1123/privacy-sdk/runtime'
declare global {
interface Window { __ENV__?: Record<string, unknown> }
}
export const env = createEnvClient({
// Ưu tiên config lúc chạy; khi dev local thì rơi về import.meta.env.
source: [import.meta.env, typeof window === 'undefined' ? undefined : window.__ENV__],
prefix: 'REACT_APP_',
required: ['REACT_APP_PORTAL_API_ENDPOINT'],
})3. Build một lần, chạy mọi nơi:
docker build -t my-spa:1.0.0 -f example/Dockerfile.spa-runtime .
# Cùng một image, khác stage
docker run -p 3000:3000 \
-v "$PWD/.privacy-env.key:/run/secrets/key:ro" \
-e PRIVACY_ENV_KEY_FILE=/run/secrets/key \
-e PRIVACY_ENV_STAGE=dev my-spa:1.0.0
docker run -p 3001:3000 \
-v "$PWD/.privacy-env.key:/run/secrets/key:ro" \
-e PRIVACY_ENV_KEY_FILE=/run/secrets/key \
-e PRIVACY_ENV_STAGE=prod my-spa:1.0.0Entrypoint chạy privacy-env verify --require-encrypted trước — container không khởi
động được nếu thiếu khoá hoặc cấu hình hỏng, thay vì chạy lên rồi lỗi lúc người dùng
bấm nút.
Mô hình C — Node server
Mạnh nhất: image chỉ chứa ciphertext, giải mã trong RAM lúc chạy.
File: example/Dockerfile.node-server
CMD ["npx", "privacy-env", "run", "-s", "prod", "--", "node", "server.js"]Bỏ -s prod đi thì stage lấy từ $PRIVACY_ENV_STAGE — lại thành một image cho mọi
môi trường:
CMD ["npx", "privacy-env", "run", "--", "node", "server.js"]docker run --rm -p 3000:3000 \
-v /path/to/key:/run/secrets/key:ro \
-e PRIVACY_ENV_KEY_FILE=/run/secrets/key \
-e PRIVACY_ENV_STAGE=prod \
my-server:1.0.0Dev local với compose — 4 stage cùng lúc
Rất tiện khi cần đối chiếu hành vi giữa các môi trường:
# compose.dev.yml
x-app: &app
build: .
volumes:
- ./.privacy-env.key:/run/secrets/key:ro
environment: &key
PRIVACY_ENV_KEY_FILE: /run/secrets/key
services:
app-dev:
<<: *app
environment: { <<: *key, PRIVACY_ENV_STAGE: dev }
ports: ['3000:3000']
app-stag:
<<: *app
environment: { <<: *key, PRIVACY_ENV_STAGE: stag }
ports: ['3001:3000']
app-uat:
<<: *app
environment: { <<: *key, PRIVACY_ENV_STAGE: uat }
ports: ['3002:3000']
app-prod:
<<: *app
environment: { <<: *key, PRIVACY_ENV_STAGE: prod }
ports: ['3003:3000']docker compose -f compose.dev.yml upYAML anchor (
&app/*app) dùng thoải mái trong file compose. Chỉenv.yamllà không được dùng anchor — vì nó phá liên kết giá-trị–đường-dẫn của lớp mã hoá.
Kubernetes
File: example/k8s-deployment.yaml
kubectl create secret generic privacy-env-key --from-file=key=./.privacy-env.key
kubectl apply -f example/k8s-deployment.yamlĐiểm cốt lõi: khoá vào container qua volume, không qua env.valueFrom.secretKeyRef.
env:
- name: PRIVACY_ENV_KEY_FILE # chỉ là đường dẫn — không bí mật
value: /run/secrets/privacy-env/key
- name: PRIVACY_ENV_STAGE
value: prod
volumeMounts:
- name: privacy-env-key
mountPath: /run/secrets/privacy-env
readOnly: true
volumes:
- name: privacy-env-key
secret:
secretName: privacy-env-key
defaultMode: 0400 # chỉ owner đọc được, kể cả trong containerVì sao không dùng secretKeyRef: giá trị đó hiện trong kubectl describe pod, trong
container spec, và trong log của mọi admission controller. Volume secret nằm trong tmpfs
và không xuất hiện trong pod spec.
Đổi stage cho một môi trường:
kubectl set env deployment/my-app PRIVACY_ENV_STAGE=uatChạy không phải root
Khoá được mount với quyền 0400 chỉ có nghĩa nếu tiến trình không phải root:
USER nodeTrong k8s thì thêm:
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ['ALL'] }Với readOnlyRootFilesystem: true mà bạn dùng Mô hình B (ghi /config.js), cần thêm
emptyDir mount vào thư mục đó.
Fail fast — đừng để container chạy lên với cấu hình sai
Entrypoint mẫu chạy trước khi start app:
npx --no-install privacy-env verify --require-encrypted >/dev/nullNó bắt được: thiếu khoá, khoá sai, file bị sửa, và trường hợp ai đó lỡ đưa env.yaml
thô vào image. Container chết ngay lúc khởi động — dễ debug hơn nhiều so với lỗi
runtime ngẫu nhiên sau đó.
Muốn chặt hơn nữa thì thêm --strict để lệch biến giữa các stage cũng làm container
không khởi động.
Kiểm tra một container đang chạy
# Stage nào đang chạy?
docker exec my-container npx --no-install privacy-env view --format json | head
# Có secret nào lọt vào image không?
npx privacy-env scan --image my-app:1.0.0
# Khoá có bị lộ trong config container không?
docker inspect my-container --format '{{json .Config.Env}}' | grep -i privacy
# → chỉ được thấy PRIVACY_ENV_KEY_FILE và PRIVACY_ENV_STAGE, KHÔNG được thấy PRIVACY_ENV_KEYChecklist trước khi đẩy image lên registry
npx privacy-env doctor --strict # Dockerfile, .dockerignore, git, quyền file
docker build --secret id=privacy_env_key,src=.privacy-env.key -t my-app:1.0.0 .
npx privacy-env scan --image my-app:1.0.0 # image config + history + filesystem| Kiểm | Phải là |
|---|---|
| .dockerignore có .privacy-env.key, env.yaml, .env* | ✔ |
| Không có ARG/ENV nào mang khoá | ✔ |
| Không COPY khoá hay env.yaml | ✔ |
| Không privacy-env decrypt trong RUN | ✔ |
| Khoá vào container qua file mount, không qua -e | ✔ |
| Entrypoint có verify fail-fast | ✔ |
| USER không phải root | ✔ |
| scan --image sạch | ✔ |
Ba lệnh đầu nên đặt luôn trong CI — xem phần CI/CD.
Chi tiết mật mã và định dạng file
| Thành phần | Lựa chọn |
|---|---|
| Mã hoá | AES-256-GCM, IV 96-bit ngẫu nhiên cho từng giá trị |
| AAD | domain \| version \| kiểu+style \| đường-dẫn |
| Che độ dài | Đệm lên bội số 32 byte, có prefix độ dài 4 byte |
| Tách khoá | HKDF-SHA-256: khoá mã hoá và khoá MAC là hai nhánh riêng |
| Toàn vẹn tài liệu | HMAC-SHA-256 phủ mọi giá trị plaintext + toàn bộ header |
| KDF (passphrase) | scrypt N=2¹⁷, r=8, p=1 (~128 MiB/lần thử) |
| Nhận diện khoá | HMAC fingerprint 16 hex trong header |
| So sánh MAC | timingSafeEqual |
| Chống hạ cấp | Version nằm trong AAD; scrypt ngoài khoảng an toàn bị từ chối |
| Quyền file | chmod 600 trên POSIX, icacls owner-only trên Windows |
| Xử lý bộ nhớ | Khoá dẫn xuất và buffer plaintext bị ghi đè 0 sau khi dùng |
Định dạng token
ENC[2:AGCM:sP:<iv>:<ciphertext>:<authtag>]
│ │ ││ │ │ └── GCM auth tag, 16 byte, base64url
│ │ ││ │ └── ciphertext đã đệm, base64url
│ │ ││ └── IV 96-bit ngẫu nhiên, base64url
│ │ │└── style YAML gốc: P plain, S 'nháy đơn', D "nháy kép", L |, F >, A auto
│ │ └── kiểu: s string, n number, b boolean, z null
│ └── cipher
└── version token (2 = có đệm; 1 = định dạng cũ, vẫn đọc được)Payload trước khi mã hoá: [độ dài 4 byte big-endian][dữ liệu UTF-8][đệm số 0 tới bội 32]
Header _privacy_env
| Trường | Ý nghĩa |
|---|---|
| version | Phiên bản định dạng tài liệu (đang ghi: 2) |
| cipher | Luôn là AES-256-GCM |
| kdf | { name: none } với khoá thô, hoặc { name: scrypt, N, r, p } |
| salt | 32 byte ngẫu nhiên, base64 — dùng cho HKDF (và scrypt) |
| key_id | Fingerprint HMAC 16 hex của master key |
| encrypted_at | Thời điểm mã hoá, ISO 8601 |
| created_by | privacy-sdk@<version> |
| mac | HMAC-SHA-256 hex phủ mọi giá trị + header |
MAC được tính trên path + kiểu+style + plaintext của từng lá, sắp xếp theo path để
không phụ thuộc thứ tự khoá, cộng với chuỗi canonical của header.
Mã lỗi
| Mã | Nghĩa |
|---|---|
| E_FILE_NOT_FOUND | Không tìm thấy env file |
| E_FILE_EXISTS | Từ chối ghi đè (dùng --force) |
| E_INVALID_DOCUMENT | YAML sai, header hỏng, hoặc tham số scrypt ngoài khoảng an toàn |
| E_ALREADY_ENCRYPTED | File đã mã hoá rồi |
| E_NOT_ENCRYPTED | Cần file mã hoá, hoặc có giá trị plaintext lén trong file mã hoá |
| E_KEY_MISSING | Không tìm thấy khoá nào |
| E_KEY_INVALID | Khoá sai định dạng hoặc sai độ dài |
| E_KEY_MISMATCH | Khoá không khớp file (kèm fingerprint để đối chiếu) |
| E_DECRYPT_FAILED | Xác thực GCM thất bại — sửa tay, sai khoá, hoặc chuyển giá trị giữa stage |
| E_INTEGRITY_FAILED | MAC toàn tài liệu sai — có biến bị thêm/xoá/sửa |
| E_STAGE_NOT_FOUND | Stage không tồn tại trong file |
| E_VAR_REQUIRED | Thiếu biến khai báo required |
| E_UNSUPPORTED | Anchor/alias YAML, hoặc kiểu giá trị không hỗ trợ |
| E_USAGE | Sai cách dùng CLI |
Xử lý sự cố
E_KEY_MISMATCH: This key does not match…
Khoá bạn đang có không phải khoá đã mã hoá file. Đối chiếu key id mà lỗi in ra với
key id trong env.enc.yaml. Xin lại khoá đúng từ người khởi tạo, hoặc nếu file mới thì
privacy-env rotate --generate.
E_INTEGRITY_FAILED: Document integrity check failed
Có người thêm/xoá/sửa biến ngoài SDK. Khôi phục file từ git:
git checkout -- env.enc.yamlĐừng commit đè lên — bạn sẽ mất thay đổi hợp lệ của người khác.
Editor "…" exited with code N
Editor thoát với mã lỗi nên SDK không mã hoá lại gì, file giữ nguyên. Đặt $EDITOR
thành editor chặn tới khi đóng file: export EDITOR="code -w".
Sửa env.yaml mà app không thấy thay đổi
Auto-protect chỉ đồng bộ lại khi env.yaml mới hơn env.enc.yaml. Kiểm tra:
privacy-env doctor
privacy-env protect # ép đồng bộ ngaynpm install không chạy auto-protect
npm bỏ qua lifecycle script khi không có gì thay đổi. Đó là lý do có lớp thứ hai: chạy
app là tự bảo vệ. Hoặc gọi thẳng npx privacy-env protect.
Pre-commit hook chặn commit hợp lệ
Xem lý do nó in ra — thường là bạn thật sự đang stage file không nên commit. Nếu chắc
chắn muốn bỏ qua: git commit --no-verify.
Guard báo "privacy-env not found"
Hook cố tình fail-closed: một guard âm thầm bỏ qua còn tệ hơn không có guard. Chạy
npm install, hoặc xoá .git/hooks/pre-commit nếu bạn muốn gỡ hẳn.
Câu hỏi thường gặp
Có commit env.enc.yaml được không?
Có — đó chính là mục đích. Nó chỉ chứa ciphertext.
Có commit .privacy-env.key được không?
Tuyệt đối không. .gitignore chặn, pre-commit guard chặn, doctor báo lỗi.
Mất khoá thì sao?
Không giải mã được nữa. Không có cửa hậu — đó là điểm của thiết kế. Nếu còn env.yaml
trên máy ai đó thì mã hoá lại bằng khoá mới; nếu không, phải dựng lại giá trị bằng tay.
Khoá từng bị commit thì sao?
Coi như đã lộ. privacy-env rotate --generate ngay, cập nhật CI secret. Xoá khỏi lịch sử
git là việc riêng (git filter-repo), nhưng rotate mới là thứ thật sự cứu bạn.
Có che được giá trị trong bundle browser không? Không, và không thư viện nào làm được. Xem Mô hình bảo mật.
Nhiều dự án dùng chung một khoá được không? Được, nhưng đừng. Mỗi dự án một khoá thì lộ một chỗ không kéo theo chỗ khác.
Có dùng được với monorepo không?
Được. env.enc.yaml được tìm ngược lên từ thư mục hiện tại, nên đặt ở gốc monorepo là
mọi package dùng chung. Muốn tách riêng thì mỗi package một file.
File mã hoá có diff sạch trong git không?
Có. Tên biến, cấu trúc, thứ tự và comment đều giữ nguyên — chỉ giá trị đổi. Mỗi lần mã
hoá lại thì IV đổi nên mọi dòng đều đổi; dùng privacy-env edit để chỉ đổi dòng cần đổi.
Tắt hoàn toàn auto-protect được không?
Được: privacyEnv({ autoProtect: false }) trong plugin, --no-protect với run. Rule
.gitignore và pre-commit hook đã cài rồi thì vẫn còn tác dụng.
Phát triển SDK này
npm run build # tsup: ESM + CJS + .d.ts
npm run dev # tsup --watch
npm run test # vitest
npm run test:watch
npm run lint # biome check
npm run lint:fix
npm run typecheck # tsc --noEmit
npm run check # lint + typecheck + testCấu trúc
src/
index.ts barrel export cho Node
cli.ts entry CLI (bin: privacy-env)
postinstall.ts auto-protect lúc npm install
core/
crypto.ts AES-256-GCM, HKDF, scrypt, đệm, MAC
document.ts duyệt cây YAML, mã hoá/giải mã tại chỗ
loader.ts loadEnv, chọn stage, giải ${VAR}, cache
operations.ts encrypt/decrypt/rotate/verify mức file
keyring.ts tìm và dẫn xuất khoá
protect.ts auto-protect
audit.ts đằng sau `doctor`
guard.ts đằng sau `guard`
docker.ts dockerignore, phân tích Dockerfile, quét image
tar.ts bộ đọc tar streaming (zero-dependency)
secrets.ts bộ nhận diện secret dùng chung
permissions.ts chmod POSIX / icacls Windows
git.ts wrapper git chỉ đọc
emit.ts định dạng xuất + sinh type
runtime/index.ts accessor browser (không crypto, không Node)
vite/index.ts plugin Vite
webpack/index.ts plugin WebpackGiấy phép
MIT
