WordPress Audit Log บน AWS Elasticsearch

เนื่องจากที่ Pronto เราใช้ระบบ Multisite ของ WordPress ดูแลเว็บไซต์ของลูกค้าเป็นพันกว่าเว็บไซต์ ทำให้การดูแล โดยเฉพาะเรื่องความปลอดภัยของแต่ละไซต์นั้นทำได้ค่อนข้างลำบาก เวลาตรวจสอบอะไรกับไซต์หนึ่งสักที ก็ต้องมานั่งกรอง log ให้มีแต่ข้อมูลไซต์ที่เราต้องการตรวจสอบ ถ้าใครเคยใช้ Multisite ก็น่าจะพอนึกภาพออกเนอะ เพราะ log มันเก็บไว้ที่เดียวกันหมดทุกไซต์ ต่างกับแยกแต่ละเว็บออกเป็นระบบส่วนตัว ซึ่งแบบนี้เราสามารถดู log เฉพาะของเว็บไซต์นั้นได้ทันที

ปัญหาอีกอย่างคือเรื่อง infrastructure ที่เราต้องไปดูแล ต้องคิดถึงการเก็บข้อมูล เพราะเนื่องจากข้อมูล log จะเป็นข้อมูลที่ไหลเข้ามาเรื่อยๆ อยู่แล้ว ไฟล์ log ก็จะยิ่งใหญ่ขึ้นเรื่อยๆ ถ้าจัดการเก็บเองแล้วคอย rotate เองเรื่อยๆ ก็คงเปลืองแรงไม่น้อย ยิ่งถ้าข้อมูลเก็บอยู่ในไฟล์ ก็คงจะลำบากที่จะดึงออกมาตรวจสอบ หรือเอาข้อมูลเหล่านั้นมาวิเคราะห์ (data analytics)

ทีม Pronto แก้ปัญหาเหล่านั้นโดยใช้ Amazon Elasticsearch (รูปที่ 1) และบริการนี้ก็แถม Kibana ที่เอาไว้สำหรับทำ data visualization เข้ามาด้วย ทุกอย่างดูลงตัวมาก ทุกวันนี้ก็ใช้อยู่โดยไม่ต้องไปกังวลอะไรทางด้าน infrastructure เลย ดีงามมากจนเราอยากจะมาแบ่งปันถึงวิธีทำ audit log ง่ายๆ บน WordPress และการเก็บข้อมูลเข้า Amazon Elasticsearch 😉

AWS Elasticsearch Dashboard

รูปที่ 1: ข้อมูล Endpoint จาก AWS Elasticsearch Dashboard

Elasticsearch

เกริ่นก่อนว่าข้อมูลใน Elasticsearch จะมองเป็น document ซึ่ง 1 document ของ Elasticsearch จะถูกเก็บเป็น JSON object ที่อยู่ในรูปแบบ index ที่มี type และ id (index/type/id) ซึ่งจะต้องมีค่าไม่ซ้ำกัน หากเปรียบเทียบกับ relational database ปกติแล้วส่วน index เปรียบเสมือน database ส่วน type จะเสมือนเป็น table และ id จะเสมือนเป็น record ใน table นั้นๆ ยกตัวอย่างเวลาเราต้องการเพิ่มข้อมูล เราสามารถเพิ่มได้ดังนี้

curl -XPUT "http://localhost:9200/teampronto/projects/1" -d'
{
    "name": "Phoenix",
    "platform": "WordPress"
}'

จากตัวอย่างข้างบน teampronto คือ index ส่วน projects คือ type และ 1 คือ id โดยที่ document นี้ก็คือ JSON ที่เราส่งเข้าไปนั่นเอง ซึ่งจริงๆ แล้วเราสามารถเพิ่มข้อมูลเข้าไปใน Elasticsearch ได้ โดยไม่ต้องใส่ id เข้าไป (เปลี่ยนจาก PUT เป็น POST) ค่าของ id จะถูกสร้างขึ้นมาโดยอัตโนมัติ จะเป็นเลข UUID เลขหนึ่ง เราแทบตัดความกังวลไปได้เลยว่าเลขจะซ้ำ

WordPress Audit Log บน Elasticsearch

มาถึงส่วนที่ว่าเราจะเขียนโค้ดใน WordPress อย่างไรเพื่อเก็บ audit log บน Elasticsearch

WordPress มีสิ่งหนึ่งที่ทำให้ง่ายต่อการพัฒนาต่อยอด สิ่งนั้นเรียกว่า hooks ครับ โดยที่ hooks เนี่ยเปิดให้เราอยากจะให้ WordPress ทำอะไรตามที่เราต้องการได้ เมื่อเกิดเหตุการณ์นั้นๆ ยกตัวอย่างเช่น เวลาที่เรา publish โพสต์ของเรา เราอยากให้ส่งอีเมลหาเพื่อนด้วย เราก็ไปเขียนฟังก์ชั่นส่งอีเมล แล้วใส่ฟังก์ชั่นนั้นเข้าไปใน hook ชื่อ publish_post ก็เป็นอันเสร็จ แต่ถ้าเราอยากให้ฟังก์ชั่นเราโดนเรียกตลอดเวลา ก็ใส่ฟังก์ชั่นเราเข้าไปที่ hook ชื่อ init ซึ่งใน WordPress นั้นมี hooks ให้เราเลือกหลากหลายมาก แทบจะบอกได้ว่าเราสามารถใส่ฟังก์ชั่นของเราไปที่ WordPress ตรงไหนก็ได้ โดยที่ไม่ผิดมาตรฐานโค้ดของ WordPress อะไรเลย

เอาล่ะ เราจะมาลองเขียนการเก็บ audit log กัน กับ hook ที่ชื่อ wp_login_failed 🙂 เอาไว้ตรวจสอบว่ามีใครที่ต้องการจะ brute force เข้ามาในระบบเราบ้าง เขียนได้ตามนี้เลย

function EventLoginFailure( $username ) {
    $es_host = "https://search-bypronto-audit-log-SOME-KIND-OF-IDENTIFIER.eu-west-1.es.amazonaws.com/";
    $es_index = "bypronto-" . date( "Y-m-d" ) . "/";
    $data = array(
        "username"  => $username,
        "ip"        => $_SERVER["HTTP_X_FORWARDED_FOR"] . ":" . $_SERVER["REMOTE_ADDR"],
        "site"      => get_blog_details(),
        "timestamp" => date( "c" ),
    );
    wp_remote_post( $es_host . $es_index . "login-failed/", array(
        "body" => json_encode( $data ),
    ) );
}

add_action( "wp_login_failed", "EventLoginFailure", 10, 1 );

$es_host คือ endpoint ที่เราได้มาหลังการสร้าง Elasticsearch cluster (รูปที่ 2) ในที่นี้เราตั้งค่า index ให้เปลี่ยนไปตามวัน หน้าตาจะประมาณ bypronto-2016-04-28 ซึ่งการเก็บ index แบบนี้ทำให้เราสามารถกรองข้อมูล log ได้ง่ายมากขึ้น และถ้าเราต้องการ backup เราก็แค่เลือก index ที่เราต้องการ แล้วพอ backup เสร็จก็ลบ index นั้นๆ ทิ้ง ถ้าเราไม่เก็บแบบนี้แล้ว เวลาจะลบเราอาจะต้องมาเขียนโค้ดเพื่อ query เลือกส่วนที่ต้องการจะลบ แค่คิดก็ไม่อยากทำแล้ว 😛

AWS Elasticsearch Cluster

รูปที่ 2: AWS Elasticsearch Cluster

ส่วน type เราตั้งชื่อที่สื่อออกมาง่ายๆ ตรงๆ อย่าง login-failed และตัวข้อมูลก็จะมี username มี IP มี site เราใช้ฟังก์ชั่น get_blog_details เพื่อดูว่าคนๆ นั้นตั้งใจจะล็อกอินเข้าไซต์ไหนของเรา (ระบบที่ Pronto เป็น Multisite) สุดท้ายก็เก็บ timestamp การเอาข้อมูลเข้า Elasticsearch ก็แค่ POST เข้าไปแค่นั้นเองครับ ใช้ wp_remote_post เลย

ถ้าเราอยากรู้ว่าใครลบโพสต์บ้าน ก็เขียนฟังก์ชั่นเข้าไปที่ hook ชื่อ delete_post ได้เลย ตามนี้

function EventPostDeleted( $post_id ) {
    $es_host = "https://search-bypronto-audit-log-SOME-KIND-OF-IDENTIFIER.eu-west-1.es.amazonaws.com/";
    $es_index = "bypronto-" . date( "Y-m-d" ) . "/";

    $current_user = wp_get_current_user();
    $data = array(
        "post_id"    => $post_id,
        "user_login" => $current_user->user_login,
        "ip"         => $_SERVER["HTTP_X_FORWARDED_FOR"] . ":" . $_SERVER["REMOTE_ADDR"],
        "site"       => get_blog_details(),
        "timestamp"  => date( "c" ),
    );
    wp_remote_post( $es_host . $es_index . "post-deleted/", array(
        "body" => json_encode( $data ),
    ) );
}

add_action( "delete_post", "EventPostDeleted", 10, 1 );

สุดท้ายแล้วถ้าเราใช้ Kibana มาดูข้อมูล ก็จะได้หน้าตาประมาณดังในรูปที่ 3

AWS Kibana Visualization ดูข้อมูลที่ส่งมาจาก WordPress

รูปที่ 3: หน้าตา AWS Kibana Visualization

แล้วเราก็สามารถเลือกได้ว่าจะดู type แบบไหน ช่วงเวลาไหน ได้อีกด้วย ตามรูปที่ 4

AWS Kibana ดูข้อมูล Event Types จาก WordPress

รูปที่ 4: เลือกดูข้อมูลตาม Event Types ใน AWS Kibana

ใน WordPress ยังมี hook อีกมากมายที่เราสามารถจะทำ audit log ได้ ซึ่งก็แล้วแต่ use case ของแต่ละบริษัทเนอะ ไปลองเล่นกันดูนะ 😀


Kan Ouivirach

Kan Ouivirach

Lead Software Architect

Being interested in Agile software development, I joined an Agile team at Pronto Tools as a Research & Development Architect (as Lead Software Architect now). I am an enthusiastic architect who not only has a scientific mindset, but also a practical approach to software solutions.