Vaultwarden có an toàn không? Cách hardening
Vaultwarden không lưu plaintext: dữ liệu được mã hóa trên client. Bài viết chỉ ra rủi ro thật từ admin token, cổng public và file backup.
Vaultwarden có an toàn không? Câu trả lời ngắn gọn
Vaultwarden an toàn ở nơi quan trọng nhất: mọi mục trong vault được mã hóa trên thiết bị của bạn trước khi đến server. Server chỉ lưu các blob mà nó không thể đọc. Ngay cả khi ai đó sao chép toàn bộ database, họ vẫn cần master password để lấy được dữ liệu hữu ích.
Câu trả lời này phụ thuộc nhiều vào cách bạn cấu hình. Admin panel sử dụng token dễ đoán. Một cổng của container được publish ra toàn bộ Internet. Một config.json dạng plaintext. Một file tar backup nằm trong home directory trên chính máy đó. Không vấn đề nào trong số này liên quan đến cryptography. Tất cả đều là lý do khiến vault tự host bị lấy sạch dữ liệu.
Phần dưới đây giả định rằng bạn đã có một bản cài đặt đang hoạt động. Nếu chưa có, trước tiên hãy thiết lập theo hướng dẫn cài đặt Vaultwarden trên VPS, sau đó quay lại và thực hiện lần lượt danh sách này.
Dữ liệu thực tế server lưu trữ
Vaultwarden triển khai data model của Bitwarden. Tên, username, password, ghi chú và URI của một mục trong vault được mã hóa bằng key dẫn xuất từ master password trên client, trước khi gửi bất kỳ request nào. Nội dung file đính kèm cũng được mã hóa theo cách tương tự. Server nhận dữ liệu không thể đọc, kèm theo một UUID (universally unique identifier).
Một số dữ liệu không phải ciphertext. Bạn cần biết chính xác đó là những gì:
- Địa chỉ email của account, ở dạng plaintext.
- Các thiết lập KDF (key derivation function) và salt, vì client cần chúng để tạo lại key trong lần đăng nhập tiếp theo.
- Hash phía server của hash master password mà client gửi, được dùng để xác thực chính lần đăng nhập đó.
- Metadata: membership trong organisation, tên thiết bị và thời điểm đăng nhập gần nhất.
- Secret của phương thức two-factor bảo vệ lần đăng nhập Vaultwarden. Secret này nằm không mã hóa trong bảng
twofactor, vì server phải tính code dự kiến để so sánh với code của bạn. Đây không phải secret TOTP (time-based one-time password) mà bạn lưu bên trong một mục trong vault; secret đó được mã hóa như mọi field khác.
Thư mục dữ liệu có kích thước nhỏ. Với bản cài Docker, đó là thư mục bạn mount tại /data.
sudo ls -l /vw-data/db.sqlite3 chứa gần như toàn bộ state. attachments/ chứa các file đã upload, mỗi file tương ứng với một UUID, và đây là loại dữ liệu quan trọng duy nhất không nằm trong các table của database. sends/ chứa attachment của Send và được thiết kế để dùng tạm thời. icon_cache/ có thể xóa và tạo lại. rsa_key.pem cùng các file liên quan dùng để ký JWT (JSON web tokens) của người dùng đã đăng nhập. Vì vậy, bản sao của private key này có thể được dùng để giả mạo session đăng nhập vào vault. config.json chỉ xuất hiện sau khi bạn bật trang admin. Project nói rõ: file này chứa admin token và thông tin xác thực SMTP ở dạng plaintext.
Vì vậy, threat model thực tế là quyền truy cập filesystem, không phải cryptography trên network. Quyền đọc thư mục đó cho phép lấy toàn bộ địa chỉ email của người dùng, secret 2FA đăng nhập, key dùng để giả mạo session và bản sao offline của mọi vault để tấn công tùy ý. Mọi bước bên dưới đều nhằm ngăn người khác truy cập thư mục đó.
Trước hết, hãy sửa admin token
/admin là một control panel đầy đủ: danh sách người dùng, lời mời, xóa người dùng và mọi thiết lập runtime. Nó chỉ được bảo vệ bằng một shared secret duy nhất. Không có username. Không có two-factor riêng cho từng người dùng.
Các hướng dẫn cũ yêu cầu bạn tạo ADMIN_TOKEN bằng openssl rand -base64 48. Cách này hoạt động, nhưng ghi secret dưới dạng plaintext vào config.json và file compose của bạn. Vaultwarden cũng chấp nhận chuỗi Argon2 PHC (password hashing competition), nên giá trị được lưu sẽ là hash. Hãy tạo hash với một container đang chạy:
docker exec -it vaultwarden /vaultwarden hashHoặc không cần tác động đến container đang chạy:
docker run --rm -it vaultwarden/server /vaultwarden hashLệnh này yêu cầu nhập password 2 lần, sau đó in ra một dòng bắt đầu bằng $argon2id$. Với bản cài đặt bare-metal, chạy ./vaultwarden hash. Nếu muốn dùng trực tiếp CLI của argon2, project có ghi rõ các tham số tối thiểu theo OWASP:
echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1Đây là lỗi dễ khiến bạn mất 1 giờ để tìm nguyên nhân. Chuỗi PHC chứa rất nhiều ký tự $, còn Docker Compose xử lý $ bằng variable interpolation. Nếu dán chuỗi này chưa escape vào block environment:, giá trị truyền đến container sẽ bị biến dạng, khiến /admin từ chối token mà bạn biết là chính xác. Có 2 cách an toàn. Trong docker-compose.yml, nhân đôi mọi $:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UITrong file .env, không cần escape, nhưng hãy dùng single quotes:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'Sau đó giới hạn rate của panel và rút ngắn thời gian session:
ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=20Nếu một client thực hiện 3 lần thử sai trong 5 phút, panel sẽ ngừng phản hồi client đó. Admin session sẽ hết hạn sau 20 phút không hoạt động.
Cách tốt hơn tất cả các cách trên là tắt hẳn trang này. Hầu hết instance chỉ cần dùng trang này một lần để cấu hình SMTP và mời những người dùng đầu tiên, sau đó không cần lại nữa. Để tắt trang, không đặt ADMIN_TOKEN hoặc DISABLE_ADMIN_TOKEN, xóa mọi key "admin_token" khỏi config.json, rồi recreate container. Việc xóa key khỏi file rất quan trọng vì trang admin ghi các thiết lập vào đó, và giá trị trong config.json sẽ được ưu tiên hơn environment. Chỉ xóa variable là chưa đủ; trang vẫn sẽ mở.
Đóng đăng ký trước khi có người phát hiện domain
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseSIGNUPS_ALLOWED mặc định là true. Hãy giữ nguyên như vậy thì bất kỳ ai truy cập được domain của bạn đều có thể tạo account, và dữ liệu của họ nằm trong cùng db.sqlite3 với dữ liệu của bạn. Đặt thành false rồi thêm người dùng qua lời mời từ trang admin; cách này cần SMTP hoạt động. INVITATIONS_ALLOWED cũng mặc định là true và cho phép owner của organisation mời người khác. Điều đó phù hợp khi bạn tin tưởng người dùng, và nên được đặt thành false trên instance chỉ có một người dùng. Nếu chỉ một số domain nhất định được phép đăng ký, SIGNUPS_DOMAINS_WHITELIST=example.com có phạm vi hẹp hơn đăng ký mở nhưng kém an toàn hơn nhiều so với lời mời.
SHOW_PASSWORD_HINT mặc định là false và nên giữ nguyên như vậy. Khi bật tùy chọn này, nhập một địa chỉ email hợp lệ vào form đăng nhập sẽ trả về gợi ý master password của account đó. Điều này vừa làm lộ gợi ý, vừa xác nhận địa chỉ email đó tồn tại.
Nếu instance của bạn từng cho phép đăng ký mở trong bất kỳ khoảng thời gian nào, hãy mở trang admin và đọc danh sách người dùng trước khi cho rằng chỉ có account của bạn trên đó.
Cổng bạn không định public
Docker image lắng nghe trên port 80 bên trong container. Bản cài bare metal mặc định dùng ROCKET_PORT=8000. Lệnh run được tài liệu hướng dẫn publish port như sau:
--publish 127.0.0.1:8000:80Prefix 127.0.0.1: mới là phần quyết định. Nếu viết -p 8000:80 thay vì vậy, Docker sẽ bind 0.0.0.0 và thêm các rule DNAT (destination network address translation) vào table nat. Các rule này được xử lý trước các chain filter do ufw quản lý. Vì vậy, ufw status báo port bị deny, nhưng port đó vẫn trả lời từ Internet. Cơ chế đầy đủ được giải thích trong hướng dẫn về cách Docker bypass ufw khi publish port.
Kiểm tra tiến trình nào thực sự đang lắng nghe:
sudo ss -tlnp | grep 8000Kết quả đúng là chỉ có một dòng bind vào 127.0.0.1:8000. Dòng bind vào 0.0.0.0:8000 nghĩa là vault đang được expose trực tiếp. Sửa mapping rồi recreate container, vì port binding được cố định khi container được tạo và docker compose restart sẽ không thay đổi binding đó:
docker compose up -d --force-recreateMột port khác vẫn xuất hiện trong các hướng dẫn cũ: 3012, port WebSocket riêng. Vaultwarden đã bỏ hỗ trợ port này từ phiên bản 1.31.0 vì traffic notification đã chuyển sang port HTTP chính. WEBSOCKET_ENABLED và WEBSOCKET_PORT đã bị bỏ qua từ phiên bản 1.29.0. Tùy chọn hiện tại là ENABLE_WEBSOCKET, mặc định là true. Nếu firewall hoặc file compose của bạn vẫn mở port 3012, hãy đóng port đó.
Kết thúc TLS tại reverse proxy, không phải trong Rocket
Vaultwarden có thể tự phục vụ TLS (transport layer security) thông qua Rocket, web framework của nó, nhưng project khuyến cáo không làm vậy trong production. TLS tích hợp sẵn của Rocket không hỗ trợ SNI (server name indication) chặt chẽ. Đây cũng là lý do hướng dẫn hardening yêu cầu truy cập instance bằng hostname và không bao giờ dùng bare IP address. Các dải public IP liên tục bị scan. Một vault phản hồi trên IP address sẽ bị phát hiện.
Các phần quan trọng trong server block của nginx:
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}nginx mặc định đặt client_max_body_size ở mức 1 MB. Vì vậy, nếu thiếu dòng đó, upload attachment sẽ fail với 413 Request Entity Too Large trong nginx error log, còn Vaultwarden hoàn toàn không ghi log. Các header Upgrade và Connection chuyển tiếp WebSocket handshake đến /notifications/hub. Nếu bỏ chúng, vault vẫn hoạt động, nhưng các thay đổi sẽ không xuất hiện trên những thiết bị khác cho đến khi bạn tự reload trang.
Caddy có cấu hình ngắn hơn và tự lấy certificate:
vw.example.com {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
}Sau đó, khai báo thông tin này cho Vaultwarden:
DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IPIP_HEADER đã mặc định là X-Real-IP. Vì vậy, cần bảo đảm proxy thực sự set header đó. Nếu không, mọi dòng log và mọi rate limit đăng nhập đều thấy 127.0.0.1, tức chính proxy. Khi đó, các lần fail của một attacker sẽ bị tính cho mọi user trên instance. Đồng thời đặt DOMAIN thành URL https thực tế, vì Vaultwarden dùng URL này để tạo link invitation và password reset. Các security key WebAuthn cũng được bind với origin đó.
Có một chi tiết thường bị bỏ sót: kết nối WebSocket truyền session token trong query string dưới dạng /notifications/hub?access_token=[JWT]. Token này sẽ xuất hiện rõ trong proxy access log. Hãy redact parameter access_token trong log format, hoặc bảo đảm các log đó không được gửi đến bất kỳ nơi nào bạn không kiểm soát.
Chặn brute force tại endpoint đăng nhập
Rate limit được bật mặc định (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Chúng làm chậm kẻ tấn công. Chúng không ngăn được kẻ tấn công. fail2ban mới làm được việc đó, nhưng Vaultwarden trước hết phải ghi log vào một file, trong khi mặc định nó không làm vậy:
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueMột lần đăng nhập thất bại sau đó sẽ tạo đúng một dòng. Đây là chuỗi mà filter của bạn phải khớp:
[2026-08-07 09:14:22][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 203.0.113.10. Username: user@example.com.Ghi filter vào /etc/fail2ban/filter.d/vaultwarden.local:
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =Và jail vào /etc/fail2ban/jail.d/vaultwarden.local:
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400Nếu bạn vẫn giữ trang admin, hãy thêm một jail thứ hai có failregex là ^.*Invalid admin token\. IP: <ADDR>.*$, vì các lần đăng nhập admin thất bại được ghi bằng message khác và login filter sẽ không bao giờ bắt được chúng. Sau đó kiểm tra cấu hình:
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwardenJail hoạt động sẽ liệt kê log file của bạn dưới File list và báo Currently failed: 0. Nhập sai password 3 lần từ một network khác. Counter đó sẽ tăng, rồi địa chỉ sẽ xuất hiện dưới Banned IP list. Nếu counter không tăng, nguyên nhân thường gặp là logpath: đây phải là path của file trên host, không phải path /data/... bên trong container. Nguyên nhân thường gặp thứ hai là thiếu X-Real-IP, khiến mọi lệnh ban đều nhắm vào chính proxy của bạn. Phần còn lại của cấu hình, bao gồm SSH jail mà bạn đáng lẽ đã chạy, có trong hướng dẫn fail2ban cho Ubuntu 24.04.
Mật khẩu master vẫn là toàn bộ hệ thống
Mã hóa phía client nghĩa là mật khẩu master chính là key. Mật khẩu master ngắn trên một instance mà attacker đã sao chép database không được bảo vệ bởi bất kỳ thiết lập nào trong bài này, vì attacker sẽ tấn công bản sao đó offline với tốc độ mà phần cứng của họ cho phép. Không có thiết lập nào trên server tác động được đến máy của attacker.
PASSWORD_ITERATIONS=600000 rất dễ bị hiểu nhầm. Thiết lập này đặt số vòng lặp cho hash phía server của login hash mà client gửi lên. Template cấu hình của project cho biết giá trị đã thay đổi sẽ được áp dụng cho các user hiện có vào lần login tiếp theo. Thiết lập này không tác động đến KDF phía client. KDF mới là thiết lập quyết định chi phí của cuộc tấn công offline vào vault. Thiết lập đó thuộc về từng account. Người đăng ký từ nhiều năm trước có thể vẫn đang dùng KDF cũ, thấp hơn, cho đến khi họ tăng mức này trong phần cài đặt bảo mật của web vault. Thao tác đó sẽ mã hóa lại key của họ. Hãy nói rõ điều này cho họ, vì server không thể làm thay họ.
Sau đó, hãy bật xác thực two-factor cho từng account. Tính năng này không bảo vệ ciphertext, vì vault key chỉ được tạo từ mật khẩu master. Nó ngăn việc mật khẩu bị đánh cắp đủ để attacker login và đồng bộ một bản sao. REQUIRE_DEVICE_EMAIL=true bắt buộc phải gửi email login trên thiết bị mới. Nếu Vaultwarden không thể gửi email, login sẽ fail. Vì vậy, một lần login từ thiết bị chưa được nhận diện sẽ không diễn ra mà không có thông báo. Tính năng này cần SMTP hoạt động bình thường; nếu không, mọi lần login trên thiết bị mới sẽ bị từ chối.
Sao lưu là điểm các vault tự host thường gặp lỗi
Một tar czf của thư mục dữ liệu, để trong thư mục home trên cùng VPS, sẽ vô hiệu hóa mọi bước ở trên. Archive đó chứa db.sqlite3 với ciphertext của mọi user, rsa_key.pem có thể giả mạo các phiên đăng nhập, và config.json chứa admin token cùng mật khẩu SMTP ở dạng plaintext. Có quyền đọc file đó đồng nghĩa với có quyền đọc vault.
Hai quy tắc là đủ. Đưa archive ra khỏi máy chủ. Mã hóa archive trước khi truyền đi.
Cũng có một vấn đề về tính toàn vẹn dữ liệu. Sao chép db.sqlite3 bằng cp khi service đang chạy có thể tạo ra file đang được ghi dở và không mở được. Bạn sẽ chỉ phát hiện ra lỗi này khi restore. Hãy dùng snapshot của chính SQLite:
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"Phần restore, là phần hầu như không ai kiểm thử, được trình bày trong hướng dẫn backup và restore Vaultwarden.
Điều bạn phải đánh đổi so với Bitwarden hosted
Đánh giá thẳng thắn. Dịch vụ hosted của Bitwarden được vận hành bởi những người có công việc toàn thời gian là quản trị dịch vụ, có các cuộc audit bên thứ ba được công bố và có người trực lúc 3 giờ sáng. Self-hosting thay thế mô hình đó bằng lịch cập nhật bản vá của chính bạn.
Vaultwarden phát hành các bản sửa lỗi bảo mật dưới dạng các release thông thường, và các release này xuất hiện khá thường xuyên. Kiểm tra vào ngày 7 tháng 10 năm 2026 theo release feed của dự án, release hiện tại là 1.37.4, được phát hành ngày 5 tháng 10 năm 2026. Release notes nêu 7 security advisory, trong đó mức nghiêm trọng nhất là High, đồng thời khuyến cáo mạnh mẽ phải cập nhật sớm nhất có thể. Đây là release thứ tư kể từ 1.37.0 vào ngày 24 tháng 7 năm 2026: 1.37.1 phát hành ngày 29 tháng 7, 1.37.2 ngày 22 tháng 8 (bắt buộc đối với client phiên bản 2026.8.0 trở lên), và 1.37.3 ngày 13 tháng 9, trong đó thu hồi remember token của 2FA khi credentials hoặc 2FA thay đổi, đồng thời rate limit các endpoint prelogin và auth request. Nếu bạn chạy các organization có admin mà bạn không hoàn toàn tin cậy, release notes của 1.37.4 cũng khuyến nghị rotate API key của từng organization sau khi cập nhật. Một instance bạn thiết lập từ một năm trước rồi quên mất vẫn đang chạy code từ một năm trước. Tag latest tự nó không giúp ích: một container đang chạy vẫn giữ image mà nó đã khởi động cùng cho đến khi bạn chạy docker compose pull rồi recreate container. Hãy triển khai unattended upgrades trên Ubuntu cho các package của host, đồng thời đặt lịch cập nhật container bằng một lời nhắc mà bạn thực sự sẽ đọc.
Kết luận mà một người đọc thẳng thắn nên rút ra là: cryptography ở đây dựa trên thiết kế của Bitwarden và vẫn đáp ứng yêu cầu, còn rủi ro vận hành hoàn toàn chuyển sang bạn. Nếu bạn patch hệ thống và backup dữ liệu ở một nơi khác, một instance Vaultwarden trên VPS do bạn kiểm soát là nơi hợp lý để lưu password. Nếu bạn không thể duy trì được 2 thói quen đó, hãy trả phí cho dịch vụ hosted và dành thời gian cho việc khác. Bảng so sánh theo từng tính năng có tại Vaultwarden so với Bitwarden self-hosted.
Hardening máy chủ bên dưới container
Vaultwarden là một process trên máy Linux. root trên máy đó vẫn có thể đọc /vw-data bất kể ứng dụng được cấu hình thế nào. Chạy container bằng user không có quyền đặc biệt với user: "1000:1000" trong file compose, đồng thời đặt owner của thư mục dữ liệu cho khớp. Gắn mọi thứ mà container không cần ghi ở chế độ chỉ đọc bằng :ro. Sau đó đóng cửa vào: Hardening SSH trên VPS hướng dẫn chỉ cho đăng nhập bằng key và tắt xác thực bằng password. Đây là cách ngăn kiểu tấn công đơn giản vượt qua tất cả các lớp bảo vệ trên.
FAQ
Người khác có thể đọc mật khẩu của tôi nếu họ đánh cắp database Vaultwarden không?
Không trực tiếp. Mỗi mục trong vault được mã hóa trên client bằng key bắt nguồn từ master password, vì vậy db.sqlite3 chỉ chứa ciphertext. Thông tin họ lấy được ngay gồm địa chỉ email của từng account, các thiết lập KDF, metadata đăng nhập và thiết bị, cùng các secret two-factor trong bảng twofactor. Các secret này được lưu không mã hóa vì server phải tính code dự kiến. Họ cũng có thể tấn công ciphertext của vault offline tùy ý trong thời gian dài. Vì vậy, độ dài master password là yếu tố quyết định kết quả.
Tôi nên dùng ADMIN_TOKEN hay tắt hoàn toàn trang admin?
Nếu có thể, hãy tắt trang này. Hầu hết instance chỉ cần dùng nó một lần để cấu hình SMTP và mời user, sau đó không cần nữa. Để tắt, không đặt ADMIN_TOKEN hoặc DISABLE_ADMIN_TOKEN, xóa mọi key "admin_token" khỏi config.json, rồi tạo lại container. Chỉ xóa biến môi trường là chưa đủ, vì các thiết lập được ghi từ trang admin nằm trong config.json và được ưu tiên. Nếu vẫn giữ trang admin, hãy lưu token dưới dạng hash Argon2 do vaultwarden hash tạo ra thay vì một chuỗi random plaintext, rồi đặt ADMIN_RATELIMIT_MAX_BURST=3.
ADMIN_TOKEN của tôi đúng nhưng /admin từ chối token. Lỗi ở đâu?
Gần như luôn là do nội suy $. Chuỗi PHC của Argon2 chứa nhiều ký tự $. Docker Compose sẽ mở rộng chúng thành biến khi chúng nằm trong block docker-compose.yml environment:, nên container nhận được giá trị bị biến dạng dù file của bạn trông vẫn đúng. Trong file compose, hãy nhân đôi mọi $ thành $$, hoặc chuyển giá trị vào file .env được bao quanh bằng dấu nháy đơn. Khi đó không cần escape. Sau đó tạo lại container, vì restart không nạp các thay đổi của environment.
Tôi còn cần mở port 3012 cho notification không?
Không. Hỗ trợ traffic WebSocket trên port 3012 đã bị xóa trong Vaultwarden 1.31.0 vì notification đã chuyển sang port HTTP chính. WEBSOCKET_ENABLED và WEBSOCKET_PORT cũng bị bỏ qua kể từ 1.29.0. Thiết lập hiện tại là ENABLE_WEBSOCKET, mặc định là true. Hãy đóng port 3012 trong firewall và xóa port này khỏi file compose. Sau đó bảo đảm reverse proxy forward các header Upgrade và Connection, vì đồng bộ real-time hiện phụ thuộc vào chúng.