Discourse + Web Application Firewall (WAF) mod_security

Qual é o WAF recomendado para rodar na frente do Discourse?

Recentemente, consegui modificar a configuração do nginx instalada no contêiner Docker do Discourse para incluir o ModSecurity e o CRS (Conjunto de Regras Básicas) do OWASP (Open Web Application Security Project).

Até agora, meus testes foram excelentes, e o ModSecurity parece funcionar muito bem com o Discourse nativamente.

Quais são as experiências de outros usuários com WAFs e o Discourse? Você tem alguma recomendação além do ModSecurity?

Uma observação sobre a importância dos WAFs: eles oferecem proteção ampla contra vulnerabilidades de dia zero (0days), não apenas da aplicação web em si, mas também de toda a sua pilha de dependências.

Foi-me apontado que a equipe do Discourse possui um programa de recompensa por bugs, o que é ótimo!

…mas, é claro, não existe código 100% seguro. Erros acontecem, e não precisa nem ser um erro da sua equipe para que uma vulnerabilidade crítica seja incluída no seu projeto.

Por exemplo, considere a história recente com CVE-2019-11043: um bug no php-fpm que poderia ser explorado como execução remota de código em servidores executando versões vulneráveis do php-fpm e do nginx.

No entanto, o CVE-2019-11043 pode ser inteiramente mitigado bloqueando solicitações que contenham retorno de carro ou quebras de linha:

Este é apenas um exemplo de como uma instalação de uma aplicação web pode ser explorada por uma vulnerabilidade crítica, mesmo que não haja falhas no código dessa aplicação web em si. No caso do Discourse, há muitos softwares externos incorporados durante a instalação via Docker, o que poderia tornar o Discourse vulnerável no futuro.

No caso acima, no entanto, o ModSecurity CRS bloqueia solicitações com quebras de linha e retornos de carro — protegendo efetivamente servidores web que, de outra forma, seriam vulneráveis ao CVE-2019-11043 antes do dia zero.

Esse tipo de vulnerabilidade é descoberto o tempo todo. Há benefícios significativos em usar um WAF como o ModSecurity para aplicações web.

Essa é uma má ideia e não é recomendada. O benefício desse tipo de coisa para um aplicativo JavaScript é extremamente limitado e adiciona complexidade significativa à sua configuração de hospedagem.

Relacionado: Acabei de encontrar este guia do @joelradon sobre como instalar o Discourse com o NAXSI WAF no nginx antes do contêiner Docker do Discourse:

Não é mais suportável do que o que você está pedindo acima.

Se ajudar, posso adicionar uma etiqueta de não suportado ali também?

Acho que o desejo por algum dispositivo mágico que mitigue automaticamente os problemas é um tanto equivocado na configuração do Discourse. Temos um programa de recompensas e corrigimos problemas no Discourse em poucas horas após serem relatados. Os sites executam tests-passed por padrão, o que, no caso de hoje, inclui commits do dia de hoje.

Claro, se você estiver executando um software que foi explorado há anos e não tem liberdade para atualizar por … motivos … um WAF faz sentido, pois poderia salvá-lo. Mas, no caso do Discourse, acho que isso é, na melhor das hipóteses, equivocado.

Consegui persistir com sucesso minhas alterações de configuração do nginx ModSecurity entre as execuções de launcher rebuild app da seguinte forma:

Primeiro, atualizamos a cópia local de install-nginx que veio do repositório discourse_docker e está clonada em /var/discourse/.

cd /var/discourse/image/base
cp install-nginx install-nginx.`date "+%Y%m%d_%H%M%S"`.orig

# adicionar um bloco para fazer checkout do módulo nginx do modsecurity logo antes de baixar o código-fonte do nginx
grep 'ModSecurity' install-nginx || sed -i 's%\(curl.*nginx\.org/download.*\)%# mod_security\napt-get install -y libmodsecurity-dev modsecurity-crs\ncd /tmp\ngit clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git\n\n\1%' install-nginx

# atualizar a linha de configuração para incluir o módulo ModSecurity feito checkout acima
sed -i '/ModSecurity/! s%^[^#]*./configure \(.*nginx.*\)%#./configure \1\n./configure \1 --add-module=/tmp/ModSecurity-nginx%' install-nginx

# adicionar uma linha na seção de limpeza
grep 'rm -fr /tmp/ModSecurity-nginx' install-nginx || sed -i 's%\(rm -fr.*/tmp/nginx.*\)%rm -fr /tmp/ModSecurity-nginx\n\1%' install-nginx

Note que o arquivo Dockerfile responsável por executar o script install-nginx é executado quando a imagem é construída. E a imagem é construída apenas pela equipe do Discourse antes de ser enviada para o Docker Hub. Quando o comando Discourse ./launcher rebuild app é executado, ele aciona (se houver atualizações disponíveis) um docker pull, que busca a última imagem Docker do Discourse no Docker Hub. Novamente, isso não reconstrói a imagem, executa o Dockerfile ou executa o script install-nginx modificado acima.

A única maneira (que eu conheço) de acionar a execução do script bash install-nginx atualizado (que é executado pelo Dockerfile) é fazer com que o Docker construa uma nova imagem. Por exemplo, isso aciona o Docker a construir uma nova imagem chamada discourse_modsecurity — que será construída usando o script install-nginx modificado localmente:

docker build --tag 'discourse_modsecurity' /var/discourse/image/base/

Infelizmente, não consegui encontrar uma maneira de dizer ao launcher para usar uma imagem personalizada (especificar um run-image usa a imagem especificada diretamente, sem executar os modelos contra ela — conforme necessário para realmente configurar [em vez de apenas instalar] o nginx). Então, substituímos a variável image definida no script launcher para usar nossa nova imagem Docker local chamada discourse_modsecurity:

# substituir a linha "image="discourse/base:<version>" por 'image="discourse_modsecurity"'
grep 'discourse_modsecurity' launcher || sed --in-place=.`date "+%Y%m%d_%H%M%S"` '/base_image/! s%^\(\s*\)image=\(.*\)$%#\1image=\2\n\1image="discourse_modsecurity"%' /var/discourse/launcher

Agora, adicionamos um novo arquivo de modelo para configurar nossas configurações do nginx para incluir os arquivos/blocos necessários do modsecurity:

cat << EOF > /var/discourse/templates/web.modsecurity.template.yml
run:
  - exec:
     cmd:
       - sudo apt-get install -y modsecurity-crs
       - cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
       - sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/modsecurity/modsecurity.conf
       - sed -i 's^\(\s*\)[^#]*SecRequestBodyInMemoryLimit\(.*\)^\1#SecRequestBodyInMemoryLimit\2^' /etc/modsecurity/modsecurity.conf
       - sed -i '/nginx/! s%^\(\s*\)[^#]*SecAuditLog \(.*\)%#\1SecAuditLog \2\n\1SecAuditLog /var/log/nginx/modsec_audit.log%' /etc/modsecurity/modsecurity.conf

  - file:
     path: /etc/nginx/conf.d/modsecurity.include
     contents: |
        ################################################################################
        # File:    modsecurity.include
        # Version: 0.1
        # Purpose: Defines mod_security rules for the discourse vhost
        #          This should be included in the server{} blocks nginx vhosts.
        # Author:  Michael Altfield <michael@opensourceecology.org>
        # Created: 2019-11-12
        # Updated: 2019-11-12
        ################################################################################
        Include "/etc/modsecurity/modsecurity.conf"
        
        # OWASP Core Rule Set, instalado a partir do pacote 'modsecurity-crs' no debian
        Include /etc/modsecurity/crs/crs-setup.conf
        Include /usr/share/modsecurity-crs/rules/*.conf

  - replace:
     filename: "/etc/nginx/conf.d/discourse.conf"
     from: /server.+{/
     to: |
       server {
         modsecurity on;
         modsecurity_rules_file /etc/nginx/conf.d/modsecurity.include;

EOF

E adicionamos este modelo (templates/web.modsecurity.template.yml) ao bloco de modelos do arquivo de configuração YAML do nosso app, para que fique algo assim:

[root@osestaging1 discourse]# vim /var/discourse/containers/app.yml
...
[root@osestaging1 discourse]# grep -A 6 'templates:' /var/discourse/containers/app.yml
templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "templates/web.ratelimited.template.yml"
  - "templates/web.socketed.template.yml"
  - "templates/web.modsecurity.template.yml"
[root@osestaging1 discourse]# 

Agora, ao reconstruir o app Docker do Discourse, ele usará sua nova imagem Docker discourse_modsecurity com nginx e modsecurity, e configurará o nginx para usar o OWASP CRS.

/var/discourse/launcher rebuild app

Eu concordo com você de que o ModSecurity ou WAFs semelhantes não são uma solução mágica.

Mas eu sei (pelo menos?) de uma vulnerabilidade que foi encontrada no RoR e afetou o Discourse que teria sido mitigada por um mecanismo semelhante ao ModSecurity.

Quando estávamos corrigindo isso, vimos em nossos logs que ela já havia sido explorada na prática em pelo menos um de nossos fóruns antes que a CVE fosse divulgada publicamente e corrigida. Isso não resultou em nenhuma divulgação de informações, mas isso aconteceu apenas porque fizemos algumas coisas de maneira diferente de uma instalação padrão (ou seja, sorte).

Não tenho certeza se a complexidade adicional compensa a segurança adicional.

Eu comparo os WAFs a scanners de vírus baseados em assinatura, que, na minha opinião, não são muito úteis em ambientes com uma superfície de ataque limitada (por exemplo, seus servidores não-Windows).

Eles não vão detectar tudo, mas vão (em teoria) capturar padrões comuns de ataques (por exemplo, injeção de SQL) e explorações conhecidas (por exemplo, a vulnerabilidade do RoR mencionada acima) em uma variedade de softwares.

Vejo valor em executá-los, especialmente em um ambiente corporativo onde você tem uma infinidade de aplicativos rodando coisas desconhecidas e precisa ter certeza de que está protegendo cada aplicativo contra essas explorações sem precisar se preocupar com cada um individualmente — isso transforma um problema de tamanho N x M em um de N + M.

Se vale a pena executá-los no seu site é outra questão e uma daquelas análises de risco (de quebra) e benefício (de proteção) que você precisará fazer por conta própria.