个人云服务器该如何做安全防护

个人云服务器该如何做安全防护

个人云服务器如何做安全防护

简述: 一台个人云服务器部署了个网站,该怎么做基础网络防护。
内容包含我当前正在使用中的一些配置,如主机配置,防火墙配置,nginx+lua策略,以及调用腾讯云API接口的示例等,年末了就整合到一起分享出来,欢迎大家给提意见。

背景

我很久前买了一个云主机,还买了个域名,当时可是雄心勃勃打算弄个人网站,做大做强,走向世界!
不过后来几年间几乎处于闲置状态,除了跑个几乎不更新的静态博客系统外,啥也没干。

PS:我的环境是1c2G的最低配置(活动价200元三年),每家云厂商几乎每个购物节都会推这样的活动;

现在眼看即将要到期了,也不打算续费,决定还是再折腾一下吧,免得太浪费;
想到很多人或许都有自己的云主机,所以打算就安全问题和大家分享讨论一下。

安全问题

云服务器有公网IP,开放后那攻击就是经常有的事情。
只要你启动了一个端口,那每天都是各种被扫描、探测、暴力破解等等。

恶意访问来源:

  1. 因为是对公网开放,所以全世界哪来都有可能;
  2. 感觉还是云厂商自己扫得比较多,毕竟你真被攻破了,还影响平台的声誉,提前发现了的话,就更方便给你推销安服产品啦。

我这台云主机虽然没啥数据也不重要,但我觉得还是要管一下,就像一群苍蝇围着牛叫,虽然牛被咬不死,但也得时不时用尾巴甩一下。

个人做安全防护的困难

对于个人的云主机来说,还是存在一些局限,比如:

  1. 缺乏能力,不像专业做安全的那么多手段,很多东西都不懂;
  2. 缺乏资金,不可能去买云厂商的安服,也不会买啥付费设备、软件;
  3. 缺乏精力,不可能一天到晚去盯着,10天半月不管不问是常事,也不想接收到太多误报告警干扰生活;

那么该怎么做防护呢?

我有位做安全的朋友(吴迪)多年前给我说了两个方法,直到现在我一遇到安全事件首先想到的就是这俩:解决或隔离

解决:有能力、有精力时,就去把问题和隐患给彻底修复了;
隔离:不能解决,就把问题和隐患给藏起来,让别人看不到摸不到。

PS: 我有位朋友目前正在隔离中,大过年的,真是惨啊!

本文接下来就这俩主题来说应该怎么办。


解决

只能是就事论事,没发生啥也不好说,所以只能简单说下通用方法了。

安全事件就两种情况: 未攻破前,被攻破后

未攻破前

一般是存在啥风险隐患可能被人利用,那就去修复,常见情况有:漏洞、权限、应用逻辑等情况。

  1. 软件有漏洞就升级
    一是主要是关注一些新闻,看看最近是否爆出了啥漏洞;
    二是时间长了,旧版本肯定会被发现一些漏洞出来,简单说就是 10年前的啥软件放到今天,你都可以认为它是不安全的。

    解决办法:

    1. 针对某个被爆出漏洞的软件及时做升级
    2. 定期更新,每2-3月登上去全更新一遍, yum -y update
    3. 既然是个人使用,那么业务程序就紧跟最新版本就行
  2. 权限不合理
    个人机器不可能去配置复杂的权限规则;
    这个没啥好办法,分个主次就行,一个root账号一个普通账号就行,非必要不上 root 权限;

  3. 程序逻辑有缺陷
    这个是特殊情况,需要具体分析,根据场景讨论吧。

被攻破后

已经被攻击成功了,或疑似被攻破了,该怎么事后处理;

  1. 评估损失,是否有什么重要资料泄漏,怎么避免二次伤害等;
  2. 删了实例重建(简单吧,重要资料本地有备份就能这么干)。
  3. 没备份可以尝试提工单找平台付费帮忙,或者自己去折腾。

隔离

简单说就是:就是通过各种方法将暴露面缩小,并增加恶意人员访问到你核心资源的路径长度,如原来一步就能访问到,现在要10几步。


本文主要说网络防护、主机系统防护、应用防护三部分。

网络防护

简单说就是允许哪些源IP访问哪些端口;
比如个人启动了个无密码的 redis,当然不想让所有人都能直接访问,只限自己或特定源IP才能访问。

方案一:充分利用云厂商的功能,比如安全组

基本规则:安全组上只开放固定的几个端口。
如果纯粹是私人用,那就限制只允许1个或多个源IP进行访问
如果有公开业务,就只对这1个端口添加 0.0.0.0/0 的源IP访问策略;

我是怎么配的安全组入策略的呢?
80端口对外全开(毕竟有个网站要开放)
ssh端口和其它端口就只允许1个源IP进行访问;

但我们经常更换网络环境,比如从公司到家里,难道每次要登录主机还得去云控制台改IP,太麻烦了;
解决办法: 我们可以通过云平台的API接口进行修改啊,一键更新;

文末给出了我目前正在用的这个方法,通过腾讯云的SDK调用API接口,获取本地电脑出网的公网IP后,再更新安全组内的ssh源IP规则,非常简单;
熟悉后还可以配合其它拦截策略调用API进行IP拦截,这样恶意流量就到不了主机侧。

方案二:利用主机系统的防护墙

如果觉得去折腾云API接口麻烦,那就折腾主机防火墙吧,这个稍微简单点,而且是通用技能,其它时候也能用得上。

原理: 先 ssh 登录主机,然后将当前源IP加入防火墙的可信区域内,且只允许可信区域的源IP对其它端口访问。

文末给出脚本方式添加访问IP到防火墙可信区域的示例

方案三:动态应用防火墙,根据认证情况放行

简单说就是需要先调认证接口,通过后才对此IP的后续请求放行。

文末给出了我当前正在使用的策略:使用 nginx 进行HTTP和TCP端口的代理,并采用lua进行简单认证的例子。

方案四:敲门性质的防火墙

一些公司采用的策略,原理也比较简单,个人要是有精力也可以尝试去鼓捣一下,我前段时间学习go的时候,做了一个半成品,后面可能会用起来。

访问流程:
客户端:先发一个特定的 UDP 包给服务器的 UDP 端口,进行敲门;
服务器:不会响应这个敲门的 UDP 请求,而是验证这个包是否符合基本逻辑,是的话就把源IP加入到TCP端口的临时白名单内;
客户端:等待一会后对服务器的 TCP 端口进行请求;
服务器:如果IP在白名单内,才允许TCP建立连接,进行二次认证;

优点:默认只开放UDP端口,而且不响应,外面怎么扫都不知道你开放了啥。

方案五:上 ipv6

目前国内的 ipv6 还没有多少人关注,但很多云服务商已经支持了;
v6的攻击来源要少很多的;
腾讯云和阿里云都是支持的,只是需要手动开启;
我后面打算将管理和测试类的应用端口都设置为仅 v6 网络可访问。
优点:有些云厂商分的是一个v6段,是一个段哇,可以随时换IP了。

主机系统层防护

关闭不需要的服务

netstat -anlp 看看有啥不需要的服务还开放着的;

用户账户防护

不建议整太复杂的策略,简洁有效就行,比如密码最短多少位、过期时间这些企业环境才有意义的配置就完全没必要搞,图增烦恼。

简单策略

  1. 禁用 root 远程登录或远程密码登录(仅允许key)
  2. 禁止普通用户密码登录,只允许key
  3. 修改 ssh 监听端口为高位随机端口

ssh 相关配置
/etc/ssh/sshd_config
Port 52222 # 高位随机端口
PermitRootLogin no # 禁止 root 用户远程登录
PermitRootLogin prohibit-password # 禁止 root 用户使用密码远程登录(允许key)
PasswordAuthentication no # 禁止所有用户密码登录

上面是最基础的几项内容了, PermitRootLogin 这个二选一;

关于密码: 最好是改一个非常复杂的密码,然后忘了它。

关于密钥:密钥key很安全,它并没有在网络中传输,传输的只是key加密后的数据;

需要注意key文件本身的安全,不要被盗走;
本地如果运行恶意程序盗取途径可能有:
读取 ~/.ssh/config 文件,看里面有没有指定 key,扫描本地的 putty 或 xshell 等的配置文件,找key路径。

目前好像还没有听说有啥恶意程序盗取私钥这种例子,或许是这个攻击需要专门针对特定人群,面太窄,于是没人这样做吧;

但还是得小心,防范和缓解办法有:

  1. 产生 key 的时候,给 key 设置密码
  2. 定期更换key
  3. 不同用户使用不同key

要是key丢了,就从云控制台上登录root去重置,真忘了root密码还可以提工单给客服进行重置


应用层防护

其它的服务我就不举了,这里只说一下我的这个 nginx;
而且一般场景中 nginx 都是做为业务入口,在这一层做防护就很有效果;

nginx 常规策略

  1. 黑白名单(ip, url, agent)
  2. 请求参数校验,过滤恶意关键字
  3. 对请求来源域名做限制(我这台没做)
  4. 访问频率限制,比如使用 lua 做异常访问次数锁定限制
  5. 流量控制
  6. 上https(我这快下线了,于是就不弄了,要是大家支持的话,我再续上)

文末给出一个我当前使用的一个例子:用 nginx+lua 对异常频率较大的IP请求进行限制;

关于异常请求应该响应什么

这个就是智者见智的事情了,我的理解是这样的;

200 可以设置跳转回首页,假装只有简单的防护策略
404 假装不知道被攻击
403 明确告诉他我有防护准备,收手吧(攻击者:或许是条大鱼,值得继续突破)
444 让攻击者误认为网络有问题
500 我快不行了,别打我了(攻击者:他快不行了,再加把劲)

应用防护的注意事项

  1. 有接口的情况必须要有鉴权;
  2. 最好不要将 java, php, python等通过框架开发的程序直接放出到公网,加层nginx很有必要;
  3. 如果有代理其它类型的应用程序最好就监听127.0.0.1就可以了。

文末案例

一、使用腾讯云SDK修改安全组策略

这个示例只是对已有安全组策略中的某一条策略进行修改,熟悉接口后配合应用程序可以做成针对某IP进行拦截。

详细规则可以去看官方的API文档
https://cloud.tencent.com/document/api

本例使用的是 python, 实际官方对主流编程语言都提供SDK,反正都是http请求,熟悉接口后就算使用 curl 也是可以的;

简单流程描述

  1. 登录控制台,获取 SecretId 和 SecretKey 信息,默认没有的需要手动启用。

  2. 在控制台找到你的安全组,记住 所在区域 和 安全组的ID
    区域就是 ap-<地址位置拼音>,如 ap-shanghai,
    安全组的ID,就在安全组信息的第一列,如 sg-ccms23e2,
    记住你要修改的ID在哪一行,最后一行的 PolicyIndex 值是 0,往上+1;

  3. 安装 sdk, 我使用的是 python 的 SDK

    pip install –upgrade tencentcloud-sdk-python

  4. 制作脚本,后面网络发生变化后,就运行一下这个脚本

我实际在用的python脚本(根据个人情况替换参数)

自动查询本机的公网出口IP地址,更新到安全组策略中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Email:
# DateTime: 2021-11-20 05:37:43
__author__ = 'chenxu'

import json
import re
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.vpc.v20170312 import vpc_client, models
import requests


def getIntelenetIP():
    '''获取当前电脑的出网公网地址'''
    url = "http://txt.go.sohu.com/ip/soip"
    try:
        r = requests.get(url)
        if r.status_code != 200:
            print('get ip address error')
            exit()
        ip = re.findall(r'\d+.\d+.\d+.\d+',r.text)[0]
    except Exception as err:
        print(err)
        exit()
    return ip

def ReplaceSecurityGroupPolicy(ip):
    SecretId = 'xxxxx'                  # 替换
    SecretKey = 'xxxxx'                 # 替换
    Region = 'ap-shanghai'              # 更换为所在区域
    SecurityGroupId = 'sg-ccms23e2'     # 更换为安全组的ID
    Port = '39021'                      # 要调整安全组规则的那个端口号

    try:
        cred = credential.Credential(SecretId, SecretKey)
        httpProfile = HttpProfile()
        httpProfile.endpoint = "vpc.tencentcloudapi.com"

        clientProfile = ClientProfile()
        clientProfile.httpProfile = httpProfile
        client = vpc_client.VpcClient(cred, Region, clientProfile)

        req = models.ReplaceSecurityGroupPolicyRequest()
        params = {
            "SecurityGroupId": SecurityGroupId,
            "SecurityGroupPolicySet": {
                "Ingress": [
                    {
                        "PolicyIndex": 0,           # 就是安全组内已有的哪一条策略,最后一条是0,往上就+1
                        "Protocol": "tcp",          # 什么协议
                        "Port": Port,               # 变量,需要修改的端口
                        "CidrBlock": ip,            # 变量,需要修改的IP
                        "Action": "ACCEPT",         # 允许
                        "PolicyDescription": "test2"    # 备注
                    }
                ]
            }
        }
        req.from_json_string(json.dumps(params))

        resp = client.ReplaceSecurityGroupPolicy(req)
        print(resp.to_json_string())
        print(f'add {ip} to SecurityGroup Success')

    except TencentCloudSDKException as err:
        print(err)

ip = getIntelenetIP()
ReplaceSecurityGroupPolicy(ip)

主机防火墙更新可信IP

简述:先配置基础的防火墙区域,然后再弄个脚本每次更新IP

区域描述:
设置有2个区域,不同的源IP会进入不同的区域;
将默认会进入的 public 区域的默认策略设置为拒绝,只对外开放1个端口;
将需要信任的源IP加入 trusted 可信任区域内,允许全部访问请求;

这样请求到主机时,防火墙会根据它的IP,选择进入不同的区域,匹配不同的规则;

root用户命令行执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 查看防火墙是否在运行,有可能没有开启
firewall-cmd --state

# 开启防火墙,并设为开机启动
systemctl stop firewalld
systemctl start firewalld
systemctl enable firewalld

# public 区域设置
firewall-cmd --zone=public --add-port=5000/tcp --permanent  # 添加允许所有IP访问的1个ssh端口
firewall-cmd --permanent --zone=public --set-target=DROP    # 设置默认策略为拒绝

# 移除区域内其它策略
firewall-cmd --permanent --zone=public --remove-service=ssh
firewall-cmd --permanent --zone=public --remove-service=dhcpv6-client
firewall-cmd --permanent --zone=public --remove-service=cockpit

# trusted 可信区域设置
firewall-cmd --permanent --zone=trusted --add-source=<当前登录云主机时的源IP>

# 加载配置
firewall-cmd --reload

# 查看当前规则
firewall-cmd --zone=public --list-all
firewall-cmd --zone=trusted --list-all

基础设定就完成了,下面来动态设定部分

流程:
先 ssh 普通用户登录主机后,执行一个这个脚本,脚本会获取当前登录使用的IP地址,并添加到可信区域内去

为了方便普通用户使用 sudo,为这个防火墙命令设置为不需要输入密码

/etc/sudoers 文件末尾添加

1
 <普通用户名称> ALL = NOPASSWD: /usr/bin/firewall-cmd

脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash
# 脚本名称: addIPtoTrusted.sh

# 重新加载防火墙配置,会清理掉上一次添加的临时源IP
sudo firewall-cmd --reload

# 获取本次登录主机所使用的源IP地址
remoteIp=$(w | grep wait| head -n 1 | awk '{print $3}')

# 添加到可信区域内
sudo firewall-cmd --zone=work --add-source=${remoteIp}

nginx+lua 认证访问源

原理和流程:

  1. 悄悄提供一个隐蔽的 http 接口,需要先到这个 URL 进行鉴权;
  2. 通过鉴权后,将IP地址放入缓存和redis
  3. 对其它接口(http+tcp)的访问,每次都去缓存内查询IP是否可信

这里使用的是 nginx 的衍生版本 openresty

我的认证逻辑

1
 curl -i -d "name=<占位内容>&key=$(($(date +%s)*9-3000))" https://xxxx.xxxx.cn/sakdwe/wefewoiefwe

这里我设置了一个非常简单的认证逻辑
值为 当前时间戳*N-3000,服务器那边需要还原出来,如果等于服务器当前的时间戳(误差5s内),就通过;
当然也可以做成其它更复杂适用的认证逻辑;

本实例中只有先通过了这个 http 的认证,才能访问代理的 http资源和tcp资源。

说明

  1. 当前设定只同时支持1个IP,当然可以改为 hash 类型, 保留多个可信任IP
  2. 我代理的TCP资源也包括 SSH,哈哈

nginx 配置文件 nginx.conf

针对 http 的设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    http {
        lua_shared_dict http_acl_zone 12k;      # 设置一个共享内存

        server {
            listen       80;

            # 秘密的认证接口和lua脚本
            location = /sakdwe/wefewoiefwe {
                content_by_lua_file /opt/app/openresty/nginx/conf/set_acl.lua;
            }

            location / {
                # 每次访问都先到这一步进行鉴权
                access_by_lua_file /opt/app/openresty/nginx/conf/http_acl.lua;
                root   html;
                index  index.html index.htm;
            }
        }
    }

针对 TCP 端口的设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    stream {
        lua_shared_dict tcp_acl_zone 12k;

        # 友好的日志格式
        log_format proxy '$remote_addr [$time_local] '
                     '$protocol $status $bytes_sent $bytes_received $session_time "$upstream_addr" '
                     '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';

        server {
            access_log  logs/tcp-ssh.log  proxy;
            listen 52222;       # 需要代理的 TCP 端口
            preread_by_lua_file /opt/app/openresty/nginx/conf/tcp_acl.lua;  # 鉴权脚本
            proxy_connect_timeout 5s;
            proxy_timeout 120s;
            proxy_pass 127.0.0.1:10022;
        }
    }

认证逻辑 set_acl.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
-- 描述:
-- 先校验: 是post请求,post参数内需要有一个 key,按设定的逻辑还原出来验证
-- 通过后将真实IP先写入内部共享缓存,再写入 redis 缓存

local function set_http_acl_zone()
    local ip = ngx.var.remote_addr    -- 获取用户真实IP地址

    -- 先设置nginx内部共享变量
    local http_acl_zone = ngx.shared.http_acl_zone
    http_acl_zone:set("ops_ip",ip)

    -- 再设置 redis key 值
    local redis = require "resty.redis"
    local red = redis:new()
    red:set_timeout(1000)       -- 连接超时时间设定 1 sec

    local ok, err = red:connect("127.0.0.1", 26379)
    if not ok then
        return false
    end

    # 将用户IP写入 redis
    ok, err = red:set("ops_ip", ip)
    if not ok then
        return false
    else
        ngx.log(ngx.ERR, "acl ip write ops_ip, ")
        return true
    end
end

local function verify_ip()
    -- 校验请求参数是否合规
    if ngx.var.request_method ~= "POST" then
        ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    ngx.req.read_body()
    local post_args = ngx.req.get_post_args()
    -- local name = post_args["name"]
    local pw = post_args["key"]

    if not pw then
        return false
    end

    local rtime = os.time() - (pw+3000)/9

    if math.abs(rtime) < 5 then
        return true
    else
        return false
    end
end

if(verify_ip() == true) then
    set_http_acl_zone()
    ngx.exit(ngx.HTTP_NOT_ALLOWED)  -- 405, 校验成功
    return
else
    ngx.exit(ngx.HTTP_NOT_FOUND)    -- 404
end

HTTP接口鉴权逻辑 http_acl.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- 校验当前IP是否与nginx共享内存中的IP地址一致
local function ops_acl()
    local clientip = ngx.var.remote_addr

    local http_acl_zone = ngx.shared.http_acl_zone
    local acl_ip = http_acl_zone:get("ops_ip")

    if (acl_ip ~= nil) then
        -- 共享变量内有数据
        if (clientip==acl_ip) then
            return true
        else
            return false
        end
    else
        return false
    end
end

if(ops_acl() ~= true) then
    ngx.log(ngx.ERR, "request ip not in ops_ip")
    ngx.exit(ngx.HTTP_FORBIDDEN)
    return
else
    return
end

TCP代理端口鉴权逻辑 tcp_acl.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
-- 用于 TCP 代理验证用户IP是否在 共享内存 或 redis 中,如果不在则拒绝代理

local function ops_acl()
    local clientip = ngx.var.remote_addr

    local tcp_acl_zone = ngx.shared.tcp_acl_zone
    local acl_ip = tcp_acl_zone:get("ops_ip")

    -- 共享变量内有数据
    if (acl_ip ~= nil) then
        if (clientip==acl_ip) then
            return true
        end
    end

    -- 共享变量内无数据,或与限制IP不一致(因为http和tcp的变量不同步)
    -- 则从redis中读一次

    ngx.log(ngx.ERR, "acl link redis +1")
    local redis = require "resty.redis"
    local red = redis:new()
    red:set_timeout(1000)       -- 1 sec

    local ok, err = red:connect("127.0.0.1", 26379)
    if not ok then
        return false
    end

    local res, err = red:get("ops_ip")
    if not res then
        return false            -- redis 中无数据
    end

    if res == ngx.null then
        return false            -- redis 中无数据
    end

    if (clientip==res) then
        -- 同时也设置一份到内存变量,避免以后再读redis
        local tcp_acl_zone = ngx.shared.tcp_acl_zone
        tcp_acl_zone:set("ops_ip",clientip)

        return true
    else
        return false
    end

end

if(ops_acl() ~= true) then
    ngx.log(ngx.ERR, "tcp request ip != ops_ip, ")
    ngx.exit(403)
    return
else
    return
end

nginx 过滤异常访问逻辑

弄一个专门的文件,然后 include 加载到主文件中去就行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    # 屏蔽指定的IP
    if ($remote_addr ~* "1.2.3.4|1.2.3.5") {
        return 403;
        break;
    }

    # 屏蔽访问方法
    if ($request_method !~ ^(GET|POST)$ ) {
        return 403;
        break;
    }

    # 屏蔽访问客户端类型
    if ($http_user_agent ~* "python|sqlmap|Go-http-client|ELinks|Certificate|bingbot|libcurl") {
        return 403;
        break;
    }

    # 屏蔽奇怪的请求路径或参数
    if ($request_uri ~* "java|.jps|.php|login|wls-wsat|admin|select|.zip|test") {
        return 403;
        break;
    }

    # 异常的referer来源,我实际没有使用,如果有防盗链需求,可以配置在需要的路径下
    if ($invalid_referer ~* "xxxxx.com") {
        return 403;
        break;
    }

nginx+lua 防护,针对异常请求计数

原理:
将每次异常请求的IP记录下来,这个IP每次异常则计数+1;
如果3分钟内多于3次异常就屏蔽此IP 3分钟(从第1次开始计的时);
如果3分钟内异常多于9次,则延长至10分钟;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# nginx.conf

 http {
     lua_shared_dict acl_ip_tables 1m;   # 开一个共享内存区域用来计数
     server {
         # 请求时逻辑
         access_by_lua_file /opt/app/nginx/conf/acl_req.lua;

         # 响应时逻辑
         header_filter_by_lua_file /opt/app/nginx/conf/acl_res.lua;

         location {}
     }
 }

请求时逻辑 acl_req.lua

1
2
3
4
5
6
7
8
9
 -- 如果这个IP已经存在异常计数,且大于3次就拒绝访问
 local dict = ngx.shared.acl_ip_tables
 local ip = ngx.var.remote_addr
 num = dict:get(ip)

 if ( num ~= nil ) and ( num > 2 )  then
     ngx.log(ngx.ERR, "acl_num >2: ", num, ' DROP 403 ')
     return ngx.exit(403)
 end

响应时逻辑 acl_res.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 -- 本次请求的响应码,如果不是 200/304 或特定响应码,视为异常

 local status_code = ngx.var.status
 local res_status = 0
 if ( status_code == '200' ) or ( status_code == '304' ) then
     res_status = 1
 elseif ( status_code == '999' ) then
     res_status = 1
 end

 if ( res_status == 0 ) then
     local ip = ngx.var.remote_addr
     local dict = ngx.shared.acl_ip_tables
     num = dict:get(ip)

     if num == nil then
         -- 第一次异常访问
         ngx.log(ngx.ERR, "num: ",num, " status_code:", status_code," remote add: ", ngx.var.remote_addr)
         dict:set(ip, 1, 180)
     else
         -- 存在异常时仍继续访问 +1
         num = dict:incr(ip, 1)
         ngx.log(ngx.ERR, "acl_num+1=", num)

         -- 3分钟内异常大于9次,延长至10分钟
         if ( num == 9 ) then
             dict:set(ip, 9, 600)
             ngx.log(ngx.ERR, "num: ",num, " set 10min: ", ip)
         end
     end
 end

本来想一次写全一些,但我自己很多时候都有“太长不看”的习惯,所以今后还是尽量以短篇为主吧。


关于我的博客

  1. 用的是 hugo
  2. 主题使用的是 maupassant, 配置极为简单
  3. 写好 Markdown 的文件后,运行一下 hugo 命令即可生成静态页面,上传至 nginx 目录即可

下一篇计划年后再写写最近接触到的一些有意思的内容。

最后再祝大家新年快乐、恭喜发财、虎年大吉,虎虎生威!!

微信搜索IT运维小秋

Licensed under CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,知会作者并注明出处
不得用于商业用途!
最后更新于 2022-01-30 00:00 UTC