はじめに

Webアプリを作ったものの、公開するにはセキュリティ面が心もとない…ということでWAFを検討していたところ、 ModSecurityがEoLになるらしく、じゃあどうすれば…?と途方に暮れていました。

そこからいろいろとググってみたところ、ModSecurityとの互換性があるという OWASP Corazaあたりが使えそうということが分かったので、 今回はその導入方法についてまとめます。

必要なもの

xcaddyの用意

まずはxcaddyのバイナリを入手し、パスの通っている適当なディレクトリに置いておきます。 (いつもの感覚でtar xfするとカレントディレクトリに散らばるので要注意。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
% mkdir -p ~/work/xcaddy
% cd ~/work/xcaddy
% curl -L -O https://github.com/caddyserver/xcaddy/releases/download/v0.3.5/xcaddy_0.3.5_linux_amd64.tar.gz
% tar xf xcaddy_0.3.5_linux_amd64.tar.gz
% ls -la
total 4248
drwx------  2 gloria gloria    4096 Sep  5 18:05 .
drwxr-x--- 18 gloria gloria    4096 Sep  5 18:04 ..
-rw-------  1 gloria gloria   11357 Aug  8 01:22 LICENSE
-rw-------  1 gloria gloria    6833 Aug  8 01:22 README.md
-rwx------  1 gloria gloria 3010560 Aug  8 01:23 xcaddy
-rw-------  1 gloria gloria 1307631 Sep  5 18:04 xcaddy_0.3.5_linux_amd64.tar.gz
% cp xcaddy ~/.local/bin
% which xcaddy
/home/gloria/.local/bin/xcaddy

coraza-caddyのビルド

次にcoraza-caddyをビルドします。 goコンパイラが必要なので、ない場合はapt install golang-1.20しておきましょう。

1
2
3
4
5
6
7
% cd ~/work
% curl -L -O https://github.com/corazawaf/coraza-caddy/archive/refs/tags/v2.0.0-rc.3.tar.gz
% tar xf v2.0.0-rc.3.tar.gz
% cd coraza-caddy-2.0.0-rc.3
% ls
LICENSE    TROUBLESHOOTING.md  coraza.go       e2e      ftw     go.sum   http_test.go    logger.go  magefile.go       testdata
README.md  caddy               coraza_test.go  example  go.mod  http.go  interceptor.go  mage.go    test.init.config  utils.go

goコンパイラは/usr/lib/go-1.20/binに入っているので、そこにPATHを通してビルドします。 (PATHを通しておかないと、内部でgoコマンドが実行された際にエラーとなってしまいます。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
% PATH="/usr/lib/go-1.20/bin:$PATH" go run mage.go buildCaddy
2023/09/05 18:18:34 [INFO] Resolved relative replacement github.com/corazawaf/coraza-caddy/v2=. to /home/gloria/work/coraza-caddy-2.0.0-rc.3
2023/09/05 18:18:34 [INFO] Temporary folder: /tmp/buildenv_2023-09-05-1818.853923260
2023/09/05 18:18:34 [INFO] Writing main module: /tmp/buildenv_2023-09-05-1818.853923260/main.go
package main
...
...
go: added github.com/tidwall/match v1.1.1
go: added github.com/tidwall/pretty v1.2.1
2023/09/05 18:18:37 [INFO] Build environment ready
2023/09/05 18:18:37 [INFO] Building Caddy
2023/09/05 18:18:37 [INFO] exec (timeout=0s): /usr/lib/go-1.20/bin/go mod tidy -e
2023/09/05 18:18:38 [INFO] exec (timeout=0s): /usr/lib/go-1.20/bin/go build -o /home/gloria/work/coraza-caddy-2.0.0-rc.3/build/caddy -ldflags -w -s -trimpath
2023/09/05 18:18:48 [INFO] Build complete: build/caddy
2023/09/05 18:18:48 [INFO] Cleaning up temporary folder: /tmp/buildenv_2023-09-05-1818.853923260

buildディレクトリ内にcaddyのバイナリができていればOKです。

1
2
3
4
5
% ls -l build
total 42440
-rwx------ 1 gloria gloria 43458560 Sep  5 18:18 caddy
% file build/caddy
build/caddy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=j89ijyzi9HFzGvNYOwym/wTJquPD4Dt_JAqmfwjAt/jDRi_q56UNnAxu7JWvk1/WN9AtgOafr7ercOLvTat, stripped

coraza-caddyをリバースプロキシサーバとして起動

さきほどビルドしたcaddyを、リバースプロキシサーバとして起動します。 設定ファイル(Caddyfile)は、coraza-caddyexampleディレクトリ内にあるものをそのまま用います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
% ./build/caddy run --config example/Caddyfile --adapter caddyfile
2023/09/05 09:31:23.995 INFO    using provided configuration    {"config_file": "example/Caddyfile", "config_adapter": "caddyfile"}
2023/09/05 09:31:23.995 WARN    Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies    {"adapter": "caddyfile", "file": "example/Caddyfile", "line": 2}
2023/09/05 09:31:23.996 INFO    admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2023/09/05 09:31:23.996 WARN    http.auto_https automatic HTTPS is completely disabled for server       {"server_name": "srv0"}
2023/09/05 09:31:23.996 DEBUG   http.auto_https adjusted config {"tls": {"automation":{"policies":[{}]}}, "http": {"servers":{"srv0":{"listen":[":8080"],"routes":[{"handle":[{"directives":"\n\t\t\tInclude @coraza.conf-recommended\n\t\t\tInclude @crs-setup.conf.example\n\t\t\tInclude @owasp_crs/*.conf\n\t\t\tSecRuleEngine On\n\t\t\tSecDebugLog /dev/stdout\n\t\t\tSecDebugLogLevel 9\n\t\t    SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny,status:403\"\n\t\t\tSecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny,status:403\"\n\t\t\tSecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny,status:403\"\n\t\t\tSecResponseBodyAccess On\n\t\t\tSecResponseBodyMimeType application/json\n\t\t\tSecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny,status:403\"\n\t\t","handler":"waf","include":[],"load_owasp_crs":true},{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:8081"}]}]}],"automatic_https":{"disable":true}}}}}
2023/09/05 09:31:23.996 INFO    tls.cache.maintenance   started background certificate maintenance      {"cache": "0xc000213f80"}
{"level":"debug","ts":1693906284.0556035,"logger":"http.handlers.waf","msg":"Parsing directive","line":"SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny,status:403\""}
{"level":"debug","ts":1693906284.0556402,"logger":"http.handlers.waf","msg":"Parsing directive","line":"SecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny,status:403\""}
{"level":"debug","ts":1693906284.0556633,"logger":"http.handlers.waf","msg":"Parsing directive","line":"SecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny,status:403\""}
{"level":"debug","ts":1693906284.055682,"logger":"http.handlers.waf","msg":"Parsing directive","line":"SecResponseBodyAccess On"}
{"level":"debug","ts":1693906284.055694,"logger":"http.handlers.waf","msg":"Parsing directive","line":"SecResponseBodyMimeType application/json"}
{"level":"debug","ts":1693906284.0556972,"logger":"http.handlers.waf","msg":"Parsing directive","line":"SecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny,status:403\""}
2023/09/05 09:31:24.055 DEBUG   http    starting server loop    {"address": "[::]:8080", "tls": false, "http3": false}
2023/09/05 09:31:24.055 INFO    http.log        server running  {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2023/09/05 09:31:24.055 INFO    tls     cleaning storage unit   {"description": "FileStorage:/home/gloria/.local/share/caddy"}
2023/09/05 09:31:24.055 INFO    tls     finished cleaning storage units
2023/09/05 09:31:24.056 INFO    autosaved config (load with --resume flag)      {"file": "/home/gloria/.config/caddy/autosave.json"}
2023/09/05 09:31:24.056 INFO    serving initial configuration

coraza.conf-recommendedcrs-setup.conf.exampleowasp_crs/*.confは、 caddyバイナリ内に組み込まれているものが参照される点に注意が必要です。

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
# example/Caddyfile
{
    debug
    auto_https off
    order coraza_waf first
}

:8080 {
        coraza_waf {
                load_owasp_crs
                directives `
                        Include @coraza.conf-recommended
                        Include @crs-setup.conf.example
                        Include @owasp_crs/*.conf
                        SecRuleEngine On
                        SecDebugLog /dev/stdout
                        SecDebugLogLevel 9
                    SecRule REQUEST_URI "@streq /admin" "id:101,phase:1,t:lowercase,deny,status:403"
                        SecRule REQUEST_BODY "@rx maliciouspayload" "id:102,phase:2,t:lowercase,deny,status:403"
                        SecRule RESPONSE_HEADERS::status "@rx 406" "id:103,phase:3,t:lowercase,deny,status:403"
                        SecResponseBodyAccess On
                        SecResponseBodyMimeType application/json
                        SecRule RESPONSE_BODY "@contains responsebodycode" "id:104,phase:4,t:lowercase,deny,status:403"
                `
        }
        reverse_proxy {$HTTPBIN_HOST:localhost}:8081
}

サンプルWebアプリサーバを起動

動作検証に用いるサンプルWebアプリサーバを、てきとうに開いた別の端末で起動します。 これはなんでも構いませんが、今回はCorazaの公式ドキュメントでも用いられているgo-httpbinを用います。

1
2
3
% /usr/lib/go-1.20/bin/go run github.com/mccutchen/go-httpbin/v2/cmd/go-httpbin@v2.9.0 -port 8081
go: downloading github.com/mccutchen/go-httpbin/v2 v2.9.0
go-httpbin listening on http://0.0.0.0:8081

動作確認

WAFとして正常に機能しているか、あやしいクエリ(id=' OR 1=1 -- ')を投入して試してみます。

単純なリクエストの場合、特にエラーにならず200 OKが返ってきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
% curl -l -i "http://localhost:8080/"
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' camo.githubusercontent.com
Content-Type: text/html; charset=utf-8
Date: Tue, 05 Sep 2023 09:40:35 GMT
Server: Caddy
Transfer-Encoding: chunked                                                                                                                                                                                       
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='content-type' value='text/html;charset=utf8'>
  <meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>

同じパスにSQLインジェクションっぽい文字列をクエリに追加すると、403 Forbiddenが返されます。

1
2
3
4
5
% curl -l -i "http://localhost:8080/?id='%20OR%201%3D1%20--%20"
HTTP/1.1 403 Forbidden
Server: Caddy
Date: Tue, 05 Sep 2023 09:38:50 GMT
Content-Length: 0

リバースプロキシサーバ側のログには、下記のようにSQL Injection Attack Detectedと出ます。

1
2
3
4
5
2023/09/05 09:45:21.062 ERROR   http.handlers.waf       [client "127.0.0.1"] Coraza: Access denied (phase 2). SQL Injection Attack Detected via libinjection [file "@owasp_crs/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [line "5101"] [id "942100"] [rev ""] [msg "SQL Injection Attack Detected via libinjection"] [data "Matched Data: s&1c found within ARGS:id: ' OR 1=1 -- "] [severity "critical"] [ver "OWASP_CRS/4.0.0-rc1"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-sqli"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/152/248/66"] [tag "PCI/6.5.2"] [hostname ""] [uri "/?id='%20OR%201%3D1%20--%20"] [unique_id "eMtYQWlXbjIxMGER"]

2023/09/05 09:45:21.063 ERROR   http.handlers.waf       [client "127.0.0.1"] Coraza: Access denied (phase 2). Inbound Anomaly Score Exceeded (Total Score: 5) [file "@owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "6836"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [data ""] [severity "emergency"] [ver "OWASP_CRS/4.0.0-rc1"] [maturity "0"] [accuracy "0"] [tag "anomaly-evaluation"] [hostname ""] [uri "/?id='%20OR%201%3D1%20--%20"] [unique_id "eMtYQWlXbjIxMGER"]

2023/09/05 09:45:21.063 DEBUG   http.log.error  interruption triggered  {"request": {"remote_ip": "127.0.0.1", "remote_port": "50636", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost:8080", "uri": "/?id='%20OR%201%3D1%20--%20", "headers": {"User-Agent": ["curl/7.81.0"], "Accept": ["*/*"]}}, "duration": 0.001005506, "status": 403, "err_id": "eMtYQWlXbjIxMGER", "err_trace": ""}

とりあえず動作することは分かったので、次回はHTTPS対応などを行っていきます。

その他