Service tokens và mTLS: authentication cho CI/CD, bot, device

Khi client không phải người dùng. Phân biệt service token vs mTLS, cách thiết lập cả hai, chiến lược rotate, audit log, và anti-pattern phổ biến.

· 13 phút đọc · Read in English
Xác thực non-human cho Cloudflare Access: service token (CF-Access-Client-Id/Secret) cho CI/CD và cron, mTLS bằng client certificate cho thiết bị IoT, kèm chiến lược rotate không downtime và audit log

TL;DR

IdP (Part 5) giải quyết xác thực cho client là con người, có browser, có người chờ MFA. Nhưng nhiều client trong hệ thống enterprise không phải con người: CI/CD runner, cron job, probe giám sát, thiết bị IoT, SDK gọi API. Họ không có browser, không nhập mật khẩu được.

Cloudflare Access có 2 cơ chế cho non-human:

  • Service token: 2 header HTTP (CF-Access-Client-Id + CF-Access-Client-Secret). Thiết lập 5 phút. Rà soát qua log.
  • mTLS: client trình cert, Cloudflare xác minh chuỗi dựa trên root CA bạn upload. Thiết lập cần PKI. Mạnh hơn khi tổ chức có PKI sẵn hoặc cần định danh thiết bị.

Bài này đi qua:

  • Khi nào dùng service token, khi nào mTLS.
  • Thiết lập đầu-cuối cho cả hai.
  • Chiến lược rotate với cửa sổ chồng lấn để không gián đoạn.
  • Log kiểm toán cho traffic non-human.
  • 4 anti-pattern phổ biến.

Luận điểm chính:

Service token là “API key có log”. mTLS là “định danh thiết bị có bằng chứng mật mã”. Đừng dùng service token cho cái cần bằng chứng. Đừng dùng mTLS cho cái không cần.

Bài này là Part 6 của Cloudflare One Handbook.


Dành cho ai

  • DevOps/SRE thiết lập CI/CD, cron job, giám sát chạm tới app đứng sau Access.
  • Kỹ sư nền tảng xây SDK/firmware thiết bị gọi về endpoint được bảo vệ.
  • Kỹ sư bảo mật thiết kế zero-trust cho toàn bộ traffic, không chỉ con người.

Bạn nên đã đọc:

Sau bài này bạn sẽ:

  • Biết chọn service token hay mTLS cho từng trường hợp.
  • Thiết lập được cả hai đầu-cuối với ví dụ code thực.
  • Có playbook rotate không gây gián đoạn.
  • Biết rà soát traffic non-human trong log Zero Trust.

Bài này không nói về gì

  • mTLS phía origin (client cert giữa Cloudflare ↔ origin): khác với client-side mTLS trong bài này.
  • API token của Cloudflare platform (quản lý zone, DNS): hoàn toàn khác, không liên quan Access.
  • OAuth client credentials flow: Cloudflare Access không hỗ trợ luồng này native; pattern tương đương là service token.

Khái niệm

  • Service token: cặp (Client ID, Client Secret) do Cloudflare sinh. Client gửi qua 2 header: CF-Access-Client-Id, CF-Access-Client-Secret. Cloudflare xác minh rồi khớp chính sách.
  • mTLS (mutual TLS): cả 2 bên bắt tay TLS đều trình cert. Client trình cert, Cloudflare xác minh chuỗi lên root CA đã tin.
  • Root CA: cert của Certificate Authority bạn upload vào Cloudflare. Mọi cert client phải chuỗi lên root này mới hợp lệ.
  • Chuỗi cert: chuỗi cert: client cert → intermediate CA → root CA.
  • Action Service Auth: action đặc biệt trong chính sách Access để khớp service token, không phải người dùng.
  • Định danh non-human: identity không gắn với người dùng trong IdP. Gắn với khối lượng công việc (service, device, CI job).

Human vs non-human: khác nhau ở đâu

Client là con người dùng IdP, non-human dùng service token hoặc mTLS. Giả định khác nhau về browser + prompt MFA

Vì sao không dùng IdP cho bot?

  • Không có browser: endpoint authorize của IdP gửi 302 redirect → cron/script không theo được.
  • Không tương tác MFA: IdP thường yêu cầu MFA bổ sung. Bot không prompt được.
  • Không có danh tính ở IdP: tạo người dùng github-actions-bot@example.com trong Okta = tốn một giấy phép, và người dùng đó dễ bị kẻ xấu lợi dụng hơn là bot thật dùng.
  • Rotate token: mật khẩu IdP thường dùng có thời hạn dài (năm). Service token thiết kế cho rotate thường xuyên hơn.

Có cách lách được không?

  • OAuth client credentials với luồng headless: không chạy trực tiếp với Cloudflare Access: luồng OIDC của Cloudflare Access là authorization_code, không phải client_credentials.
  • Tài khoản robot với mật khẩu + token lưu trữ: khả thi về kỹ thuật nhưng anti-pattern: tài khoản robot thành identity đặc quyền, chính sách khó viết.

Câu trả lời đúng: service token hoặc mTLS.


Service token vs mTLS: khi nào chọn cái nào

Yếu tốService tokenmTLS
Độ phức tạp thiết lậpThấp, click tạo, copy headerCao, cần root CA, pipeline cert client
Loại secretChuỗi (ID + secret)Cert X.509 + private key
Công sức rotateTạo token mới, cập nhật cấu hìnhCấp lại cert, phân phối, thu hồi cũ
Định danh theo từng clientCó (mỗi client một token)Có (mỗi cert một định danh)
Bằng chứng định danh phần cứngKhôngCó, cert có thể bind TPM/HSM
Phụ thuộc PKIKhôngCần (root CA, intermediate, pipeline)
Mức độ chi tiết kiểm toánToken ID trong logSubject của cert trong log
Phù hợp khiCI/CD, tích hợp nhanh, SDKĐội thiết bị IoT, thiết bị có PKI, compliance bắt buộc

Nguyên tắc:

  • Bắt đầu bằng service token. Đơn giản, đủ dùng cho 80% trường hợp sử dụng.
  • Chuyển sang mTLS khi:
    • Tổ chức có PKI sẵn.
    • Quy định yêu cầu định danh thiết bị bằng mật mã (tài chính, y tế).
    • Quy mô hàng ngàn thiết bị (rotate service token thủ công không mở rộng được).
    • Cần bind định danh với phần cứng (TPM).

Thiết lập 1: Service token

Bước 1: Tạo token

Zero Trust dashboard → AccessService AuthService TokensCreate Service Token.

  • Name: ci-deploy-prod (đặt tên mô tả trường hợp sử dụng: pattern <team>-<purpose>-<env>)
  • Duration: mặc định 1 năm. Đặt ngắn hơn cho trường hợp sử dụng rủi ro.

Lưu. Cloudflare hiện Client IDClient Secret một lần duy nhất. Copy cả hai vào secret manager ngay.

  • Định dạng Client ID: abc123.access (tiền tố đọc được)
  • Định dạng Client Secret: chuỗi dài kiểu base64, không hiện lại được nếu bỏ lỡ.

Bước 2: Thêm vào chính sách Access

Chỉnh sửa Access application (ví dụ api.example.com) → Thêm chính sách:

  • Name: CI deployment
  • Action: Service Auth (đây là điểm quan trọng: không phải Allow thường)
  • Include: Service Token → chọn ci-deploy-prod vừa tạo.

Lưu. Không có Require block, service token không đi qua kiểm tra posture.

Bước 3: Sử dụng từ client

curl:

curl https://api.example.com/deploy \
  -H "CF-Access-Client-Id: abc123.access" \
  -H "CF-Access-Client-Secret: s3cr3t_long_string_here" \
  -X POST -d '{"version":"v1.2.3"}'

GitHub Actions:

- name: Trigger deployment
  env:
    CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
    CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}
  run: |
    curl https://api.example.com/deploy \
      -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
      -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
      -X POST -d '{"version":"${{ github.sha }}"}'

Terraform (nếu Cloudflare provider gọi Access-protected endpoint):

provider "cloudflare" {
  api_token = var.cf_api_token
}

data "http" "deploy" {
  url = "https://api.example.com/deploy"
  request_headers = {
    "CF-Access-Client-Id"     = var.cf_access_client_id
    "CF-Access-Client-Secret" = var.cf_access_client_secret
  }
}

Python (requests):

import requests

resp = requests.post(
    "https://api.example.com/deploy",
    headers={
        "CF-Access-Client-Id": os.environ["CF_ACCESS_CLIENT_ID"],
        "CF-Access-Client-Secret": os.environ["CF_ACCESS_CLIENT_SECRET"],
    },
    json={"version": "v1.2.3"},
    timeout=10,
)
resp.raise_for_status()

Bước 4: Xác minh trong log

Zero Trust → LogsAccess → lọc theo Application api.example.com. Sự kiện từ service token hiển thị:

  • User: ci-deploy-prod.access (tên token làm định danh)
  • Connection method: Service Token
  • Policy matched: CI deployment

Nếu sự kiện không hiện → header sai, secret sai, hoặc chính sách không include token.

Luồng service token dạng hình

