So sánh secrets manager self-hosted trên một VPS
So sánh OpenBao, Infisical, SOPS với age, systemd credentials và env file bị khóa: chọn gì cho một VPS, kèm chi phí và rủi ro vận hành thực tế.
Secrets manager tự host làm được gì mà password manager không làm được
Self-hosted secrets manager cung cấp credential cho các process. Password manager cung cấp credential cho con người. Mọi khác biệt khác đều bắt nguồn từ điểm này. Password manager được mở khóa bởi một người đang có mặt và chú ý. Secrets manager phải cung cấp mật khẩu database cho application vào lúc 03:00, khi không có ai thức.
Các kiểu lỗi khác nhau theo cách rất quan trọng. Password manager bị khóa chỉ gây bất tiện: bạn nhập lại master password. Secrets manager bị seal gây outage: mọi service restart trong lúc nó bị seal sẽ khởi động mà không có credential và tiếp tục down. Chạy Vaultwarden làm password manager riêng giải quyết tốt vấn đề của con người. Nó không giải quyết vấn đề của máy và vốn không được thiết kế cho việc đó. Nếu bạn chạy Vaultwarden cùng với hệ thống này, phần đáng hardening là admin token và file backup của nó, không phải nội dung vault mà client đã mã hóa; hardening Vaultwarden bao quát cả hai phần này.
Các lựa chọn thực tế cho một server được chia thành 2 nhóm. OpenBao và Infisical là các service: một API, một database, TLS (transport layer security), bước đăng nhập và một process mà bạn phải duy trì hoạt động. SOPS với age, systemd credentials và Docker secrets là các file: được mã hóa khi lưu trữ, được giải mã bởi một thành phần đã chạy sẵn và không có thành phần bổ sung nào cần monitor.
Đây là câu trả lời thẳng ngay từ đầu. Với một máy duy nhất có 1 hoặc 2 người dùng, các lựa chọn dựa trên file thường phù hợp hơn. Một OpenBao mà không ai unseal đúng cách và không ai rotate secret sẽ tệ hơn một env file có mode 600, vì nó thêm một thành phần dễ hỏng và một bản backup mà bạn có thể cấu hình sai, nhưng không mang lại khả năng rotation nào mà trước đó bạn chưa tự thực hiện.
File env mode 600 có đủ an toàn không?
Thường là có. Mối đe dọa mà nó ngăn chặn là một user khác trên máy đọc được database password của bạn. Unix file permissions xử lý việc đó trước khi network hoạt động.
sudo install -d -m 750 -o root -g myapp /etc/myapp
sudo install -m 640 -o root -g myapp /dev/null /etc/myapp/env
sudoedit /etc/myapp/envKiểm tra từ cả hai phía:
sudo -u myapp cat /etc/myapp/env
sudo -u nobody cat /etc/myapp/envLệnh đầu in nội dung file. Lệnh thứ hai in cat: /etc/myapp/env: Permission denied vì nobody không thuộc group myapp và file không có world bits. Đó là toàn bộ security model, và nó có hiệu lực thực tế.
Rò rỉ xảy ra ở bước tiếp theo. Một systemd unit có EnvironmentFile= sẽ sao chép các giá trị đó vào process environment, và process environment có thể được đọc.
[Service]
User=myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myappsudo cat /proc/$(pgrep -n myapp)/environ | tr '\0' '\n'Lệnh đó in các secret ở dạng plaintext vì /proc/<pid>/environ có thể được root và user chạy process đọc. Crash reporter gắn environment vào report cũng thấy được các giá trị tương tự. Bất kỳ tool nào chạy dưới cùng account cũng vậy. Vì thế, đưa secret ra khỏi AI agent bắt đầu bằng việc đưa chúng ra khỏi environment. Kết hợp file với một service user có quyền thấp riêng để “user chạy process” không phải là root.
SOPS với age: mã hóa secret để có thể commit vào git
SOPS (secrets operations) mã hóa các value trong file YAML hoặc JSON và giữ nguyên các key ở dạng cleartext. age là một công cụ mã hóa nhỏ, cung cấp cho bạn một key pair mà không cần key server. Kết hợp lại, chúng cho phép bạn commit secrets.enc.yaml cùng với code, còn git diff vẫn cho biết setting nào đã thay đổi mà không tiết lộ giá trị mới cho người đọc.
age được đóng gói trong Ubuntu 24.04. SOPS thì không, nên hãy lấy .deb từ release page. Version 3.13.3 là phiên bản hiện tại vào tháng 8 năm 2026.
sudo apt update && sudo apt install -y age
curl -LO https://github.com/getsops/sops/releases/download/v3.13.3/sops_3.13.3_amd64.deb
sudo apt install -y ./sops_3.13.3_amd64.deb
sops --versionTạo một key pair. age-keygen ghi private key vào file và in public key ra màn hình, nên bạn sẽ thấy một dòng bắt đầu bằng Public key: age1....
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txtĐặt public key vào .sops.yaml tại thư mục gốc của repository, để bạn không phải nhớ recipient trên command line.
creation_rules:
- age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23csops encrypt secrets.yaml > secrets.enc.yaml
sops decrypt secrets.enc.yamlRule không có path_regex sẽ match mọi thứ. Đây là điều bạn muốn lúc đầu. Nếu sau đó thêm một rule, hãy viết rule để match file bạn truyền vào sops, vì các rule được kiểm tra dựa trên input path, không phải file mà bạn redirect output vào.
Khi chạy, chỉ truyền các value cho một process:
sops exec-env secrets.enc.yaml './myapp'sops exec-env decrypt trong memory và set các value vào environment của child process, nên không có plaintext nào được ghi xuống disk. Lưu ý về environment trong section trước vẫn áp dụng cho child process đó.
Có 2 vấn đề thường gây lỗi. Lỗi Failed to get the data key required to decrypt the SOPS file dưới systemd gần như luôn có nghĩa là SOPS đã tìm sai home directory, vì unit không kế thừa HOME của bạn. Hãy đặt path một cách rõ ràng bằng Environment=SOPS_AGE_KEY_FILE=/etc/sops/age.txt trong unit. Ngoài ra, chỉnh sửa .sops.yaml không re-encrypt những file đã tồn tại: thêm public key của đồng nghiệp chỉ ảnh hưởng đến file mới, nên hãy chạy sops updatekeys secrets.enc.yaml trên từng file hiện có. Nếu cấu hình của bạn đã chạy qua Ansible, mã hóa các value tương tự bằng Ansible Vault sẽ cho kết quả tương tự mà không cần thêm công cụ thứ hai.
systemd credentials: secret không bao giờ xuất hiện trong environment
Ubuntu 24.04 đi kèm systemd 255, nên không cần cài thêm. systemd-creds mã hóa secret trên host, còn systemd giải mã secret vào một thư mục riêng mà chỉ service đó có thể đọc.
sudo systemd-creds setup
sudo install -d -m 700 /etc/myapp
echo -n 'hunter2' | sudo systemd-creds encrypt --name=db_password - /etc/myapp/db_password.cred[Service]
User=myapp
LoadCredentialEncrypted=db_password:/etc/myapp/db_password.cred
ExecStart=/usr/local/bin/myappService đọc giá trị từ file tên là db_password bên trong thư mục được chỉ định bởi $CREDENTIALS_DIRECTORY. Giá trị này không nằm trong environment, nên /proc/<pid>/environ không hiển thị thông tin hữu ích nào, và plaintext không bao giờ được ghi vào root filesystem.
Hãy xác minh file giải mã được trước khi trỏ unit vào đó:
sudo systemd-creds decrypt /etc/myapp/db_password.cred -Bạn phải biết key nào đã dùng để mã hóa, vì điều đó quyết định backup có thể sử dụng được hay không. --with-key=auto mặc định sử dụng chip TPM2 (trusted platform module version 2) khi chip này tồn tại và có thể sử dụng; nếu không, nó dùng host key. Hầu hết VPS không có TPM2.
systemd-analyze has-tpm2no có nghĩa là host key đã được sử dụng, và key đó nằm trong /var/lib/systemd/credential.secret, chỉ root mới có thể đọc. Nếu restore db_password.cred lên một VPS mới mà không có file đó thì không thể giải mã được, dù ở thời điểm nào. Hãy copy credential.secret vào cùng backup, hoặc lưu plaintext ở một nơi mà bạn vẫn có thể truy cập.
Docker secrets: các file trong /run/secrets
Compose đọc một file từ host rồi mount file đó vào container tại /run/secrets/<name>.
services:
app:
image: myapp:latest
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtdocker compose exec app cat /run/secrets/db_password
docker compose exec app env | grep -i passwordLệnh đầu tiên in secret. Lệnh thứ hai chỉ in DB_PASSWORD_FILE=/run/secrets/db_password. Đây mới là điểm quan trọng: giá trị không bao giờ nằm trong environment của container, nên không xuất hiện trong output của docker inspect. Nhiều image chính thức đã được thiết kế để dùng cách này. Image Postgres cũng đọc POSTGRES_PASSWORD_FILE đúng theo cách đó.
Cần hiểu rõ cơ chế này. Ngoài Swarm mode, không có lớp nào mã hóa dữ liệu: ./db_password.txt là một file plaintext trên host, và chỉ được bảo vệ bằng mode cùng owner của file. Tự đặt cả hai thuộc tính này, vì Compose vẫn mount một file cho phép mọi người đọc mà không cảnh báo. Xem hướng dẫn về env file và secret trong Compose để biết đầy đủ hơn về các đánh đổi so với cách viết tắt env_file.
Chi phí thực tế để vận hành OpenBao và Vault
OpenBao là fork của HashiCorp Vault do Linux Foundation duy trì, bắt đầu sau khi HashiCorp cấp lại giấy phép cho Vault theo Business Source License vào năm 2023. OpenBao vẫn dùng MPL 2.0 (Mozilla Public License). Bản phát hành 2.6.2 là bản hiện tại tính đến tháng 8 năm 2026. Gần như mọi nội dung dưới đây cũng áp dụng cho Vault, vì fork này giữ nguyên command surface.
docker pull docker.io/openbao/openbaoCác package Debian và Ubuntu có trên trang tải xuống của OpenBao nếu bạn muốn dùng apt để quản lý việc nâng cấp. Server cần một file cấu hình chứa listener và storage backend:
listener "tcp" {
address = "127.0.0.1:8200"
tls_cert_file = "/path/to/full-chain.pem"
tls_key_file = "/path/to/private-key.pem"
}
storage "raft" {
path = "/path/to/raft/data"
node_id = "raft_node_1"
}Sau đó khởi động server một lần:
bao operator initMặc định, OpenBao chia root key thành 5 share và yêu cầu 3 share để unseal. Đây là các flag -key-shares và -key-threshold. OpenBao in ra các share và initial root token một lần duy nhất, sau đó không in lại.
Đây là phần mà hầu hết các bài so sánh bỏ qua. Server được restart sẽ ở trạng thái sealed. OpenBao chỉ giữ root key trong memory. Vì vậy, sau khi restart, OpenBao không thể giải mã storage của chính nó cho đến khi có người cung cấp đủ số share theo threshold. Do đó, một lần cập nhật kernel hoặc một lần bị kill vì hết memory sẽ khiến server ở trạng thái sealed và các ứng dụng không thể đăng nhập.
Trên một VPS chỉ có một người quản trị, việc chia key bằng Shamir không bảo vệ được gì, vì cả 5 share cuối cùng đều nằm trong cùng một password manager thuộc cùng một người. Auto unseal chuyển key sang một device hoặc service đáng tin cậy. Trong cloud lớn, đó thường là managed key service. Trên VPS, đó thường là một key file nằm cùng disk với dữ liệu mà nó bảo vệ. Đây là một sự giảm bảo mật thực sự, đổi lại server có thể tự khởi động lại sau reboot. Hãy cân nhắc rõ sự đánh đổi này và ghi lại bạn đã chọn phương án nào.
Infisical: UI, database và master key vẫn do bạn giữ
Infisical là một secrets platform có web interface, project, environment và access control theo từng user. Self-host bằng Compose khá đơn giản:
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
docker compose -f docker-compose.prod.yml up -dHãy chỉnh sửa .env trước lệnh cuối cùng đó. Bạn phải tự tạo 2 giá trị, và một trong số đó không được thay đổi sau này:
openssl rand -hex 16
openssl rand -base64 32Giá trị đầu tiên là ENCRYPTION_KEY, một chuỗi hex dài 16 byte. Đây là key dùng để mã hóa secrets bên trong PostgreSQL. Nếu làm mất key này, một bản backup database hoàn chỉnh cũng chỉ còn là một tập ciphertext. Nếu thay đổi key trên instance đang chạy, các secrets hiện có sẽ không thể giải mã. Giá trị thứ hai là AUTH_SECRET, một chuỗi base64 dài 32 byte dùng cho session. SITE_URL phải là URL tuyệt đối mà bạn thực sự dùng để truy cập, bao gồm cả protocol. Nếu không, login redirect sẽ bị lỗi.
Infisical phù hợp hơn OpenBao khi nhu cầu thực tế của bạn là quản lý người dùng: cung cấp web interface cho một team nhỏ và tách biệt giữa các environment, thay vì quản lý database credential tự hết hạn. Đổi lại, bạn phải vận hành PostgreSQL, Redis và TLS certificate. Bạn cũng phải tự patch và backup tất cả các thành phần này.
Điều gì xảy ra khi secrets service ngừng hoạt động và app của bạn khởi động lại
Câu hỏi này quyết định secrets service có nên chạy trên một máy riêng hay không. File có thể đọc được trước khi network khởi động. Service thì không.
Khởi động lại máy, application và OpenBao bắt đầu cùng lúc. Application yêu cầu database password, OpenBao vẫn đang sealed, request thất bại, rồi systemd restart application liên tục cho đến khi có người nhập các unseal share. Không có thành phần nào bị hỏng. Nhưng cũng không có thành phần nào thực sự hoạt động.
Có 2 cách xử lý rõ ràng. Sắp xếp thứ tự các unit và cho application retry: After= secrets service, cùng với Restart=on-failure và RestartSec= đủ dài để bạn không gửi request dồn dập đến API. Hoặc fetch secret lúc deploy thay vì lúc boot: ghi secret vào file có mode 600 hoặc systemd credential, để hệ thống đang chạy chỉ phụ thuộc vào file thay vì API.
Token expiry cũng là vấn đề tương tự nhưng diễn ra theo chu kỳ dài hơn. OpenBao token và lease có time to live, vì vậy một process chạy lâu nhưng không renew sẽ mất quyền truy cập vào thời điểm không liên quan đến lần deploy nào. Lỗi này khó hiểu chính vì trong ngày hôm đó không có gì thay đổi.
Sao lưu chính kho lưu trữ
Mỗi tùy chọn ở đây đều có một key, và bản sao lưu không có key đó thì vô dụng. Hãy ghi lại key của bạn đang nằm ở đâu.
Với file env, bản thân file là secret, nên bản sao lưu phải được mã hóa. Với SOPS, file đã mã hóa có thể đặt ở bất kỳ nơi công khai nào, còn private key age tại ~/.config/sops/age/keys.txt là thứ bạn không được làm mất. Với systemd credentials, hãy sao lưu /var/lib/systemd/credential.secret cùng với các file .cred. Với Infisical, hãy tạo PostgreSQL dump và lưu ENCRYPTION_KEY ở một nơi riêng.
OpenBao dùng raft storage có snapshot riêng:
bao operator raft snapshot save backup.snap
bao operator raft snapshot restore backup.snapSnapshot chứa storage đã mã hóa của bạn, nên khi restore vào server mới, bạn vẫn cần các unseal share từ bao operator init. Một job chạy mỗi đêm để chép snapshot vào object storage, trong khi các share không được lưu ở đâu, thì thực chất không sao lưu được gì. Hãy test restore trên một VPS tạm thời trước khi phụ thuộc vào bản sao lưu đó.
Audit logging: ai đã đọc secret nào
File không cung cấp audit trail. Mode và owner cho biết ai có thể đọc secret. Chúng không bao giờ cho biết ai đã đọc. auditd kết hợp với việc monitor path là phương án thay thế gần nhất, nhưng nó chỉ báo rằng một file đã được mở, không cho biết giá trị nào đã được sử dụng.
OpenBao ghi log mọi request vào audit device mà bạn enable rõ ràng:
bao audit enable file file_path=/var/log/openbao_audit.logCó 2 điểm trong log này làm thay đổi cách bạn vận hành server. Hầu hết chuỗi trong request và response được hash bằng HMAC-SHA256 và salt. Vì vậy, bạn có thể đối chiếu một giá trị đã biết với log mà không cần ghi plaintext trực tiếp trong log. Integer và boolean được ghi nguyên dạng, nên secret dạng số không được bảo vệ bởi cơ chế hash này.
Bẫy vận hành nằm ở đây: OpenBao sẽ không phản hồi request khi không có audit device nào đang enable và có thể ghi log. Một device bị lỗi theo cách blocking sẽ khiến request treo cho đến khi có người khắc phục. Full disk trên /var/log sẽ khiến secrets API ngừng hoạt động theo đúng thiết kế. Hãy cấp không gian riêng cho audit log và tạo rule logrotate ngay từ ngày đầu, không phải sau lần outage đầu tiên.
Nên chạy secrets manager self-hosted nào?
Hãy đếm số máy và số người, rồi chọn.
- Một máy, một người: dùng một file env mode 600, thuộc sở hữu của root và được service user đọc. Khi muốn đưa giá trị ra khỏi process environment, hãy thêm systemd credentials.
- Một máy, từ hai đến năm người, cấu hình đã có trong git: dùng SOPS với age. Mỗi người có một key pair, còn
.sops.yamlliệt kê mọi public key được phép giải mã. - Nhiều máy, một repository cấu hình, không cần credentials hết hạn: vẫn dùng SOPS với age, với một recipient key cho mỗi host. Khi đó, nếu host key bị đánh cắp, key đó chỉ giải mã được các file của host tương ứng.
- Nhiều máy và nhiều team thực sự cần database credentials có thời hạn, kèm audit trail có người đọc: dùng OpenBao. Hãy dành một giờ mỗi tháng trong ngân sách cho thời gian của operator để thực hiện unseal và diễn tập restore.
Quy tắc chung của cả bốn trường hợp là như nhau. Hãy chạy thành phần nhỏ nhất đáp ứng được một yêu cầu mà bạn có thể nói rõ, vì một secrets manager đang down không thể phân biệt được với một secrets manager không có secret nào.
FAQ
Có đáng tự host secrets manager cho một VPS duy nhất không?
Thường là không, nếu bạn nói đến một service như OpenBao hoặc Infisical. Trên một máy duy nhất với một hoặc hai người dùng, file env có mode 600 hoặc encrypted credential của systemd cung cấp mức bảo vệ tương đương trước user cục bộ khác, mà không cần bước unseal và không phải patch thêm service. Secrets service bắt đầu đáng dùng khi bạn có nhiều máy, nhiều người hoặc thực sự cần credential tự hết hạn mà không ai phải tự xoay vòng.
Password manager khác secrets manager như thế nào?
Password manager lưu credential do con người nhập và được một người mở khóa khi họ có mặt. Secrets manager cung cấp credential cho process, nên phải hoạt động lúc 03:00 mà không có ai giám sát. Điểm khác biệt nằm ở đây: password manager bị khóa sẽ buộc bạn nhập lại master password, còn secrets manager bị sealed sẽ dừng mọi service khởi động lại trong lúc nó đang sealed.
Ứng dụng của tôi sẽ ra sao nếu OpenBao bị sealed sau khi reboot?
Chúng không thể lấy secret, nên không khởi động được. systemd sẽ restart chúng liên tục cho đến khi có người cung cấp đủ ngưỡng unseal, mặc định là 3 trong 5 share. OpenBao chỉ giữ root key trong memory, nên mỗi lần restart nó lại bị sealed. Bạn có thể bật auto unseal, nhưng trên một VPS duy nhất, unseal key sẽ nằm trên cùng disk với dữ liệu. Hoặc bạn có thể render secret ra file khi deploy để quá trình boot không bao giờ phụ thuộc vào API.
Có thể commit file được mã hóa bằng SOPS vào repository public không?
Các value đã được mã hóa, nên an toàn trước những người không có age private key. Các key không được mã hóa: người đọc có thể thấy bạn đang giữ STRIPE_SECRET_KEY và SMTP_PASSWORD, cũng như tần suất thay đổi của từng key. Metadata đó có thể chấp nhận được với hầu hết project nhưng không phù hợp với một số project. Không đưa age private key vào repository. Chạy sops updatekeys trên mọi file hiện có mỗi khi thêm hoặc xóa recipient.