Mật khẩu Postgres chỉ sống một giờ với Vault AppRole
Mỗi service có tài khoản riêng và tự hết hạn, còn SecretID chỉ đăng nhập được đúng một lần.
Tóm tắt nhanh
- Ứng dụng không nên giữ credential đặc quyền. Nó dùng Vault token để xin tài khoản Postgres quyền hẹp, có hạn dùng.
- RoleID và SecretID phải đi qua hai kênh khác nhau và chỉ gặp nhau trên máy chạy ứng dụng.
- Bọc SecretID bằng response wrapping và đặt secret_id_num_uses=1, để SecretID có bị lộ cũng gần như vô dụng.
Tài khoản Postgres mà ứng dụng của bạn dùng hôm nay có thể vẫn còn hiệu lực sau ba năm nữa, và rất có thể đang nằm chung một file .env với năm service khác. Với secret động của HashiCorp Vault, tài khoản đó được tạo đúng lúc ứng dụng khởi động và tự hết hạn sau một giờ.
Bài này hướng dẫn bạn dựng lại luồng đó trên laptop. Ứng dụng đăng nhập Vault bằng AppRole, nhận một token ngắn hạn, rồi dùng token ấy xin một tài khoản Postgres riêng, quyền hẹp, có hạn dùng.
Bạn sẽ dựng gì, và cần chuẩn bị gì?
Kết quả cuối cùng: một service tên orders-app (tên giả định) đọc được bảng trong database orders, nhưng trong mã và cấu hình của nó không có mật khẩu database nào.
Bạn cần một Vault server chạy ở chế độ dev, một Postgres (container là đủ), CLI vault và psql. Các lệnh dưới đây là bản rút gọn để chạy lab. Tên tham số và đường dẫn có thể khác nhau giữa các phiên bản, nên hãy đối chiếu với trang tài liệu AppRole và PostgreSQL của Vault ứng với phiên bản khách hàng đang chạy.
Bước 1: để Vault giữ chìa khóa database, ứng dụng thì không
Hướng dẫn của Aiven nêu nguyên tắc nền: ứng dụng không nên cầm credential đặc quyền. Credential đó do Vault giữ. Ứng dụng chỉ dùng Vault token của mình để xin tài khoản quyền thấp hơn và có thời hạn.
vault secrets enable database
vault write database/config/orders-db \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/orders" \
allowed_roles="orders-readonly" \
username="vault_admin" password="mat-khau-tam"
Sau đó xoay mật khẩu gốc ngay, để mật khẩu bạn vừa gõ vào terminal hết giá trị. Tài liệu Vault có riêng một endpoint rotate root credentials cho việc này.
vault write -f database/rotate-root/orders-db
Kiểm tra: đăng nhập lại bằng psql với mat-khau-tam. Lần này phải thất bại. Từ giờ chỉ Vault biết mật khẩu của vault_admin.
Bước 2: một role tương ứng với một câu SQL
Tài liệu PostgreSQL của Vault mô tả role là một ánh xạ từ một cái tên trong Vault sang câu SQL dùng để tạo credential. Ví dụ trong tài liệu đặt default_ttl là 1h và max_ttl là 24h, bài này giữ nguyên hai con số đó.
vault write database/roles/orders-readonly \
db_name=orders-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl=1h max_ttl=24h
Thử tính với hai con số này. Giả sử một credential lọt vào log. Với mật khẩu tĩnh, kẻ nhặt được nó dùng được cho tới khi có người nhớ ra phải đổi.
Với role trên, credential hết hạn sau 1 giờ nếu không được gia hạn, và dù có gia hạn cũng không sống quá 24 giờ. Mặt trái là một service chạy liên tục ba ngày phải xin credential mới, nên mã kết nối của nó phải chịu được việc mật khẩu thay đổi.
Tiếp theo, viết policy chỉ cho phép đọc đúng đường dẫn này:
# orders-app.hcl
path "database/creds/orders-readonly" {
capabilities = ["read"]
}
vault policy write orders-app orders-app.hcl
Bước 3: AppRole với batch token và SecretID dùng một lần
AppRole dùng RoleID để chọn role. Theo mặc định, lúc login còn phải có thêm SecretID. Giống token, SecretID có giới hạn số lần dùng, TTL và thời điểm hết hạn. Trang best practice của HashiCorp viết rõ: muốn an toàn nhất thì đặt secret_id_num_uses bằng 1. Tài liệu AppRole cũng khuyến nghị dùng batch token.
vault auth enable approle
vault write auth/approle/role/orders-app \
token_type=batch \
token_policies=orders-app \
token_ttl=20m \
secret_id_num_uses=1 \
secret_id_ttl=10m
token_ttl=20m và secret_id_ttl=10m là giá trị lab tự chọn, không phải khuyến nghị của tài liệu. Ở khách hàng, hãy đặt theo thời gian khởi động thực tế của service.
Bước 4: tách hai nửa chìa khóa ra hai kênh
Đây là bước dễ làm sai nhất, vì chỉ cần một nơi cầm được cả hai thứ là cả cơ chế mất tác dụng. HashiCorp khuyến nghị giao RoleID và SecretID qua hai kênh khác nhau, để chúng chỉ gặp nhau trên máy chạy ứng dụng. Kênh thứ nhất mang RoleID:
vault read -field=role_id auth/approle/role/orders-app/role-id
SecretID đi kênh thứ hai và không được gửi dưới dạng plaintext. Với response wrapping, Vault cất SecretID vào cubbyhole của một token mới có số lần dùng là 1, rồi trả về token đó.
vault write -wrap-ttl=60s -f auth/approle/role/orders-app/secret-id
Lệnh trên trả về một wrapping token. Trên máy chạy ứng dụng, mở gói ra rồi login:
vault unwrap -field=secret_id <wrapping_token>
vault write auth/approle/login \
role_id=<role_id> secret_id=<secret_id>
Bài blog của HashiCorp về AppRole giải thích vì sao bọc lại như vậy đáng công: token chỉ dùng được một lần thì lộ ra cũng ít hại, và nếu ai đó đã mở gói trước thì ứng dụng biết ngay, vì nó không mở được nữa. Bài blog cũng nhấn mạnh SecretID chỉ nên tới tay ứng dụng vào lúc cần dùng, thường là khi khởi động.
Kiểm tra: chạy lại lệnh login với đúng SecretID vừa dùng. Lần này phải bị từ chối. Nếu vẫn login được, secret_id_num_uses chưa được áp dụng.
Bước 5: xin tài khoản Postgres và tận mắt thấy nó hết hạn
VAULT_TOKEN=<client_token> vault read database/creds/orders-readonly
Vault trả về username, password và thời hạn lease. Dùng cặp này đăng nhập bằng psql, chạy \du, và bạn sẽ thấy một role có tên do Vault sinh ra, kèm mốc VALID UNTIL.
Tài liệu database secrets engine chỉ ra một lợi ích ít người để ý: mỗi service vào database bằng credential riêng, nên khi phát hiện truy cập đáng ngờ, việc audit dễ hơn nhiều. Thử hình dung 12 service dùng chung một user app.
Log Postgres ghi app đã đọc bảng khách hàng lúc 3 giờ sáng, và bạn không biết đó là service nào. Khi mỗi lần xin credential sinh ra một tên riêng, câu hỏi đó có câu trả lời.
Những lỗi khiến cả hệ thống chỉ còn đẹp trên giấy
Lỗi đầu tiên cần tránh: cho CI worker quyền lấy cả RoleID lẫn SecretID. Trang best practice của HashiCorp gọi thẳng đây là làm trái best practice, vì ai chiếm được worker là có đủ cả hai nửa chìa khóa. Đúng ra, bên giao SecretID chỉ được phép sinh SecretID đã bọc, còn RoleID đến từ một kênh khác.
Lỗi thứ hai là bỏ qua wrapping, rồi nhét SecretID dạng plaintext vào biến môi trường của job. Lỗi thứ ba là để secret_id_num_uses không giới hạn. Khi đó SecretID thực chất lại thành một mật khẩu tĩnh, chỉ mang tên khác.
Lỗi thứ tư hay gặp ở các hệ thống cũ: cấu hình xong secret động nhưng vẫn để ứng dụng giữ credential gốc “phòng khi cần”. Nếu một số user cũ chưa bỏ được, Vault có static role để tự xoay mật khẩu của user đã tồn tại. Đó là bước chuyển tiếp hợp lý hơn nhiều so với một mật khẩu gốc nằm trong file cấu hình.
Ở site khách hàng, kỹ năng này trông thế nào?
Hiếm khi có ai giao cho bạn việc “triển khai AppRole”. Thường bạn chỉ nghe một câu kiểu “đội bảo mật không duyệt việc agent của các anh nối thẳng vào database”.
Bài blog của HashiCorp gọi gốc rễ của chuyện này là bài toán secret zero: ứng dụng cần một bí mật đầu tiên để lấy mọi bí mật khác. AppRole giải bài toán đó bằng một trusted broker, tức một hệ thống đáng tin chịu trách nhiệm giao SecretID.
Vì thế, việc đầu tiên nên làm ở khách hàng là vẽ ra ai đang giữ phần nào: RoleID đi qua kênh nào, hệ thống nào giao SecretID, và có job nào cầm được cả hai không. Bản vẽ một trang đó thường thuyết phục đội bảo mật nhanh hơn bất kỳ đoạn demo nào.
Trong CV, viết theo kết quả, chẳng hạn “thay credential Postgres tĩnh bằng secret động TTL 1h, SecretID dùng một lần, mỗi service một tài khoản để audit”.
Một mật khẩu không ai phải giữ là mật khẩu không ai làm lộ được. Ở site khách hàng, chính điều đó thường quyết định họ có cho bạn chạm vào dữ liệu thật hay không.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!
6 nguồn
- AppRole auth method | Vault | HashiCorp Developer
- Best practices for AppRole authentication | Vault | HashiCorp Developer
- How (and Why) to Use AppRole Correctly in HashiCorp Vault · 2021-08-30
- Database secrets engines | Vault | HashiCorp Developer
- PostgreSQL database secrets engine | Vault | HashiCorp Developer
- Secure your database access with HashiCorp Vault