Luồng service token: client gửi 2 header, CF xác minh, chuyển tiếp qua Tunnel tới origin, 1 vòng khứ hồi, không redirect


Thiết lập 2: mTLS

mTLS phức tạp hơn. Nhưng khi đã có PKI, nó mạnh hơn rõ rệt.

Bước 1: Chuẩn bị root CA

Bạn cần một root CA, có thể:

  • CA tổ chức có sẵn (AD CS, HashiCorp Vault PKI, Smallstep, AWS Private CA).
  • Tự tạo CA kiểm thử: openssl req -x509 -sha256 -days 3650 -newkey rsa:4096 -keyout ca.key -out ca.crt.

Yêu cầu:

  • Cert dạng PEM.
  • Có private key tương ứng để ký cert client (private key không upload lên Cloudflare).

Bước 2: Upload root CA vào Cloudflare

Zero Trust → SettingsAuthenticationMutual TLS authenticationAdd mTLS certificate.

  • Paste nội dung ca.crt (bao gồm các dòng -----BEGIN CERTIFICATE-----).
  • Name: Corporate Device CA
  • Associated hostnames: api.example.com (và các hostname khác muốn bật mTLS).

Lưu. Cloudflare xác minh định dạng cert → trạng thái Active.

Bước 3: Cấp cert client

Từ CA, cấp cert cho client:

# Tạo CSR từ client
openssl req -newkey rsa:2048 -keyout client.key -out client.csr \
  -subj "/CN=github-actions-bot/O=Example Corp"

# CA sign CSR thành cert
openssl x509 -req -in client.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt -days 365 -sha256

Kết quả: client.crt (cert công khai) + client.key (private key, giữ bí mật).

Bước 4: Khớp chính sách theo thuộc tính cert

Tạo chính sách Access application:

  • Name: mTLS from Corporate CA
  • Action: Non-Identity (đây là action cho mTLS, không phải Allow thường).
  • Include: Common Namegithub-actions-bot (hoặc khớp pattern, ví dụ kết thúc bằng .corp.example.com).

Lưu.

Khớp có thể theo:

  • Common Name (CN): tên trong subject.
  • Issuer: ai ký cert (nếu nhiều CA).
  • Country / Organization: các thuộc tính khác trong subject.

Bước 5: Client sử dụng cert

curl:

curl https://api.example.com/deploy \
  --cert client.crt \
  --key client.key \
  -X POST -d '{"version":"v1.2.3"}'

Python:

resp = requests.post(
    "https://api.example.com/deploy",
    cert=("client.crt", "client.key"),
    json={"version": "v1.2.3"},
    timeout=10,
)

Go:

cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            Certificates: []tls.Certificate{cert},
        },
    },
}
resp, _ := client.Post("https://api.example.com/deploy", ...)

Luồng bắt tay mTLS

Bắt tay mTLS: ClientHello, ServerHello + CertificateRequest, Client Certificate + chuỗi, CF xác minh chuỗi lên root, chuyển tiếp qua Tunnel


Rotate: pattern cửa sổ chồng lấn

Cả service token và cert mTLS đều cần rotate định kỳ. Rotate sai = gián đoạn.

Chồng lấn khi rotate: tạo mới → chuyển cấu hình client → thu hồi cũ. Cả hai token/cert cùng hợp lệ trong cửa sổ chuyển đổi.

Rotate service token

Quy trình đúng:

  1. T0, Tạo token mới. Dashboard → Create new token ci-deploy-prod-v2.
  2. T0, Thêm token mới vào cùng chính sách. Chính sách CI deployment giờ include cả cũ và mới.
  3. T0 → T+1h, Cập nhật client secret trong secret manager (GitHub secrets, Vault, v.v.). Triển khai theo đợt, không cập nhật tất cả CI pipeline cùng lúc.
  4. T+1h, Xác minh token mới đang được dùng. Kiểm tra log, sự kiện có tên ci-deploy-prod-v2.
  5. T+1h, Thu hồi token cũ. Dashboard → token cũ → Delete.

Cửa sổ chồng lấn (T0 → T+1h) là lúc cả hai token cùng hợp lệ. Nếu bạn thu hồi cũ trước khi chuyển xong, sẽ gián đoạn.

Rotate cert mTLS

Tương tự, nhưng qua CA:

  1. T0, Cấp cert mới với CN trùng (hoặc CN khác, tùy cách khớp chính sách).
  2. T0, Đảm bảo chuỗi tin cậy CA chưa đổi (nếu đổi Intermediate CA cùng lúc, upload mới vào Cloudflare trước).
  3. T0 → T+1h, Chuyển file cert client ở CI/thiết bị.
  4. T+24h, Thu hồi cert cũ trong CA (CRL hoặc OCSP).

Lưu ý: Cloudflare không kiểm tra CRL cho cert client mTLS native, nếu cần áp dụng thu hồi, bạn phải gỡ cert ID khỏi chính sách một cách tường minh, hoặc cập nhật pattern khớp (CN khác).

Tần suất rotate khuyến nghị

Loại clientService tokenmTLS cert
CI job ngắn hạn90 ngàyN/A (thường dùng service token)
Dịch vụ chạy dài180 ngày1 năm
Fleet thiết bịN/A1-2 năm
Bảo mật cao / compliance30 ngày90 ngày

Chính sách cho non-human: các pattern

Pattern 1: Endpoint dịch vụ chỉ cho CI

# Access application: api.example.com
policies:
  - name: "CI deployment"
    action: service_auth
    include:
      - service_token: "ci-deploy-prod"
      - service_token: "ci-deploy-staging"

Chỉ token CI vào. Không có người dùng, không truy cập qua browser.

Pattern 2: Endpoint cho cả người dùng và bot

# Access application: api.example.com
policies:
  # Policy 1: human users (order 1)
  - name: "Developers manual"
    action: allow
    include:
      - groups: [Engineering]
    require:
      - device_posture: [managed_device]

  # Policy 2: CI service tokens (order 2)
  - name: "CI auto-deploy"
    action: service_auth
    include:
      - service_token: "ci-deploy-prod"

Endpoint ứng dụng cho phép cả developer là con người (qua browser + IdP) lẫn CI (qua service token). Một endpoint, hai đường xác thực.

Pattern 3: mTLS cho đội thiết bị

# Access application: telemetry.example.com
policies:
  - name: "Factory devices"
    action: non_identity
    include:
      - common_name: "^device-[a-z0-9-]+\\.factory\\.corp$"

Khớp pattern CN để cho phép hàng ngàn thiết bị khác nhau mà không cần tạo từng entry.


Log kiểm toán cho non-human

Zero Trust → LogsAccess → lọc:

  • Connection method: Service Token / Non-Identity mTLS
  • User: tên token hoặc CN cert

Mỗi sự kiện có:

  • Timestamp
  • Application
  • Chính sách đã khớp
  • Tên token hoặc CN cert
  • IP nguồn
  • Quốc gia
  • User agent

Đẩy log sang SIEM

Log được đẩy về SIEM qua Logpush (Part 17 sẽ bàn). Dataset: access_requests. Trường quan trọng cho non-human:

{
  "app_name": "api.example.com",
  "allowed": true,
  "service_token_id": "abc123.access",
  "service_token_name": "ci-deploy-prod",
  "connection_method": "service_token",
  "created_at": "2026-05-07T10:24:33Z",
  "ip_address": "203.0.113.42",
  "country": "SG"
}

Phát hiện bất thường

Pattern sự kiện đáng chú ý:

  • Service token gọi từ quốc gia lạ: CI thường gọi từ IP của nhà cung cấp CI. Request từ quốc gia khác = rò rỉ credential.
  • Tần suất tăng đột biến: đường nền 100 req/giờ, đột biến 10,000 = lạm dụng hoặc script chạy lạc.
  • Xác thực thất bại sau khi rotate: token cũ vẫn đang thử = client chưa chuyển xong.

Đánh đổi

Quyết địnhOption AOption BKhuyến nghị
Loại xác thựcService tokenmTLSToken cho đa số. mTLS khi có PKI hoặc compliance
Phạm vi token1 token cho nhiều chính sách1 token mỗi trường hợp sử dụng1 theo từng trường hợp sử dụng, kiểm toán + thu hồi chi tiết
Lưu trữ secretGitHub secrets / VaultBiến môi trường plaintextLuôn dùng secret manager. Không bao giờ plaintext
Nhịp rotateTheo chính sách (90/180 ngày)Thủ công khi có vấn đềTự động, đặt lịch, tạo runbook rotate
Hết hạnToken không hết hạnToken hết hạn 1 nămCó hết hạn, ép buộc nhịp rotate
Action của chính sáchService AuthAllow (bypass Access)Service Auth, giữ dấu vết kiểm toán, không bypass

Các anti-pattern phổ biến

1. “Hardcode secret vào source code”

# BAD
CF_SECRET = "s3cr3t_long_string_here"

Secret rò rỉ vào git history. Rotate khó, cần build lại container. Dùng secret manager.

2. “Dùng một service token cho tất cả CI job”

Một token → một định danh trong log → không phân biệt được job nào vừa triển khai. Dấu vết kiểm toán vô dụng. Tạo token riêng cho từng pipeline quan trọng.

3. “Bypass Access cho endpoint API”

policies:
  - name: "API public"
    action: bypass       # anti-pattern

Bypass = traffic không qua kiểm tra Access, cũng không có log. Thay vì bypass, dùng Service Auth với token. Log + chính sách vẫn hoạt động.

4. “Để private key của root CA mTLS trên CI runner”

Private key của root CA phải ở HSM hoặc offline. Chỉ intermediate CA mới được để trên runner/vault. Nếu key root CA rò rỉ → toàn bộ chuỗi tin cậy vỡ, mọi cert client phải cấp lại.

5. “Không đặt hết hạn cho service token”

Token không hết hạn = secret tồn tại vĩnh viễn. Nếu nhà cung cấp CI rò rỉ (đã xảy ra với GitHub, CircleCI), kẻ tấn công dùng được cho đến khi bạn phát hiện. Đặt hết hạn tối đa 1 năm.

6. “Rotate bằng cách tắt cũ trước, tạo mới sau”

Cửa sổ gián đoạn. Luôn tạo mới → chuyển xong → thu hồi cũ.


Danh sách kiểm tra: trước khi đưa non-human auth vào production

Thiết lập:

  • Tên token/cert theo quy ước đặt tên (<team>-<purpose>-<env>).
  • Secret lưu trong secret manager, không hardcode.
  • Action chính sách đúng: Service Auth cho token, Non-Identity cho mTLS.
  • Session duration / hiệu lực cert đặt tường minh, không để mặc định.

Rotate:

  • Runbook rotate đã tài liệu hóa (khi nào, ai làm, bước nào).
  • Nhắc lịch cho ngày rotate.
  • Quy trình cửa sổ chồng lấn đã kiểm thử ít nhất 1 lần trong staging.

Kiểm toán:

  • Log đẩy về SIEM.
  • Cảnh báo thiết lập cho: token từ quốc gia lạ, xác thực thất bại tăng đột biến, cert hết hạn.
  • Dashboard theo dõi khối lượng traffic non-human.

Riêng mTLS:

  • Private key root CA offline hoặc trong HSM.
  • Intermediate CA được dùng để cấp cert client.
  • Xử lý thu hồi (cập nhật chính sách thủ công) đã tài liệu hóa.

Bài học thực tế

  • CI pipeline gián đoạn vì rotate sai: thu hồi cũ trước khi chuyển xong. Lỗi kinh điển, dễ tránh nếu theo pattern chồng lấn.
  • Service token rò rỉ qua commit GitHub: hardcode vào source để “kiểm thử nhanh”, quên thu hồi. Sau 2 tuần, script kid dùng token cào API nội bộ. Bài học: quét secret trong CI + pre-commit hook.
  • Cert mTLS hết hạn trong production lúc 2h sáng: không có giám sát hạn. Một sáng API gọi về thất bại 100%. Giám sát hạn cert = cảnh báo ưu tiên cao.
  • Tên token token-1, token-2, temp-token: kiểm toán vô dụng. Quy ước đặt tên là khoản đầu tư rẻ lãi nhiều.
  • Đừng viết một chính sách “allow any service token”. Mỗi token phải có chính sách riêng hoặc danh sách tường minh. Wildcard = không biết ai đang truy cập gì.

Kết luận

Xác thực non-human là phần thường bị coi nhẹ trong triển khai Zero Trust. Tập trung vào luồng con người vì nhiều hơn và dễ nhìn hơn, nhưng tài khoản bot là nơi kẻ tấn công thường nhắm vì:

  • Ít giám sát.
  • Rotate thường xuyên bị bỏ qua.
  • Secret rò rỉ qua lịch sử CI/CD.

Service token + mTLS của Cloudflare Access giải quyết đúng bài toán. Nhưng chúng chỉ hoạt động tốt khi:

  1. Quy ước đặt tên nhất quán.
  2. Lịch rotate tự động (không dựa vào trí nhớ).
  3. Log kiểm toán đẩy SIEM + cảnh báo bất thường.
  4. Lưu trữ secret qua secret manager, không hardcode.

Nếu phải nhớ một câu:

Service token cho CI dễ thiết lập 5 phút. Nhưng vận hành đúng, đặt tên, rotate, kiểm toán, không hardcode, mất 1 tuần. Cắt góc ở 1 tuần đó là chỗ lỗi production nằm chờ.

Trong Part 7 mình sẽ bàn SCIM + group sync, giải quyết triệt để vấn đề claim lỗi thời từ Part 5, đồng thời làm off-boarding thời gian thực.


Tài liệu tham khảo

Trong series này: