您的位置:首頁>正文

「騰訊雲CDB」MySQL審計功能大揭秘

更多騰訊海量技術文章, 請關注雲加社區:https://cloud.tencent.com/developer

作者:騰訊雲資料庫內核團隊

什麼是資料庫審計

對於一個倉庫, 如果要防盜, 常見做法是出入口全裝上監控, 一旦有問題了, 調監控查找異常情況。 對資料庫來說也類似, 資料庫也有出入口, 對所有連接出入口監控, 記錄下所有動作, 一旦有問題了, 查詢歷史動作, 找到關鍵資訊。 如果倉庫中的東西價值高, 損失已經造成, 監控只是事後諸葛, 所以往往請專業人士在巡邏防盜, 防止偷盜發生。 同樣資料庫審計也有更專業的手段, sql阻斷, 在動作發生前, 對動作行為分析,

如果判斷符合預先設置的高危動作, 直接中止執行, 防止對資料造成破壞。

審計定義:能夠即時記錄網路上的資料庫活動, 對資料庫遭受到的風險行為進行告警, 對攻擊行為進行阻斷

常見的審計方式

應用層審計

在應用系統中直接審計, 語句還沒往資料庫後臺發就先做了審計, 不影響資料庫性能, 對底層用的是什麼資料庫也不關心, 但對應用系統壓力比較大, 並且應用系統需要解析語句, 有一定複雜度。

傳輸層審計

往往抓包解析實現, 對上下層都沒什麼影響, 但同樣要解析語句, 有一定複雜度, 並且如果傳輸層是通過加密通訊, 將無法解析。

外掛程式審計

對於開來源資料庫, 通常都有提供外掛程式方式增加功能。

審計可以以外掛程式直接嵌在內核上, 當然會對資料庫性能有一定影響, 但同樣因為直接嵌在內核, 很多一手資訊能直接拿到, 比方說上面沒辦法回避的語法解析就不用做, 而且還能直接拿更多的運行態資訊, 能開發功能強大又靈活的審計功能。

內核審計

直接在內核上實現, 所有功能都能實現, 也能將性能影響降到最低, 但是對後臺穩定性會有影響, 對開發人員要求高, 不管是開源還是非開來源資料庫, 都會非常慎重考慮直接在內核上支持審計。

oracle審計簡介

談到資料庫審計, 不能不提oracle, oracle 資料庫的審計功能有10多年甚至更長的歷史了, 對資料庫sql審計功能影響深遠, 幾乎任何資料庫審計都會參考, 下面我們簡單瞭解下oracle審計。

Oracle中審計總體上可分為“標準審計”和“細細微性審計”, 標準審計包含語句審計, 許可權審計, 對象審計, 細細微性審計也稱為“基於政策的審計”:

審 計 類 型

說 明

語句審計

按照語句類型審計SQL語句.而不論訪問何種特定的模式物件。 也可以在資料庫中指定一個或多個用戶, 針對特定的語句審計這些用戶

許可權審計

審計系統許可權, 例如GRANT。 和語句審計一樣, 許可權審計可以指定一個或多個特定的使用者作為審計的目標

物件審計

審計特定模式物件上運行的特定語句(例如, DEPARTMENTS表上的UPDATE語句)。 模式物件審計總是應用於資料庫中的所有使用者

細細微性的審計

根據訪問物件的內容來審計表訪問和許可權。 使用套裝程式DBMS_FGA來建立特定表上的策略

預設情況下審計是關閉的, 因為開啟性能影響大, 需要設置一些參數打開它, 審計記錄結果既可以放檔也可以放系統表中。 那麼怎麼配置需要審計的內容呢?oracle提供了一套審計配置語法來做這事, audit/noaudit,審計和取消審計, 是互逆的, 如下所示:

圖1

圖2

示例如下:

// 審計u1.t2這個表的update 不成功的語句

audit update on u1.t2 by access whenever not successful;

// 審計u1.t2這個表的update 成功的語句

audit update on u1.t2 by access whenever successful;

運算式:f+s=a 連續運行上面兩個配置語句, 等價審計全部

// 審計u1.t2表update全部語句, 全部包括兩種, 成功和失敗

audit update on u1.t2 by access;

// 不審計u1.t2表update成功的語句

noaudit update on u1.t2 by access whenever successful;

運算式:a-s=f 連續運行上面兩個配置語句, 等價只審計失敗語句

a:all, 全部

f:fail, 失敗

s:success, 成功

這就是oracle審計配置語法用法, 配置完成後就能審計到相應的語句, 它的成功、失敗、全部是存在運算關係的。 如何查詢比較簡單, 直接查詢相關表就行, 不再介紹。

mysql審計外掛程式

前文提到, 外掛程式是資料庫實現審計功能的常見手段, mysql上也有不少審計外掛程式, 比較有名的Macfee外掛程式, 官方audit plugin, mariadb audit plugin, Percona audit plugin。

從功能上來說, 這幾個外掛程式大同小異, 只是展示的審計內容和格式略有差異。 從性能上來說,除了macfee外掛程式外,其它幾個性能相差不多,宣稱都是15%左右影響,macfee則可能達到50%或更多。實際上,確定一個測試性能損失的標準場景是需要商榷的,跑非常簡單的語句,幾十萬Qps,性能影響肯定大,審計性能消耗和語句量直接關聯,跑OLAP查詢,幾秒甚至幾十秒執行一個語句,審計性能影響可以說沒有,所以審計性能損失得看具體場景。

從實現方式來說,這幾個外掛程式也差不太多,從thd物件中取到所需要的屬性,按一定規則過濾,按某種策略存檔,以json或xml等格式展示審計結果。

mysql審計介面詳細分析

從5.5開始,mysql內核中已經增加了一套的對伺服器操作的審計機制的介面,添加了額外的審計流程來對我們所關心的地方進行事件捕獲,如果要對伺服器進行審計,只要基於這些完善一個外掛程式即可。mysql審計介面官方第一次是提交到代碼是2009年底,歷史也比較悠久了,修改和增加的檔並不多,幾個關鍵點介紹下。

在plugin.h中增加一個新的巨集 MYSQL_AUDIT_PLUGIN 用來標識一類全新的外掛程式類型:AUDIT外掛程式

增加了一個新的標頭檔:plugin_audit.h,裡面有審計相關的關鍵的結構體及一些巨集定義,如下: mysql_event_general結構,query資訊結構,對應非登陸相關的動作,如執行一個語句select或一個命令exit。

struct mysql_event_general{unsigned int event_subclass; //子事件類型int general_error_code; //錯誤碼 unsigned long general_thread_id; //執行緒號const char *general_user; //用戶名unsigned int general_user_length; //長度const char *general_command; //命令,如connect,query,shutdown等,error類型存錯誤,其它為nullunsigned int general_command_length;const char *general_query; //sql語句 unsigned int general_query_length;struct charset_info_st *general_charset; //字元集unsigned long long general_time; //時間戳記unsigned long long general_rows; //行計數器MYSQL_LEX_STRING general_host; //來自主機,如localhostMYSQL_LEX_STRING general_sql_command; //操作類型,如selectMYSQL_LEX_STRING general_external_user; // external 用戶MYSQL_LEX_STRING general_ip; //ip地址};

mysql_event_connection,連接資訊結構,對應登陸登出動作

struct mysql_event_connection{unsigned int event_subclass;int status; //錯誤碼unsigned long thread_id;const char *user;unsigned int user_length;const char *priv_user;unsigned int priv_user_length;const char *external_user;unsigned int external_user_length;const char *proxy_user;unsigned int proxy_user_length;const char *host;unsigned int host_length;const char *ip;unsigned int ip_length;const char *database; //資料庫名unsigned int database_length;};

所有動作都可以用這兩個結構來描述,但是並不是說就能滿足全部需求,比方說,對general動作,我想知道語句是針對哪些表的,執行了多長時間,無法得到,對於connect動作,我想知道是什麼時間發生的,無從得知,這肯定是不夠的,仍需我們去繼續豐富功能點。

幾個關鍵的巨集定義:

#define MYSQL_AUDIT_GENERAL_CLASS 0 //general動作#define MYSQL_AUDIT_GENERAL_LOG 0 //log時,語句execute前#define MYSQL_AUDIT_GENERAL_ERROR 1 //error時,回饋用戶前 #define MYSQL_AUDIT_GENERAL_RESULT 2 //結果集返回後#define MYSQL_AUDIT_GENERAL_STATUS 3 //審計流程結束狀態#define MYSQL_AUDIT_CONNECTION_CLASS 1 //connect動作#define MYSQL_AUDIT_CONNECTION_CONNECT 0 //連接#define MYSQL_AUDIT_CONNECTION_DISCONNECT 1 //斷開#define MYSQL_AUDIT_CONNECTION_CHANGE_USER 2 //切換用戶

3. 核心功能實現代碼:sql_auditc.h,sql_audit.cc,初始化,回收,以及最重要的取出thd物件有效值等一系列動作均在此完成。

在sql_auditc.h中,定義了兩個內聯函數和三個巨集,功能是獲取到thd物件中有用值,如用戶名,資料庫名,時間,sql內容等所需要資訊,以參數的形式傳給下一層介面。每個動作,都會從這裡獲取原始資訊,會頻繁調用這幾個函數,所以使用了宏和內聯函數來提高性能。

在sql_audit.cc中,除了審計外掛程式初始化,回收外,最重要的功能獲得操作原始資訊,在這裡實現了函數獲得上面sql_auditc.h中通過參數形式傳入的有用資訊,並存入mysql_event_general和mysql_event_connection結構。

4. mysql其它一些連接、插入、解析、日誌等介面也增加了審計入口函數。如mysqld.cc、sql_parse.cc等檔中函數增加了審計入口。

審計執行流程簡介

登陸登出流程

#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45440cb0) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:491#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=, ap=)at /mysql56/sql/sql_audit.cc:90#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=0) at /mysql56/sql/sql_audit.cc:217#5 0x000000000063e7cb in mysql_audit_general_log (thd=0x7fdcc7ff6000, cmd=0xc76b64 "Connect", cmdlen=7, query_str=, query_len=)at /mysql56/sql/sql_audit.h:125#6 0x000000000063ea5a in log_command (thd=, command=, format=0xc6d27b "%s@%s on %s") at /mysql56/sql/log.cc:2094#7 general_log_print (thd=, command=, format=0xc6d27b "%s@%s on %s") at /mysql56/sql/log.cc:2122#8 0x0000000000682030 in acl_authenticate (thd=0x7fdcc7ff6000, com_change_user_pkt_len=) at /mysql56/sql/sql_acl.cc:11244#9 0x00000000006a8ad0 in check_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:685#10 0x00000000006a8d97 in login_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:754#11 thd_prepare_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:914#12 0x00000000006a9119 in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:983#13 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#14 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860#15 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#16 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

登陸如上所示,登陸時在do_handle_one_connection 中迴圈等待連接,有請求過來時再到準備連接,再到許可權認證檢查,最在log_command中進入審計介面,準備審計相關動作。

#0 audit_null_notify (thd=0x7fdcc7eb0000, event_class=1, event=0x7fdd46ca0b40) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000684402 in plugins_dispatch (thd=0x7fdcc7eb0000, event_subclass=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7eb0000, event_subclass=, ap=) at /mysql56/sql/sql_audit.cc:491#3 connection_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7eb0000, event_subclass=, ap=)at /mysql56/sql/sql_audit.cc:114#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7eb0000, event_class=1, event_subtype=1) at /mysql56/sql/sql_audit.cc:217#5 0x000000000057bb53 in close_connection (thd=0x7fdcc7eb0000, sql_errno=0) at /mysql56/sql/mysqld.cc:2634#6 0x00000000006a90b2 in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:996#7 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#8 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be7a0) at /mysql56/storage/perfschema/pfs.cc:1860#9 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#10 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

登出時相對簡單,在close_connection中增加了審計動作。

query審計流程執行前

#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45440da0) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:491#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=, ap=)at /mysql56/sql/sql_audit.cc:90#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=0) at /mysql56/sql/sql_audit.cc:217#5 0x000000000063e7cb in mysql_audit_general_log (thd=0x7fdcc7ff6000, cmd=0xc76b19 "Query", cmdlen=5, query_str=, query_len=)at /mysql56/sql/sql_audit.h:125#6 0x000000000063e93e in log_command (thd=0x7fdcc7ff6000 , command=, query=0x7fdcc201c010 "SELECT DATABASE()", query_length=17) at /mysql56/sql/log.cc:2094#7 general_log_write (thd=0x7fdcc7ff6000, command=, query=0x7fdcc201c010 "SELECT DATABASE()", query_length=17) at /mysql56/sql/log.cc:2136#8 0x00000000006e0b37 in mysql_parse (thd=0x7fdcc7ff6000, rawbuf=, length=, parser_state=)at /mysql56/sql/sql_parse.cc:6578#9 0x00000000006e1e78 in dispatch_command (command=COM_QUERY, thd=0x7fdcc7ff6000, packet=0x7fdcc7ffa001 "SELECT DATABASE()", packet_length=3254894625)at /mysql56/sql/sql_parse.cc:1359#10 0x00000000006a908d in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:990#11 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#12 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860#13 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#14 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

執行後:

#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45441180) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:491#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=, ap=)at /mysql56/sql/sql_audit.cc:90#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=3) at /mysql56/sql/sql_audit.cc:217#5 0x00000000006e1543 in mysql_audit_general (command=, thd=0x7fdcc7ff6000, packet=, packet_length=1) at /mysql56/sql/sql_audit.h:196#6 dispatch_command (command=, thd=0x7fdcc7ff6000, packet=, packet_length=1) at /mysql56/sql/sql_parse.cc:1793#7 0x00000000006a908d in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:990#8 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#9 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860#10 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#11 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

Query動作審計工作流程如上圖,需要留意的是在語句執行前和執行後都有審計,通過上面的宏MYSQL_AUDIT_GENERAL_LOG和MYSQL_AUDIT_GENERAL_RESULT告訴Audit是執行前後進入的審計模組,基於此可以做一些更細分的功能。當執行結果有錯時,通過MYSQL_AUDIT_GENERAL_ERROR來告知審計模組。

實例流程演示:

文字描述抽象,下面以一個select 語句實例演示下審計流程。

執行語句: select *from t2;

當語法解析重寫完成後,在execute執行前,會在LOGGER::log_command()中進入審計流程,代碼中增加了處理審計邏輯代碼:

mysql_audit_general_log(thd, command_name[(uint) command].str,command_name[(uint) command].length,query_str, query_length);

不論審計外掛程式有沒有安裝都會進入這個函數,進入這個函數第一件事就會判斷是否有審計外掛程式,沒有安裝審計會直接跳過所有審計邏輯,這個函數就是上面第3點介紹的獲取有用值兩個內聯函數之一 : mysql_audit_general_log

static inlinevoid mysql_audit_general_log(THD *thd, const char *cmd, uint cmdlen, const char *query_str, size_t query_len){if (mysql_global_audit_mask[0] & MYSQL_AUDIT_GENERAL_CLASSMASK) //沒裝外掛程式則跳過 { //取得有用的各種值 ..... .... .... mysql_audit_notify(thd, MYSQL_AUDIT_GENERAL_CLASS, MYSQL_AUDIT_GENERAL_LOG, error_code, time, user, userlen, cmd, cmdlen, query.str, query.length, clientcs, rows, sql_command, host, external_user, ip); //把有用的值往下傳 }}

在mysql_audit_notify會判斷下是連接語句還是通用語句,然後調用對應的函數,如本例中會調通用語句處理介面general_class_handler

static void general_class_handler(THD *thd, uint event_subtype, void *tb, va_list ap){mysql_event_general event;....event.general_query= va_arg(ap, const char *);event.general_query_length= va_arg(ap, unsigned int);event.general_charset= va_arg(ap, struct charset_info_st *);event.general_rows= (unsigned long long) va_arg(ap, ha_rows);event.general_host= va_arg(ap, MYSQL_LEX_STRING);event_class_dispatch(thd, MYSQL_AUDIT_GENERAL_CLASS, &event);}

在general_class_handler函數之後,語句的資訊就已經有了,保存在event結構中,上面語句在gdb中 實際列印的結果如下:

(gdb) p *(mysql_event_general *)event

$2 = {event_subclass = 0, general_error_code = 0, general_thread_id = 1, general_user = 0x7fd27aa51050

"root[root] @ localhost [127.0.0.1]",

general_user_length = 34, general_command = 0xc76b19 "Query", general_command_length = 5,

general_query = 0x7fd1f5c1a010 "select *from t2", general_query_length = 15, general_charset = 0x12e27e0, general_time = 1505712354,general_rows = 0, general_host = {str = 0xbe4d2f "localhost", length = 9}, general_sql_command = {str = 0xd29b04 "select", length = 6},

general_external_user = {str = 0xc70579 "", length = 0}, general_ip = {str = 0x7fd1f5c12040 "127.0.0.1", length = 9}}

可以非常清楚的看到語句內容,時間,類型等資訊。 當 execute執行後,會在 dispatch_command () 函數尾部會調用審計介面:

if (!thd->is_error() && !thd->killed_errno()) mysql_audit_general(thd, MYSQL_AUDIT_GENERAL_RESULT, 0, 0); mysql_audit_general(thd, MYSQL_AUDIT_GENERAL_STATUS, thd->get_stmt_da()->is_error() ? thd->get_stmt_da()->sql_errno() : 0, command_name[command].str);

這裡調用的是 mysql_audit_general,也是上面第3點介紹的獲取有用值另一個內聯函數,功能和mysql_audit_general_log相同,帶log的為執行前調,不帶log的為執行後調,區別在於記錄的值略有差異,如執行前的值裡沒錯誤狀態,執行後的值是有的,成功為0,失敗為對應的錯誤代碼。在mysql_audit_general之後流程和上面mysql_audit_general_log一樣,不再介紹。

這兩個內聯函數處理的是Query語句,另外有三個宏MYSQL_AUDIT_NOTIFY_CONNECTION_CONNECT、MYSQL_AUDIT_NOTIFY_CONNECTION_DISCONNECT、MYSQL_AUDIT_NOTIFY_CONNECTION_CHANGE_USER是處理登陸、登出、換用戶時用,功能和前兩個函數大同小異,不再贅述。

審計進一步思考

審計是記錄歷史動作,但有些情況下,光歷史記錄遠遠不夠。對於應用系統,很多語句都是都是綁定參數運行的,直觀審計到的就是一條條帶問號的語句,不能看到實際動作,若要還原真實語句,需要聯繫上下語句分析,非常不便,這就要求審計有一定的語句還原拼接功能,把參數和內容放一起展示。更進一步,假如有人非常瞭解審計系統,知道審計記錄不會永久存放,在某時刻建一個帶有破壞性的存儲過程,等超期不再保留建立過存儲程記錄後,執行並刪除,真實運行動作將無法通過審計得知,對這類操作,就有了反向追蹤運行內容語句的需求,尤其在金融行業中。

另一方面,審計用戶也應該是個特權用戶,和超級用戶互相制約。審計用戶應該獨立出來,有超級用戶不能修改的帳號密碼,專門用於審計相關操作,如開關審計,設置審計策略,查閱審計結果,超級用戶不能干涉這些動作,保證審計記錄的真實性和完整性。審計使用者帳號應該掌握在客戶最核心人手中,以對超級用戶活動形成制衡,更加保證資料的安全。

再深入一層,有客戶需求可能希望不同使用者看到的內容不一樣,對敏感性資料根據不同使用者進行自動過濾,嚴格來說這已經不再屬於審計範圍了,屬於資料安全問題,需要有更嚴密的處理邏輯來實現。對於資料庫產品來說,基於BLP模型的強制存取控制(MAC)是實現該需求的方法之一,該模型關鍵在於許可權標籤,對主客體進行標識,每個主體客體都有自己的標籤,許可權高的可訪問修改低的物件,低許可權的不能看到高許可權的內容,對每個表、列甚至行都打上標籤,對內容過濾可以做到列級直至行級。

從性能上來說,除了macfee外掛程式外,其它幾個性能相差不多,宣稱都是15%左右影響,macfee則可能達到50%或更多。實際上,確定一個測試性能損失的標準場景是需要商榷的,跑非常簡單的語句,幾十萬Qps,性能影響肯定大,審計性能消耗和語句量直接關聯,跑OLAP查詢,幾秒甚至幾十秒執行一個語句,審計性能影響可以說沒有,所以審計性能損失得看具體場景。

從實現方式來說,這幾個外掛程式也差不太多,從thd物件中取到所需要的屬性,按一定規則過濾,按某種策略存檔,以json或xml等格式展示審計結果。

mysql審計介面詳細分析

從5.5開始,mysql內核中已經增加了一套的對伺服器操作的審計機制的介面,添加了額外的審計流程來對我們所關心的地方進行事件捕獲,如果要對伺服器進行審計,只要基於這些完善一個外掛程式即可。mysql審計介面官方第一次是提交到代碼是2009年底,歷史也比較悠久了,修改和增加的檔並不多,幾個關鍵點介紹下。

在plugin.h中增加一個新的巨集 MYSQL_AUDIT_PLUGIN 用來標識一類全新的外掛程式類型:AUDIT外掛程式

增加了一個新的標頭檔:plugin_audit.h,裡面有審計相關的關鍵的結構體及一些巨集定義,如下: mysql_event_general結構,query資訊結構,對應非登陸相關的動作,如執行一個語句select或一個命令exit。

struct mysql_event_general{unsigned int event_subclass; //子事件類型int general_error_code; //錯誤碼 unsigned long general_thread_id; //執行緒號const char *general_user; //用戶名unsigned int general_user_length; //長度const char *general_command; //命令,如connect,query,shutdown等,error類型存錯誤,其它為nullunsigned int general_command_length;const char *general_query; //sql語句 unsigned int general_query_length;struct charset_info_st *general_charset; //字元集unsigned long long general_time; //時間戳記unsigned long long general_rows; //行計數器MYSQL_LEX_STRING general_host; //來自主機,如localhostMYSQL_LEX_STRING general_sql_command; //操作類型,如selectMYSQL_LEX_STRING general_external_user; // external 用戶MYSQL_LEX_STRING general_ip; //ip地址};

mysql_event_connection,連接資訊結構,對應登陸登出動作

struct mysql_event_connection{unsigned int event_subclass;int status; //錯誤碼unsigned long thread_id;const char *user;unsigned int user_length;const char *priv_user;unsigned int priv_user_length;const char *external_user;unsigned int external_user_length;const char *proxy_user;unsigned int proxy_user_length;const char *host;unsigned int host_length;const char *ip;unsigned int ip_length;const char *database; //資料庫名unsigned int database_length;};

所有動作都可以用這兩個結構來描述,但是並不是說就能滿足全部需求,比方說,對general動作,我想知道語句是針對哪些表的,執行了多長時間,無法得到,對於connect動作,我想知道是什麼時間發生的,無從得知,這肯定是不夠的,仍需我們去繼續豐富功能點。

幾個關鍵的巨集定義:

#define MYSQL_AUDIT_GENERAL_CLASS 0 //general動作#define MYSQL_AUDIT_GENERAL_LOG 0 //log時,語句execute前#define MYSQL_AUDIT_GENERAL_ERROR 1 //error時,回饋用戶前 #define MYSQL_AUDIT_GENERAL_RESULT 2 //結果集返回後#define MYSQL_AUDIT_GENERAL_STATUS 3 //審計流程結束狀態#define MYSQL_AUDIT_CONNECTION_CLASS 1 //connect動作#define MYSQL_AUDIT_CONNECTION_CONNECT 0 //連接#define MYSQL_AUDIT_CONNECTION_DISCONNECT 1 //斷開#define MYSQL_AUDIT_CONNECTION_CHANGE_USER 2 //切換用戶

3. 核心功能實現代碼:sql_auditc.h,sql_audit.cc,初始化,回收,以及最重要的取出thd物件有效值等一系列動作均在此完成。

在sql_auditc.h中,定義了兩個內聯函數和三個巨集,功能是獲取到thd物件中有用值,如用戶名,資料庫名,時間,sql內容等所需要資訊,以參數的形式傳給下一層介面。每個動作,都會從這裡獲取原始資訊,會頻繁調用這幾個函數,所以使用了宏和內聯函數來提高性能。

在sql_audit.cc中,除了審計外掛程式初始化,回收外,最重要的功能獲得操作原始資訊,在這裡實現了函數獲得上面sql_auditc.h中通過參數形式傳入的有用資訊,並存入mysql_event_general和mysql_event_connection結構。

4. mysql其它一些連接、插入、解析、日誌等介面也增加了審計入口函數。如mysqld.cc、sql_parse.cc等檔中函數增加了審計入口。

審計執行流程簡介

登陸登出流程

#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45440cb0) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:491#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=, ap=)at /mysql56/sql/sql_audit.cc:90#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=0) at /mysql56/sql/sql_audit.cc:217#5 0x000000000063e7cb in mysql_audit_general_log (thd=0x7fdcc7ff6000, cmd=0xc76b64 "Connect", cmdlen=7, query_str=, query_len=)at /mysql56/sql/sql_audit.h:125#6 0x000000000063ea5a in log_command (thd=, command=, format=0xc6d27b "%s@%s on %s") at /mysql56/sql/log.cc:2094#7 general_log_print (thd=, command=, format=0xc6d27b "%s@%s on %s") at /mysql56/sql/log.cc:2122#8 0x0000000000682030 in acl_authenticate (thd=0x7fdcc7ff6000, com_change_user_pkt_len=) at /mysql56/sql/sql_acl.cc:11244#9 0x00000000006a8ad0 in check_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:685#10 0x00000000006a8d97 in login_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:754#11 thd_prepare_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:914#12 0x00000000006a9119 in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:983#13 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#14 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860#15 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#16 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

登陸如上所示,登陸時在do_handle_one_connection 中迴圈等待連接,有請求過來時再到準備連接,再到許可權認證檢查,最在log_command中進入審計介面,準備審計相關動作。

#0 audit_null_notify (thd=0x7fdcc7eb0000, event_class=1, event=0x7fdd46ca0b40) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000684402 in plugins_dispatch (thd=0x7fdcc7eb0000, event_subclass=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7eb0000, event_subclass=, ap=) at /mysql56/sql/sql_audit.cc:491#3 connection_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7eb0000, event_subclass=, ap=)at /mysql56/sql/sql_audit.cc:114#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7eb0000, event_class=1, event_subtype=1) at /mysql56/sql/sql_audit.cc:217#5 0x000000000057bb53 in close_connection (thd=0x7fdcc7eb0000, sql_errno=0) at /mysql56/sql/mysqld.cc:2634#6 0x00000000006a90b2 in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:996#7 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#8 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be7a0) at /mysql56/storage/perfschema/pfs.cc:1860#9 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#10 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

登出時相對簡單,在close_connection中增加了審計動作。

query審計流程執行前

#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45440da0) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:491#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=, ap=)at /mysql56/sql/sql_audit.cc:90#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=0) at /mysql56/sql/sql_audit.cc:217#5 0x000000000063e7cb in mysql_audit_general_log (thd=0x7fdcc7ff6000, cmd=0xc76b19 "Query", cmdlen=5, query_str=, query_len=)at /mysql56/sql/sql_audit.h:125#6 0x000000000063e93e in log_command (thd=0x7fdcc7ff6000 , command=, query=0x7fdcc201c010 "SELECT DATABASE()", query_length=17) at /mysql56/sql/log.cc:2094#7 general_log_write (thd=0x7fdcc7ff6000, command=, query=0x7fdcc201c010 "SELECT DATABASE()", query_length=17) at /mysql56/sql/log.cc:2136#8 0x00000000006e0b37 in mysql_parse (thd=0x7fdcc7ff6000, rawbuf=, length=, parser_state=)at /mysql56/sql/sql_parse.cc:6578#9 0x00000000006e1e78 in dispatch_command (command=COM_QUERY, thd=0x7fdcc7ff6000, packet=0x7fdcc7ffa001 "SELECT DATABASE()", packet_length=3254894625)at /mysql56/sql/sql_parse.cc:1359#10 0x00000000006a908d in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:990#11 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#12 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860#13 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#14 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

執行後:

#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45441180) at /mysql56/plugin/audit_null/audit_null.c:99#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:455#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=, ap=) at /mysql56/sql/sql_audit.cc:491#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=, ap=)at /mysql56/sql/sql_audit.cc:90#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=3) at /mysql56/sql/sql_audit.cc:217#5 0x00000000006e1543 in mysql_audit_general (command=, thd=0x7fdcc7ff6000, packet=, packet_length=1) at /mysql56/sql/sql_audit.h:196#6 dispatch_command (command=, thd=0x7fdcc7ff6000, packet=, packet_length=1) at /mysql56/sql/sql_parse.cc:1793#7 0x00000000006a908d in do_handle_one_connection (thd_arg=) at /mysql56/sql/sql_connect.cc:990#8 0x00000000006a91c2 in handle_one_connection (arg=) at /mysql56/sql/sql_connect.cc:906#9 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860#10 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0#11 0x00007fdd459a38fd in clone () from /lib64/libc.so.6

Query動作審計工作流程如上圖,需要留意的是在語句執行前和執行後都有審計,通過上面的宏MYSQL_AUDIT_GENERAL_LOG和MYSQL_AUDIT_GENERAL_RESULT告訴Audit是執行前後進入的審計模組,基於此可以做一些更細分的功能。當執行結果有錯時,通過MYSQL_AUDIT_GENERAL_ERROR來告知審計模組。

實例流程演示:

文字描述抽象,下面以一個select 語句實例演示下審計流程。

執行語句: select *from t2;

當語法解析重寫完成後,在execute執行前,會在LOGGER::log_command()中進入審計流程,代碼中增加了處理審計邏輯代碼:

mysql_audit_general_log(thd, command_name[(uint) command].str,command_name[(uint) command].length,query_str, query_length);

不論審計外掛程式有沒有安裝都會進入這個函數,進入這個函數第一件事就會判斷是否有審計外掛程式,沒有安裝審計會直接跳過所有審計邏輯,這個函數就是上面第3點介紹的獲取有用值兩個內聯函數之一 : mysql_audit_general_log

static inlinevoid mysql_audit_general_log(THD *thd, const char *cmd, uint cmdlen, const char *query_str, size_t query_len){if (mysql_global_audit_mask[0] & MYSQL_AUDIT_GENERAL_CLASSMASK) //沒裝外掛程式則跳過 { //取得有用的各種值 ..... .... .... mysql_audit_notify(thd, MYSQL_AUDIT_GENERAL_CLASS, MYSQL_AUDIT_GENERAL_LOG, error_code, time, user, userlen, cmd, cmdlen, query.str, query.length, clientcs, rows, sql_command, host, external_user, ip); //把有用的值往下傳 }}

在mysql_audit_notify會判斷下是連接語句還是通用語句,然後調用對應的函數,如本例中會調通用語句處理介面general_class_handler

static void general_class_handler(THD *thd, uint event_subtype, void *tb, va_list ap){mysql_event_general event;....event.general_query= va_arg(ap, const char *);event.general_query_length= va_arg(ap, unsigned int);event.general_charset= va_arg(ap, struct charset_info_st *);event.general_rows= (unsigned long long) va_arg(ap, ha_rows);event.general_host= va_arg(ap, MYSQL_LEX_STRING);event_class_dispatch(thd, MYSQL_AUDIT_GENERAL_CLASS, &event);}

在general_class_handler函數之後,語句的資訊就已經有了,保存在event結構中,上面語句在gdb中 實際列印的結果如下:

(gdb) p *(mysql_event_general *)event

$2 = {event_subclass = 0, general_error_code = 0, general_thread_id = 1, general_user = 0x7fd27aa51050

"root[root] @ localhost [127.0.0.1]",

general_user_length = 34, general_command = 0xc76b19 "Query", general_command_length = 5,

general_query = 0x7fd1f5c1a010 "select *from t2", general_query_length = 15, general_charset = 0x12e27e0, general_time = 1505712354,general_rows = 0, general_host = {str = 0xbe4d2f "localhost", length = 9}, general_sql_command = {str = 0xd29b04 "select", length = 6},

general_external_user = {str = 0xc70579 "", length = 0}, general_ip = {str = 0x7fd1f5c12040 "127.0.0.1", length = 9}}

可以非常清楚的看到語句內容,時間,類型等資訊。 當 execute執行後,會在 dispatch_command () 函數尾部會調用審計介面:

if (!thd->is_error() && !thd->killed_errno()) mysql_audit_general(thd, MYSQL_AUDIT_GENERAL_RESULT, 0, 0); mysql_audit_general(thd, MYSQL_AUDIT_GENERAL_STATUS, thd->get_stmt_da()->is_error() ? thd->get_stmt_da()->sql_errno() : 0, command_name[command].str);

這裡調用的是 mysql_audit_general,也是上面第3點介紹的獲取有用值另一個內聯函數,功能和mysql_audit_general_log相同,帶log的為執行前調,不帶log的為執行後調,區別在於記錄的值略有差異,如執行前的值裡沒錯誤狀態,執行後的值是有的,成功為0,失敗為對應的錯誤代碼。在mysql_audit_general之後流程和上面mysql_audit_general_log一樣,不再介紹。

這兩個內聯函數處理的是Query語句,另外有三個宏MYSQL_AUDIT_NOTIFY_CONNECTION_CONNECT、MYSQL_AUDIT_NOTIFY_CONNECTION_DISCONNECT、MYSQL_AUDIT_NOTIFY_CONNECTION_CHANGE_USER是處理登陸、登出、換用戶時用,功能和前兩個函數大同小異,不再贅述。

審計進一步思考

審計是記錄歷史動作,但有些情況下,光歷史記錄遠遠不夠。對於應用系統,很多語句都是都是綁定參數運行的,直觀審計到的就是一條條帶問號的語句,不能看到實際動作,若要還原真實語句,需要聯繫上下語句分析,非常不便,這就要求審計有一定的語句還原拼接功能,把參數和內容放一起展示。更進一步,假如有人非常瞭解審計系統,知道審計記錄不會永久存放,在某時刻建一個帶有破壞性的存儲過程,等超期不再保留建立過存儲程記錄後,執行並刪除,真實運行動作將無法通過審計得知,對這類操作,就有了反向追蹤運行內容語句的需求,尤其在金融行業中。

另一方面,審計用戶也應該是個特權用戶,和超級用戶互相制約。審計用戶應該獨立出來,有超級用戶不能修改的帳號密碼,專門用於審計相關操作,如開關審計,設置審計策略,查閱審計結果,超級用戶不能干涉這些動作,保證審計記錄的真實性和完整性。審計使用者帳號應該掌握在客戶最核心人手中,以對超級用戶活動形成制衡,更加保證資料的安全。

再深入一層,有客戶需求可能希望不同使用者看到的內容不一樣,對敏感性資料根據不同使用者進行自動過濾,嚴格來說這已經不再屬於審計範圍了,屬於資料安全問題,需要有更嚴密的處理邏輯來實現。對於資料庫產品來說,基於BLP模型的強制存取控制(MAC)是實現該需求的方法之一,該模型關鍵在於許可權標籤,對主客體進行標識,每個主體客體都有自己的標籤,許可權高的可訪問修改低的物件,低許可權的不能看到高許可權的內容,對每個表、列甚至行都打上標籤,對內容過濾可以做到列級直至行級。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示