0

Hướng dẫn Setup Jenkins cho hệ thống Monolith + Bài học 39 + Bảo mật hệ thống Jenkins (Jenkins Hardening): Cấu hình HTTPS, Reverse Proxy, CSRF và vá lỗ hổng bảo mật

Ở Bài 28, chúng ta đã thiết lập lớp bảo vệ bên trong bằng cơ chế phân quyền RBAC. Tuy nhiên, nếu bạn vẫn đang truy cập vào Jenkins qua địa chỉ http://<IP_SERVER>:8080 thuần túy mà không có mã hóa SSL/TLS, hoặc cho phép lập trình viên tự do sửa tệp Jenkinsfile trên nhánh feature/* để chạy những đoạn mã Groovy tùy ý, hệ thống CI/CD của bạn đang đứng trước những lỗ hổng an ninh chí mạng.

Hãy nhớ rằng: Ai chiếm được quyền điều khiển máy chủ Jenkins Master, người đó mặc nhiên chiếm được toàn bộ máy chủ Production, Database và mã nguồn của khối Monolith! Bài học hôm nay sẽ hướng dẫn bạn gia cố pháo đài Jenkins (Jenkins Hardening) từ tầng mạng (Nginx Reverse Proxy & HTTPS) cho đến tầng lõi thực thi (Groovy Sandbox, Agent-to-Controller Isolation và CSRF Protection).

1. Năm bề mặt tấn công (Attack Surfaces) nguy hiểm nhất trên Jenkins

Trước khi bắt tay vào cấu hình, hãy điểm qua 5 con đường phổ biến nhất mà tin tặc (hoặc một tài khoản nội bộ bị lộ) thường lợi dụng để tấn công máy chủ Jenkins của dự án Monolith:

Bề mặt tấn công Kịch bản khai thác thực tế Hậu quả đối với hệ thống Monolith
1. Nghe lén đường truyền (Plaintext HTTP :8080) Quản trị viên đăng nhập Jenkins hoặc GitLab bắn Webhook qua giao thức http:// không mã hóa. Lộ mật khẩu Admin, lộ Session Cookie hoặc bị đánh cắp khóa bí mật ngay trên đường truyền mạng.
2. Tấn công qua Jenkinsfile độc hại (Pipeline Poisoning) Một lập trình viên đẩy nhánh feature/hack chứa lệnh Groovy Jenkins.instance.credentials... hoặc dùng sh để in trộm khóa SSH của Production. Toàn bộ mật khẩu Production bị in ra Console Log dù lập trình viên đó không hề có quyền xem trên UI!
3. Giả mạo yêu cầu liên trang (CSRF) & Lộ Endpoint nhạy cảm Kẻ tấn công lừa Admin bấm vào một đường link chứa lệnh gọi ngầm tới /script (Script Console) hoặc /prometheus. Thực thi mã từ xa (RCE - Remote Code Execution) chiếm quyền điều khiển máy chủ Host.
4. Máy chủ Agent tấn công ngược lên Master (Agent-to-Controller) Máy chủ Worker Agent bị nhiễm mã độc khi chạy một thư viện bên thứ 3, sau đó gửi lệnh điều khiển ngược về Jenkins Master. Từ một máy Worker phụ bị nhiễm độc, tin tặc leo thang đặc quyền chiếm luôn não bộ Jenkins Master.
5. Chạy Container Jenkins dưới quyền root + docker.sock Việc mount /var/run/docker.sock tương đương với việc trao quyền root của máy chủ vật lý cho container. Chỉ cần thoát khỏi container (Container Escape), kẻ tấn công sẽ làm chủ hoàn toàn hệ điều hành Linux bên dưới.

2. Lớp giáp 1: Đóng cổng 8080 và bọc Jenkins sau Nginx Reverse Proxy + SSL/TLS (HTTPS)

Nguyên tắc đầu tiên khi đưa Jenkins vào vận hành chính thức là: Tuyệt đối không bao giờ phơi trực tiếp cổng 8080 của Java (Jetty Web Server) ra mạng Internet!

Chúng ta sẽ khóa cổng 8080 chỉ cho phép lắng nghe nội bộ (127.0.0.1:8080), và đặt Nginx Reverse Proxy đứng chắn phía trước tại cổng 443 (HTTPS) để gánh toàn bộ việc giải mã chứng chỉ SSL, chống tấn công DDoS cơ bản và lọc bỏ các đường dẫn nhạy cảm.

Bước 1: Khóa cổng 8080 nội bộ trong docker-compose.yml của Jenkins Master

Mở tệp docker-compose.yml của Jenkins Master (Bài 34) và sửa phần ports:

YAML

services:
  jenkins-master:
    image: jenkins/jenkins:lts-jdk17
    container_name: jenkins-master
    ports:
      # CHỈ lắng nghe ở giao diện Loopback nội bộ (Người ngoài quét IP:8080 sẽ bị từ chối hoàn toàn!)
      - "127.0.0.1:8080:8080"
      # Cổng 50000 chỉ cần mở nếu bạn dùng JNLP Agent; nếu dùng SSH Agent (Bài 31) thì đóng luôn cổng 50000!

Bước 2: Cấu hình Nginx Reverse Proxy chuẩn Hardening cho Jenkins

Tạo tệp /etc/nginx/sites-available/jenkins-secure.conf trên máy chủ Host (đã cài đặt chứng chỉ SSL miễn phí bằng Let's Encrypt Certbot hoặc chứng chỉ nội bộ):

Nginx

upstream jenkins_controller {
    keepalive 32;
    server 127.0.0.1:8080 fail_timeout=0;
}

# Map hỗ trợ nâng cấp kết nối WebSocket (Bắt buộc cho Jenkins CLI và Blue Ocean / Stage View live logs)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
    listen 80;
    server_name jenkins.yourcompany.com;
    # Ép buộc chuyển hướng 100% HTTP sang HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name jenkins.yourcompany.com;

    # 1. Cấu hình chứng chỉ SSL/TLS chuẩn hiện đại (Chỉ cho phép TLS 1.2 và TLS 1.3)
    ssl_certificate     /etc/letsencrypt/live/jenkins.yourcompany.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jenkins.yourcompany.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # 2. Bổ sung các HTTP Security Headers chống Clickjacking và XSS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "same-origin" always;

    # 3. CHỐT CHẶN BẢO MẬT ĐẮT GIÁ: Chỉ cho phép dải IP VPN/Văn phòng công ty truy cập vào /script (Script Console) và /prometheus!
    location ~* ^/(script|prometheus|manage) {
        allow 10.0.0.0/8;          # Cho phép mạng LAN / VPN nội bộ
        allow 118.69.123.45/32;    # Cho phép IP tĩnh văn phòng công ty
        deny all;                  # Chặn đứng mọi truy cập từ Internet công cộng vào trang quản trị!

        proxy_pass http://jenkins_controller;
        include /etc/nginx/proxy_params;
    }

    # 4. Cấu hình chuyển tiếp chuẩn cho Jenkins (Chống lỗi "Reverse Proxy Setup is Broken")
    ignore_invalid_headers off;
    client_max_body_size 50m;

    location / {
        proxy_pass         http://jenkins_controller;
        proxy_redirect     http:// https://;
        proxy_http_version 1.1;

        # Hỗ trợ WebSocket cho Live Console Log
        proxy_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        $connection_upgrade;

        # 4 Header bắt buộc phải có để Jenkins nhận diện đúng HTTPS và chống lỗi CSRF Crumb!
        proxy_set_header   Host              $http_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   X-Forwarded-Port  443;

        proxy_connect_timeout      90;
        proxy_send_timeout         90;
        proxy_read_timeout         90;
        proxy_request_buffering    off;
    }
}

Giải thích lỗi kinh điển "It appears that your reverse proxy set up is broken":

Khi mới đặt Nginx trước Jenkins, rất nhiều người gặp cảnh báo màu đỏ này trong Manage Jenkins. Nguyên nhân là do Nginx thiếu 2 header X-Forwarded-Proto $scheme; và X-Forwarded-Port 443;, hoặc trong mục Manage Jenkins -> System -> Jenkins URL bạn vẫn để là http://... thay vì sửa thành [https://jenkins.yourcompany.com/](https://jenkins.yourcompany.com/). Khi cả Nginx và Jenkins URL cùng khớp https://, cảnh báo sẽ biến mất hoàn toàn!

3. Lớp giáp 2: Bật bảo vệ CSRF (Crumb Issuer) và Phân quyền Thư mục Credentials

A. Chống giả mạo yêu cầu với Strict Crumb Issuer (CSRF Protection)

Mặc định Jenkins sử dụng một mã token dùng 1 lần gọi là Jenkins-Crumb để bảo vệ mọi thao tác POST (như bấm Build, xóa Job, sửa cấu hình). Tuy nhiên, để nâng mức bảo vệ lên tối đa (kiểm tra khớp cả địa chỉ IP của phiên đăng nhập), bạn hãy:

  1. Cài đặt plugin Strict Crumb Issuer.

  2. Vào Manage Jenkins -> Security -> Tại mục CSRF Protection, chọn Strict Crumb Issuer -> Bấm vào Advanced và tích chọn Check the session ID (Khóa chặt mã Crumb với đúng Session đăng nhập hiện tại).

B. Cô lập Kho khóa bí mật của Production khỏi nhánh feature/* (Folder-level Credentials)

Hãy tưởng tượng tình huống này: Ở Bài 9 và Bài 24, bạn lưu khóa monolith-env-prod và SSH Key của Production ở phạm vi Global Credentials (Toàn cục).

  • Nếu một lập trình viên mở nhánh feature/test-code, tự tay sửa tệp Jenkinsfile trên nhánh đó thành:

    Groovy

    withCredentials([file(credentialsId: 'monolith-env-prod', variable: 'HACK')]) {
        sh 'cat $HACK | base64' // Mã hóa base64 để qua mặt bộ lọc dấu *** của Jenkins!
    }
    
    
  • Do monolith-env-prod đang để ở phạm vi Global, bất kỳ Job nào trên Jenkins cũng có quyền gọi withCredentials tới ID đó!

Giải pháp khóa chặt triệt để:

  1. Sử dụng Folders Plugin tạo ra 2 thư mục tách biệt hoàn toàn trên trang chủ Jenkins:

    • Thư mục 1: Monolith-CI-and-Staging (Nơi chứa Multibranch Pipeline quét các nhánh feature/*, develop, staging).

    • Thư mục 2: Monolith-Production-Release (Chỉ chứa đúng Job chạy từ nhánh main đã được bảo vệ nghiêm ngặt trên GitLab/GitHub).

  2. Tuyệt đối không lưu mật khẩu Production ở Global Credentials! Hãy bấm vào bên trong thư mục Monolith-Production-Release -> Chọn menu Credentials ở cột bên trái (chỉ thuộc phạm vi Folder này) -> Lưu monolith-env-prod và prod-ssh-key vào đây!

  3. Kết quả: Các nhánh feature/* nằm ở thư mục Monolith-CI-and-Staging hoàn toàn mù tịt và không bao giờ có thể truy xuất được tới khóa của thư mục Monolith-Production-Release!

4. Lớp giáp 3: Kiểm soát In-process Script Approval & Groovy Sandbox

Khi chạy Pipeline, Jenkins cung cấp 2 chế độ thực thi mã Groovy:

  1. Groovy Sandbox (Hộp cát an toàn): Bật mặc định cho mọi Jenkinsfile lấy từ Git. Trong chế độ này, các câu lệnh nguy hiểm của Java như System.exit(0), new File('/etc/passwd').text hoặc Jenkins.instance... sẽ bị chặn đứng ngay lập tức!

  2. In-process Script Approval (Manage Jenkins -> In-process Script Approval): Khi một đoạn code vượt rào bị chặn, Jenkins sẽ dừng bản build và yêu cầu Admin vào trang In-process Script Approval bấm Approve.

Quy tắc sinh tử cho Admin:

  • Tuyệt đối không bao giờ bấm nút Approve một cách mù quáng đối với các chữ ký hàm nhạy cảm như staticMethod jenkins.model.Jenkins getInstance hoặc new java.io.File xuất phát từ Jenkinsfile của lập trình viên!

  • Như chúng ta đã thiết kế ở Bài 35 (Shared Libraries): Toàn bộ các logic nâng cao cần quyền hệ thống chỉ được viết trong kho Global Trusted Shared Library (jenkins-monolith-shared-lib.git) mà chỉ có đội ngũ DevOps mới có quyền Merge code. Còn tệp Jenkinsfile nằm trong kho code của dự án Monolith chỉ được phép gọi duy nhất hàm monolithStandardPipeline(...)!

5. Lớp giáp 4: Kích hoạt Content Security Policy (CSP) và Cô lập Agent

  1. Bảo vệ trình duyệt khi xem báo cáo HTML (Content-Security-Policy): Khi Jenkins hiển thị các trang báo cáo HTML (như Coverage Report ở Bài 15 hay kết quả Test), nếu bên trong tên một test case có chèn mã độc <script>...</script>, người xem báo cáo có thể bị tấn công XSS. Jenkins mặc định bật một bộ luật CSP rất chặt. Hãy đảm bảo trong JAVA_OPTS của bạn không bao giờ xóa trắng thuộc tính hudson.model.DirectoryBrowserSupport.CSP.

  2. Kiểm tra Agent -> Controller Security: Từ phiên bản Jenkins 2.326 trở đi, hệ thống Agent-to-Controller Access Control đã được bật cứng mặc định, ngăn chặn tiến trình chạy trên máy chủ Worker Agent (monolith-worker-01) đọc hoặc ghi đè các tệp tin nhạy cảm trong /var/jenkins_home của Master.

  3. Chuyển từ Docker Socket (/var/run/docker.sock) sang Rootless Docker hoặc Kaniko trên Agent: Trên máy chủ Worker Agent (Bài 31), để tránh việc container đang build chiếm quyền root của máy chủ thông qua /var/run/docker.sock, bạn nên cấu hình Docker Rootless Mode (chạy Docker Daemon dưới quyền của chính user jenkins không có đặc quyền root) hoặc bật tính năng userns-remap trong /etc/docker/daemon.json:

    JSON

    {
      "userns-remap": "default",
      "no-new-privileges": true
    }
    
    

    Cờ "no-new-privileges": true ngăn chặn tuyệt đối mọi tiến trình bên trong container leo thang đặc quyền thông qua các tệp thực thi có gắn cờ setuid!

6. Tự động hóa toàn bộ cấu hình Hardening trong jenkins.yaml (JCasC)

Để đảm bảo mọi thiết lập bảo mật ở trên không bao giờ bị ai đó vô tình tắt đi trên giao diện web, bạn hãy bổ sung khối cấu hình Security Hardening chuẩn dưới đây vào tệp jenkins.yaml (Bài 34):

YAML

jenkins:
  # 1. Tắt hoàn toàn cổng TCP Agent (JNLP) vì chúng ta chỉ dùng SSH Agent an toàn (Bài 31)
  slaveAgentPort: -1

  # 2. Kích hoạt bảo vệ chống giả mạo yêu cầu CSRF (Crumb Issuer)
  crumbIssuer:
    standard:
      excludeClientIPFromCrumb: false

  # 3. Chặn các giao thức CLI cũ không an toàn qua Remoting
  remotingSecurity:
    enabled: true

security:
  # 4. Ẩn thông tin phiên bản Jenkins trên Header phản hồi HTTP để chống dò quét lỗ hổng tự động
  apiToken:
    creationOfLegacyTokenEnabled: false
    tokenGenerationOnCreationEnabled: false
    usageStatisticsEnabled: true

  # 5. Khóa chặt cấu hình Git Host Key Verification tránh tấn công Man-in-the-Middle (MitM)
  gitHostKeyVerificationConfiguration:
    sshHostKeyVerificationStrategy: "knownHostsFileVerificationStrategy"


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí