npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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 dev

Mục lục

  1. Mô hình bảo mật — đọc trước khi dùng
  2. Cài đặt
  3. Bắt đầu nhanh — không cần cấu hình
  4. Ba khái niệm cốt lõi
  5. Định dạng env.yaml
  6. Tích hợp vào dự án
  7. Tham chiếu CLI đầy đủ
  8. Tham chiếu API Node
  9. Tham chiếu API runtime (browser)
  10. Quản lý khoá
  11. Quy trình làm việc nhóm
  12. CI/CD
  13. Docker — giữ secret ngoài image
  14. Chạy dự án bằng container
  15. Chi tiết mật mã và định dạng file
  16. Mã lỗi
  17. Xử lý sự cố
  18. Câu hỏi thường gặp
  19. 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:

  1. AES-256-GCM bảo vệ từng giá trị — sửa 1 bit là fail.
  2. AAD cột ciphertext vào đúng stage.KEY — copy giá trị prod sang dev sẽ fail xác thực, không phải âm thầm rò rỉ giữa các môi trường.
  3. 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-sdk

Yê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 / plugin

Xong. 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.yaml của bạn (chỉ gitignore + guard chặn commit)
  • ghi đè env.enc.yaml nế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ật

Ba 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.key trê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:

  1. --stage / options.stage (tường minh)
  2. $PRIVACY_ENV_STAGE
  3. $APP_ENV
  4. $REACT_APP_ENV
  5. $APP_STAGE
  6. $NODE_ENV
  7. 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 định

Tì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: 1a99407908bc3970e2361c8092869b156758ef124a6df1c8bb09f7ad188cfe44

decrypt 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.env

loadEnv 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 --key hoặc --passphrase trên dòng lệnh: giá trị đó hiện trong ps, 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:

  1. Ghi rule thiếu vào .gitignore
  2. Ghi rule thiếu vào .dockerignore (nếu có Dockerfile / compose file)
  3. Cài pre-commit guard
  4. Mã hoá env.yaml → env.enc.yaml, hoặc đồng bộ lại nếu env.yaml mớ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 deploy

encrypt

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.yaml

decrypt

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 đĩa

edit

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 edit

Editor 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.js

Trê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 AppEnv

verify

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/salt

Nhớ 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 guard

Kiể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 guard

Bỏ 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ác

Tiệ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àm

Thao 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.yaml

Bả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): boolean

Khoá

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(): void

Lỗ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ém InvalidEnvError
  • list: 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ối
  • number: không phải số hữu hạn → ném InvalidEnvError

Quản lý khoá

Thứ tự tìm khoá

Dừng ở cái đầu tiên tìm thấy:

  1. --key / options.key
  2. $PRIVACY_ENV_KEY ← dùng cái này trong CI
  3. $PRIVACY_ENV_KEY_FILE hoặc --key-file
  4. .privacy-env.key (tìm ngược lên từ thư mục hiện tại)
  5. ~/.privacy-env/default.key
  6. $PRIVACY_ENV_PASSPHRASE
  7. 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ử shell

Passphrase đ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 --generate

Là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 start

Khô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ại

Thê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 commit

Hằng ngày

privacy-env view -s dev            # xem đang có gì
privacy-env doctor                 # trước khi push

CI/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 build

Lư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 --strict thay cho verify --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/html
docker 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-server

Dù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: true

File 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:latest

Kiể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 build

ARG 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:prod

Mô 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.0

Entrypoint 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.0

Dev 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 up

YAML anchor (&app / *app) dùng thoải mái trong file compose. Chỉ env.yaml là 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 container

Vì 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=uat

Chạ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 node

Trong 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/null

Nó 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_KEY

Checklist 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ộ ngay

npm 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 + test

Cấ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 Webpack

Giấy phép

MIT