部越權(quán)防護(hù)指南:從權(quán)限最小化到日志審計(jì)的落地實(shí)踐)
MySQL如何防止內(nèi)部員工越權(quán)查看數(shù)據(jù)_實(shí)施嚴(yán)格的日志審計(jì)策略1. 先搞清楚“內(nèi)部越權(quán)”到底長(zhǎng)什么樣1.1 越權(quán)不是黑客專利多數(shù)事故其實(shí)是“順手”干的很多人一想到數(shù)據(jù)安全腦子里冒出來的都是“外部攻擊”“拖庫”“注入”但實(shí)際上真正讓人半夜被電話叫醒的往往不是外面的黑客而是自己人。我干過幾年數(shù)據(jù)庫運(yùn)維也處理過不少內(nèi)部數(shù)據(jù)泄露的事件。舉幾個(gè)最常見的場(chǎng)景客服崗位的員工利用系統(tǒng)權(quán)限偷偷查詢明星或大客戶的訂單記錄財(cái)務(wù)部同事拿著高權(quán)限賬號(hào)把全公司工資表導(dǎo)出后發(fā)到了沒有權(quán)限的同事群里開發(fā)人員為了排查線上問題順手SELECT了整張用戶表還復(fù)制到了本地甚至出現(xiàn)過離職員工在交接期把核心業(yè)務(wù)表數(shù)據(jù)打包帶走的情況。這些事有一個(gè)共同點(diǎn)都不是外部攻擊而是內(nèi)部人員“越權(quán)”訪問了本不該看的數(shù)據(jù)。這里的“越權(quán)”不一定是繞過系統(tǒng)更多時(shí)候是“權(quán)限過大”導(dǎo)致的超范圍訪問。你給了業(yè)務(wù)人員一個(gè)能查訂單的賬號(hào)他沒查訂單反而把用戶地址和手機(jī)號(hào)全拉走了這就是越權(quán)。1.2 內(nèi)部數(shù)據(jù)泄露的幾個(gè)常見場(chǎng)景我把實(shí)際工作中遇到的內(nèi)部越權(quán)場(chǎng)景整理了一下基本可以歸為幾類場(chǎng)景類型典型行為后果好奇型越權(quán)員工利用權(quán)限查看明星、熟人、競(jìng)爭(zhēng)客戶的隱私數(shù)據(jù)隱私泄露企業(yè)被投訴甚至起訴利益型越權(quán)離職前批量導(dǎo)出客戶資料、價(jià)格體系、核心報(bào)表賣給對(duì)手商業(yè)機(jī)密流失直接損失巨大誤操作型越權(quán)開發(fā)或DBA執(zhí)行了沒有WHERE條件的UPDATE、DELETE或全表SELECT數(shù)據(jù)被破壞審計(jì)時(shí)無法定位責(zé)任人憑證復(fù)用型越權(quán)多個(gè)員工共用一個(gè)高權(quán)限賬號(hào)密碼常年不換出事后無法追責(zé)只能“集體背鍋”你可能覺得這些場(chǎng)景離自己很遠(yuǎn)但說實(shí)話只要公司里MySQL賬號(hào)有這兩個(gè)特征——賬號(hào)多人共用、權(quán)限明顯大于崗位需要——那離出事就只差一個(gè)“手滑”或者“好奇”。1.3 為什么單靠MySQL自帶的權(quán)限表擋不住人MySQL的權(quán)限體系做得很細(xì)從全局、庫、表、列到存儲(chǔ)過程級(jí)別都有權(quán)限控制。但問題是權(quán)限控制解決的是“能不能做”的問題解決不了“做了什么”和“是誰做的”的問題。MySQL原生的權(quán)限表mysql.user、mysql.db、mysql.tables_priv這些只能告訴你這個(gè)賬號(hào)能查哪些庫、哪些表但不會(huì)記錄這個(gè)賬號(hào)實(shí)際查了什么、什么時(shí)候查的、查了多少行。更麻煩的是如果公司圖省事給所有人共用一個(gè)賬號(hào)那就算查到了是哪個(gè)賬號(hào)執(zhí)行了SELECT也無法定位到具體的人。所以要防內(nèi)部越權(quán)不能只靠“把權(quán)限鎖死”還得讓每一次訪問都“留痕”。留痕這件事就是日志審計(jì)的核心。2. 事前防線把賬號(hào)權(quán)限做“小”比做“嚴(yán)”更管用2.1 最小權(quán)限原則落地到MySQL的賬號(hào)設(shè)計(jì)很多人理解的“權(quán)限控制”是一刀切能不給就不給。但生產(chǎn)環(huán)境里業(yè)務(wù)要跑、人要干活完全不給權(quán)限不現(xiàn)實(shí)而且會(huì)逼著員工用管理員賬號(hào)“湊合著查”反而更不安全。我做賬號(hào)規(guī)劃時(shí)習(xí)慣遵循一個(gè)思路讓每個(gè)賬號(hào)剛好處在“能完成本職工作但多一步越權(quán)都做不了”的位置上。這其實(shí)就是最小權(quán)限原則。給賬號(hào)授權(quán)時(shí)不要順手GRANT ALL要精確到庫、表、甚至字段。舉個(gè)例子客服人員只能查訂單表里的訂單號(hào)和狀態(tài)不能查客戶的手機(jī)號(hào)、地址那就這樣授權(quán)-- 創(chuàng)建客服專用賬號(hào)密碼務(wù)必用強(qiáng)密碼并定期更換 CREATE USER cs_query10.0.% IDENTIFIED BY 強(qiáng)密碼; -- 只給訂單表的特定列授予SELECT權(quán)限不授予整表權(quán)限 GRANT SELECT (order_id, order_status, create_time) ON trade_db.t_order TO cs_query10.0.%; -- 不加這個(gè)客服賬號(hào)連其他庫都看不到 REVOKE ALL PRIVILEGES ON *.* FROM cs_query10.0.%; FLUSH PRIVILEGES;這樣操作后查詢端根本過不了“客戶隱私字段”這一關(guān)連SELECT都執(zhí)行不了。權(quán)限在源頭就斷了這比事后盯著審計(jì)日志找問題要高效得多。2.2 一人一賬號(hào)避免“公共賬號(hào)”背鍋另一個(gè)必須堅(jiān)持的原則是“一人一賬號(hào)”。我知道很多小團(tuán)隊(duì)的習(xí)慣是建一個(gè)root_bi、dev_all之類的公共賬號(hào)十幾個(gè)開發(fā)共用。這樣確實(shí)省事可一旦出現(xiàn)數(shù)據(jù)泄露你根本沒法確認(rèn)當(dāng)時(shí)坐在電腦前的是誰只能從業(yè)務(wù)系統(tǒng)登錄日志里慢慢查如果業(yè)務(wù)系統(tǒng)也沒日志那這事就成了懸案。一人一賬號(hào)的操作不復(fù)雜就是給每個(gè)人單獨(dú)建賬號(hào)按崗位授權(quán)。真正麻煩的是賬號(hào)多了之后的維護(hù)所以配套要做兩件事第一賬號(hào)命名規(guī)則要能對(duì)應(yīng)到人。比如用“姓名拼音業(yè)務(wù)線”命名zhangsan_bi一看就知道是張三在報(bào)表線。第二定期清理離職和調(diào)崗賬號(hào)。季度巡檢時(shí)跑一條SQL就能把長(zhǎng)期不用的賬號(hào)撈出來SELECT user, host, db, account_locked, password_expired FROM mysql.user WHERE account_locked N AND password_expired N;再結(jié)合運(yùn)維側(cè)的登錄記錄和業(yè)務(wù)工單系統(tǒng)一一核對(duì)哪些賬號(hào)該禁用、哪些賬號(hào)該降權(quán)。這個(gè)過程很枯燥但我被“離職同事三個(gè)月后還能登錄數(shù)據(jù)庫”這種事嚇過好幾次真不能偷懶。2.3 用存儲(chǔ)過程包一層把查詢?nèi)肟谑照鰴?quán)限控制時(shí)我遇到的另一個(gè)尷尬情況是業(yè)務(wù)人員確實(shí)需要查敏感數(shù)據(jù)但又不能給他們直接操作表的權(quán)限。比如客服要查訂單但只能查自己負(fù)責(zé)的那部分訂單不能查全庫。這時(shí)候最好的辦法是“存儲(chǔ)過程收口”。把查詢邏輯寫進(jìn)存儲(chǔ)過程只給業(yè)務(wù)人員執(zhí)行存儲(chǔ)過程的權(quán)限不給他們直接SELECT表或UPDATE表的權(quán)限。舉個(gè)例子做個(gè)帶商戶ID校驗(yàn)的查詢存儲(chǔ)過程DELIMITER $$ CREATE PROCEDURE trade_db.sp_query_order_by_id( IN p_emp_id VARCHAR(32), IN p_order_id VARCHAR(64) ) BEGIN -- 員工編號(hào)用于權(quán)限校驗(yàn)確保只能查歸屬自己的訂單 SELECT order_id, order_status, order_amount FROM trade_db.t_order WHERE order_id p_order_id AND emp_id p_emp_id; -- 硬性歸屬校驗(yàn)防止橫向越權(quán) END$$ DELIMITER ;授權(quán)時(shí)只給EXECUTE權(quán)限GRANT EXECUTE ON PROCEDURE trade_db.sp_query_order_by_id TO cs_query10.0.%;這樣設(shè)計(jì)之后業(yè)務(wù)人員完全沒有直接操作表的權(quán)限只能通過存儲(chǔ)過程查而且在存儲(chǔ)過程里可以內(nèi)置各種限制條件從源頭堵住越權(quán)查詢。這個(gè)方案我實(shí)際用了很久效果是立竿見影的唯一的缺點(diǎn)是需要把常用查詢都整理成存儲(chǔ)過程前期要花一些開發(fā)時(shí)間。3. 中間環(huán)節(jié)開審計(jì)你要知道的三條路徑3.1 企業(yè)版審計(jì)插件最省事但要花錢權(quán)限做得再小也只能減少越權(quán)面不可能完全杜絕。比如你有權(quán)限查A表但你出于好奇去查相鄰的B表權(quán)限上又不違規(guī)這種事權(quán)限層面是攔不住的。這時(shí)候就需要“審計(jì)”來兜底。MySQL官方提供的企業(yè)版審計(jì)插件MySQL Enterprise Audit是最“正統(tǒng)”的審計(jì)方案。它基于MySQL插件架構(gòu)實(shí)現(xiàn)可以審計(jì)連接、查詢、登錄失敗等信息還能按用戶、按訪問源、按操作類型做過濾審計(jì)日志也支持XML和JSON格式方便對(duì)接外部日志分析平臺(tái)。企業(yè)版插件的好處是性能好、功能全、官方長(zhǎng)期維護(hù)但需要購買MySQL企業(yè)版授權(quán)很多中小公司不一定會(huì)為此單獨(dú)付費(fèi)。我自己在客戶現(xiàn)場(chǎng)接觸到的大多是社區(qū)版所以下面的內(nèi)容會(huì)重點(diǎn)講社區(qū)版能落地、成本低、效果也還不錯(cuò)的方案。如果你公司正好有企業(yè)版授權(quán)開審計(jì)也就是改個(gè)參數(shù)的事# my.cnf plugin-load-addaudit_log.so audit-logFORCE_PLUS_PERMANENT audit-log-formatJSON audit-log-policyALL3.2 社區(qū)版怎么開binlog保住變更記錄社區(qū)版MySQL雖然沒有官方審計(jì)插件但自帶的binlog二進(jìn)制日志就是一套天然的日志審計(jì)工具只是很多人只拿它做主從復(fù)制和數(shù)據(jù)恢復(fù)忽略了它的審計(jì)價(jià)值。binlog記錄的是所有改變數(shù)據(jù)的操作INSERT、UPDATE、DELETE等只要把binlog_format設(shè)置為ROW模式還能記錄每一行數(shù)據(jù)變更前后的值。這意味著只要誰改了數(shù)據(jù)都能從binlog里找到蛛絲馬跡。開啟binlog要改配置文件這是MySQL 8.0的常見配置# my.cnf server-id 1003306 log-bin /data/mysql/logs/mysql-bin binlog_format ROW # binlog過期時(shí)間線上建議7天起步這個(gè)按企業(yè)合規(guī)要求來 binlog_expire_logs_seconds 604800 max_binlog_size 512M改完重啟MySQL后可以執(zhí)行下面這條命令確認(rèn)binlog已經(jīng)開啟并且是ROW格式SHOW VARIABLES LIKE log_bin; SHOW VARIABLES LIKE binlog_format;有了binlog之后當(dāng)需要排查某個(gè)時(shí)間段的UPDATE操作時(shí)可以直接用mysqlbinlog工具把日志拉出來mysqlbinlog --base64-outputDECODE-ROWS -v \ --start-datetime2024-06-01 00:00:00 \ --stop-datetime2024-06-01 23:59:59 \ /data/mysql/logs/mysql-bin.000088 | grep -A 20 DELETE FROM trade_db.t_order但要注意binlog本身不記錄“誰執(zhí)行了這條SQL”它記錄的是server-id、線程id、時(shí)間戳和執(zhí)行位置的元信息。所以binlog必須配合“一人一賬號(hào)”的賬號(hào)策略才能把操作追溯到具體的人。3.3 general_log和init-connect低成本補(bǔ)上查詢審計(jì)binlog只能看到數(shù)據(jù)變更操作查詢操作SELECT它是不記錄的。而恰恰是SELECT最容易發(fā)生越權(quán)——因?yàn)椤翱匆谎邸辈涣艉勐?。想查“誰在什么時(shí)候查了什么”就得靠general_log通用查詢?nèi)罩净蛘咦越▽徲?jì)表。general_log是MySQL記錄所有客戶端請(qǐng)求的日志包括連接、斷開、每一條SQL不管你有沒有改數(shù)據(jù)它都記。開起來也簡(jiǎn)單-- 動(dòng)態(tài)開啟不用重啟 SET GLOBAL general_log ON; SET GLOBAL general_log_file /data/mysql/logs/general_query.log;看到這里你可能會(huì)想那直接把general_log一開不就完事了先別急general_log有個(gè)很大的問題它會(huì)把所有庫的所有SQL全都記下來生產(chǎn)環(huán)境一旦開著日志文件可能幾個(gè)小時(shí)就沖到幾十個(gè)G很容易把磁盤寫滿導(dǎo)致數(shù)據(jù)庫直接不可用。我見過不止一次因?yàn)檎`開general_log把生產(chǎn)搞掛的案例所以線上一般不建議長(zhǎng)時(shí)間全量開啟。那怎么平衡“要審計(jì)”和“不能把磁盤打爆”我自己常用的替代方案是init-connect。init-connect是MySQL在每次客戶端建立連接時(shí)自動(dòng)執(zhí)行的一小段初始化SQL。我們可以利用它往一張審計(jì)表里記一條“誰、從哪來、以什么賬號(hào)、什么時(shí)候建立了連接”的記錄。這個(gè)方案能覆蓋“哪個(gè)賬號(hào)在哪個(gè)時(shí)間點(diǎn)連過數(shù)據(jù)庫”雖然記不了每一條SQL但對(duì)于絕大多數(shù)內(nèi)部越權(quán)場(chǎng)景已經(jīng)足夠定位問題了先定位到人再結(jié)合binlog或業(yè)務(wù)系統(tǒng)日志還原具體操作。4. 自建審計(jì)表被問到“誰查了什么”時(shí)能立刻答上來4.1 審計(jì)表結(jié)構(gòu)設(shè)計(jì)init-connect這塊我用得比較多每次在培訓(xùn)里講完大家都覺得不錯(cuò)這里把完整方案寫出來可以直接抄作業(yè)。首先要有一張存放連接審計(jì)記錄的表。這張表單獨(dú)放在一個(gè)沒有業(yè)務(wù)數(shù)據(jù)的庫里比如審計(jì)庫audit_db避免被業(yè)務(wù)DROP掉CREATE DATABASE IF NOT EXISTS audit_db DEFAULT CHARSET utf8mb4; USE audit_db; CREATE TABLE IF NOT EXISTS audit_db.access_log ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, connect_time DATETIME NOT NULL COMMENT 連接建立時(shí)間, user_host VARCHAR(64) NOT NULL COMMENT 賬號(hào)和來源主機(jī), thread_id BIGINT UNSIGNED NOT NULL COMMENT 線程ID, server_ip VARCHAR(32) NOT NULL COMMENT 應(yīng)用服務(wù)器IP, query_content VARCHAR(1024) NULL COMMENT init_connect記錄的最終SQL不一定有 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT登錄行為審計(jì)表;這里我再說明一下訪問IP的獲取在MySQL 8.0里可以直接用SUBSTRING_INDEX(USER(), , -1)拿到客戶端IP如果前面有代理層會(huì)拿到代理IP這點(diǎn)生產(chǎn)環(huán)境要注意最好在業(yè)務(wù)系統(tǒng)的應(yīng)用層也記錄真實(shí)用戶IP兩邊對(duì)照。4.2 init-connect結(jié)合審計(jì)表的具體配置表建好之后關(guān)鍵的一步是配置init-connect。先在my.cnf里加上下面這行# my.cnf init_connect INSERT INTO audit_db.access_log (connect_time, user_host, thread_id, server_ip) VALUES (NOW(), USER(), CONNECTION_ID(), SUBSTRING_INDEX(USER(), , -1))這里有個(gè)坑必須提醒大家init-connect在執(zhí)行時(shí)如果當(dāng)前賬號(hào)對(duì)audit_db.access_log表沒有INSERT權(quán)限那么連接就會(huì)失敗相當(dāng)于把人擋在數(shù)據(jù)庫外面。所以要把所有需要審計(jì)的賬號(hào)都單獨(dú)授予這INSERT權(quán)限GRANT INSERT ON audit_db.access_log TO cs_query10.0.%; GRANT INSERT ON audit_db.access_log TO app_read10.0.%; -- 其他賬號(hào)同理逐個(gè)授權(quán)另外管理員賬號(hào)root不會(huì)執(zhí)行init-connect這是MySQL的一個(gè)安全設(shè)計(jì)也算合理畢竟管理員一旦寫壞init-connect至少能留個(gè)口子進(jìn)去修復(fù)。配置好后重啟MySQL用任何普通賬號(hào)連一次稍等一會(huì)查一下審計(jì)表SELECT connect_time, user_host, thread_id, server_ip FROM audit_db.access_log ORDER BY id DESC LIMIT 10;能看到每次連接都有記錄之后這個(gè)“最低成本的連接審計(jì)”就算生效了。這套方案的性能開銷很小因?yàn)樗辉诮⑦B接時(shí)執(zhí)行一次INSERT不會(huì)像general_log那樣每條SQL都寫盤。4.3 手動(dòng)記錄敏感表查詢的存儲(chǔ)過程示例init-connect解決的是“誰來過”但對(duì)“具體查了什么”還是空白。對(duì)于特別敏感的表比如客戶信息表、訂單表、薪資表我建議再加一層“手動(dòng)審計(jì)”。具體做法是把所有對(duì)敏感表的訪問收口到帶審計(jì)邏輯的存儲(chǔ)過程里。前面已經(jīng)提過用存儲(chǔ)過程收權(quán)限這里再往前推一步存儲(chǔ)過程里不僅做權(quán)限校驗(yàn)順帶把“誰查了什么、查了多少行、什么時(shí)間查的”都寫進(jìn)審計(jì)表。舉個(gè)實(shí)際的例子給財(cái)務(wù)部做一個(gè)“按月份查詢工資匯總”的入口DELIMITER $$ CREATE PROCEDURE finance_db.sp_query_salary_summary( IN p_emp_no VARCHAR(32), IN p_query_month VARCHAR(6) ) BEGIN DECLARE v_result_count INT DEFAULT 0; DECLARE v_emp_name VARCHAR(64); -- 從業(yè)務(wù)系統(tǒng)表取當(dāng)前賬號(hào)對(duì)應(yīng)的員工信息 SELECT real_name INTO v_emp_name FROM hr_db.t_employee WHERE emp_no p_emp_no; -- 查詢工資匯總這里可以按需求寫業(yè)務(wù)邏輯 SELECT dept_name, SUM(salary) AS total_salary FROM finance_db.t_salary WHERE month p_query_month GROUP BY dept_name; -- 記錄審計(jì)信息 INSERT INTO audit_db.salary_query_log ( query_time, emp_no, emp_name, query_month ) VALUES ( NOW(), p_emp_no, v_emp_name, p_query_month ); END$$ DELIMITER ;這樣一來每次有人查詢薪資數(shù)據(jù)審計(jì)表里就會(huì)留下痕跡。配合前面的一人一賬號(hào)方案一旦出問題查出來就是具體的人、具體的時(shí)間、具體查的哪個(gè)月份責(zé)任劃分清清楚楚。這個(gè)方法的核心思路是把審計(jì)能力嵌進(jìn)業(yè)務(wù)入口里而不是事后翻日志。它的好處是審計(jì)信息更精確壞處是需要開發(fā)配合改造不是所有查詢都能收口。所以我的建議是最重要的幾個(gè)敏感場(chǎng)景一定要收口改造一般場(chǎng)景就用init-connect加binlog做兜底。5. 日志審計(jì)要形成閉環(huán)定期審、定時(shí)清、定人盯5.1 審計(jì)日志日常巡檢的幾個(gè)SQL日志開了表建好了如果只是放著不管那審計(jì)等于白做。真正有價(jià)值的審計(jì)必須形成“記錄—檢查—處置”的閉環(huán)。日常巡檢我一般會(huì)做三件事。第一定期檢查登錄審計(jì)表看看有沒有異常的連接記錄比如凌晨三點(diǎn)有人連接數(shù)據(jù)庫、某個(gè)賬號(hào)在短時(shí)間內(nèi)頻繁連接等。-- 排查凌晨時(shí)段的登錄記錄 SELECT connect_time, user_host, thread_id, server_ip FROM audit_db.access_log WHERE HOUR(connect_time) BETWEEN 0 AND 5 ORDER BY connect_time DESC LIMIT 50;第二通過binlog檢查敏感表的變更情況尤其是非業(yè)務(wù)高峰期有沒有人手動(dòng)改數(shù)據(jù)。排查DELETE操作比較好用的SQL是直接分析binlog事件但對(duì)多數(shù)人來說可能太底層所以我一般先把binlog導(dǎo)出成文件再用grep去匹配目標(biāo)表名和關(guān)鍵操作這樣更直觀。第三定期梳理賬號(hào)權(quán)限看有沒有“權(quán)限過大”的賬號(hào)一直沒人管。不通用的SQL是查詢每個(gè)賬號(hào)擁有的全局權(quán)限SELECT user, host, Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, Alter_priv, Drop_priv, Grant_priv FROM mysql.user WHERE account_locked N ORDER BY user;把有Grant_priv授權(quán)權(quán)限的賬號(hào)重點(diǎn)圈出來這些賬號(hào)一旦被濫用可以給別人授權(quán)是內(nèi)部越權(quán)里風(fēng)險(xiǎn)最高的一類。5.2 日志的保留、輪轉(zhuǎn)和歸檔策略審計(jì)日志如果一直寫不清理再大的磁盤也會(huì)被撐爆。所以日志管理一定要提前定好策略我一般建議按“3-2-1”原則設(shè)計(jì)日志類型保留時(shí)間歸檔方式備注binlog7~30天轉(zhuǎn)儲(chǔ)到對(duì)象存儲(chǔ)或備份一體機(jī)按合規(guī)要求確定至少覆蓋審計(jì)周期general_log1~3天壓縮后歸檔一般不長(zhǎng)期開臨時(shí)排查用access_log審計(jì)表180天定期導(dǎo)出CSV歸檔清理舊數(shù)據(jù)配合等保和內(nèi)部合規(guī)要求存儲(chǔ)過程審計(jì)表180天同上敏感業(yè)務(wù)重點(diǎn)留存binlog的自動(dòng)清理可以在MySQL里配置前面提過的binlog_expire_logs_seconds就是干這個(gè)的。MySQL 8.0里也可以在執(zhí)行PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;來手動(dòng)清理不過生產(chǎn)環(huán)境我更推薦用配置參數(shù)自動(dòng)過期避免人為操作失誤。access_log這張表我是定期手動(dòng)或者用計(jì)劃任務(wù)清理的比如每個(gè)季度導(dǎo)出一次CSV后執(zhí)行-- 刪除90天前的審計(jì)記錄注意先導(dǎo)出備份再刪 DELETE FROM audit_db.access_log WHERE connect_time NOW() - INTERVAL 90 DAY;提示DELETE大表會(huì)帶來主從延遲和表碎片建議用TRUNCATE按月分表的思路或者直接按月份建分區(qū)表。簡(jiǎn)單場(chǎng)景下也可以先RENAME舊表再建一張結(jié)構(gòu)相同的新表這樣既保留了舊數(shù)據(jù)又不會(huì)影響寫入。5.3 權(quán)限重審和離職賬號(hào)清理日志審計(jì)的閉環(huán)里還有一個(gè)很容易忽視的環(huán)節(jié)權(quán)限重審。我見過不少公司的MySQL賬號(hào)列表里躺著幾十個(gè)“歷史遺留賬號(hào)”有些賬號(hào)的主人早就離職了但賬號(hào)還在權(quán)限還在這種賬號(hào)就是最大的安全隱患。權(quán)限重審我建議每季度做一次重點(diǎn)看三塊離職和轉(zhuǎn)崗賬號(hào)是否已禁用、長(zhǎng)期不用的賬號(hào)是否已鎖定、高權(quán)限賬號(hào)是否還是“必要的人”在用。-- 查看所有賬號(hào)信息包括鎖定狀態(tài)和密碼過期策略 SELECT user, host, account_locked, password_expired, password_last_changed FROM mysql.user ORDER BY user;然后拿著這份清單和HR、部門負(fù)責(zé)人核對(duì)。發(fā)現(xiàn)離職的人直接鎖定賬號(hào)ALTER USER zhangsan_bi10.0.% ACCOUNT LOCK;發(fā)現(xiàn)某個(gè)崗位不需要這么高權(quán)限的逐步降權(quán)REVOKE SELECT ON sensitive_db.* FROM zhangsan_bi10.0.%;這一步看著簡(jiǎn)單但執(zhí)行起來最大的阻力其實(shí)是“業(yè)務(wù)部門說還要用”。我的經(jīng)驗(yàn)是別硬扛做成流程每次權(quán)限變更都要走審批每季度和業(yè)務(wù)團(tuán)隊(duì)一起過一遍清單把“你還需要這些權(quán)限嗎”變成例行話題。一旦習(xí)慣養(yǎng)成了后面就會(huì)順暢很多。6. 踩坑實(shí)錄關(guān)于審計(jì)這件事我吃過哪些虧6.1 general log開著忘關(guān)磁盤被日志打爆這是我早年踩過最狠的坑。當(dāng)時(shí)為了排查一個(gè)慢查詢問題隨手執(zhí)行了SET GLOBAL general_log ON然后就去忙別的了。結(jié)果第二天早上數(shù)據(jù)庫突然連不上了一看磁盤100%被general_query.log占滿數(shù)據(jù)目錄所在分區(qū)直接寫滿數(shù)據(jù)庫整個(gè)不可用。后來我給自己定了幾條規(guī)矩排查問題需要開general_log時(shí)一定要先確認(rèn)日志寫入路徑的剩余磁盤空間。開完general_log后設(shè)個(gè)提醒半小時(shí)內(nèi)必須關(guān)閉并檢查日志大小。線上敏感環(huán)境不要全量開general_log優(yōu)先用init-connect加審計(jì)表替代。注意general_log不能存放在數(shù)據(jù)目錄所在磁盤的根分區(qū)最好單獨(dú)掛載一塊盤或者指向磁盤空間較大的路徑否則磁盤寫滿后MySQL會(huì)直接拒絕寫入影響正常業(yè)務(wù)。6.2 binlog格式選錯(cuò)審計(jì)線索斷了有一次排查數(shù)據(jù)被篡改的問題我興沖沖地打開binlog想找出誰改了某張核心表的數(shù)據(jù)結(jié)果發(fā)現(xiàn)binlog_format是STATEMENT日志里只記錄了一條UPDATE語句的原文沒有舊值和新值。數(shù)據(jù)被改成什么樣子、原來是什么值全都沒法還原。從那以后我所有環(huán)境都統(tǒng)一要求binlog_format必須設(shè)置成ROW。ROW模式雖然日志會(huì)大一些恢復(fù)和審計(jì)的時(shí)候真的方便太多它能清清楚楚告訴你哪一行從什么值變成了什么值審計(jì)能力完全不在一個(gè)級(jí)別。配上mysqlbinlog命令排查數(shù)據(jù)變更的體驗(yàn)是這樣的mysqlbinlog --base64-outputDECODE-ROWS -v \ --start-datetime2024-06-01 09:00:00 \ --stop-datetime2024-06-01 10:00:00 \ /data/mysql/logs/mysql-bin.000112 | less輸出里能看到SQL執(zhí)行時(shí)的thread_id如果當(dāng)時(shí)用的是一人一賬號(hào)就能結(jié)合thread_id反查access_log鎖定具體是誰。6.3 只審計(jì)不追責(zé)審計(jì)就白做了還有一類問題是管理層面的。辛辛苦苦把日志審計(jì)做起來了結(jié)果某天真的查到一個(gè)員工在凌晨批量導(dǎo)出了客戶數(shù)據(jù)公司層面因?yàn)椤皼]有明確的處罰制度”最后只是口頭警告連權(quán)限都沒收回。這大概是做數(shù)據(jù)安全人最無奈的時(shí)刻。審計(jì)的威懾力不取決于日志記了多全而取決于“查出來之后會(huì)怎樣”。我在給公司搭審計(jì)體系的時(shí)候一定會(huì)同步推動(dòng)一件事讓管理層明確內(nèi)部數(shù)據(jù)泄露的處置辦法。沒有問責(zé)機(jī)制審計(jì)日志就是一堆占地兒的文本文件時(shí)間長(zhǎng)了大家看都不會(huì)看。6.4 “開了審計(jì)就萬事大吉”的誤區(qū)說句大實(shí)話沒有任何一個(gè)方案能一勞永逸地解決內(nèi)部越權(quán)問題。日志審計(jì)是最后一道防線真正起作用的還是前面那幾步權(quán)限最小化、一人一賬號(hào)、存儲(chǔ)過程收口、定期權(quán)限重審。我把這四件事比喻成一道門禁系統(tǒng)權(quán)限最小化是門鎖一人一賬號(hào)是門禁卡存儲(chǔ)過程收口是安保人員日志審計(jì)是監(jiān)控?cái)z像頭。門鎖再結(jié)實(shí)、門禁卡再嚴(yán)格如果攝像頭是壞的出了事照樣抓不到人但反過來如果門鎖形同虛設(shè)攝像頭拍得再清楚也只能是事后補(bǔ)救。所以做MySQL數(shù)據(jù)安全真別指望一項(xiàng)技術(shù)搞定所有問題。每次有人問我“到底該上什么方案”我都會(huì)反問一句你的賬號(hào)權(quán)限管住了嗎如果連一人一賬號(hào)都沒做到我建議先別急著上復(fù)雜的審計(jì)系統(tǒng)把最基礎(chǔ)的賬號(hào)體系理清楚比什么都管用。7. 聊一些更實(shí)操的補(bǔ)充想法7.1 日志審計(jì)如何與等保合規(guī)對(duì)齊順便說一句國(guó)內(nèi)很多行業(yè)都有數(shù)據(jù)安全合規(guī)要求比如電力物聯(lián)網(wǎng)場(chǎng)景里就有關(guān)于數(shù)據(jù)安全分級(jí)保護(hù)的標(biāo)準(zhǔn)核心思想都是“分等級(jí)保護(hù)、分權(quán)限訪問、全流程審計(jì)”。做MySQL日志審計(jì)時(shí)如果能提前對(duì)齊這幾個(gè)原則后面過合規(guī)評(píng)審會(huì)省很多事。具體落到操作上要做到這幾點(diǎn)敏感數(shù)據(jù)分級(jí)打標(biāo)知道哪些表是高危的高危表單獨(dú)建審計(jì)策略訪問必須留痕日志留存時(shí)間至少覆蓋一個(gè)審計(jì)周期定期生成審計(jì)報(bào)表證明你在持續(xù)做這件事。這套東西不復(fù)雜但堅(jiān)持做下去不容易。7.2 團(tuán)隊(duì)規(guī)模小審計(jì)怎么做才不累人團(tuán)隊(duì)里如果只有三五個(gè)人沒有專職DBA做一套完整審計(jì)方案確實(shí)費(fèi)勁。這里我給自己小團(tuán)隊(duì)的讀者一個(gè)“最小可行方案”第一步全部賬號(hào)改成一人一賬號(hào)關(guān)掉公共賬號(hào)。第二步所有業(yè)務(wù)賬號(hào)按崗位最小授權(quán)敏感表一律走存儲(chǔ)過程。第三步開啟binlog格式用ROW。第四步配置init-connect建access_log審計(jì)表。第五步每季度花兩小時(shí)做一次權(quán)限清單核對(duì)和審計(jì)表抽查。就這五步不用采購任何商業(yè)軟件不增加太多工作量就能覆蓋90%以上內(nèi)部越權(quán)場(chǎng)景的追責(zé)需求。我自己在多個(gè)項(xiàng)目里都是用這套組合拳落地的效果穩(wěn)定維護(hù)成本也可控。最后再分享一個(gè)實(shí)際的小技巧別把審計(jì)日志和業(yè)務(wù)數(shù)據(jù)放在同一個(gè)磁盤分區(qū)哪怕覺得“應(yīng)該夠用”也要分開放。磁盤被日志寫滿導(dǎo)致業(yè)務(wù)停擺的案例我見得太多了這個(gè)簡(jiǎn)單隔離能幫你省掉無數(shù)次半夜救火的痛苦。