Cấu hình server cho ứng dụng Node.js: từ server trắng đến production an toàn
Cấu hình server cho ứng dụng Node.js thường bị rút gọn thành ba bước: cài Node, chạy node server.js, mở port 3000. Cách đó chạy được, nhưng ứng dụng đang chạy bằng root, lộ thẳng ra Internet và không tự khởi động lại khi bị crash. Lỗ hổng không nằm ở code Node.js mà nằm ở lớp hạ tầng chưa ai cấu hình. Bài này đi từng bước để dựng một server Ubuntu chạy Node.js đúng chuẩn production, mỗi bước kèm rủi ro nếu bỏ qua và lệnh kiểm tra.
Bài này giúp bạn
- Dựng nền server: user riêng, SSH chỉ dùng key, firewall chỉ mở cổng cần thiết, cập nhật bảo mật tự động.
- Chạy Node.js bằng systemd với các tuỳ chọn hardening, đặt sau Nginx và TLS.
- Có checklist trước khi go-live và bảng lỗi thường gặp.
Môi trường mẫu: Ubuntu 24.04 LTS, Node.js 26 (LTS), Nginx từ kho Ubuntu. Tên miền, IP và đường dẫn trong bài đều là ví dụ.
Chuẩn bị nền server trước khi cài Node.js
Tôi luôn làm phần nền trước, vì mọi lớp bảo vệ của ứng dụng đều vô nghĩa nếu kẻ tấn công vào được bằng SSH. Phần này gồm bốn việc, và nên làm xong trước khi đưa bất kỳ dòng code nào lên server.
Chọn hệ điều hành và phiên bản Node.js LTS
Hãy chọn một bản phân phối LTS như Ubuntu 24.04 và một phiên bản Node.js đang ở trạng thái LTS. Trang lịch phát hành của Node.js nói rõ ứng dụng production chỉ nên dùng bản Active LTS hoặc Maintenance LTS (Node.js Releases). Tại thời điểm viết (11/2026), các nhánh LTS là 26, 24 và 22; nhánh 20 đã hết hỗ trợ.
sudo apt update && sudo apt upgrade -y
# Cài Node.js 26 từ NodeSource (kiểm tra lại script trước khi chạy)
curl -fsSL https://deb.nodesource.com/setup_26.x -o nodesource_setup.sh
less nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install -y nodejs
node -v
Rủi ro nếu bỏ qua: chạy bản Node.js đã hết hỗ trợ đồng nghĩa với việc lỗ hổng mới phát hiện sẽ không bao giờ được vá.
Tạo user riêng, tắt đăng nhập root và mật khẩu SSH
Tạo một user quản trị, chép SSH key sang user đó, rồi mới tắt đăng nhập bằng root và bằng mật khẩu.
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Tạo file /etc/ssh/sshd_config.d/99-hardening.conf:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
sudo sshd -t && sudo systemctl reload ssh
Giữ nguyên phiên SSH hiện tại và mở một terminal mới để thử đăng nhập bằng deploy. Chỉ đóng phiên cũ khi đã đăng nhập được.
Rủi ro nếu bỏ qua: server có IP public bị dò mật khẩu SSH liên tục. Chỉ cần một mật khẩu yếu là mất quyền root.
Firewall: chỉ mở 22, 80 và 443
Ứng dụng Node.js không cần lộ cổng ra ngoài, vì Nginx sẽ đứng trước nó. Trên server chỉ cần mở SSH, HTTP và HTTPS.
sudo apt install -y nginx
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose
Nếu nhà cung cấp cloud có firewall hoặc security group ở tầng mạng, hãy cấu hình thêm ở đó với cùng nguyên tắc. Hai lớp lọc độc lập an toàn hơn một lớp. Lưu ý: nếu sau này các bạn chạy container bằng Docker, cổng publish bằng -p có thể đi vòng qua luật UFW, nên hãy kiểm tra lại bằng cách quét từ bên ngoài.
Rủi ro nếu bỏ qua: cổng 3000, cổng database hay cổng debug bị lộ ra Internet và bị quét trong vài giờ.
Bật cập nhật bảo mật tự động
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Cấu hình mặc định trên Ubuntu chỉ cài các bản vá từ kho security. Kernel mới vẫn cần reboot, nên các bạn hãy lên lịch reboot định kỳ trong giờ thấp điểm.
Rủi ro nếu bỏ qua: các lỗ hổng OpenSSL, OpenSSH hay kernel đã có bản vá nhưng server không bao giờ được cài.
Chạy ứng dụng Node.js an toàn trong production
Khi nền đã chắc, bước tiếp theo là chạy ứng dụng: với ít quyền nhất có thể, tự khởi động lại khi lỗi, và chỉ nói chuyện với bên ngoài qua Nginx. Phần này gồm bốn việc.
Chạy bằng systemd với user không có quyền đăng nhập
Tạo user hệ thống chỉ để chạy ứng dụng. Code thuộc user deploy, còn tiến trình chạy bằng nodeapp và chỉ có quyền đọc code.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin nodeapp
sudo mkdir -p /srv/myapp && sudo chown deploy:deploy /srv/myapp
Tạo file /etc/systemd/system/myapp.service:
[Unit]
Description=My Node.js app
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/node /srv/myapp/server.js
Restart=on-failure
RestartSec=5
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
StateDirectory=myapp
CapabilityBoundingSet=
MemoryMax=1G
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Ý nghĩa các dòng hardening theo tài liệu systemd (systemd.exec): NoNewPrivileges chặn tiến trình nâng quyền qua setuid; ProtectSystem=strict biến toàn bộ hệ thống file thành chỉ đọc, trừ /dev, /proc, /sys; ProtectHome ẩn /home và /root; PrivateTmp cho dịch vụ một /tmp riêng. StateDirectory=myapp tạo /var/lib/myapp thuộc user nodeapp, đây là chỗ duy nhất ứng dụng được ghi. CapabilityBoundingSet= để trống nghĩa là bỏ hết capability, vì ứng dụng chỉ nghe cổng 3000 nên không cần quyền bind cổng dưới 1024.
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
journalctl -u myapp -f
# Chấm điểm mức độ phơi nhiễm của service (điểm càng thấp càng tốt)
systemd-analyze security myapp
Rủi ro nếu bỏ qua: chạy Node.js bằng root thì một lỗ hổng RCE trong một package phụ thuộc là đủ để chiếm toàn bộ server. Tài liệu bảo mật của Node.js cũng xếp module bên thứ ba độc hại vào nhóm mối đe doạ chính (Node.js Security Best Practices).
Đặt Nginx và TLS phía trước, Node.js chỉ nghe 127.0.0.1
Trong code, hãy cho ứng dụng nghe 127.0.0.1:3000 thay vì 0.0.0.0. Nginx nhận request từ bên ngoài và chuyển vào ứng dụng. Tài liệu bảo mật của Express cũng khuyến nghị dùng Nginx để xử lý TLS (Express: Security Best Practices).
Tạo file /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name example.com;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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 "upgrade";
}
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
Thêm server_tokens off; trong khối http của /etc/nginx/nginx.conf để Nginx không trả về số phiên bản. Với Express, hãy đặt app.set('trust proxy', 1) để ứng dụng đọc đúng IP thật và giao thức HTTPS từ header mà Nginx gửi vào.
Kiểm tra lại cổng đang nghe:
sudo ss -tlnp | grep -E ':(3000|80|443)\b'
# 3000 phải hiện 127.0.0.1:3000, không phải 0.0.0.0:3000
Rủi ro nếu bỏ qua: ứng dụng lộ trực tiếp ra Internet mà không có TLS, không giới hạn kích thước request, và để lộ thông tin phiên bản cho kẻ dò quét.
Giữ secrets ngoài mã nguồn
Đặt biến môi trường trong /etc/myapp/myapp.env. Chỉ root đọc được file này; systemd đọc nó trước khi chuyển quyền sang nodeapp.
sudo mkdir -p /etc/myapp
sudo install -m 600 -o root -g root /dev/null /etc/myapp/myapp.env
sudoedit /etc/myapp/myapp.env
NODE_ENV=production
HOST=127.0.0.1
PORT=3000
DATABASE_URL=postgres://app:CHANGE_ME@10.0.0.5:5432/app
Rủi ro nếu bỏ qua: file .env nằm trong thư mục code dễ bị commit lên Git, hoặc bị đọc qua một lỗi path traversal.
Không bật inspector trong production
Không chạy node --inspect trên server production. Tài liệu bảo mật Node.js nêu rõ inspector có thể bị khai thác qua tấn công DNS rebinding và khuyến nghị không chạy inspector protocol trong production. Cần debug thì hãy tái hiện lỗi ở môi trường staging, hoặc mở tạm qua SSH tunnel tới 127.0.0.1 rồi tắt ngay sau khi xong.
Rủi ro nếu bỏ qua: cổng 9229 của inspector cho phép chạy mã tuỳ ý trong tiến trình Node.js.
Lỗi thường gặp và checklist trước khi go-live
| Lỗi | Dấu hiệu | Cách sửa |
|---|---|---|
Service khởi động lỗi EACCES khi ghi file |
journalctl -u myapp báo không có quyền ghi |
Chuyển đường dẫn ghi sang /var/lib/myapp (StateDirectory) hoặc thêm ReadWritePaths= |
502 Bad Gateway từ Nginx |
Ứng dụng chưa chạy hoặc nghe sai địa chỉ | systemctl status myapp, kiểm tra HOST/PORT khớp với proxy_pass |
| Ứng dụng ghi log sai IP người dùng (luôn 127.0.0.1) | Rate limit chặn nhầm tất cả mọi người | Đặt app.set('trust proxy', 1) và header X-Forwarded-For |
| Mất SSH sau khi sửa sshd | Không đăng nhập được | Luôn giữ một phiên cũ khi thử; dùng console của nhà cung cấp cloud để sửa |
| Service bị kill liên tục | systemctl status báo oom-kill |
Tìm nguyên nhân rò bộ nhớ trước, sau đó mới tăng MemoryMax |
| WebSocket không kết nối được | Lỗi 400 khi upgrade | Kiểm tra proxy_http_version 1.1 và các header Upgrade, Connection |
Checklist trước khi go-live
- [ ] Node.js đang ở nhánh LTS, hệ điều hành đã cập nhật đầy đủ
- [ ] SSH chỉ dùng key,
PermitRootLogin no, đã thử đăng nhập lại bằng user thường - [ ] UFW chỉ mở 22/80/443; quét từ bên ngoài không thấy cổng 3000
- [ ] Ứng dụng chạy bằng
nodeappqua systemd,systemd-analyze securityđã được xem - [ ] Nginx có HTTPS,
server_tokens off, đã đặt giới hạn kích thước request - [ ] Secrets nằm trong
/etc/myapp/myapp.envquyền 600, không nằm trong Git - [ ] Không có
--inspecttrongExecStart - [ ]
unattended-upgradesđang bật và đã có lịch reboot
Triển khai trên Bizfly Cloud Server: cần chuẩn bị gì
Các bước trên áp dụng được cho mọi máy chủ Ubuntu, kể cả khi chạy trên Cloud Server của Bizfly Cloud. Trang sản phẩm có nêu mỗi khách hàng có mạng VPC riêng để tách biệt với khách hàng khác. Đây là lớp mạng nằm ngoài server, bổ sung cho UFW chứ không thay thế. Trước khi đưa ứng dụng Node.js lên, các bạn nên chuẩn bị sẵn SSH key, chọn image Ubuntu LTS và quyết định cấu hình CPU/RAM theo tải thực tế đã đo.
Checklist tính năng cần đối chiếu khi triển khai:
- [ ] Có firewall hoặc security group ở tầng mạng để chỉ mở 22/80/443 không
- [ ] Database đặt trong mạng riêng (VPC), không có IP public
- [ ] Có console truy cập khẩn cấp khi lỡ khoá SSH không
- [ ] Có snapshot ổ đĩa để tạo điểm quay lui trước khi nâng cấp không
- [ ] Cách tăng CPU/RAM khi tải tăng và thời gian gián đoạn khi nâng cấp
Câu hỏi thường gặp
Nên dùng PM2 hay systemd để chạy Node.js? Cả hai đều giữ được tiến trình chạy liên tục. systemd có sẵn trên Ubuntu, quản lý log qua journald và có các tuỳ chọn hardening như trong bài. PM2 tiện hơn khi cần chạy cluster nhiều tiến trình hoặc reload không downtime, nhưng các bạn vẫn nên chạy PM2 bằng user không phải root.
Có cần Nginx khi đã có Node.js xử lý HTTPS được không? Không bắt buộc, nhưng nên có. Nginx xử lý TLS, giới hạn kích thước request, phục vụ file tĩnh và che cổng ứng dụng khỏi Internet, nên ứng dụng chỉ cần tập trung vào nghiệp vụ.
Server cần bao nhiêu RAM cho ứng dụng Node.js?
Không có con số chung. Hãy đo bộ nhớ thực tế của tiến trình khi chịu tải thử, cộng thêm phần dư cho Nginx, hệ điều hành và các đợt tải đột biến, rồi đặt MemoryMax theo kết quả đo.
Có nên mở cổng database ra Internet để tiện quản trị không? Không. Hãy để database chỉ nghe trong mạng riêng và quản trị qua SSH tunnel. Database lộ cổng ra Internet sẽ bị dò quét và thử mật khẩu liên tục, giống như SSH.
Kết luận
Cấu hình server cho ứng dụng Node.js an toàn bắt đầu từ lớp hạ tầng, không phải từ code. Nguyên tắc cốt lõi là quyền tối thiểu: user riêng, chỉ mở cổng cần thiết, ứng dụng nằm sau Nginx và TLS. Việc đầu tiên nên làm là chạy ss -tlnp và systemd-analyze security trên server hiện tại để biết mình đang lộ những gì. Các bài tiếp theo trong series Bảo mật hạ tầng cho dev sẽ đi tiếp vào từng lớp bảo vệ của server.
All rights reserved