CyStack logo
  • Sản phẩm & Dịch vụ
  • Giải pháp
  • Bảng giá
  • Công ty
  • Tài liệu
Vi

vi

Trang chủHướng dẫnBảo mật ứng dụng Node.js dạng Container với Nginx, Let’s Encrypt và Docker
Chuyên gia

Bảo mật ứng dụng Node.js dạng Container với Nginx, Let’s Encrypt và Docker

CyStack blog 20 phút để đọc
CyStack blog07/08/2025
Locker Avatar

Chris Pham

Technical Writer

Locker logo social
Reading Time: 20 minutes

Có nhiều phương pháp để tăng cường tính linh hoạt và bảo mật cho ứng dụng Node.js. Việc sử dụng reverse proxy như Nginx cho phép bạn cân bằng tải các yêu cầu, cache nội dung tĩnh và triển khai giao thức Transport Layer Security (TLS). Việc kích hoạt HTTPS được mã hóa trên máy chủ sẽ giúp đảm bảo quá trình truyền dữ liệu giữa người dùng và ứng dụng luôn được bảo vệ.

Bảo mật ứng dụng Node.js

Triển khai reverse proxy với TLS/SSL trong môi trường container có sự khác biệt so với khi thực hiện trực tiếp trên hệ điều hành máy chủ. Ví dụ, nếu bạn lấy chứng chỉ từ Let’s Encrypt cho một ứng dụng chạy trực tiếp trên máy chủ, bạn sẽ cần cài đặt phần mềm liên quan ngay trên hệ điều hành đó.

Tuy nhiên, trong môi trường container, bạn có thể tiếp cận theo hướng khác. Với Docker Compose, bạn có thể tạo các container riêng biệt cho ứng dụng, máy chủ web và client Certbot, công cụ hỗ trợ lấy chứng chỉ. Quy trình này cho phép bạn tận dụng ưu điểm về tính mô-đun và khả năng di chuyển cao của môi trường container.

Trong hướng dẫn này, bạn sẽ triển khai một ứng dụng Node.js sử dụng reverse proxy Nginx thông qua Docker Compose. Bạn sẽ lấy chứng chỉ TLS/SSL cho tên miền liên kết với ứng dụng và đảm bảo rằng ứng dụng đạt xếp hạng bảo mật cao trên SSL Labs. Cuối cùng, bạn sẽ thiết lập một cron job để tự động gia hạn chứng chỉ, giúp tên miền luôn được bảo vệ.

Chuẩn bị trước khi thực hiện

Để thực hiện theo hướng dẫn này, bạn cần chuẩn bị:

  • Một máy chủ Ubuntu 18.04 với người dùng không phải root có quyền sudo, cùng với một tường lửa đang hoạt động.
  • Docker và Docker Compose đã được cài đặt trên máy chủ. Để cài đặt Docker, hãy thực hiện Bước 1 và Bước 2 trong bài Hướng Dẫn Cài Đặt và Sử Dụng Docker trên Ubuntu 18.04.
  • Một tên miền đã được đăng ký. Trong hướng dẫn này, tên miền ví dụ được sử dụng là your_domain. Bạn có thể đăng ký miễn phí thông qua Freenom hoặc sử dụng dịch vụ từ nhà cung cấp tên miền mà bạn lựa chọn.
  • Hai bản ghi DNS được cấu hình cho máy chủ của bạn. Nếu bạn sử dụng DigitalOcean, có thể tham khảo bài hướng dẫn về DNS của DigitalOcean để biết cách thêm các bản ghi:
    • Bản ghi A trỏ your_domain đến địa chỉ IP công khai của máy chủ.
    • Bản ghi A trỏ www.your_domain đến cùng địa chỉ IP công khai đó.

Khi đã hoàn tất các bước chuẩn bị, bạn có thể bắt đầu bước đầu tiên của hướng dẫn.

Bước 1: Cloning và kiểm tra ứng dụng Node

Trước tiên, bạn sẽ sao chép kho mã chứa ứng dụng Node, trong đó bao gồm Dockerfile dùng để xây dựng image ứng dụng thông qua Compose. Sau đó, bạn sẽ kiểm tra ứng dụng bằng cách xây dựng và chạy nó với lệnh docker run, chưa sử dụng reverse proxy hay SSL.

Tại thư mục chính của người dùng không phải root, bạn hãy sao chép kho nodejs-image-demo từ tài khoản GitHub của DigitalOcean Community. Kho này chứa mã nguồn từ bài hướng dẫn Cách Xây Dựng Ứng Dụng Node.js với Docker.

