• MySQL企业版之Firewall(SQL防火墙)

    小编:啊南 38阅读 2020.11.20

    1. 关于Firewall插件2. Firewall插件的工作方式3. Firewall插件测试4. 总结延伸阅读

    1. 关于Firewall插件

    Friewall是MySQL企业版非常不错的功能插件之一,启用Firewall功能后,SQL的执行流程见下图示意:

    2. Firewall插件的工作方式

    Firewall插件的工作机制大概是这样的:

    0.将某个账号Register(注册)到Firewall插件中,未注册的账号将不会被Firewall插件保护。

    1.先将Firewall插件设置 recording(记录)模式,将各种SQL格式化/模式化之后,形成各种不同的SQL fingerprint(指纹)。例如下面的两条SQL,都会被格式化成一条:

    # 原始SQL
    a) SELECT * FROM t1 WHERE c1 = 1;
    b) SELECT * FROM t1 WEHRE c1 = 1024;
    
    # 格式化之后的SQL
    SELECT * FROM t1 WHERE c1 = ?;

    备注:在这个过程中,如果总有超长SQL的话,需要加大参数max_digest_length的设置,其默认值是1024。

    2.Firewall插件会学习上述SQL,形成一个白名单。

    3.经过一段时间的训练后,可以将Firewall插件工作模式切换为 protecting(保护)模式,开始工作。这时候就能自动判断有哪些SQL可能是恶意的,会被自动拒绝,并且记录到日志中,如果启用参数mysql_firewall_trace的话。

    4.如果还发生个别SQL被拒绝的情况,则可以将插件切换回 recording(记录)模式,继续学习训练一段时间再切换到工作状态。

    5.此外,还有一种工作模式是detecting(探测),在这个模式下,符合白名单的会被放过执行,而其他SQL则会被记录到日志中,但并不会被拒绝执行,这就相当于正式开始工作前的灰度测试模式了。

    简言之,就是在业务账号对外正式开放前,先自行模拟各种正常业务请求,使之完成前期必要的学习,正式上线后再开启保护模式。因为外网生产环境中坏人太多,任意时候都有可能有坏蛋提交各种恶意破坏的请求。

    3. Firewall插件测试

    接下来我们做个简单的测试场景。 首先,先尝试将一个新账号直接设置为 protecting 模式,这时候该账号还未学习任何规则,因此所有的SQL应该都会被拒绝才对。

    [root@yejr.run]>CALL mysql.sp_set_firewall_mode('yejr@%', 'PROTECTING');
    +-------------------------------------------------------------------------+
    | result                                                                  |
    +-------------------------------------------------------------------------+
    | ERROR: PROTECTING mode requested for yejr@% but the whitelist is empty. |
    +-------------------------------------------------------------------------+

    嗯,看来这个插件还挺聪明的,发现规则是空的,直接不让设置了,要不然可能会害的DBA直接下岗走人了吧,哈哈。

    还是先设置为 recording 模式吧。

    [root@yejr.run]>CALL mysql.sp_set_firewall_mode('yejr@%', 'recording');
    +-----------------------------------------------+
    | read_firewall_whitelist(arg_userhost,FW.rule) |
    +-----------------------------------------------+
    | Imported users: 0
    Imported rules: 0
              |
    +-----------------------------------------------+
    1 row in set (0.01 sec)
    
    # 手动查询 mysql.firewall_users 表确认
    [root@yejr.run]>select * from mysql.firewall_users;
    +----------+-----------+
    | USERHOST | MODE      |
    +----------+-----------+
    | yejr@%   | RECORDING |
    +----------+-----------+
    1 row in set (0.00 sec)

    设置成功。

    然后用这个新账号连接MySQL,执行一些合规的SQL操作后,再查看白名单规则表:

    [root@yejr.run]>select * from mysql.firewall_whitelist;
    +----------+--------------------------------------------------------+----+
    | USERHOST | RULE                                                   | ID |
    +----------+--------------------------------------------------------+----+
    | yejr@%   | SHOW CREATE TABLE `t1`                                 |  8 |
    | yejr@%   | SELECT * FROM `t1`                                     |  9 |
    | yejr@%   | SELECT * FROM `t1` WHERE `id` >= ?                     | 10 |
    | yejr@%   | SELECT * FROM `t1` WHERE `id` = `rand` ( ) * ?         | 11 |
    | yejr@%   | SELECT * FROM `t1` WHERE `id` = `rand` ( ) * ? LIMIT ? | 12 |
    | yejr@%   | SELECT * FROM `t1` WHERE `c1` >= ?                     | 13 |
    +----------+--------------------------------------------------------+----+

    再将该账号修改为 PROTECTING 模式:

    [root@yejr.run]>CALL mysql.sp_set_firewall_mode('yejr@%', 'PROTECTING');
    Query OK, 6 rows affected (0.01 sec)

    执行一些不合规的SQL后,看怎么报错的。

    [yejr@yejr.run] [test]>select * from t1 order by rand() limit 1;
    ERROR 1045 (28000): Statement was blocked by Firewall

    error log中也记录了相应的行为:

    2020-06-09T09:50:38.286878Z 26 [Note] [MY-011192] [Server] Plugin MYSQL_FIREWALL reported: 'ACCESS DENIED for 'yejr@%'. Reason: No match in whitelist. Statement: SELECT * FROM `t1` ORDER BY `rand` ( ) LIMIT ?'

    再查看几个相关的全局状态参数:

    [root@yejr.run] [mysql]>show global status like '%firewall%';
    +----------------------------+-------+
    | Variable_name              | Value |
    +----------------------------+-------+
    | Firewall_access_denied     | 10    | #拒绝次数
    | Firewall_access_granted    | 11    | #通过次数
    | Firewall_access_suspicious | 9     | #在DETECTING模式下不匹配白名单的次数
    | Firewall_cached_entries    | 6     | #在cache中的白名单数量
    +----------------------------+-------+

    想要关闭该账号的Firewall规则,执行下面的指令即可:

    [root@yejr.run] [mysql]>call mysql.sp_set_firewall_mode('yejr@%', 'RESET');

    或者只是简单地设置为 OFF 也可以:

    [root@yejr.run] [mysql]>call mysql.sp_set_firewall_mode('yejr@%', 'OFF');

    二者的区别在于,设置为 RESET 时,除了关闭Firewall保护,同时也会将该账号之前训练学习的白名单全部清空,这样下次再想采用Firewall保护就需要重头开始了,除非再也不用了,否则不建议这么做。

    当已经对一个账号启用Firewall保护后,此时有新增业务SQL不在白名单中,除了将模式改回 RECORDINGDETECTING 之外,其实还可以手动往mysql.firewall_whitelist表中插入格式化之后的SQL,再刷新使之生效即可,例如:

    # 先确认一条SQL的格式化写法
    [root@yejr.run]>SELECT normalize_statement('SELect *,sleep(1) from t1 where 1');
    +----------------------------------------------------------+
    | normalize_statement('SELect *,sleep(1) from t1 where 1') |
    +----------------------------------------------------------+
    | SELECT * , `sleep` (?) FROM `t1` WHERE ?                 |
    +----------------------------------------------------------+
    
    # 手动插入新规则
    [root@yejr.run]>insert into mysql.firewall_whitelist select 'yejr@%', 'SELECT * , `sleep` (?) FROM `t1` WHERE ?', 1;
    Query OK, 1 row affected (0.00 sec)
    Records: 1  Duplicates: 0  Warnings: 0
    
    # 查询内存中的规则,此时为空
    [root@yejr.run] [mysql]>SELECT * FROM INFORMATION_SCHEMA.MYSQL_FIREWALL_WHITELIST;
    Empty set (0.00 sec)
    
    # reload加载规则,使之生效
    [root@yejr.run] [mysql]>CALL mysql.sp_reload_firewall_rules('yejr@%');
    +--------+
    | Result |
    +--------+
    | OK     |
    +--------+
    1 row in set (0.00 sec)
    
    +--------------------------------------+
    | Result                               |
    +--------------------------------------+
    | Imported users: 0
    Imported rules: 1
     |
    +--------------------------------------+
    1 row in set (0.00 sec)
    
    Query OK, 0 rows affected (0.00 sec)
    
    # 再次查询,此时就可以看到规则加载到内存中了,已生效
    [root@yejr.run]>SELECT * FROM INFORMATION_SCHEMA.MYSQL_FIREWALL_WHITELIST;
    +----------+------------------------------------------+
    | USERHOST | RULE                                     |
    +----------+------------------------------------------+
    | yejr@%   | SELECT * , `sleep` (?) FROM `t1` WHERE ? |
    +----------+------------------------------------------+
    1 row in set (0.00 sec)
    
    # 最后记得启用保护模式
    [root@yejr.run]>call mysql.sp_set_firewall_mode('yejr@%', 'PROTECTING');
    Query OK, 3 rows affected (0.00 sec)
    4. 总结

    简单测试下来,我认为Firewall插件至少有几个地方可以更完善:

    1.对SQL进行格式化时,不支持正则匹配模式,建议增加。

    2.设置RECORDING模式测试期间,我执行一个SQL后,又手动执行CTRL+C终止,结果记录了一条KILL QUERY ?的规则,这个规则就不建议记录了。

    3.存储过程mysql.sp_reload_firewall_rules()可以增加一个参数表示重载规则后,是否要顺便设置账号的规则,例如CALL mysql.sp_reload_firewall_rules('yejr@%', 'PROTECTING')表示重载完规则后,顺便将该账号设置为 PROTECTING 模式。

    总的来说,Firewall插件工作方式还是比较简单的,直观感觉就是对规则的学习比较初级,真正在线上生产环境启用的话,可能需要记录大量的规则,这也势必会造成性能的下降,以后再做个性能测试吧。

    关联标签: