Skip to content

Custom Rules Match Language (CEL)

Why this matters in production

Preconfigured WAF (file 02) xử lý các lớp tấn công chuẩn. Nhưng phần lớn logic bảo mật đặc thù cho ứng dụng của bạn — chặn theo geo, allowlist đối tác qua IP/ASN, chặn user-agent bất thường, ghép nhiều điều kiện — đều viết bằng custom rules match language. Đây là nơi Cloud Armor chuyển từ "WAF đóng hộp" thành "programmable edge policy".

Match language dựa trên CEL (Common Expression Language) — cùng ngôn ngữ dùng trong IAM Conditions, GKE admission policy (xem Chapter 10). Hiểu chính xác các attribute nào có sẵn, chúng được điền giá trị thế nào ở edge, và operator nào tồn tại, là điều tách biệt giữa việc viết rule đúng và viết rule tưởng là đúng nhưng không bao giờ khớp.

Internal model — cơ chế vận hành

Cấu trúc một match expression

Một advanced match condition là một biểu thức CEL trả về bool. Mỗi biểu thức gồm hai thành phần: attribute (dữ liệu để inspect) và operation (cách dùng dữ liệu đó). Ví dụ:

origin.region_code == 'CN' && request.path.startsWith('/admin')

Cloud Armor đánh giá biểu thức này cho mỗi request; nếu trả true, action của rule áp dụng (theo first-match-wins đã mô tả ở file 01).

Các request attribute

Đây là bề mặt dữ liệu mà rule có thể inspect. Biết chính xác cái gì có sẵn tránh việc cố match một thứ Cloud Armor không expose:

AttributeKiểuÝ nghĩa
origin.ipstringIP của client khởi tạo request (kết nối trực tiếp tới GFE)
origin.user_ipstringIP client lấy từ header upstream proxy — cần cấu hình userIpRequestHeaders
origin.region_codestringMã quốc gia Unicode/ISO 3166-1 alpha-2, ví dụ US, VN
origin.asnintAutonomous System Number gắn với IP client
request.headersmapMap string→string của HTTP header; key luôn viết thường
request.methodstringHTTP verb: GET, POST, …
request.pathstringPath của URL request
request.querystringQuery string dạng name=value
request.schemestringhttp hoặc https (viết thường)
origin.tls_ja3_fingerprintstringJA3 TLS/SSL fingerprint (HTTPS/HTTP2/HTTP3)
origin.tls_ja4_fingerprintstringJA4 TLS/SSL fingerprint (HTTPS/HTTP2/HTTP3)

Vài điểm cơ chế dễ sai:

  • request.headers có key viết thường. request.headers['user-agent'], không phải 'User-Agent'. Đây là nguồn lỗi phổ biến — rule không bao giờ khớp vì key sai case.
  • origin.ip vs origin.user_ip: khi có proxy/CDN đứng trước Cloud Armor, origin.ip là IP của proxy đó, không phải end-user. Muốn lấy IP thật của user, bạn phải cấu hình userIpRequestHeaders và dùng origin.user_ip. Rate limiting theo IP mà quên điều này sẽ vô tình gộp toàn bộ user sau một proxy thành một "client".
  • origin.region_code dựa trên geo-IP mapping cập nhật hàng tuần với sai số (xem file 01) — không dùng cho quyết định compliance cứng.

JA3/JA4 fingerprint — nhận diện client ở tầng TLS

origin.tls_ja3_fingerprinttls_ja4_fingerprinthash của TLS ClientHello — nó nắm bắt "chữ ký" của thư viện/tool tạo ra kết nối TLS (cipher suite, extension, thứ tự…). Cơ chế quan trọng: một bot dùng cùng một thư viện HTTP thường có cùng JA3/JA4 bất kể nó đổi IP hay User-Agent. Điều này khiến fingerprint trở thành công cụ mạnh chống bot/scraper xoay IP:

origin.tls_ja4_fingerprint == 't13d1516h2_8daaf6152771_b186095e22b6'

JA3/JA4 chỉ có với kết nối được mã hóa (HTTPS/HTTP2/HTTP3) vì cần thấy TLS handshake. Đây là ví dụ điển hình về giá trị của việc Cloud Armor chạy tại GFE — nơi TLS handshake được terminate và fingerprint tính được.

Operator và function

Comparison & logic: ==, !=, &&, ||, !

String operation:

  • .contains(s) — chứa substring
  • .startsWith(s), .endsWith(s) — khớp đầu/cuối
  • .matches(re) — regex RE2 (không phải PCRE — không có backreference, đảm bảo chạy tuyến tính, chống ReDoS)
  • .lower(), .upper() — đổi case
  • size(s) — độ dài chuỗi
  • int(s) — chuyển kiểu

Decode function (chống obfuscation):

  • .urlDecode(), .urlDecodeUni() — giải mã URL-encoding
  • .base64Decode() — giải mã base64
  • .utf8ToUnicode() — chuyển encoding

Function chuyên biệt:

  • inIpRange(address, cidr) — kiểm tra IP thuộc CIDR. Đây là cách đúng để match IP range, thay vì so sánh chuỗi.
  • has(map['key']) — kiểm tra key tồn tại trong map (dùng cho request.headers).
  • evaluatePreconfiguredWaf(...) — gọi WAF ruleset (xem file 02).
  • evaluateThreatIntelligence(...) — match với threat intelligence list của Google (xem file 06).
  • evaluateAddressGroup(...) — match với address group (danh sách IP quản lý tập trung).
  • evaluateAdaptiveProtectionAutoDeploy() — dùng cho auto-deploy của Adaptive Protection (xem file 05).

Vì sao RE2 chứ không phải PCRE: RE2 đảm bảo thời gian match tuyến tính theo độ dài input, không có catastrophic backtracking. Ở edge xử lý hàng triệu request/giây, một regex PCRE tồi có thể bị lợi dụng làm ReDoS. RE2 loại bỏ rủi ro này về mặt bản chất — đánh đổi là không có backreference (\1) và một số tính năng nâng cao.

Ghép điều kiện và giới hạn 5 subexpression

Một advanced match cho phép tối đa 5 subexpression nối bằng &&/||. Ví dụ hợp lệ:

inIpRange(origin.ip, '203.0.113.0/24')
  && request.method == 'POST'
  && request.path.startsWith('/api/upload')
  && has(request.headers['content-type'])
  && request.headers['content-type'].contains('multipart')

Khi logic vượt 5 subexpression, bạn phải tách thành nhiều rule — và lúc này thứ tự priority (first-match-wins) lại trở thành công cụ tổ hợp logic. Ví dụ, thay vì một rule khổng lồ, dùng chuỗi rule: rule priority thấp allow các trường hợp ngoại lệ, rule priority cao hơn deny phần còn lại.

origin.user_ip và enforce đúng client

Khi kiến trúc có CDN/proxy bên thứ ba trước Cloud Armor, bạn cấu hình userIpRequestHeaders (ví dụ X-Forwarded-For) ở cấp security policy. Sau đó origin.user_ip được điền từ header đó, và mọi rule/rate-limit dùng IP thật của end-user thay vì IP proxy. Không có bước này, mọi quyết định theo IP đều sai lệch ở kiến trúc multi-proxy.

Một điểm bảo mật tinh tế: X-Forwarded-For là header do client/proxy tự đặt, nên attacker có thể giả mạo nó. Cloud Armor xử lý bằng cách chỉ tin phần header được thêm bởi hạ tầng đáng tin (GFE thêm IP thật vào cuối), và userIpRequestHeaders cần được cấu hình khớp với kiến trúc proxy cụ thể của bạn. Nếu cấu hình sai — tin nhầm phần XFF do attacker kiểm soát — attacker có thể spoof origin.user_ip để vượt qua rule dựa trên IP hoặc né rate limit. Quy tắc: chỉ dùng origin.user_ip khi bạn kiểm soát toàn bộ chuỗi proxy và biết chính xác header nào đáng tin.

evaluatePreconfiguredExpr — hàm legacy và quan hệ với WAF hiện đại

Ngoài evaluatePreconfiguredWaf() (thế hệ mới, hỗ trợ sensitivity/opt-in/opt-out — xem file 02), match language còn có hàm cũ evaluatePreconfiguredExpr('ruleset-id'). Đây là API thế hệ đầu của preconfigured WAF: nó chỉ nhận tên ruleset, không có tham số sensitivity hay tinh chỉnh signature. Bạn có thể gặp nó trong policy cũ hoặc tài liệu cũ.

Mental model: evaluatePreconfiguredExpr là "bật cả ruleset ở mức mặc định", trong khi evaluatePreconfiguredWaf cho bạn trục điều khiển (sensitivity, exclusion). Với rule mới, luôn ưu tiên evaluatePreconfiguredWaf vì khả năng tune quyết định hiệu quả WAF (như đã phân tích ở file 02). Nếu bạn kế thừa policy dùng evaluatePreconfiguredExpr, migrate sang evaluatePreconfiguredWaf là một bước cải thiện khả năng vận hành đáng làm.

request.headers: cơ chế map và header trùng lặp

request.headers là một map<string, string>, nhưng HTTP cho phép một header xuất hiện nhiều lần (ví dụ nhiều X-Forwarded-For). Cloud Armor gộp các giá trị của cùng một header thành một chuỗi (thường nối bằng dấu phẩy) trước khi đưa vào map. Hệ quả cơ chế: khi bạn viết request.headers['x-forwarded-for'].contains('1.2.3.4'), bạn đang match trên chuỗi đã gộp, không phải từng giá trị riêng lẻ. Với logic cần match chính xác một giá trị trong danh sách, dùng .contains()/.matches() cẩn thận thay vì == (vì == so với cả chuỗi gộp).

Ngoài ra, has(request.headers['x-custom']) là cách đúng để kiểm tra sự tồn tại của header trước khi truy cập giá trị — truy cập một key không tồn tại trong map có thể làm biểu thức không khớp như mong đợi. Pattern an toàn: has(request.headers['x-api-key']) && request.headers['x-api-key'] == 'expected'.

Kết hợp decode function để chống obfuscation

Attacker thường encode payload để né match trực tiếp. Ví dụ, thay vì gửi ../../etc/passwd, họ gửi ..%2f..%2fetc%2fpasswd (URL-encoded). Một rule match thô request.path.contains('etc/passwd') sẽ trượt. Chuỗi decode function giải quyết:

request.path.urlDecode().contains('etc/passwd')

Có thể ghép nhiều lớp decode cho double-encoding: .urlDecode().urlDecode(). Nhưng lưu ý cơ chế: preconfigured WAF (file 02) đã tự thực hiện nhiều bước normalization/decode nội bộ, nên với các lớp tấn công chuẩn (LFI/SQLi/XSS) bạn nên dựa vào WAF hơn là tự viết chuỗi decode — dễ bỏ sót biến thể. Decode function trong custom rule hữu ích nhất cho logic đặc thù ứng dụng mà WAF không cover, ví dụ chặn một pattern nghiệp vụ cụ thể có thể bị encode.

Constraints, trade-offs & failure modes

Attribute nào không có: bạn không match được response, không match được body ngoài giới hạn WAF (custom rule inspect header/path/query, còn body chỉ qua evaluatePreconfiguredWaf), không có state giữa các request (ngoài rate-limit counter). Custom rule là stateless per-request matching.

Case sensitivity của header key — như đã nói, request.headers key viết thường. Rule request.headers['User-Agent'] sẽ không bao giờ khớp. Đây là failure mode âm thầm: rule tồn tại, syntax đúng, nhưng không có tác dụng. Debug bằng cách xem enforcedSecurityPolicy.priority trong log để biết rule nào thực sự khớp.

Geo và ASN là xấp xỉ. Region code trễ vài ngày; ASN có thể thay đổi khi IP được reassign. Không dùng cho quyết định pháp lý; dùng cho giảm abuse/scoping rủi ro.

origin.ip sau proxy — failure mode nghiêm trọng: rate-limit theo origin.ip khi có CDN trước → toàn bộ traffic gộp thành một IP → hoặc ban nhầm tất cả, hoặc không ban được ai. Luôn xác định đúng attribute IP theo kiến trúc.

RE2 không phải PCRE — regex quen thuộc dùng backreference/lookahead sẽ báo lỗi. Phải viết lại theo RE2.

Trade-off giữa custom rule và preconfigured WAF: custom rule cho kiểm soát chính xác nhưng bạn phải tự bảo trì logic khi threat thay đổi. Preconfigured WAF được Google cập nhật signature nhưng thô về ngữ cảnh. Thiết kế tốt kết hợp cả hai: preconfigured cho lớp tấn công chuẩn, custom cho logic đặc thù ứng dụng và các quyết định geo/IP/fingerprint.

Production pattern: chống bot scraper xoay IP bằng JA4 + rate limit

Một site bị scrape bởi bot đổi IP liên tục nhưng dùng cùng thư viện HTTP. Rate limit theo origin.ip vô dụng vì mỗi request một IP mới. Giải pháp theo cơ chế:

  1. Xác định JA4 fingerprint của bot qua log (Adaptive Protection cũng có thể gợi ý — xem file 05).
  2. Rate limit với enforce_on_key = TLS_JA4_FINGERPRINT thay vì IP — bộ đếm giờ theo chữ ký client, không theo IP, nên xoay IP không thoát được (xem file 04).
  3. Hoặc deny thẳng nếu fingerprint đủ đặc trưng và không đụng client hợp lệ.

Bài học cơ chế: chọn đúng attribute làm khóa nhận diện quan trọng hơn chọn action. Bot xoay IP đánh bại IP-based control nhưng không đánh bại fingerprint-based control.

Official references