Discourse + 웹 애플리케이션 방화벽 (WAF) mod_security

Discourse 앞에 실행할 권장 WAF는 무엇인가요?

최근 Discourse 도커 컨테이너에 설치된 nginx 설정을 수정하여 ModSecurity와 OWASP(Open Web Application Security Project) CRS(Core Rule Set)를 포함하는 데 성공했습니다.

지금까지 테스트 결과는 훌륭하며, ModSecurity는 Discourse와 기본적으로 매우 잘 호환되는 것으로 보입니다.

다른 사용자분들의 WAF와 Discourse 사용 경험은 어떠신가요? ModSecurity 외의 추천 사항이 있으신가요?

WAF의 중요성에 대한 참고 사항: WAF는 웹 애플리케이션 자체뿐만 아니라 해당 웹 애플리케이션의 전체 의존성 스택에 대한 0일(0day) 취약점에 대해서도 광범위한 보호를 제공합니다.

Discourse 팀이 버그 바운티 프로그램을 운영하고 있다는 점을 알려주셨는데, 이는 매우 훌륭한 조치입니다!

…하지만 물론 100% 안전한 코드는 존재하지 않습니다. 실수는 언제든 발생할 수 있으며, 프로젝트에 치명적인 취약점이 포함되는 것이 반드시 여러분의 팀의 실수일 필요도 없습니다.

예를 들어, 최근의 CVE-2019-11043 사례를 생각해 보세요. 이는 취약한 버전의 php-fpm과 nginx를 실행 중인 서버에서 원격 코드 실행(RCE)으로 악용될 수 있는 php-fpm의 버그였습니다.

그러나 CVE-2019-11043은 캐리지 리턴(CR) 또는 개행 문자(LF)를 포함하는 요청을 차단함으로써 완전히 완화될 수 있습니다:

이것은 웹 애플리케이션 자체의 코드에 결함이 없더라도, 웹 애플리케이션의 설치 환경이 치명적인 취약점을 통해 악용될 수 있음을 보여주는 예시입니다. Discourse의 경우, 도커 설치 과정에서 외부 소프트웨어가 대량으로 가져오기 때문에, 향후 Discourse가 취약점에 노출될 가능성이 있습니다.

그러나 위 사례의 경우, ModSecurity CRS는 이미 개행 문자와 캐리지 리턴을 포함하는 요청을 차단하고 있어, 0일(0day) 취약점이 공개되기 이전에 잠재적으로 취약한 웹 서버를 CVE-2019-11043으로부터 효과적으로 보호합니다.

이러한 유형의 취약점은 상시 발견되고 있습니다. 웹 애플리케이션에 ModSecurity와 같은 WAF를 사용하는 것에는 매우 큰 이점이 있습니다.

이것은 나쁜 아이디어이며 권장되지 않습니다. 이 방식이 JavaScript 앱에 가져다주는 이점은 극히 제한적이며, 호스팅 환경에 상당한 복잡성을 더합니다.

관련: @joelradon 님이 Discourse를 Nginx에서 Discourse Docker 컨테이너 앞에 NAXSI WAF를 설치하는 방법에 대한 가이드를 발견했습니다:

위에서 요청하신 것보다 더 지원하기 어렵습니다.

도움이 된다면 거기에 미지원 태그도 추가해 드릴까요?

Discourse 환경에서 문제를 자동으로 완화해 주는 어떤 마법 같은 장치를 원한다는 욕구는 다소 잘못된 방향이라고 생각합니다. 우리는 버그ounty(보상) 프로그램을 운영하고 있으며, Discourse의 문제는 보고된 지 몇 시간 내에 패치됩니다. 사이트들은 기본적으로 tests-passed를 실행하며, 이번 사례에서는 이는 오늘 날짜의 커밋을 포함합니다.

물론 몇 년 전에 악용된 소프트웨어를 실행 중이고 … 이유… 때문에 업그레이드할 자유가 없는 경우라면, WAF가 당신을 구할 수 있으므로 의미가 있습니다. 하지만 Discourse의 경우에는 그것이 아무리 좋게 봐주어도 잘못된 방향이라고 생각합니다.

nginx ModSecurity 설정 변경 사항을 launcher rebuild app 실행 사이에 성공적으로 유지하는 방법을 다음과 같이 공유합니다.

먼저, discourse_docker 저장소에서 가져와 /var/discourse/에 클론된 install-nginx의 로컬 복사본을 업데이트합니다.

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

# nginx 소스를 다운로드하기 직전에 modsecurity 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

# 위에서 체크아웃한 ModSecurity 모듈을 포함하도록 configure 라인 업데이트
sed -i '/ModSecurity/! s%^[^#]*./configure \(.*nginx.*\)%#./configure \1\n./configure \1 --add-module=/tmp/ModSecurity-nginx%' install-nginx

# 정리 섹션에 라인 추가
grep 'rm -fr /tmp/ModSecurity-nginx' install-nginx || sed -i 's%\(rm -fr.*/tmp/nginx.*\)%rm -fr /tmp/ModSecurity-nginx\n\1%' install-nginx

install-nginx 스크립트를 실행하는 Dockerfile은 이미지가 빌드될 때 실행된다는 점에 유의하세요. 그리고 이미지는 docker hub에 업로드되기 전에 Discourse 팀에 의해만 빌드됩니다. Discourse의 ./launcher rebuild app 명령이 실행되면 (업데이트가 사용 가능한 경우) docker pull이 트리거되어 docker hub에서 최신 Discourse docker 이미지를 가져옵니다. 다시 말하지만, 이 과정은 이미지를 다시 빌드하거나 Dockerfile을 실행하거나, 위에서 수정한 install-nginx 스크립트를 실행하지 않습니다.

(위에서 수정된) 업데이트된 install-nginx bash 스크립트가 실행되도록(Dockerfile에 의해 실행됨) 트리거하는 유일한 방법(제가 아는 한)은 docker가 새로운 이미지를 빌드하도록 하는 것입니다. 예를 들어, 아래 명령은 docker가 discourse_modsecurity라는 이름의 새 이미지를 빌드하도록 트리거하며, 이 이미지는 로컬에서 수정된 install-nginx 스크립트를 사용하여 빌드됩니다:

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

불행히도, launcher가 커스텀 이미지를 사용하도록 지시하는 방법을 찾지 못했습니다(run-image를 지정하면 템플릿을 실행하지 않고 지정된 이미지를 직접 사용하므로, nginx를 실제로 구성[설치만 하는 것이 아님]하는 데 필요합니다). 따라서 launcher 스크립트에 정의된 image 변수를 대체하여 discourse_modsecurity라는 이름의 새 로컬 docker 이미지를 사용하도록 합니다.

# "image="discourse/base:<version>"" 라인을 '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

이제 필요한 modsecurity 파일/블록을 포함하도록 nginx 설정을 설정하기 위해 새로운 템플릿 파일을 추가합니다.

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, installed from the 'modsecurity-crs' package in 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

그리고 이 템플릿(templates/web.modsecurity.template.yml)을 앱의 yaml 설정 파일의 templates 블록에 추가하여 다음과 같은 모양이 되도록 합니다:

[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]# 

이제 Discourse docker 앱을 다시 빌드하면, modsecurity가 포함된 nginx를 사용하는 새 discourse_modsecurity docker 이미지를 사용하게 되며, nginx가 OWASP CRS를 사용하도록 구성됩니다.

/var/discourse/launcher rebuild app

ModSecurity나 유사한 WAF가 만능 해결책은 아니라는 점에는 동의합니다.

하지만 (최소한) Discourse에 영향을 미친 RoR의 취약점 하나를 알고 있습니다. 이 취약점은 ModSecurity와 유사한 메커니즘으로 완화될 수 있었습니다.

우리가 이 취약점을 패치했을 때, 로그를 확인한 결과 CVE가 공개되고 패치되기 전에 적어도 하나의 포럼에서 실제 공격에 사용된 적이 있음을 확인했습니다. 이로 인해 정보 유출이 발생하지 않은 것은 단순히 표준 설치와 다르게 몇 가지 사항을 설정했기 때문이었습니다(즉, 운이 좋았던 것입니다).

그러나 추가되는 복잡성이 추가되는 보안보다 더 큰 부담이 되는지는 확신할 수 없습니다.

WAF는 서명 기반 바이러스 스캐너와 비슷하다고 생각합니다. 제 생각에는 공격 표면이 제한된 환경(예: Windows가 아닌 서버)에서는 그다지 유용하지 않습니다.

모든 것을 잡아내는 것은 아니지만, 이론적으로 SQLI와 같은 일반적인 공격 패턴이나 알려진 익스플로이트(예: 위의 RoR 취약점)를 다양한 소프트웨어에 걸쳐 잡아낼 수 있습니다.

특히 기업 환경처럼 무엇을 실행하고 있는지 알 수 없는 수많은 애플리케이션이 돌아가는 상황에서 WAF를 실행하는 가치를 이해할 수 있습니다. 각 애플리케이션이 이러한 익스플로이트로부터 보호되고 있는지 확인해야 하지만 개별 애플리케이션 하나하나를 신경 쓸 필요는 없기 때문입니다. 이는 NxM 크기의 문제를 N+M으로 줄여줍니다.

자신의 사이트에서 WAF를 실행하는 것이 가치가 있는지는 별개의 문제이며, 보호의 이점과 (파손의) 위험에 대한 분석을 스스로 수행해야 합니다.