Vaultwarden ปลอดภัยไหม? วิธีตั้งค่าให้ปลอดภัยสูงสุด
Vaultwarden มีระบบเข้ารหัสข้อมูลที่ปลอดภัยสูง แต่ความเสี่ยงมักเกิดจาก ADMIN_TOKEN ที่เดาง่าย การเปิดพอร์ตสาธารณะ และการสำรองข้อมูลที่ไม่รัดกุม เรียนรู้วิธีป้องกันการถูกบุกรุก
Vaultwarden ปลอดภัยหรือไม่? คำตอบโดยสรุป
Vaultwarden มีความปลอดภัยในจุดที่สำคัญที่สุด เนื่องจากข้อมูลทุกรายการใน vault จะถูกเข้ารหัสบนอุปกรณ์ของคุณก่อนที่จะส่งไปยังเซิร์ฟเวอร์ เซิร์ฟเวอร์จะจัดเก็บเพียงข้อมูลที่อ่านไม่ได้ หากมีผู้คัดลอกฐานข้อมูลทั้งหมดไป ก็ยังจำเป็นต้องใช้ master password เพื่อถอดรหัสข้อมูลเหล่านั้นออกมา
คำตอบข้างต้นเป็นเพียงส่วนหนึ่งเท่านั้น ส่วนที่มักจะเกิดปัญหาคือส่วนที่คุณเป็นผู้กำหนดค่าเอง เช่น แผงควบคุมผู้ดูแลระบบที่ใช้ token ซึ่งเดาได้ง่าย, การเปิดพอร์ตของ container สู่สาธารณะบนอินเทอร์เน็ต, การเก็บ config.json ไว้ในรูปแบบข้อความธรรมดา หรือการเก็บไฟล์สำรองข้อมูล tarball ไว้ใน home directory บนเครื่องเดียวกัน สิ่งเหล่านี้ไม่ใช่ปัญหาด้านวิทยาการรหัสลับ แต่เป็นสาเหตุที่ทำให้ vault ที่โฮสต์ด้วยตนเองถูกบุกรุก
เนื้อหาทั้งหมดด้านล่างนี้ตั้งอยู่บนสมมติฐานว่าคุณได้ติดตั้งระบบเรียบร้อยแล้ว หากคุณยังไม่มีการติดตั้ง ให้ดำเนินการตาม คู่มือการติดตั้ง Vaultwarden สำหรับ VPS ให้เสร็จสิ้นก่อน แล้วจึงกลับมาทำตามรายการนี้ตามลำดับ
สิ่งที่เซิร์ฟเวอร์จัดเก็บจริง
Vaultwarden ใช้รูปแบบข้อมูลของ Bitwarden รายการใน vault ทั้งชื่อ, ชื่อผู้ใช้, รหัสผ่าน, บันทึก และ URI จะถูกเข้ารหัสด้วยกุญแจที่สร้างจากรหัสผ่านหลักของคุณที่ฝั่งไคลเอนต์ ก่อนที่จะส่งคำขอใดๆ ออกไป เนื้อหาของไฟล์แนบจะถูกเข้ารหัสด้วยวิธีเดียวกัน เซิร์ฟเวอร์จะได้รับเพียงข้อมูลที่อ่านไม่ออกพร้อมกับ UUID (universally unique identifier) ที่แนบมาด้วย
มีข้อมูลบางอย่างที่ไม่ใช่ ciphertext และคุณควรทราบว่ามีอะไรบ้าง:
- ที่อยู่อีเมลบัญชีของคุณ ซึ่งอยู่ในรูปแบบ plaintext
- การตั้งค่า KDF (key derivation function) และ salt ของคุณ เนื่องจากไคลเอนต์จำเป็นต้องใช้ข้อมูลเหล่านี้เพื่อสร้างกุญแจใหม่ในการเข้าสู่ระบบครั้งถัดไป
- ค่า hash ฝั่งเซิร์ฟเวอร์ของ hash รหัสผ่านหลักที่ไคลเอนต์ส่งมา ซึ่งใช้สำหรับยืนยันตัวตนในการเข้าสู่ระบบ
- ข้อมูลเมตา (Metadata): การเป็นสมาชิกองค์กร, ชื่ออุปกรณ์, เวลาที่เข้าสู่ระบบล่าสุด
- ความลับ (Secret) สำหรับวิธีการยืนยันตัวตนสองชั้น (2FA) ที่ใช้ป้องกันการเข้าสู่ระบบ Vaultwarden ข้อมูลนี้จะอยู่ในตาราง
twofactorโดยไม่มีการเข้ารหัส เนื่องจากเซิร์ฟเวอร์ต้องคำนวณรหัสที่คาดหวังเพื่อนำมาเปรียบเทียบกับรหัสของคุณ ข้อมูลนี้ไม่เหมือนกับความลับของ TOTP (time-based one-time password) ที่คุณจัดเก็บไว้ ภายใน รายการ vault ซึ่งจะถูกเข้ารหัสเหมือนกับฟิลด์อื่นๆ
โฟลเดอร์ข้อมูลมีขนาดเล็ก สำหรับการติดตั้งแบบ Docker ข้อมูลจะอยู่ที่ตำแหน่งที่คุณ mount ไว้ที่ /data
sudo ls -l /vw-data/db.sqlite3 จัดเก็บสถานะเกือบทั้งหมด attachments/ จัดเก็บไฟล์ที่อัปโหลด โดยแยกไฟล์ตาม UUID และเป็นข้อมูลประเภทเดียวที่สำคัญซึ่งไม่ได้อยู่ในตารางฐานข้อมูล sends/ จัดเก็บไฟล์แนบของ Send และมีไว้เพื่อเป็นข้อมูลชั่วคราว icon_cache/ เป็นข้อมูลที่ทิ้งได้ rsa_key.pem และไฟล์ที่เกี่ยวข้องใช้สำหรับลงนาม JWTs (JSON web tokens) ของผู้ใช้ที่เข้าสู่ระบบ ดังนั้นหากได้กุญแจส่วนตัว (private key) นี้ไป ก็สามารถใช้ปลอมแปลงเซสชันการเข้าสู่ระบบ vault ได้ config.json จะปรากฏขึ้นก็ต่อเมื่อคุณเปิดใช้งานหน้าผู้ดูแลระบบ (admin page) และโครงการได้ระบุไว้อย่างชัดเจนว่า ไฟล์นี้จัดเก็บโทเค็นผู้ดูแลระบบและข้อมูลรับรอง SMTP ของคุณในรูปแบบ plaintext
ดังนั้น แบบจำลองภัยคุกคามในทางปฏิบัติคือการเข้าถึงระบบไฟล์ ไม่ใช่การเข้ารหัสผ่านเครือข่าย การเข้าถึงแบบอ่าน (read access) ในไดเรกทอรีนั้นจะทำให้ผู้บุกรุกได้อีเมลของผู้ใช้ทุกคน, ความลับ 2FA สำหรับเข้าสู่ระบบ, กุญแจสำหรับปลอมแปลงเซสชัน และสำเนาของ vault ทุกรายการแบบออฟไลน์เพื่อนำไปโจมตีได้ตามต้องการ ทุกขั้นตอนด้านล่างนี้มีไว้เพื่อป้องกันไม่ให้ผู้อื่นเข้าถึงไดเรกทอรีดังกล่าว
แก้ไข admin token ก่อน
/admin เป็นแผงควบคุมแบบเต็มรูปแบบ ประกอบด้วยรายการผู้ใช้, การเชิญ, การลบ และการตั้งค่ารันไทม์ทุกอย่าง โดยได้รับการป้องกันด้วยรหัสลับร่วม (shared secret) เพียงอย่างเดียว ไม่มีชื่อผู้ใช้ และไม่มีการตรวจสอบสิทธิ์สองชั้น (2FA) รายบุคคล
คู่มือรุ่นเก่าแนะนำให้สร้าง ADMIN_TOKEN ด้วย openssl rand -base64 48 ซึ่งใช้งานได้จริง โดยจะเขียนรหัสลับแบบข้อความธรรมดาลงใน config.json และในไฟล์ compose ของคุณ อย่างไรก็ตาม Vaultwarden รองรับสตริง Argon2 PHC (password hashing competition) ด้วย ดังนั้นค่าที่จัดเก็บจึงเป็นค่าแฮชแทน คุณสามารถสร้างค่านี้จากคอนเทนเนอร์ที่กำลังทำงานอยู่ได้ดังนี้:
docker exec -it vaultwarden /vaultwarden hashหรือสร้างโดยไม่ต้องยุ่งกับคอนเทนเนอร์ที่กำลังทำงานอยู่เลย:
docker run --rm -it vaultwarden/server /vaultwarden hashระบบจะถามรหัสผ่านสองครั้ง จากนั้นจะแสดงบรรทัดที่ขึ้นต้นด้วย $argon2id$ หากเป็นการติดตั้งแบบ bare-metal ให้รัน ./vaultwarden hash หากคุณต้องการใช้ CLI ของ argon2 โดยตรง โครงการได้ระบุพารามิเตอร์ขั้นต่ำตามมาตรฐาน OWASP ไว้ดังนี้:
echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1ตอนนี้คือกับดักที่ทำให้เสียเวลาไปเป็นชั่วโมง สตริง PHC นั้นเต็มไปด้วยอักขระ $ และ Docker Compose จะถือว่า $ เป็นการแทนที่ตัวแปร (variable interpolation) หากคุณวางสตริงโดยไม่ได้ escape ลงในบล็อก environment: ค่าที่ส่งไปยังคอนเทนเนอร์จะผิดเพี้ยน ทำให้ /admin ปฏิเสธ token ที่คุณมั่นใจว่าถูกต้อง มีวิธีที่ปลอดภัยสองวิธี ใน docker-compose.yml ให้เบิ้ลอักขระ $ เป็นสองเท่า:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UIในไฟล์ .env ไม่จำเป็นต้องทำการ escape แต่ให้ใช้เครื่องหมายคำพูดเดี่ยว (single quotes):
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'จากนั้นให้จำกัดอัตราการเข้าถึง (rate limit) แผงควบคุมและลดระยะเวลา session ลง:
ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=20หากพยายามเข้าใช้งานผิดพลาด 3 ครั้งภายใน 5 นาที แผงควบคุมจะหยุดตอบสนองต่อไคลเอ็นต์นั้น และ session ของผู้ดูแลระบบจะหมดอายุหลังจากไม่มีการใช้งานเป็นเวลา 20 นาที
วิธีที่ดีกว่านี้คือการปิดหน้าดังกล่าวไปเลย อินสแตนซ์ส่วนใหญ่ต้องการหน้า admin เพียงครั้งเดียวเพื่อตั้งค่า SMTP และเชิญผู้ใช้กลุ่มแรก จากนั้นก็ไม่จำเป็นต้องใช้อีก หากต้องการปิดใช้งาน ให้เว้นว่างทั้ง ADMIN_TOKEN และ DISABLE_ADMIN_TOKEN ลบคีย์ "admin_token" ออกจาก config.json แล้วสร้างคอนเทนเนอร์ขึ้นใหม่ การลบคีย์ออกจากไฟล์มีความสำคัญเนื่องจากหน้า admin จะเขียนการตั้งค่าลงในไฟล์นั้น และสิ่งที่อยู่ใน config.json จะมีความสำคัญเหนือกว่าตัวแปรสภาพแวดล้อม (environment variable) การลบเพียงตัวแปรออกอย่างเดียวจะยังคงเปิดหน้าดังกล่าวทิ้งไว้
ปิดการลงทะเบียนก่อนที่จะมีใครพบโดเมนของคุณ
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseSIGNUPS_ALLOWED มีค่าเริ่มต้นเป็น true หากปล่อยไว้เช่นนั้น ใครก็ตามที่เข้าถึงโดเมนของคุณจะสามารถสร้างบัญชีได้ และข้อมูลของพวกเขาจะถูกเก็บไว้ใน db.sqlite3 เดียวกับคุณ ให้ตั้งค่าเป็น false และเพิ่มผู้ใช้งานผ่านคำเชิญจากหน้าผู้ดูแลระบบ ซึ่งจำเป็นต้องมีการตั้งค่า SMTP ที่ใช้งานได้ INVITATIONS_ALLOWED มีค่าเริ่มต้นเป็น true เช่นกัน ซึ่งอนุญาตให้เจ้าขององค์กรเชิญผู้อื่นได้ วิธีนี้เหมาะสมหากคุณเชื่อใจผู้ใช้งานของคุณ และควรตั้งค่าเป็น false สำหรับ instance ที่มีผู้ใช้งานเพียงคนเดียว หากต้องการจำกัดให้เฉพาะบางโดเมนเท่านั้นที่ลงทะเบียนได้ SIGNUPS_DOMAINS_WHITELIST=example.com จะมีความจำกัดมากกว่าการเปิดลงทะเบียนทั่วไป แต่ก็มีความปลอดภัยน้อยกว่าการใช้ระบบคำเชิญมาก
SHOW_PASSWORD_HINT มีค่าเริ่มต้นเป็น false และควรคงค่านี้ไว้ หากเปิดใช้งาน การพิมพ์ที่อยู่อีเมลที่ถูกต้องลงในแบบฟอร์มเข้าสู่ระบบจะแสดงคำใบ้รหัสผ่านหลักของบัญชีนั้น ซึ่งเป็นการเปิดเผยทั้งคำใบ้และยืนยันว่าที่อยู่อีเมลดังกล่าวมีอยู่จริง
หาก instance ของคุณเปิดให้ลงทะเบียนทิ้งไว้ในช่วงเวลาใดก็ตาม ให้เปิดหน้าผู้ดูแลระบบและตรวจสอบรายชื่อผู้ใช้งานก่อนที่จะสรุปว่าคุณเป็นผู้ใช้งานเพียงคนเดียวในระบบ
พอร์ตที่คุณไม่ได้ตั้งใจจะเปิดเผย
Docker image จะรับฟังที่พอร์ต 80 ภายในคอนเทนเนอร์ ในขณะที่การติดตั้งแบบ bare-metal จะใช้ค่าเริ่มต้นเป็น ROCKET_PORT=8000 คำสั่ง run ที่ระบุไว้ในเอกสารจะทำการเผยแพร่พอร์ตดังนี้:
--publish 127.0.0.1:8000:80คำนำหน้า 127.0.0.1: คือประเด็นสำคัญทั้งหมด หากคุณเขียนเป็น -p 8000:80 แทน Docker จะทำการ bind เข้ากับ 0.0.0.0 โดยการเขียนกฎ DNAT (destination network address translation) ลงในตาราง nat กฎเหล่านี้จะถูกประเมินก่อน chain filter ที่ ufw จัดการ ส่งผลให้ ufw status รายงานว่าพอร์ตถูกปฏิเสธการเข้าถึง ทั้งที่จริงแล้วพอร์ตนั้นยังคงตอบสนองต่ออินเทอร์เน็ตตามปกติ คุณควรศึกษาข้อมูลกลไกการทำงานทั้งหมดได้จาก คู่มือการข้าม ufw ของพอร์ต Docker
ตรวจสอบว่ามีอะไรกำลังรับฟังอยู่จริง:
sudo ss -tlnp | grep 8000ผลลัพธ์ที่ถูกต้องคือบรรทัดเดียวที่ bind อยู่กับ 127.0.0.1:8000 หากมีบรรทัดที่ bind อยู่กับ 0.0.0.0:8000 หมายความว่า vault ถูกเปิดเผยโดยตรง ให้แก้ไขการ mapping แล้วสร้างคอนเทนเนอร์ขึ้นมาใหม่ เนื่องจากพอร์ตที่ bind จะถูกกำหนดไว้ตั้งแต่ตอนสร้างคอนเทนเนอร์ และ docker compose restart จะไม่สามารถเปลี่ยนแปลงค่านี้ได้:
docker compose up -d --force-recreateยังมีอีกหนึ่งพอร์ตที่ยังคงปรากฏอยู่ในคู่มือเก่า คือ 3012 ซึ่งเป็นพอร์ต WebSocket แยกต่างหาก การรองรับพอร์ตนี้ถูกถอดออกใน Vaultwarden 1.31.0 เนื่องจากทราฟฟิกการแจ้งเตือนถูกย้ายไปรวมไว้ที่พอร์ต HTTP หลักแล้ว ทั้ง WEBSOCKET_ENABLED และ WEBSOCKET_PORT ถูกละเว้นมาตั้งแต่เวอร์ชัน 1.29.0 สวิตช์ที่ใช้ในปัจจุบันคือ ENABLE_WEBSOCKET ซึ่งมีค่าเริ่มต้นเป็น true หากไฟร์วอลล์หรือไฟล์ compose ของคุณยังคงเปิดพอร์ต 3012 อยู่ ให้ปิดพอร์ตดังกล่าวเสีย
ยุติการเชื่อมต่อ TLS ที่ reverse proxy แทนที่จะเป็น Rocket
Vaultwarden สามารถให้บริการ TLS (transport layer security) ได้ด้วยตัวเองผ่าน Rocket ซึ่งเป็น web framework ของตัวมันเอง แต่ทางโครงการแนะนำว่าไม่ควรทำเช่นนั้นในสภาพแวดล้อม production การรองรับ TLS ในตัวของ Rocket นั้นขาดการสนับสนุน SNI (server name indication) ที่เข้มงวด ซึ่งเป็นเหตุผลว่าทำไมคำแนะนำในการเพิ่มความปลอดภัยจึงระบุให้เข้าถึง instance ผ่าน hostname เท่านั้นและห้ามใช้ IP address โดยตรง ช่วง IP สาธารณะจะถูกสแกนอยู่ตลอดเวลา และ vault ที่ตอบสนองต่อ IP address คือ vault ที่จะถูกค้นพบได้ง่าย
ส่วนประกอบของ nginx server block ที่สำคัญ:
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 กำหนดค่าเริ่มต้นของ client_max_body_size ไว้ที่ 1 MB ดังนั้นหากไม่มีบรรทัดดังกล่าว การอัปโหลดไฟล์แนบจะล้มเหลวพร้อมข้อความ 413 Request Entity Too Large ใน nginx error log ในขณะที่ Vaultwarden จะไม่บันทึกอะไรเลย ส่วน header Upgrade และ Connection จะทำหน้าที่ส่งต่อ WebSocket handshake ไปยัง /notifications/hub หากตัดส่วนนี้ออก vault จะยังคงทำงานได้ แต่การเปลี่ยนแปลงข้อมูลจะไม่ปรากฏบนอุปกรณ์อื่นของคุณจนกว่าจะกดโหลดหน้าเว็บใหม่ด้วยตนเอง
Caddy มีการตั้งค่าที่สั้นกว่าและสามารถขอ certificate ได้ด้วยตัวเอง:
vw.example.com {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
}จากนั้นกำหนดค่าให้ Vaultwarden รับทราบ:
DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IPIP_HEADER มีค่าเริ่มต้นเป็น X-Real-IP อยู่แล้ว ดังนั้นงานของคุณคือการตรวจสอบให้แน่ใจว่า proxy ได้ตั้งค่า header นี้จริง หากไม่ได้ตั้งค่า ทุกบรรทัดใน log และทุกการจำกัดอัตราการเข้าสู่ระบบ (login rate limit) จะมองเห็นเป็น 127.0.0.1 ซึ่งก็คือตัว proxy เอง ส่งผลให้ความล้มเหลวของผู้โจมตีเพียงรายเดียวถูกนับรวมกับผู้ใช้ทุกคนใน instance นั้น นอกจากนี้ให้ตั้งค่า DOMAIN เป็น URL https ที่แท้จริงด้วย เพราะ Vaultwarden จะใช้ URL นี้ในการสร้างลิงก์คำเชิญและลิงก์รีเซ็ตรหัสผ่าน รวมถึง WebAuthn security keys ก็จะผูกติดกับ origin นั้นด้วย
รายละเอียดหนึ่งที่ผู้คนมักมองข้ามคือ การเชื่อมต่อ WebSocket จะส่ง session token ผ่าน query string ในชื่อ /notifications/hub?access_token=[JWT] ซึ่งจะปรากฏใน proxy access log ของคุณแบบไม่เข้ารหัส คุณควรทำการ redact พารามิเตอร์ access_token ในรูปแบบ log หรือตรวจสอบให้แน่ใจว่า log เหล่านั้นจะไม่ถูกส่งไปยังที่ใดก็ตามที่คุณไม่สามารถควบคุมได้
การป้องกันการเดารหัสผ่านแบบ Brute force ที่จุดเชื่อมต่อการเข้าสู่ระบบ
การจำกัดอัตราการเข้าถึง (Rate limits) ถูกเปิดใช้งานไว้เป็นค่าเริ่มต้น (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10) ซึ่งช่วยชะลอการโจมตี แต่ไม่สามารถหยุดยั้งผู้โจมตีได้ fail2ban สามารถทำหน้าที่นี้ได้ แต่ Vaultwarden จำเป็นต้องเขียนไฟล์ log ก่อน ซึ่งโดยปกติแล้วไม่ได้เปิดใช้งานไว้:
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueเมื่อการเข้าสู่ระบบล้มเหลว ระบบจะสร้างบรรทัดข้อความขึ้นมาหนึ่งบรรทัด ซึ่งเป็นข้อความที่ filter ของคุณต้องตรวจจับให้ตรง:
[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.เขียน filter ลงใน /etc/fail2ban/filter.d/vaultwarden.local:
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =และเขียน jail ลงใน /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 = 14400หากคุณยังคงเปิดใช้งานหน้า admin อยู่ ให้เพิ่ม jail ตัวที่สองที่มี failregex เป็น ^.*Invalid admin token\. IP: <ADDR>.*$ เนื่องจากความล้มเหลวในการเข้าสู่ระบบ admin จะถูกบันทึกด้วยข้อความที่ต่างออกไป และ filter สำหรับการเข้าสู่ระบบปกติจะไม่ตรวจพบ จากนั้นให้ตรวจสอบการทำงาน:
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwardenjail ที่ทำงานถูกต้องจะแสดงรายการไฟล์ log ของคุณภายใต้ File list และรายงาน Currently failed: 0 ให้ลองใส่รหัสผ่านผิด 3 ครั้งจากเครือข่ายอื่น ตัวนับจะเพิ่มขึ้น จากนั้นที่อยู่ IP จะปรากฏภายใต้ Banned IP list หากตัวนับไม่เพิ่มขึ้น สาเหตุที่พบบ่อยคือ logpath: ซึ่งต้องเป็น path ของไฟล์บน host ไม่ใช่ path /data/... ภายใน container สาเหตุที่พบบ่อยเป็นอันดับสองคือการขาด X-Real-IP ซึ่งจะทำให้การแบนทุกครั้งพุ่งเป้าไปที่ proxy ของคุณเอง สำหรับการตั้งค่าส่วนที่เหลือ รวมถึง SSH jail ที่คุณควรเปิดใช้งานอยู่แล้ว สามารถดูได้ที่ คู่มือการใช้งาน fail2ban สำหรับ Ubuntu 24.04
รหัสผ่านหลักคือหัวใจสำคัญของทั้งระบบ
การเข้ารหัสฝั่งไคลเอนต์หมายความว่ารหัสผ่านหลักคือตัวกุญแจสำคัญ หากผู้โจมตีคัดลอกฐานข้อมูลไปได้ รหัสผ่านหลักที่สั้นจะไม่มีสิ่งใดในบทความนี้ช่วยป้องกันได้ เพราะผู้โจมตีจะนำสำเนาฐานข้อมูลไปถอดรหัสแบบออฟไลน์ด้วยความเร็วเท่าที่ฮาร์ดแวร์ของพวกเขาจะทำได้ การตั้งค่าบนเซิร์ฟเวอร์ไม่มีผลใดๆ ต่อเครื่องของผู้โจมตี
PASSWORD_ITERATIONS=600000 เป็นสิ่งที่เข้าใจผิดได้ง่าย การตั้งค่านี้กำหนดจำนวนรอบการทำ hash ฝั่งเซิร์ฟเวอร์สำหรับ login hash ที่ไคลเอนต์ส่งมา และเทมเพลตการตั้งค่าของโปรเจกต์ระบุว่าค่าที่เปลี่ยนใหม่จะมีผลกับผู้ใช้เดิมเมื่อมีการล็อกอินครั้งถัดไป แต่มันไม่ได้ส่งผลต่อ KDF ฝั่งไคลเอนต์ ซึ่งเป็นค่าที่กำหนดความยากง่ายในการโจมตี vault แบบออฟไลน์ ค่านี้ขึ้นอยู่กับแต่ละบัญชี ผู้ที่สมัครใช้งานเมื่อหลายปีก่อนอาจยังใช้ค่า KDF ต่ำแบบเดิมอยู่จนกว่าพวกเขาจะปรับเพิ่มในหน้าการตั้งค่าความปลอดภัยของ web vault ซึ่งจะเป็นการเข้ารหัสกุญแจใหม่ คุณควรแจ้งให้ผู้ใช้ทราบ เพราะเซิร์ฟเวอร์ไม่สามารถดำเนินการแทนพวกเขาได้
จากนั้นให้เปิดใช้งานการยืนยันตัวตนแบบสองชั้น (two-factor authentication) สำหรับแต่ละบัญชี วิธีนี้ไม่ได้ช่วยป้องกันข้อมูลที่เข้ารหัสไว้ เนื่องจากกุญแจของ vault มาจากรหัสผ่านหลักเพียงอย่างเดียว แต่ช่วยป้องกันไม่ให้รหัสผ่านที่ถูกขโมยไปเพียงอย่างเดียวสามารถล็อกอินและซิงค์สำเนาข้อมูลได้ REQUIRE_DEVICE_EMAIL=true จะบังคับให้ต้องมีการส่งอีเมลแจ้งเตือนเมื่อมีการล็อกอินจากอุปกรณ์ใหม่ หาก Vaultwarden ไม่สามารถส่งอีเมลได้ การล็อกอินจะล้มเหลว ดังนั้นการล็อกอินจากอุปกรณ์ที่ไม่รู้จักจะไม่เกิดขึ้นโดยไม่มีการแจ้งเตือน วิธีนี้จำเป็นต้องมีการตั้งค่า SMTP ที่ใช้งานได้จริง มิฉะนั้นการล็อกอินจากอุปกรณ์ใหม่ทั้งหมดจะถูกปฏิเสธ
การสำรองข้อมูลคือจุดที่ระบบ self-hosted vault มักเกิดความผิดพลาด
การทำ tar czf ของโฟลเดอร์ข้อมูลแล้วทิ้งไว้ใน home directory บน VPS เครื่องเดียวกัน จะทำให้ขั้นตอนทั้งหมดที่กล่าวมาข้างต้นไร้ผล ไฟล์ archive นั้นเก็บ db.sqlite3 ซึ่งมี ciphertext ของผู้ใช้ทุกคน, rsa_key.pem ที่ใช้ปลอมแปลง session การเข้าสู่ระบบ และ config.json ที่เก็บ admin token และรหัสผ่าน SMTP ไว้ในรูปแบบข้อความธรรมดา การเข้าถึงไฟล์นั้นได้เพียงไฟล์เดียวเท่ากับการเข้าถึงข้อมูลใน vault ทั้งหมด
มีกฎ 2 ข้อที่ต้องปฏิบัติตาม คือ นำไฟล์ archive ออกจากเซิร์ฟเวอร์เครื่องนั้น และเข้ารหัสข้อมูลก่อนที่จะย้ายออกไป
นอกจากนี้ยังมีปัญหาเรื่องความถูกต้องของข้อมูล การคัดลอก db.sqlite3 ด้วย cp ในขณะที่บริการกำลังทำงานอยู่อาจทำให้ได้ไฟล์ที่อยู่ในสถานะกำลังเขียนข้อมูล ซึ่งจะไม่สามารถเปิดใช้งานได้ และคุณจะไม่มีทางทราบเลยจนกว่าจะถึงเวลาที่ต้องกู้คืนข้อมูล ให้ใช้ฟังก์ชัน snapshot ของ SQLite แทน:
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"ในส่วนของการกู้คืนข้อมูล ซึ่งเป็นขั้นตอนที่ไม่มีใครทดสอบกันนั้น ได้อธิบายไว้ใน คู่มือการสำรองและกู้คืนข้อมูลของ Vaultwarden
สิ่งที่คุณต้องแลกเมื่อเทียบกับการใช้บริการ Bitwarden แบบโฮสต์
การประเมินตามความเป็นจริง บริการแบบโฮสต์ของ Bitwarden ดำเนินการโดยทีมงานมืออาชีพที่มีหน้าที่ดูแลระบบโดยเฉพาะ มีการตรวจสอบจากบุคคลภายนอกที่เปิดเผยต่อสาธารณะ และมีเจ้าหน้าที่คอยแก้ไขปัญหาตลอด 24 ชั่วโมง การโฮสต์ด้วยตนเองหมายถึงคุณต้องรับหน้าที่จัดการการอัปเดตแพตช์ด้วยตนเอง
Vaultwarden ออกแพตช์ความปลอดภัยในรูปแบบการปล่อยเวอร์ชันปกติ ซึ่งมีความถี่สูง จากการตรวจสอบข้อมูลเมื่อวันที่ 7 ตุลาคม 2026 เทียบกับฟีดการปล่อยเวอร์ชันของโครงการ เวอร์ชันปัจจุบันคือ 1.37.4 ซึ่งเผยแพร่เมื่อวันที่ 5 ตุลาคม 2026 บันทึกการเปลี่ยนแปลงครอบคลุมคำแนะนำด้านความปลอดภัย 7 รายการ โดยรายการที่รุนแรงที่สุดถูกจัดอยู่ในระดับสูง และแนะนำให้รีบอัปเดตโดยเร็วที่สุด นี่เป็นการปล่อยเวอร์ชันที่ 4 นับตั้งแต่เวอร์ชัน 1.37.0 เมื่อวันที่ 24 กรกฎาคม 2026 โดยเวอร์ชัน 1.37.1 ตามมาในวันที่ 29 กรกฎาคม, 1.37.2 ในวันที่ 22 สิงหาคม (จำเป็นสำหรับไคลเอนต์เวอร์ชัน 2026.8.0 ขึ้นไป) และ 1.37.3 ในวันที่ 13 กันยายน ซึ่งมีการเพิกถอนโทเค็นการจดจำ 2FA เมื่อมีการเปลี่ยนข้อมูลรับรองหรือ 2FA และมีการจำกัดอัตราการเข้าถึง (rate limit) สำหรับ endpoint ของการตรวจสอบสิทธิ์และการเข้าสู่ระบบล่วงหน้า หากคุณดูแลองค์กรที่มีผู้ดูแลระบบที่คุณไม่ไว้วางใจเต็มร้อย บันทึกของเวอร์ชัน 1.37.4 ยังแนะนำให้หมุนเวียน API key ของแต่ละองค์กรหลังจากอัปเดตแล้ว อินสแตนซ์ที่คุณตั้งค่าไว้เมื่อปีก่อนและลืมไปแล้วนั้นกำลังรันโค้ดที่มีอายุหนึ่งปี แท็ก latest ไม่ได้ช่วยอะไรด้วยตัวมันเอง คอนเทนเนอร์ที่รันอยู่จะยังคงใช้ image เดิมที่เริ่มทำงานจนกว่าคุณจะรันคำสั่ง docker compose pull และสร้างคอนเทนเนอร์ขึ้นมาใหม่ คุณควรตั้งค่า การอัปเกรดอัตโนมัติบน Ubuntu สำหรับแพ็กเกจของโฮสต์ และตั้งการแจ้งเตือนในปฏิทินที่คุณจะเปิดอ่านจริงๆ เพื่ออัปเดตคอนเทนเนอร์
ข้อสรุปที่ผู้อ่านควรตระหนักคือ การเข้ารหัสลับในที่นี้เป็นการออกแบบของ Bitwarden ซึ่งมีความปลอดภัยสูง แต่ความเสี่ยงในการดำเนินงานจะตกอยู่กับคุณทั้งหมด หากคุณหมั่นอัปเดตแพตช์และสำรองข้อมูลไว้ที่อื่น อินสแตนซ์ Vaultwarden บน VPS ที่คุณควบคุมเองถือเป็นสถานที่ที่เหมาะสมในการเก็บรหัสผ่านของคุณ แต่หากคุณไม่สามารถสร้างนิสัยทั้งสองอย่างนี้ได้ ให้จ่ายเงินใช้บริการแบบโฮสต์และนำเวลาไปใช้กับเรื่องอื่นจะดีกว่า การเปรียบเทียบฟีเจอร์แบบรายการต่อรายการอยู่ใน การเปรียบเทียบ Vaultwarden กับ Bitwarden แบบโฮสต์เอง
การเสริมความปลอดภัยให้กับโฮสต์ที่รันคอนเทนเนอร์
Vaultwarden เป็นเพียงกระบวนการหนึ่งบนเครื่อง Linux ซึ่งผู้ใช้ระดับ root บนเครื่องนั้นสามารถอ่านไฟล์ /vw-data ได้เสมอ ไม่ว่าแอปพลิเคชันจะถูกตั้งค่าไว้อย่างไรก็ตาม ให้รันคอนเทนเนอร์ในฐานะผู้ใช้ที่ไม่มีสิทธิ์ (unprivileged user) โดยใช้ user: "1000:1000" ในไฟล์ compose ของคุณ พร้อมทั้งกำหนดความเป็นเจ้าของโฟลเดอร์ข้อมูลให้ตรงกัน และ mount สิ่งที่คอนเทนเนอร์ไม่ได้เขียนข้อมูลลงไปให้เป็นแบบอ่านอย่างเดียวด้วย :ro จากนั้นให้ปิดช่องทางเข้าหลัก: การเสริมความปลอดภัย SSH บน VPS ซึ่งครอบคลุมถึงการล็อกอินด้วยคีย์เพียงอย่างเดียวและการปิดการยืนยันตัวตนด้วยรหัสผ่าน ซึ่งเป็นสิ่งที่ช่วยหยุดการโจมตีทั่วไปที่สามารถผ่านการป้องกันทั้งหมดที่กล่าวมาข้างต้นได้
FAQ
มีใครสามารถอ่านรหัสผ่านของฉันได้หรือไม่หากพวกเขาขโมยฐานข้อมูล Vaultwarden ไป?
ไม่ได้โดยตรง ข้อมูลทุกรายการใน vault จะถูกเข้ารหัสที่ฝั่งไคลเอนต์ด้วยคีย์ที่สร้างจากรหัสผ่านหลัก ดังนั้น db.sqlite3 จึงมีเพียงข้อมูลที่ถูกเข้ารหัส (ciphertext) สิ่งที่ผู้บุกรุกจะได้ไปทันทีคือที่อยู่อีเมลของแต่ละบัญชี, การตั้งค่า KDF, ข้อมูล metadata ของการเข้าสู่ระบบและอุปกรณ์ รวมถึงรหัสลับสำหรับการยืนยันตัวตนสองชั้น (2FA) ในตาราง twofactor ซึ่งถูกจัดเก็บไว้โดยไม่ได้เข้ารหัสเนื่องจากเซิร์ฟเวอร์จำเป็นต้องใช้ข้อมูลนี้ในการคำนวณรหัสที่ถูกต้อง นอกจากนี้พวกเขายังสามารถโจมตีข้อมูลที่ถูกเข้ารหัสใน vault แบบออฟไลน์ได้นานเท่าที่ต้องการ นี่คือเหตุผลว่าทำไมความยาวของรหัสผ่านหลักจึงเป็นตัวตัดสินผลลัพธ์
ฉันควรใช้ ADMIN_TOKEN หรือปิดหน้า admin ไปเลยดีกว่า?
หากทำได้ควรปิดการใช้งาน เนื่องจากอินสแตนซ์ส่วนใหญ่ต้องการหน้าดังกล่าวเพียงครั้งเดียวเพื่อตั้งค่า SMTP และเชิญผู้ใช้เข้าสู่ระบบเท่านั้น หลังจากนั้นก็ไม่จำเป็นต้องใช้อีก หากต้องการปิด ให้ตั้งค่าโดยไม่ใส่ทั้ง ADMIN_TOKEN และ DISABLE_ADMIN_TOKEN, ลบคีย์ "admin_token" ออกจาก config.json จากนั้นสร้างคอนเทนเนอร์ขึ้นมาใหม่ การลบเพียงตัวแปรสภาพแวดล้อม (environment variable) นั้นไม่เพียงพอ เพราะการตั้งค่าที่เขียนผ่านหน้า admin จะถูกบันทึกไว้ใน config.json ซึ่งมีลำดับความสำคัญสูงกว่า หากคุณยังจำเป็นต้องใช้หน้า admin ให้จัดเก็บ token ในรูปแบบ Argon2 hash ที่สร้างจาก vaultwarden hash แทนการใช้ข้อความธรรมดา (plaintext) และตั้งค่า ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_TOKEN ของฉันถูกต้อง แต่ /admin ปฏิเสธการเข้าถึง เกิดอะไรขึ้น?
เกือบทุกกรณีเกิดจากการตีความ $ ผิดพลาด สตริง Argon2 PHC มีอักขระ $ อยู่หลายตัว และ Docker Compose จะขยายอักขระเหล่านี้เสมือนเป็นตัวแปรภายในบล็อก docker-compose.yml environment: ทำให้คอนเทนเนอร์ได้รับค่าที่ผิดเพี้ยนไปแม้ว่าไฟล์ของคุณจะดูถูกต้อง ให้ทำการเบิ้ลอักขระ $ เป็น $$ ในไฟล์ compose หรือย้ายค่าดังกล่าวไปไว้ในไฟล์ .env โดยครอบด้วยเครื่องหมายคำพูดเดี่ยว (single quotes) ซึ่งไม่จำเป็นต้องใช้การ escape อักขระ หลังจากนั้นให้สร้างคอนเทนเนอร์ใหม่ เนื่องจากระบบจะไม่โหลดการเปลี่ยนแปลงของ environment variable หากใช้วิธีรีสตาร์ทเพียงอย่างเดียว
ฉันยังจำเป็นต้องเปิดพอร์ต 3012 สำหรับการแจ้งเตือน (notifications) หรือไม่?
ไม่จำเป็น การรองรับทราฟฟิก WebSocket บนพอร์ต 3012 ถูกถอดออกใน Vaultwarden 1.31.0 เนื่องจากการแจ้งเตือนถูกย้ายไปทำงานบนพอร์ต HTTP หลัก และ WEBSOCKET_ENABLED กับ WEBSOCKET_PORT ก็ถูกละเลยมาตั้งแต่เวอร์ชัน 1.29.0 การตั้งค่าปัจจุบันคือ ENABLE_WEBSOCKET ซึ่งมีค่าเริ่มต้นเป็น true ให้ปิดพอร์ต 3012 ในไฟร์วอลล์และลบออกจากไฟล์ compose ของคุณ จากนั้นตรวจสอบให้แน่ใจว่า reverse proxy ของคุณส่งต่อ header Upgrade และ Connection ได้อย่างถูกต้อง เพราะนั่นคือสิ่งที่การซิงค์ข้อมูลแบบเรียลไทม์ต้องใช้ในปัจจุบัน