Django - 生产环境中的安全

生产环境的安全设计

生产环境安全要考虑的因素

  • 防火墙:把攻击挡在外面,建立安全区
  • 应用安全:密码攻击 & 访问限流 – 防恶意攻击
  • 架构安全:部署架构的安全性,应用架构安全设计
  • 数据安全:SSL,敏感数据加密与日志脱敏
  • 密码安全与业务安全:权限控制 & 密码安全策略

防火墙

  • 防火墙的作用

    • 建立安全区,把攻击挡在外面
  • 防火墙的类别

    • 硬件防火墙
    • WAF 防火墙
    • 操作系统防火墙
  • WAF 防火墙

    • WAF:Web application firewall,基于预先定义的规则, 如预先定义的正则表 达式的黑名单,不安全 URL 请求等
    • 防止 SQL 注入,XSS, SSRF 等 web 攻击
    • 防止 CC 攻击屏蔽常见的扫描黑客工具,扫描器
    • 屏蔽异常的网络请求屏蔽图片附件类目录 php 执行权限
    • 防止 web shell 上传

    WAF防火墙

  • 系统防火墙:常用的 Linux 系统防火墙

    • iptables:Linux 原始自带的防火墙工具 iptables

    • ufw:Ubuntu 的防火墙工具 ufw 即:uncomplicated firewall 的简称,简单防火墙。

      • 简介
        • Linux 原始的防火墙工具 iptables 过于繁琐
        • Ubuntu 提供了基于 iptables 之上的防火墙 ufw
        • ufw 支持图形界面操作
      • 规则
        • 开启 ufw 后,默认是允许所有连接通讯
        • 且配置的策略也有先后顺序,每一条策略都有序号
        • 服务器上配置,建议先 deny from any (阻止所有连接),再放开需要开放的访问
      • 图示
        • ufw
    • firewalld:CentOS 的防火墙工具 firewalld

应用安全

  • 防恶意密码攻击

    • 防恶意密码攻击策略

      • 在用户连续登陆 n 次失败后, 要求输入验证码登陆
    • 可选方案:使用 simple captcha 插件

      1. 安装 & 配置

        (1)安装

        1
        pip install django-simple-captcha

        (2)在您的 settings.py 中添加 captcha 到 INSTALLED_APPS

        1
        2
        3
        4
        INSTALLED_APPS = [
        # ...
        'captcha',
        ]

        (3)运行:python manage.py migrate
        (4)urls.py 中添加路径映射

        1
        2
        3
        urlpatterns += [
        path('captcha/', include('captcha.urls')),
        ]

        (5)注意:需要提前安装 Pillow 依赖包

      2. 添加登陆验证 Form 和 Views 视图
        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
        from django import forms
        from captcha.fields import CaptchaField
        from django.contrib.auth.forms import AuthenticationForm


        class CaptchaTestForm(forms.Form):
        captcha = CaptchaField(label='验证码')


        max_failed_login_count = 3


        def login_with_captcha(request):
        if request.POST:
        failed_login_count = request.session.get('failed_login_count', 0)

        # 没有连续的登陆失败, 使用默认的登陆页; 连续 n 次登陆失败, 要求输入验证码
        if failed_login_count >= max_failed_login_count:
        form = CaptchaLoginForm(data=request.POST)
        else:
        form = AuthenticationForm(data=request.POST)

        # Validate the form: the captcha field will automatically
        # check the input
        if form.is_valid():
        request.session['failed_login_count'] = 0
        # authenticate user with credentials
        user = authenticate(username=form.cleaned_data["username"], password=form.cleaned_data["password"])
        if user is not None:
        # attach the authenticated user to the current session
        login(request, user)
        return HttpResponseRedirect(reverse_lazy('admin:index'))
        else:
        failed_login_count += 1
        request.session['failed_login_count'] = failed_login_count
        logger.warning(
        " ----- failed login for user: %s, failed times:%s" % (form.data["username"], failed_login_count))
        if failed_login_count >= max_failed_login_count:
        form = CaptchaLoginForm(request.POST)
        messages.add_message(request, messages.INFO, 'Not a valid request')
        else:
        ## 没有连续的登陆失败, 使用默认的登陆页; 连续 n 次登陆失败, 要求输入验证码
        failed_login_count = request.session.get('failed_login_count', 0)
        if failed_login_count >= max_failed_login_count:
        form = CaptchaLoginForm(request.POST)
        else:
        form = AuthenticationForm()

        return render(request, 'templates/login.html', {'form': form})
      3. 添加登陆模板页
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        {% extends 'base.html' %}

        {% block content %}
        <form method="post" style="margin-left: 10px;">
        {% csrf_token %}
        {{ form.as_p }}

        <div class="form-actions">
        <input type="submit" class="btn btn-primary" style="width:120px;" value="登录"/>
        </div>
        </form>
        {% endblock %}
      4. 添加登陆失败的频次控制
        1
        2
        3
        4
        if failed_login_count >= max_failed_login_count:
        form = CaptchaLoginForm(data=request.POST)
        else:
        form = AuthenticationForm(data=request.POST)
      5. 设置管理员的登陆页, 默认使用带连续失败需要验证码的页面
  • 应用访问限流 – 防恶意攻击

    • Rest Framework API 限流

      • 可以对匿名用户,具名用户进行限流
      • 可以设置峰值流量(如每分钟 60 次请求)
      • 也可以设置连续一段时间的流量限制(比如每天 3000 次)
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        REST_FRAMEWORK = {
        'DEFAULT_THROTTLE_CLASSEs': [
        'example.throttles.BurstRateThrottle',
        'example.throttles.sustainedRateThrottle'
        ],
        'DEFAULT_THROTTLE_RATES': {
        'burst': '60/min',
        'sustained': '3000/day'
        }
        }
    • 应用限流:对页面的访问频次进行限流

      • 可以选方案: 使用 django-ratelimit 插件
        1. 安装
          1
          pip install django-ratelimit
        2. 用装饰器修饰:views.py
          1
          2
          3
          4
          5
          6
          7
          8
          9
          from ratelimit.decorators import ratelimit

          @ratelimit(key='ip')
          def myview(request):
          # ...

          @ratelimit(key='ip', rate='100/h')
          def secondview(request):
          # ...
      • 示例策略:一分钟最多请求 5 次登陆页,防止暴力攻击登陆页
        1
        2
        3
        4
        5
        from ratelimit.decorators import ratelimit
        @ratelimit(key='ip', rate='5/m" )
        def login_with_captcha(request):
        # ...

架构安全

  • 防火墙

  • XSS

  • CSRF

  • SQL

  • 应用的部署架构

    • 典型中小型互联网应用部署架构
    • 服务器内部组成私有网络
    • 图示
  • 密钥存储原则

    • 基础的用法: 使用环境变量 / 独立的配置文件, 不放在代码库中
    • 使用 Key Server:使用开源的 Key Server, 或阿里云 / AWS 的 KMS 服务
    • 从独立的配置文件中读取配置密钥
    • 容器环境,启动容器时作为环境变量传入 – 密钥不落地到容器存储中
      1
      2
      3
      # Read secret key from a file
      with open( '/etc/secret_key.txt' ) as f:
      SECRET__KEY = f.read( ).strip()

数据安全

  • SSL 证书的使用

    Let’s Encrypt SSL 证书的使用

    • Let's Encrypt 是一家非盈利机构, 免费提供 SSL 证书。
    • Let's Encrypt 的目标是为了构建一个安全的互联网。
    • Let's Encrypt 的证书被各大主流的浏览器和网络服务商支持。
    • 提供的证书 90 天过期,需要自动重新申请。 有相应的工具可以使用。

    Let’s Encrypt:Certbot 的两种使用方式

    • Webroot 方式

      certbot 会利用既有的 web server,在其 web root 目录下创建隐藏文件,Let's Encrypt 服务端会通过域名来访问这些隐藏文件,以确认你的确拥有对应域名的控制权。

      • 编辑 nginx.conf 配置文件,确保可以访问 /.well-known/ 路径及里边存放的验证文件

        1
        2
        3
        4
        location /.well-known/acme-challenge/ {
        default_type "text/plain";
        root /data/www/example;
        }
      • 重新加载 nginx

        1
        nginx -s reload
      • 调用命令生成证书

        1
        2
        /usr/bin/certbot certonly --email admin@example.com --webroot -w
        /data/www/example -d example.com -d www.example.com
        • --email 为申请者邮箱
        • --webroot 为 webroot 方式,-w 为站点目录,-d 为要加 https 证书的域名
        • 命令会在 web root 目录中创建 /.well-known 文件夹,其中包含了域名所有权的验证文件
        • Certbot 会访问域名下面 /.well-known/acme-challenge/ 来验证域名是否绑定,并生成证书
      • 在 nginx/tengine 中开启 https

        • 证书生成完成后可以到 /etc/letsencrypt/live/ 目录下查看对应域名的证书文件。 编辑 nginx 配置文件监听 443 端口,启用 SSL,并配置 SSL 的公钥、私钥证书路径。

          文件 描述
          cert.pem 服务器证书
          chain.pem 包含 Web 浏览器为验证服务器而需要的证书或附加中间证书
          fullchain.pem cert.pem + chain.pem
          privkey.pem 证书的私钥
      • 在 Nginx 使用证书

        • Nginx 配置文件中配置 SSL 证书, 以及监听端口, 重新加载 nginx

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          server {
          listen 443 ssl;
          listen [::]:443 ssl;

          server_name example.com www.example.com;
          index index.html index.htm index.php;
          root /home/wwwroot/example.com;

          ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
          }
          1
          2
          # 重新加载nginx/tengine
          nginx -s reload
    • Standalone 方式

      Certbot 会自己运行一个 web server 来进行验证。如果我们自己的服务器上已经有 web server 正在运行 (比如 Nginx 或 Apache ), 用 standalone 方式的话需要先关掉它,以免冲突。

      • 示例:Debian 上面安装 certbot 工具

        1
        2
        3
        4
        5
        6
        7
        # 以debian为例子,先安装snapd应用包管理工具(或者直接使用apt-get安装certobt)
        sudo apt update
        sudo apt-get install snapd
        sudo snap install core; sudo snap refresh core
        # 然后使用snap安装certbot
        sudo snap install --classic certbot
        sudo ln -s /snap/bin/certbot /usr/bin/certbot
    • Let’s Encrypt SSL 证书续期
      • 证书 3 个月过期一次
      • 使用 snapd 安装的 certbot,会启动一个 cron job 或者 systemd 的定时任务,在证书过期前自动续期
      • 运行这个命令测试自动续期
        1
        2
        3
        4
        5
        ## 测试自动续期命令
        certbot renew --dry-run

        ## ―检查定时任务
        systemctl list-timers
  • 敏感数据加密

    • 对敏感数据,比如用户提交的内容,财务报告,第三方合同等数据进行加密
    • 使用 Python 的 cryptography 库
      1
      pip install cryptography
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      # Fernet module is imported from the
      #cryptography package
      from cryptography.fernet import Fernet

      # key is generated
      key = Fernet.generate_key()

      # value of key is assigned to a variable
      f = Fernet(key)

      # the plaintext is converted to ciphertext
      token = f.encrypt(b"welcome to django")

      # display the ciphertext
      print(token)

      # decrypting the ciphertext
      d = f.decrypt(token)

      # display the plaintext
      print(d)
  • 日志脱敏

    在日志记录中,过滤掉敏感信息存储,避免敏感信息泄漏。
    可以用 sensitive_variables 装饰器阻止错误日志内容包含这些变量的值
    具体参考 sensitive_post_parameters, sensitive_post_parameters

    1
    2
    3
    4
    5
    6
    7
    8
    from django.views.decorators.debug import sensitive_variables

    asensitive_variables('user', 'pw', 'cc')
    def process_info(user):
    pw = user.pass_word
    cc = user.credit_card_number
    name = user.name
    ...
    • 用户名
    • 密码
    • 手机号
    • 银行卡号
    • 地址

    密码安全与业务安全

    • 权限控制

      • 遵循最小原则, 长时间没用自动回收。
      • 思路: 定时任务检查所有用户,找到长时间没有登陆的用户,回收相应的权限, 或删除账号。
    • 密码策略

      • 密码复杂度策略 AUTH_PASSWORD_VALIDATORS

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        AUTH_PASSWORD_VALIDATORS = [
        {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityvalidator',
        },
        {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
        'min_length': 9,
        }
        },
        {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordvalidator',
        },
        {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
        },
        ]
      • 密码过期策略

        1. 启用中间件
          1
          2
          3
          4
          5
          MIDDLEWARE__CLASSES = [
          ...
          "account.middleware.ExpiredPasswordMiddleware",
          ...
          ]
        2. 配置过期策略
          1
          2
          3
          ACCOUNT_PASSWORD_USE_HISTORY = True
          # number of secs,this is 5 days
          ACCOUNT_PASSWORD_EXPIRY = 60*60*24*5