Auditor SOC2 không hỏi “có security không”. Họ hỏi: “Tháng 3, lúc 02:14 UTC, user [email protected] đọc index customer-orders-*, chứng minh đi”. Nếu không trả lời được trong dưới 24h thì control FAIL. Audit log là bằng chứng duy nhất. Phải bật từ trước, không thể recreate.

Đây là bài 15 trong series Kibana từ A đến Z. Sau bài này bạn sẽ làm được:

  • Bật ES audit log và Kibana audit log đúng cách
  • Chọn event quan trọng, drop event noise
  • Ship audit log sang index riêng, giữ 1 năm theo ILM
  • Trả lời 6 câu hỏi auditor SOC2 hay hỏi bằng KQL
  • Tránh pitfall audit log full disk và mất event do buffer

Phần 1: Hai luồng audit khác nhau

Elastic stack có 2 audit log độc lập:

AuditGhi gìOutput
Elasticsearch auditAuth, authorization, security config, index accessFile JSON trên node ES
Kibana auditUI action: save dashboard, copy object, login KibanaFile JSON trên Kibana host

Không bật cả hai = mù một nửa. Auth thất bại ghi ở ES audit. Dashboard bị xoá ghi ở Kibana audit. Cả hai phải tồn tại để có bức tranh đầy đủ.

Yêu cầu license: audit log thuộc nhóm Enterprise feature. Basic license không có. Check GET /_xpack trước khi cài.

Phần 2: Bật Elasticsearch audit

Trong elasticsearch.yml của MỌI node:

xpack.security.audit.enabled: true
xpack.security.audit.logfile.events.include:
  - access_denied
  - access_granted
  - anonymous_access_denied
  - authentication_failed
  - authentication_success
  - connection_denied
  - tampered_request
  - run_as_denied
  - run_as_granted
  - security_config_change
xpack.security.audit.logfile.events.exclude:
  - access_granted   # noise nếu cluster busy
xpack.security.audit.logfile.events.emit_request_body: false

Lưu ý:

  • access_granted ghi MỌI thao tác đọc được phép. Cluster busy ghi vài triệu event/ngày. Cân nhắc exclude khi traffic lớn, chỉ giữ access_denied plus auth event.
  • emit_request_body: true ghi cả body request. Hữu ích để forensic nhưng có thể leak data nhạy cảm. Default tắt là đúng.

Restart node theo rolling. File audit xuất hiện ở logs/<cluster_name>_audit.json (path tuỳ install).

Sample event

{
  "type": "audit",
  "timestamp": "2026-05-17T02:14:33.512+0000",
  "cluster.name": "prod",
  "cluster.uuid": "abc-123",
  "node.name": "es-node-1",
  "event.type": "ip_filter",
  "event.action": "access_granted",
  "authentication.type": "REALM",
  "user.name": "alice",
  "user.realm": "azure_ad",
  "user.roles": ["tenant_a_reader"],
  "origin.type": "rest",
  "origin.address": "10.0.1.42:54321",
  "request.id": "req-xyz",
  "request.method": "POST",
  "url.path": "/customer-orders-*/_search",
  "url.query": "size=10"
}

Field event.action, user.name, url.path, origin.address là 4 field truy vấn nhiều nhất.

Phần 3: Bật Kibana audit

Trong kibana.yml:

xpack.security.audit.enabled: true
xpack.security.audit.appender:
  type: rolling-file
  fileName: /var/log/kibana/audit.log
  policy:
    type: time-interval
    interval: 24h
  strategy:
    type: numeric
    pattern: -%i
    max: 10
  layout:
    type: json

Audit log Kibana được giữ 10 file, mỗi file 24h.

Sample event

{
  "@timestamp": "2026-05-17T02:14:33.512Z",
  "message": "User is changing dashboard",
  "event": {
    "action": "saved_object_update",
    "category": ["database"],
    "type": ["change"],
    "outcome": "success"
  },
  "kibana": {
    "saved_object": { "type": "dashboard", "id": "abc-123" },
    "space_id": "prod"
  },
  "user": {
    "name": "alice",
    "roles": ["kibana_admin"]
  },
  "http": {
    "request": { "method": "put" },
    "headers": { "x-forwarded-for": "203.0.113.10" }
  }
}

Action phổ biến: user_login, user_logout, saved_object_create, saved_object_update, saved_object_delete, space_copy_saved_objects, failure, success.

Phần 4: Ship audit log vào ES (đệ quy nhưng đúng)

Audit log nằm trên filesystem node là khó query. Ship vào index ES audit-* bằng Filebeat hoặc Elastic Agent.

Filebeat config:

filebeat.inputs:
  - type: filestream
    id: es-audit
    paths:
      - /var/log/elasticsearch/*_audit.json
    parsers:
      - ndjson:
          target: ""
          add_error_key: true
    fields:
      audit_source: elasticsearch
  - type: filestream
    id: kibana-audit
    paths:
      - /var/log/kibana/audit.log*
    parsers:
      - ndjson:
          target: ""
          add_error_key: true
    fields:
      audit_source: kibana

output.elasticsearch:
  hosts: ["https://es.example.com:9200"]
  index: "audit-%{+yyyy.MM.dd}"
  api_key: "${AUDIT_API_KEY}"

setup.template.name: "audit"
setup.template.pattern: "audit-*"
setup.ilm.enabled: false   # set ILM riêng cho audit, không dùng filebeat default

API key cho Filebeat audit chỉ cần create_doc trên audit-*. Quyền tối thiểu để tránh Filebeat bị compromise rồi xoá audit.

Chống tampering

Vì audit là pháp y, người được audit không được xoá log của chính họ. Hai biện pháp:

  1. Audit index lưu cluster khác (forwarder bằng cross-cluster replication hoặc Logstash với output qua HTTPS một chiều).
  2. Snapshot audit index hàng ngày sang S3 immutable bucket với object lock.

Snapshot S3 immutable là tiêu chuẩn vàng cho SOC2 evidence. Bài 17 sẽ đi sâu.

Phần 5: ILM cho audit log

Audit cần giữ tối thiểu 1 năm theo SOC2 (12 tháng rolling window). Policy mẫu:

PUT _ilm/policy/audit-policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_age": "30d",
            "max_primary_shard_size": "30gb"
          },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "30d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 },
          "set_priority": { "priority": 50 }
        }
      },
      "cold": {
        "min_age": "90d",
        "actions": {
          "freeze": {},
          "set_priority": { "priority": 0 }
        }
      },
      "delete": {
        "min_age": "395d",
        "actions": { "delete": {} }
      }
    }
  }
}

13 tháng để dư margin cho audit window. Index template gắn policy này:

PUT _index_template/audit-template
{
  "index_patterns": ["audit-*"],
  "data_stream": {},
  "template": {
    "settings": {
      "index.lifecycle.name": "audit-policy",
      "index.lifecycle.rollover_alias": "audit"
    }
  }
}

Bài 16 sẽ đi sâu về ILM (hot/warm/cold/shrink).

Phần 6: Sáu câu hỏi auditor hay hỏi với KQL sẵn

Câu 1: User X đã login bao nhiêu lần tháng trước?

event.action : "user_login" and user.name : "[email protected]"

Bật Discover, time range “Last 30 days”, count records.

Câu 2: Có lần auth failed nào của user X không?

event.action : "authentication_failed" and user.name : "[email protected]"

Câu 3: User X đã đọc index Y khi nào?

event.action : "access_granted"
and user.name : "[email protected]"
and url.path : *customer-orders*

Câu 4: Có ai từng truy cập từ IP ngoài VPN?

event.action : ("access_granted" or "authentication_success")
and not origin.address : "10.0.0.0/8"
and not origin.address : "172.16.0.0/12"

Subnet VPN tuỳ tổ chức. Lưu lại Saved Search này là báu vật.

Câu 5: Ai xoá dashboard Error Overview?

event.action : "saved_object_delete"
and kibana.saved_object.id : "<dashboard-id>"

Câu 6: Có ai từng thay đổi role hoặc API key?

event.action : "security_config_change"

Hoặc cụ thể hơn:

event.action : "security_config_change"
and url.path : (*role* or *api_key*)

Phần 7: Build dashboard “Audit Overview”

Panel đề xuất:

PanelVisualizationFilter
Total auth failures (24h)Metricevent.action : "authentication_failed"
Top users failed loginPieevent.action : "authentication_failed", slice by user.name
Access denied over timeBarevent.action : "access_denied", bucket @timestamp 1h
Sensitive index accessTableevent.action : "access_granted" and url.path : *customer-orders*, columns user.name, origin.address, @timestamp
Security config changesTableevent.action : "security_config_change"
Dashboard mutationsTableevent.action : ("saved_object_update" or "saved_object_delete") and kibana.saved_object.type : "dashboard"

Export NDJSON dashboard, commit vào repo compliance-evidence/. Auditor có URL tĩnh xem được, không cần share account Kibana.

Phần 8: Pitfall hay gặp

Pitfall 1: bật audit rồi quên rotation

Audit log không tự rotate nếu chỉ dùng file appender mặc định. File có thể lên 50GB sau 1 tháng. Disk full plus node crash. Fix: dùng rolling-file appender với policy time-interval plus numeric strategy max: 10, hoặc rely on logrotate host-level.

Pitfall 2: include event quá nhiều

Bật full events.include cho cluster traffic cao = audit log size lớn hơn log ứng dụng. Một team tôi từng thấy ghi 200GB audit/ngày. Fix: exclude access_granted, chỉ giữ access_denied plus auth event. Bù lại bằng cách bật slowlog cho query bất thường.

Pitfall 3: ship audit log dùng API key superuser

Filebeat compromised mà có key superuser thì attacker xoá luôn audit. Key dành cho Filebeat audit chỉ cần create_doc trên audit-*. Không cấp delete, không cấp read.

Pitfall 4: thiếu chứng minh tampering

Audit nằm cùng cluster với data = audit có thể bị admin sửa. SOC2 hỏi “chứng minh audit chưa bị sửa”. Cách trả lời: snapshot S3 object lock hàng ngày. Snapshot SHA-256 plus timestamp là evidence.

Pitfall 5: timezone lệch

ES audit ghi UTC. Kibana hiển thị theo browser timezone. Khi paste evidence cho auditor luôn quy về UTC để khớp với ES log. Ghi rõ “All timestamps in UTC” trong report.

Phần 9: Pattern viết runbook compliance

Tạo file compliance/runbook.md:

# Audit Evidence Runbook

## Câu hỏi 1: User X login khi nào?
Index: audit-*
KQL: event.action : "user_login" and user.name : "<X>"
Saved Search: AuditUserLogin

## Câu hỏi 2: User X truy cập index Y?
KQL: event.action : "access_granted"
     and user.name : "<X>"
     and url.path : *<Y>*
Saved Search: AuditUserIndexAccess

...

Khi auditor hỏi, on-call mở runbook, copy KQL, đổi parameter, export CSV từ Discover (Share plus CSV reports). Tổng thời gian trả lời under 10 phút.

Cheatsheet

ViệcCách
Bật ES auditxpack.security.audit.enabled: true trong elasticsearch.yml
Bật Kibana auditxpack.security.audit.enabled: true trong kibana.yml
Format JSONKibana audit layout.type: json
Ship vào ESFilebeat filestream, parser ndjson
Retention 1 nămILM delete.min_age: 395d
Chống tamperingSnapshot S3 object lock hàng ngày
Audit user loginevent.action : "user_login"
Audit access deniedevent.action : "access_denied"
Audit config changeevent.action : "security_config_change"
Audit object deleteevent.action : "saved_object_delete"

Lời kết

Audit log là tài sản pháp lý, không phải metric. Setup phải làm trước khi auditor đến. Khi rồi mới bật là không kịp (audit log từ tháng trước không recreate được). Tuân thủ 3 nguyên tắc: bật cả hai (ES plus Kibana), ship sang storage độc lập, giữ tối thiểu 13 tháng.

Bài 16 mở Part 5 Production Operations với Index Lifecycle Management (ILM): hot/warm/cold/delete phase, shrink, force-merge và rollover. Đây là cơ chế nền cho mọi index trong cluster, bao gồm cả audit-* mà bài này vừa setup.