Sao chép kho mã về một thư mục. Trong ví dụ này, chúng ta sử dụng tên thư mục là node_project. Bạn có thể đổi tên thư mục tùy theo ý muốn:

git clone <https://github.com/do-community/nodejs-image-demo.git> node_project

Chuyển vào thư mục node_project:

cd node_project

Trong thư mục này có một Dockerfile chứa các hướng dẫn để xây dựng ứng dụng Node từ image node:10 của Docker và nội dung của thư mục dự án hiện tại. Bạn có thể xem trước nội dung của Dockerfile bằng lệnh sau:

cat Dockerfile

Kết quả:

FROM node:10-alpine

RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

WORKDIR /home/node/app

COPY package*.json ./

USER node

RUN npm install

COPY --chown=node:node . .

EXPOSE 8080

CMD [ "node", "app.js" ]

Các chỉ thị trên xây dựng một image Node bằng cách sao chép mã dự án từ thư mục hiện tại vào container và cài đặt các phụ thuộc thông qua lệnh npm install. Quá trình này đồng thời tận dụng khả năng cache và phân lớp image của Docker bằng cách tách riêng thao tác sao chép các tệp package.jsonpackage-lock.json (chứa thông tin về các phụ thuộc của dự án) khỏi phần mã nguồn còn lại.

Cuối cùng, các chỉ thị cũng quy định rằng container sẽ chạy dưới quyền của người dùng không phải root, là node, với quyền truy cập phù hợp cho mã nguồn ứng dụng và thư mục node_modules.

Để tìm hiểu chi tiết hơn về Dockerfile này cũng như các thực tiễn tốt nhất khi làm việc với Node image, bạn có thể tham khảo toàn bộ phần trình bày trong Bước 3 của bài Hướng Dẫn Xây Dựng Ứng Dụng Node.js Với Docker.

Nếu muốn kiểm tra ứng dụng mà không cần cấu hình SSL, bạn có thể xây dựng image và gán nhãn bằng lệnh docker build với tùy chọn -t. Trong ví dụ này, image được đặt tên là node-demo, tuy nhiên bạn hoàn toàn có thể sử dụng tên khác tùy ý:

docker build -t node-demo .

Khi quá trình xây dựng hoàn tất, bạn có thể liệt kê các image hiện có bằng lệnh docker images:

docker images

Kết quả sau xác nhận rằng image của ứng dụng đã được tạo thành công:

Kết quả

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
node-demo           latest              23961524051d        7 seconds ago       73MB
node                10-alpine           8a752d5af4ce        3 weeks ago         70.7MB

Tiếp theo, tạo container bằng lệnh docker run. Câu lệnh này sử dụng ba tùy chọn:

  • p: Xuất bản cổng từ container và ánh xạ nó với một cổng trên máy chủ. Trong ví dụ này, bạn sẽ sử dụng cổng 80 trên máy chủ nhưng bạn có thể thay đổi nếu cổng này đang chạy một tiến trình khác. Để tìm hiểu thêm về cơ chế hoạt động, bạn hãy tham khảo tài liệu Docker về port binding.
  • d: Chạy container ở chế độ nền.
  • -name: Đặt tên dễ nhớ cho container.

Chạy lệnh sau để tạo container:

docker run --name node-demo -p 80:8080 -d node-demo

Kiểm tra các container đang chạy bằng lệnh docker ps:

docker ps

Kết quả sau xác nhận rằng container ứng dụng đang hoạt động:

Kết quả

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
4133b72391da        node-demo           "node app.js"       17 seconds ago      Up 16 seconds       0.0.0.0:80->8080/tcp   node-demo

Bây giờ, bạn có thể truy cập tên miền của mình để kiểm tra hoạt động: http://your_domain. Hãy thay thế your_domain bằng tên miền thực tế của bạn. Ứng dụng sẽ hiển thị trang landing page như sau:

bao-mat-ung-dung-Node-js

Sau khi đã kiểm tra ứng dụng thành công, bạn có thể dừng container và xóa các image. Sử dụng docker ps để lấy CONTAINER ID:

docker ps

Kết quả

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
4133b72391da        node-demo           "node app.js"       17 seconds ago      Up 16 seconds       0.0.0.0:80->8080/tcp   node-demo

Dừng container bằng lệnh docker stop. Rồi thay CONTAINER ID bên dưới bằng ID thực tế của container ứng dụng mà bạn đang sử dụng:

docker stop 4133b72391da

Bây giờ, bạn có thể xóa container đã dừng cùng toàn bộ image, bao gồm cả các image không sử dụng và image treo, bằng lệnh docker system prune với tùy chọn -a:

docker system prune -a

Nhấn y khi được hỏi để xác nhận rằng bạn muốn xóa container và image đã dừng. Lưu ý, thao tác này cũng sẽ xóa bộ nhớ đệm được tạo trong quá trình build.

Khi image ứng dụng đã được kiểm tra thành công, bạn có thể tiếp tục thiết lập phần còn lại với Docker Compose.

Bước 2: Định nghĩa cấu hình máy chủ Web

Khi đã có Dockerfile của ứng dụng, bạn sẽ tạo một tập tin cấu hình để chạy container Nginx. Trước tiên, bạn sẽ thiết lập một cấu hình cơ bản, bao gồm tên miền, thư mục gốc tài liệu, thông tin proxy và một khối location để xử lý các yêu cầu từ Certbot đến thư mục .well-known, tại đây Certbot sẽ đặt tệp tạm để xác minh rằng DNS của tên miền đã được ánh xạ đến máy chủ của bạn.

Đầu tiên, tạo một thư mục mới trong thư mục dự án hiện tại node_project để lưu tập tin cấu hình:

mkdir nginx-conf

Tạo và mở tập tin với nano hoặc trình soạn thảo mà bạn ưa thích:

nano nginx-conf/nginx.conf

Thêm khối server sau để chuyển tiếp các yêu cầu từ người dùng đến container ứng dụng Node, đồng thời định tuyến các yêu cầu từ Certbot đến thư mục .well-known. Đừng quên thay your_domain bằng tên miền thực tế:

~/node_project/nginx-conf/nginx.conf

server {
        listen 80;
        listen [::]:80;

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;

        server_name your_domain www.your_domain;

        location / {
                proxy_pass <http://nodejs:8080>;
        }

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }
}

Khối server này cho phép bạn khởi chạy container Nginx như một reverse proxy, sẽ chuyển tiếp các yêu cầu đến container ứng dụng Node. Nó cũng cho phép bạn sử dụng plugin webroot của Certbot để lấy chứng chỉ cho tên miền.

Plugin này dựa trên phương thức xác minh HTTP-01, sử dụng một yêu cầu HTTP để xác thực rằng Certbot có thể truy cập tài nguyên từ một máy chủ phản hồi đúng với tên miền được yêu cầu.

Khi chỉnh sửa xong, hãy lưu và đóng tập tin. Nếu bạn dùng nano, thao tác này được thực hiện bằng cách nhấn tổ hợp phím CTRL + X, sau đó nhấn Y, rồi ENTER.

Khi đã hoàn tất cấu hình máy chủ web, bạn có thể tiếp tục tạo tệp docker-compose.yml, tệp này sẽ cho phép bạn khai báo các dịch vụ của ứng dụng cũng như container Certbot dùng để lấy chứng chỉ.

Bước 3: Tạo tập tin Docker Compose

Tập tin docker-compose.yml sẽ định nghĩa các dịch vụ, bao gồm ứng dụng Node và máy chủ web. Tập tin này cũng khai báo các volume được đặt tên (named volumes), đây là yếu tố quan trọng giúp chia sẻ chứng chỉ SSL giữa các container, cũng như thông tin về network và cổng.

Ngoài ra, bạn cũng có thể chỉ định các lệnh sẽ được thực thi khi container được tạo. Đây là tập tin trung tâm định hình cách các dịch vụ phối hợp với nhau.

Tạo và mở tập tin trong thư mục hiện tại:

nano docker-compose.yml

Trước tiên, định nghĩa dịch vụ ứng dụng:

~/node_project/docker-compose.yml

version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped

Định nghĩa nodejs bao gồm các thành phần sau:

  • build: Chỉ định các tùy chọn cấu hình, bao gồm contextdockerfile, sẽ được áp dụng khi Compose xây dựng image của ứng dụng. Nếu bạn muốn sử dụng một image có sẵn từ Docker Hub, bạn có thể thay thế bằng chỉ thị image, kèm theo username, repository và tag của image.
  • context: Đây là phần định nghĩa ngữ cảnh build (build context) cho việc tạo image của ứng dụng. Trong trường hợp này, ngữ cảnh chính là thư mục gốc của dự án hiện tại, được biểu diễn bằng dấu chấm (.).
  • dockerfile: Chỉ định Dockerfile sẽ được Compose sử dụng để build – là Dockerfile đã đề cập ở Bước 1.
  • image, container_name: Đặt tên cho image và container.
  • restart: Xác định chính sách khởi động lại. Mặc định là no, nhưng ở đây container sẽ khởi động lại trừ khi bị dừng thủ công.

Lưu ý, bạn không sử dụng bind mounts với dịch vụ này, vì môi trường được xây dựng nhằm mục đích triển khai chứ không phải phát triển. Để tìm hiểu thêm, bạn hãy tham khảo tài liệu Docker về bind mounts và volumes.

Để cho phép các container ứng dụng và máy chủ web giao tiếp với nhau, bạn hãy thêm một bridge network có tên app-network sau phần định nghĩa restart:

    networks:
      - app-network

Một bridge network do người dùng tự định nghĩa như trên cho phép các container trên cùng một máy Docker daemon giao tiếp trực tiếp với nhau. Cách này đơn giản hóa luồng xử lý và giao tiếp trong ứng dụng của bạn, vì nó mở toàn bộ cổng giữa các container cùng network mà không công khai ra bên ngoài. Nhờ đó, bạn có thể kiểm soát chính xác cổng nào cần được mở cho dịch vụ frontend.

Tiếp theo, định nghĩa dịch vụ webserver:

  webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

Một số thiết lập tương tự như trong dịch vụ nodejs, nhưng có các điểm khác biệt sau:

  • image: Hướng dẫn Compose tải image Nginx mới nhất dựa trên Alpine từ Docker Hub về. Để tìm hiểu thêm về các image Alpine, bạn hãy tham khảo Bước 3 trong bài Hướng Dẫn Xây Dựng Ứng Dụng Node.js Với Docker.
  • ports: Mở cổng 80 để kích hoạt các tùy chọn cấu hình mà bạn đã định nghĩa trong tập tin Nginx.

Các volume được đặt tên và bind mounts bao gồm:

  • web-root:/var/www/html: Thao tác này sẽ thêm các tài nguyên tĩnh của trang web, được sao chép vào một volume có tên là web-root, vào thư mục /var/www/html bên trong container.
  • ./nginx-conf:/etc/nginx/conf.d: Thao tác này sẽ bind mount thư mục cấu hình Nginx từ host vào thư mục tương ứng trên container. Điều này đảm bảo rằng mọi thay đổi bạn thực hiện đối với các file trên host sẽ được phản ánh vào trong container.
  • certbot-etc:/etc/letsencrypt: Thao tác này sẽ mount các chứng chỉ và khóa tương ứng của Let’s Encrypt cho tên miền của bạn vào thư mục thích hợp trên container.
  • certbot-var:/var/lib/letsencrypt: Thao tác này sẽ mount thư mục làm việc mặc định của Let’s Encrypt vào thư mục thích hợp trên container.

Tiếp theo, thêm các tùy chọn cấu hình cho container certbot. Phải chắc chắn là bạn đã thay thế thông tin tên miền và địa chỉ email bằng tên miền thực tế và email liên hệ của bạn:

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain -d www.your_domain

Định nghĩa này hướng dẫn Compose tải image certbot/certbot từ Docker Hub về, đồng thời sử dụng các volume được đặt tên để chia sẻ tài nguyên với container Nginx: chứng chỉ và khóa trong certbot-etc, thư mục làm việc Let’s Encrypt trong certbot-var và nội dung webroot trong web-root.

Trường depends_on đảm bảo container certbot chỉ được khởi động sau khi webserver đã chạy.

Tùy chọn command chỉ định lệnh sẽ được thực thi khi container khởi động, bao gồm subcommand certonly và các tùy chọn:

  • --webroot: Yêu cầu Certbot sử dụng plugin webroot để đặt các file vào thư mục webroot nhằm mục đích xác thực.
  • --webroot-path: Chỉ định đường dẫn đến thư mục webroot.
  • --email: Email bạn muốn sử dụng để đăng ký và khôi phục.
  • --agree-tos: Xác nhận rằng bạn đồng ý với Thỏa thuận Người đăng ký của ACME.
  • --no-eff-email: Thông báo cho Certbot biết bạn không muốn chia sẻ email của mình với Electronic Frontier Foundation (EFF). Bạn có thể bỏ qua cờ này nếu muốn.
  • --staging: Yêu cầu Certbot sử dụng môi trường staging của Let’s Encrypt để lấy chứng chỉ thử nghiệm. Tùy chọn này giúp bạn kiểm tra các cấu hình và tránh các giới hạn về số lần yêu cầu tên miền. Để biết thêm thông tin về các giới hạn này, bạn có thể đọc tài liệu về rate limits của Let’s Encrypt.
  • -d : Cho phép bạn chỉ định các tên miền bạn muốn áp dụng cho yêu cầu của mình. Trong trường hợp này, bạn đã bao gồm your_domainwww.your_domain. Đừng quên thay thế bằng tên miền của riêng bạn.

Cuối cùng, thêm phần định nghĩa volume và network. Nhớ thay sammy bằng tên người dùng thực tế của bạn:

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge

Các volume được đặt tên bao gồm chứng chỉ và thư mục làm việc của Certbot, cùng với volume dành cho tài nguyên tĩnh của trang web, web-root. Trong hầu hết các trường hợp, trình điều khiển mặc định cho Docker volume là local driver, vốn trên hệ thống Linux sẽ chấp nhận các tùy chọn tương tự như lệnh mount. Nhờ đó, bạn có thể chỉ định một danh sách các tùy chọn của driver thông qua thuộc tính driver_opts, cho phép mount thư mục views trên máy chủ (chứa các tài nguyên tĩnh của ứng dụng) vào volume trong thời gian chạy. Sau đó, nội dung của thư mục này có thể được chia sẻ giữa các container. Để biết thêm chi tiết về nội dung thư mục views, bạn có thể tham khảo Bước 2 trong bài Cách Xây dựng Ứng dụng Node.js với Docker.

Tập tin docker-compose.yml hoàn chỉnh như sau:

~/node_project/docker-compose.yml

version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    networks:
      - app-network

  webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain -d www.your_domain

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge

Với các định nghĩa dịch vụ đã hoàn tất, bạn đã sẵn sàng khởi động container và kiểm tra yêu cầu cấp chứng chỉ.

Bước 4: Cấp chứng chỉ SSL và thông tin xác thực

Bạn có thể khởi động các container bằng lệnh docker-compose up. Lệnh này sẽ tạo và chạy các container, dịch vụ theo thứ tự mà bạn đã khai báo. Sau khi tên miền của bạn yêu cầu thành công, các chứng chỉ SSL sẽ được mount vào thư mục /etc/letsencrypt/live trong container webserver.

Khởi tạo các dịch vụ với docker-compose up và tùy chọn -d để các container của Node.js và webserver chạy ở chế độ nền:

docker-compose up -d

Kết quả đầu ra sẽ xác nhận rằng các dịch vụ đã được tạo:

Creating nodejs ... done
Creating webserver ... done
Creating certbot   ... done

Sử dụng docker-compose ps để kiểm tra trạng thái của các dịch vụ:

docker-compose ps

Nếu mọi thứ thành công, các dịch vụ nodejswebserver sẽ ở trạng thái Up, còn container certbot sẽ kết thúc với mã trạng thái 0:

  Name                 Command               State          Ports
------------------------------------------------------------------------
certbot     certbot certonly --webroot ...   Exit 0
nodejs      node app.js                      Up       8080/tcp
webserver   nginx -g daemon off;             Up       0.0.0.0:80->80/tcp

Nếu bạn thấy bất kỳ trạng thái nào khác ngoài “Up” trong cột State đối với các dịch vụ nodejswebserver, hoặc trạng thái thoát khác 0 đối với container certbot, bạn hãy kiểm tra nhật ký dịch vụ bằng lệnh docker-compose logs.

Ví dụ, nếu bạn muốn kiểm tra nhật ký của Certbot, hãy chạy:

docker-compose logs certbot

Tiếp theo, xác minh rằng chứng chỉ đã được mount thành công vào container webserver với docker-compose exec:

docker-compose exec webserver ls -la /etc/letsencrypt/live

Khi bạn yêu cầu thành công, kết quả sẽ tương tự như sau:

total 16
drwx------ 3 root root 4096 Dec 23 16:48 .
drwxr-xr-x 9 root root 4096 Dec 23 16:48 ..
-rw-r--r-- 1 root root  740 Dec 23 16:48 README
drwxr-xr-x 2 root root 4096 Dec 23 16:48 your_domain

Khi đã xác nhận rằng yêu cầu chứng chỉ sẽ thành công, bạn có thể chỉnh sửa phần định nghĩa dịch vụ certbot để loại bỏ cờ --staging.

Mở tệp docker-compose.yml:

nano docker-compose.yml

Tìm đến phần định nghĩa dịch vụ certbot trong tệp và thay thế cờ --staging trong tùy chọn command bằng cờ --force-renewal. Cờ này sẽ yêu cầu Certbot rằng bạn muốn yêu cầu một chứng chỉ mới với các tên miền giống như chứng chỉ hiện có. Phần định nghĩa dịch vụ certbot sau khi chỉnh sửa sẽ như sau:

~/node_project/docker-compose.yml

...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --force-renewal -d your_domain -d www.your_domain
...

Sau khi chỉnh sửa xong, lưu và thoát khỏi tệp. Bây giờ bạn có thể chạy lại docker-compose up để tạo mới container certbot cùng các volume tương ứng. Việc sử dụng tùy chọn --no-deps cho phép Compose bỏ qua việc khởi động lại dịch vụ webserver, vì nó đã đang chạy:

docker-compose up --force-recreate --no-deps certbot

Kết quả đầu ra dưới đây cho thấy yêu cầu chứng chỉ đã thành công:

Recreating certbot ... done
Attaching to certbot
certbot      | Account registered.
certbot      | Renewing an existing certificate for your_domain and www.your_domain
certbot      |
certbot      | Successfully received certificate.
certbot      | Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
certbot      | Key is saved at:         /etc/letsencrypt/live/your_domain/privkey.pem
certbot      | This certificate expires on 2022-11-03.
certbot      | These files will be updated when the certificate renews.
certbot      | NEXT STEPS:
certbot      | - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See <https://certbot.org/renewal-setup> for instructions.
certbot      | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot      |
certbot      | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot      | If you like Certbot, please consider supporting our work by:
certbot      |  * Donating to ISRG / Let's Encrypt:   <https://letsencrypt.org/donate>
certbot      |  * Donating to EFF:                    <https://eff.org/donate-le>
certbot      | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot exited with code 0

Khi chứng chỉ đã được cấp thành công, bạn có thể tiếp tục chỉnh sửa cấu hình Nginx để tích hợp SSL.

Bước 5: Chỉnh sửa cấu hình máy chủ web và định nghĩa dịch vụ

Việc bật SSL trong cấu hình Nginx sẽ bao gồm:

  • Thêm chuyển hướng HTTP sang HTTPS
  • Chỉ định vị trí của chứng chỉ SSL và khóa bí mật.
  • Chỉ định nhóm Diffie-Hellman, mà bạn sẽ sử dụng cho Bảo mật Chuyển tiếp Hoàn hảo (Perfect Forward Secrecy).

Vì bạn sẽ tạo lại dịch vụ webserver để bổ sung các thay đổi này, bạn có thể dừng dịch vụ hiện tại:

docker-compose stop webserver

Tiếp theo, tạo một thư mục trong thư mục dự án hiện tại để chứa khóa Diffie-Hellman:

mkdir dhparam

Tạo khóa bằng lệnh openssl:

sudo openssl dhparam -out /home/sammy/node_project/dhparam/dhparam-2048.pem 2048

Quá trình tạo khóa có thể mất vài phút.

Để thêm thông tin về Diffie-Hellman và SSL vào cấu hình Nginx, trước tiên hãy xóa tệp cấu hình Nginx mà bạn đã tạo trước đó:

rm nginx-conf/nginx.conf

Mở lại tệp này để chỉnh sửa:

nano nginx-conf/nginx.conf

Thêm đoạn mã sau để chuyển hướng HTTP sang HTTPS và cấu hình chứng chỉ SSL, giao thức cũng như các header bảo mật. Hãy thay thế your_domain bằng tên miền thực tế:

~/node_project/nginx-conf/nginx.conf

server {
        listen 80;
        listen [::]:80;
        server_name your_domain www.your_domain;

        location ~ /.well-known/acme-challenge {
          allow all;
          root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name your_domain www.your_domain;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;

        ssl_buffer_size 8k;

        ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

        ssl_protocols TLSv1.2;
        ssl_prefer_server_ciphers on;

        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

        ssl_ecdh_curve secp384r1;
        ssl_session_tickets off;

        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8;

        location / {
                try_files $uri @nodejs;
        }

        location @nodejs {
                proxy_pass <http://nodejs:8080>;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # chỉ bật strict transport security nếu bạn hiểu rõ các hệ quả
        }

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;
}

Khối cấu hình HTTP xác định thư mục webroot để Certbot xử lý các yêu cầu gia hạn chứng chỉ, cụ thể là thư mục .well-known/acme-challenge. Nó cũng bao gồm một chỉ thị rewrite để chuyển hướng tất cả yêu cầu HTTP đến HTTPS.

Khối cấu hình HTTPS kích hoạt SSL và HTTP/2. Để tìm hiểu thêm về cách HTTP/2 cải tiến so với các phiên bản trước của giao thức HTTP cũng như các lợi ích về hiệu suất website.

Khối này cũng bao gồm một loạt tùy chọn nhằm đảm bảo rằng bạn đang sử dụng các giao thức SSL và bộ mã hóa hiện đại nhất, đồng thời kích hoạt tính năng OCSP stapling. OCSP stapling cho phép máy chủ cung cấp phản hồi có dấu thời gian từ tổ chức cấp phát chứng chỉ trong quá trình bắt tay TLS ban đầu, từ đó tăng tốc độ xác thực.

Khối này cũng chỉ định các thông tin chứng chỉ SSL và Diffie-Hellman cũng như vị trí lưu trữ tương ứng.

Cuối cùng, bạn cần chuyển thông tin proxy pass vào khối này, bao gồm một khối location sử dụng try_files để chuyển tiếp yêu cầu tới container ứng dụng Node.js thông qua alias, và một khối location cho alias đó, nơi cấu hình các HTTP header bảo mật giúp đạt điểm A trên các trang kiểm tra như SSL Labs và Security Headers.

Các header này bao gồm X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-PolicyX-XSS-Protection. Header HTTP Strict Transport Security (HSTS) hiện đang bị chú thích — chỉ bật tính năng này nếu bạn hiểu rõ các hệ quả và đã đánh giá khả năng sử dụng chức năng “preload” của nó.

Sau khi chỉnh sửa xong, hãy lưu và đóng tệp.

Trước khi tạo lại dịch vụ webserver, bạn cần bổ sung một số cấu hình vào định nghĩa dịch vụ trong tệp docker-compose.yml, bao gồm thông tin port cho HTTPS và cấu hình volume cho Diffie-Hellman.

Mở tệp:

nano docker-compose.yml

Trong phần định nghĩa dịch vụ webserver, thêm ánh xạ cổng sau và volume dhparam:

~/node_project/docker-compose.yml

...
 webserver:
    image: nginx:latest
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - dhparam:/etc/ssl/certs
    depends_on:
      - nodejs
    networks:
      - app-network

Tiếp theo, thêm volume dhparam vào phần khai báo volumes. Hãy thay thế thư mục sammynode_project cho phù hợp với hệ thống của bạn:

~/node_project/docker-compose.yml

...
volumes:
  ...
  webroot:
  ...
  dhparam:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/dhparam/
      o: bind

Tương tự như volume web-root, volume dhparam sẽ gắn khóa Diffie-Hellman từ máy chủ host vào container webserver.

Sau khi chỉnh sửa xong, lưu và đóng tệp.

Tạo lại dịch vụ webserver:

docker-compose up -d --force-recreate --no-deps webserver

Kiểm tra các dịch vụ với lệnh docker-compose ps:

docker-compose ps

Kết quả đầu ra dưới đây cho thấy dịch vụ nodejswebserver đang chạy:

  Name                 Command               State                     Ports
----------------------------------------------------------------------------------------------
certbot     certbot certonly --webroot ...   Exit 0
nodejs      node app.js                      Up       8080/tcp
webserver   nginx -g daemon off;             Up       0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp

Cuối cùng, bạn có thể truy cập tên miền của mình để kiểm tra hoạt động. Mở trình duyệt và truy cập vào https://your_domain, nhớ thay your_domain bằng tên miền thật của bạn.

bao-mat-ung-dung-Node-js

Một biểu tượng ổ khóa sẽ xuất hiện trong thanh địa chỉ của trình duyệt. Nếu muốn, bạn có thể truy cập vào trang kiểm tra SSL Labs Server Test hoặc trang Security Headers Server Test. Với cấu hình hiện tại, trang web của bạn sẽ đạt điểm A trên bài kiểm tra của SSL Labs. Để đạt điểm A trên bài kiểm tra Security Headers, bạn cần bỏ chú thích dòng cấu hình Strict Transport Security (HSTS) trong tệp nginx-conf/nginx.conf:

~/node_project/nginx-conf/nginx.conf

…
location @nodejs {
                proxy_pass <http://nodejs:8080>;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # chỉ bật strict transport security nếu bạn hiểu rõ các hệ quả
        }
…

Một lần nữa, chỉ nên bật tùy chọn này nếu bạn hiểu rõ các tác động và đã đánh giá chức năng “preload”.

Bước 6: Gia hạn chứng chỉ

Chứng chỉ Let’s Encrypt có thời hạn hiệu lực là 90 ngày. Để tránh chứng chỉ bị hết hạn, bạn có thể thiết lập quy trình gia hạn tự động. Một trong những cách thực hiện việc này là sử dụng tiện ích lập lịch cron. Bạn có thể tạo một tác vụ cron kèm theo một script để tự động gia hạn chứng chỉ và tải lại cấu hình của Nginx.

Mở một tệp script tên là ssl_renew.sh trong thư mục dự án của bạn:

nano ssl_renew.sh

Thêm đoạn mã sau vào tệp để thực hiện gia hạn chứng chỉ và tải lại cấu hình máy chủ web:

~/node_project/ssl_renew.sh

#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --ansi never"
DOCKER="/usr/bin/docker"

cd /home/sammy/node_project/
$COMPOSE run certbot renew --dry-run && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

Script này đầu tiên gán đường dẫn thực thi của docker-compose vào biến COMPOSE và sử dụng tùy chọn --no-ansi để loại bỏ ký tự điều khiển màu. Tiếp theo, script gán đường dẫn thực thi của docker vào biến DOCKER. Cuối cùng, nó chuyển đến thư mục ~/node_project và chạy các lệnh sau:

  • docker-compose run: Khởi chạy một container certbot và ghi đè lệnh được khai báo trong định nghĩa dịch vụ. Thay vì sử dụng subcommand certonly, lệnh sử dụng renew để gia hạn các chứng chỉ sắp hết hạn. Tùy chọn -dry-run được dùng để kiểm tra hoạt động của script.
  • docker-compose kill: Gửi tín hiệu SIGHUP đến container webserver để tải lại cấu hình Nginx.
  • docker system prune: Xóa các container và image không còn sử dụng để giải phóng tài nguyên.

Sau khi hoàn tất chỉnh sửa, lưu và thoát khỏi tệp, sau đó cấp quyền thực thi:

chmod +x ssl_renew.sh

Tiếp theo, mở crontab của người dùng root để thiết lập lịch chạy script:

sudo crontab -e

Nếu đây là lần đầu bạn chỉnh sửa crontab, hệ thống sẽ yêu cầu bạn chọn trình soạn thảo:

no crontab for root - using an empty one
Select an editor.  To change later, run 'select-editor'.
  1. /bin/ed
  2. /bin/nano        <---- dễ sử dụng nhất
  3. /usr/bin/vim.basic
  4. /usr/bin/vim.tiny
Choose 1-4 [2]:

Cuối tệp crontab, thêm dòng sau:

*/5 * * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

Thiết lập này sẽ chạy tác vụ mỗi 5 phút để bạn kiểm tra xem yêu cầu gia hạn đã hoạt động như mong đợi chưa. Ngoài ra, bạn cũng đã tạo tệp cron.log để ghi lại các đầu ra liên quan từ quá trình chạy tác vụ.

Sau 5 phút, kiểm tra tệp log để xác nhận xem yêu cầu gia hạn đã thành công hay chưa:

tail -f /var/log/cron.log

Nếu gia hạn mô phỏng thành công, bạn sẽ thấy kết quả đầu ra như sau:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Chúc mừng, tất cả chứng chỉ đã được gia hạn thành công:
  /etc/letsencrypt/live/your_domain/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Killing webserver ... done

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Deleted Containers:
00cad94050985261e5b377de43e314b30ad0a6a724189753a9a23ec76488fd78

Total reclaimed space: 824.5kB

Nhấn CTRL + C để thoát khỏi chế độ theo dõi log.

Bây giờ bạn có thể sửa lại tệp crontab để thiết lập lịch chạy hàng ngày. Ví dụ, để chạy script mỗi ngày lúc 12 giờ trưa, bạn có thể thay dòng cuối cùng của tệp thành:

0 12 * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

Bạn cũng có thể xóa tùy chọn --dry-run khỏi script ssl_renew.sh:

~/node_project/ssl_renew.sh

#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /home/sammy/node_project/
$COMPOSE run certbot renew && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

Cron job này sẽ đảm bảo rằng chứng chỉ Let’s Encrypt luôn được gia hạn đúng hạn. Bạn cũng có thể thiết lập cơ chế quay vòng log bằng tiện ích logrotate để xoay vòng và nén tệp log khi cần thiết.

Kết luận

Bạn đã sử dụng container để thiết lập và chạy ứng dụng Node với Nginx reverse proxy. Bạn cũng đã cài đặt thành công chứng chỉ SSL cho tên miền ứng dụng và thiết lập một cron job để gia hạn các chứng chỉ khi cần thiết.

Nếu bạn muốn tìm hiểu thêm về các plugin của Let’s Encrypt, hãy tham khảo các bài viết liên quan đến cách sử dụng plugin Nginx hoặc plugin standalone.

Tài liệu chính thức của Compose cũng là nguồn tham khảo rất hay để bạn tìm hiểu thêm về các ứng dụng nhiều container.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất