So sánh secrets manager tự host trên một VPS
So sánh OpenBao, Infisical, SOPS với age, systemd credentials và env file khóa chặt trên một VPS, kèm chi phí và cách chọn cho từng kiểu triển khai.
Secrets manager tự host cung cấp thông tin xác thực cho process, còn password manager cung cấp thông tin xác thực 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ú ý thao tác. Secrets manager phải cung cấp password database cho ứng dụng lúc 03:00, khi không ai thức.
Cách các hệ thống này gặp lỗi cũng khác nhau, và khác biệt đó 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 là một sự cố ngừng dịch vụ: mọi service khởi động lại trong lúc nó bị seal sẽ chạy lên 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 xây dựng cho mục đích đó.
Các lựa chọn thực tế cho một server thường chia thành 2 nhóm. OpenBao và Infisical là các service: một API, một database, TLS (bảo mật tầng truyền tải), một bước đăng nhập và một process mà bạn phải giữ cho 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, không có thành phần bổ sung cần monitor.
Đây là câu trả lời thực tế ngay từ đầu. Với một máy duy nhất có 1 hoặc 2 người sử dụng, các lựa chọn dựa trên file thường là phù hợp. Một OpenBao mà không ai unseal đúng cách và cũng 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ễ lỗi và một bản backup mà bạn sẽ cấu hình sai, nhưng không mang lại khả năng rotation nào mà bạn chưa tự thực hiện thủ công.
Tệp env có permission 600 đã đủ an toàn chưa?
Thường là đủ. Mối đe dọa mà cách này ngăn chặn là một user khác trên máy đọc được password database của bạn. Unix file permission 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 tiên 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ó thực sự có hiệu lực.
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 secrets dưới dạng plain text, vì /proc/<pid>/environ có thể được root và user chạy process đọc. Crash reporter đính kèm environment vào report cũng thấy được nội dung tương tự. Mọi tool chạy dưới cùng account cũng vậy. Vì thế, giữ secrets ngoài AI agents bắt đầu bằng việc đưa chúng ra khỏi environment. Kết hợp file này với một service user riêng có ít privilege để "user chạy process" không phải là root.
SOPS với age: mã hóa secret để 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 plaintext. age là một công cụ mã hóa nhỏ, cung cấp một cặp key duy nhất và không cần key server. Kết hợp hai công cụ này, bạn có thể commit secrets.enc.yaml cùng với code, và 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, vì vậy hãy lấy .deb từ trang release. Version 3.13.3 là 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 cặp key. age-keygen ghi private key vào file và in public key ra màn hình, vì vậy 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 root 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.yamlMột rule không có path_regex sẽ khớp với mọi thứ. Đây là điều bạn muốn lúc đầu. Nếu sau này thêm một rule, hãy viết rule đó để khớp với file bạn truyền cho 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 duy nhất:
sops exec-env secrets.enc.yaml './myapp'sops exec-env giải mã trong memory và đặt các value vào environment của child process, vì vậy không có plaintext nào được ghi xuống disk. Lưu ý về environment ở 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. Đặ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 mã hóa lại những file đã tồn tại: thêm public key của đồng nghiệp chỉ ảnh hưởng đến file mới, vì vậy hãy chạy sops updatekeys secrets.enc.yaml trên từng file hiện có. Nếu configuration của bạn đã chạy qua Ansible, mã hóa các value tương tự bằng Ansible Vault sẽ cho cùng kết quả mà không cần thêm một tool khác.
systemd credentials: các 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 một secret trên host, còn systemd giải mã secret đó vào một thư mục riêng mà chỉ service tương ứng mới đọc đượ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 có tên 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 có thể được giải mã trước khi trỏ unit vào đó:
sudo systemd-creds decrypt /etc/myapp/db_password.cred -Bạn cần biết key nào đã dùng để mã hóa, vì điều đó quyết định backup của bạn có thể khôi phục được hay không. --with-key=auto mặc định sử dụng chip TPM2 (trusted platform module version 2) nếu chip này tồn tại và có thể sử dụng được; nếu không, nó dùng host key. Hầu hết VPS không có TPM2.
systemd-analyze has-tpm2no nghĩa là host key đã được sử dụng, và key đó nằm trong /var/lib/systemd/credential.secret, chỉ root mới đọc được. Nếu khôi phục db_password.cred lên một VPS mới nhưng thiếu file đó thì không có gì có thể giải mã secret, dù bao lâu đi nữa. Hãy sao chép 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 chính là mục đích: 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, và image Postgres đọ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 mã hóa nào: ./db_password.txt là một file plaintext trên host, và biện pháp bảo vệ duy nhất là mode cùng owner của file. Tự đặt cả hai thuộc tính này, vì Compose sẽ mount một file cho phép mọi user đọc mà không cảnh báo. Các trade-off đầy đủ hơn so với cách viết tắt env_file được trình bày trong hướng dẫn về env file và secret của Compose.
Chi phí vận hành thực tế của OpenBao và Vault
OpenBao là fork của HashiCorp Vault do Linux Foundation duy trì, bắt đầu sau khi HashiCorp chuyển Vault sang 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 vào tháng 8 năm 2026. Hầu hết nội dung dưới đây cũng áp dụng cho Vault, vì fork này vẫn 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 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 có thể khiến server ở trạng thái sealed, làm 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ị, cơ chế Shamir split 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 của cùng một người. Auto unseal chuyển key vào một thiết bị 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ự hoạt động trở 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 do bạn vẫn nắm giữ
Infisical là một secrets platform có web interface, project, environment và cơ chế kiểm soát quyền truy cập theo từng user. Self-host bằng Compose rất ngắn gọ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 -dSửa .env trước lệnh cuối cùng đó. Bạn phải tự đặt 2 giá trị, và 1 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 16 byte. Đây là key dùng để mã hóa secrets bên trong PostgreSQL. Nếu mất key này, một bản backup database nguyên vẹn cũng chỉ còn là một đống 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 32 byte dùng cho session. SITE_URL phải là absolute URL mà bạn thực sự sẽ 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 thứ bạn thực sự cần là quản lý user: một web interface cho team nhỏ và khả năng tách biệt giữa các environment, thay vì các 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 khởi động lại
Câu hỏi này quyết định liệu có nên đặt secrets service trên cùng một máy hay không. File có thể đọc được trước khi network khởi động. Service thì không.
Reboot máy chủ, rồi application và OpenBao khởi động cùng lúc. Application yêu cầu database password, nhưng OpenBao vẫn đang sealed. Request thất bại và systemd restart application lặp đi lặp lại cho đến khi có người nhập các unseal share. Không có gì bị hỏng. Nhưng cũng không có gì 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à một RestartSec= đủ dài để bạn không liên tục gửi request đến API. Hoặc fetch secret tại thời điểm deploy thay vì lúc boot: ghi secret vào file có mode 600 hoặc systemd credential, để hệ thống đang chạy 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 thời gian dài hơn. OpenBao token và lease có thời gian sống, 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 bất kỳ 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 store
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 public nào; age private key tại ~/.config/sops/age/keys.txt là thứ bạn tuyệt đối 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 một PostgreSQL dump và lưu ENCRYPTION_KEY ở nơi tách biệt với dump đó.
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 encrypted storage 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 hằng đêm để copy snapshot vào object storage trong khi các share không được lưu ở đâu thì không phải là bản sao lưu có thể sử dụng. Hãy test việc restore trên một VPS dùng để loại bỏ trước khi phụ thuộc vào nó.
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 cho biết ai đã thực sự đọc. auditd kèm cơ chế watch trên path là phương án thay thế gần nhất. Cách này 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 bật một cách 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 string trong request và response được hash bằng HMAC-SHA256 với salt. Vì vậy, bạn có thể đối chiếu một giá trị đã biết với log mà không để plaintext của giá trị đó xuất hiện trong log. Integer và boolean được ghi nguyên dạng, nên numeric secret không được bảo vệ bởi cơ chế hash này.
Tiếp theo là một bẫy vận hành: OpenBao sẽ không phản hồi request nếu không có audit device đang bật nào có thể ghi chúng. Một device bị lỗi theo kiểu blocking sẽ khiến request treo cho đến khi có người khắc phục. Disk đầy trên /var/log sẽ khiến secrets API ngừng hoạt động theo đúng thiết kế. Hãy dành riêng space cho audit log và tạo rule logrotate ngay từ ngày đầu, không phải sau outage đầu tiên.
Nên chạy secrets manager self-hosted nào?
Đế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 với quyền
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, để nếu host key bị đánh cắp thì chỉ các file của host đó bị giải mã.
- Nhiều máy và nhiều team thực sự cần database credentials có thời hạn, đồng thời có audit trail được đọc và kiểm tra: dùng OpenBao, và dành trong ngân sách 1 giờ mỗi tháng cho thời gian vận hành để unseal và thực hành restore.
Quy tắc chung cho cả 4 trường hợp là như nhau. Hãy chạy thành phần nhỏ nhất đáp ứng 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 với một secrets manager đang rỗng.
FAQ
Trình quản lý secrets tự host có đáng dùng cho một VPS duy nhất không?
Thường là không, nếu bạn nói đến các service như OpenBao hoặc Infisical. Trên một máy chỉ có một hoặc hai người sử dụng, file env với mode 600 hoặc credential được mã hóa của systemd cũng bảo vệ tương tự trước người dùng cục bộ khác, mà không cần bước unseal hay thêm service phải cập nhật bản vá. 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 xoay vòng thủ công.
Password manager khác secrets manager như thế nào?
Password manager lưu credential do con người nhập và người dùng sẽ mở khóa khi 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 theo dõi. Đ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ị seal sẽ dừng mọi service khởi động lại trong lúc nó vẫn bị seal.
Ứng dụng của tôi sẽ ra sao nếu OpenBao bị seal sau khi reboot?
Chúng không thể lấy secrets, nên khởi động thất bại. systemd sẽ restart chúng lặp đi lặp lại 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ó sẽ bị seal lại. Bạn có thể bật auto unseal, chấp nhận rằ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 render secrets ra file trong lúc deploy để việc boot không bao giờ phụ thuộc vào API.
Tôi có thể commit các file được mã hóa bằng SOPS vào repository công khai 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 đó 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. Giữ age private key bên ngoài repository và chạy sops updatekeys trên mọi file hiện có mỗi khi bạn thêm hoặc xóa recipient.