関係ない図を貼ってみたけどこいつかわいいですね*1.
背景
先月中頃, Nginxのアクセスログを眺めていると, 不穏なコードがインジェクトされたリクエストが幾つか来ていました.
/securityRealm/user/admin/descruoterByName/org.jenkinsci.(以下略)
というエンドポイントであったため, おそらくJenkins関連の攻撃だろうと推測し, 調べてみたらこんな記事がありました.
記事を見るに, Jenkinsの2つの脆弱性を狙った攻撃コードが観測されていたらしい.
もちろん, こういったことにすぐに対応できるように, Jenkinsを常に最新にしておくというのが当然の対策ではあります. とはいえ, CI/CD環境なんて, 油断をすればすぐに巨大戦艦になるし, きちんと設計していてもワークアラウンドの手組み修正が発生しがちでしょう.
そういったときに, なるべく手をかけずにさくっとアクセス制御をしたいと思い, ngx_mrubyを思い立ちました.
環境
まあこんな一般的な構成です. Nginxで接続を受け付けて, リバプロでJenkinsに流してやる構成ですね.
この図のうち, attackerからのアクセスはJenkinsに到達させたくない, でもBotやWebhookは受け付けたいというのが要求です. しかし, 開発者はまあどうにでもなるとはいえ, attackerはwebhookになりすましたりbotになりすましたりもできるので, そのへんを上手く制御したいですね.
そんなわけで, 図中のnginxにmrubyを仕込んであげて, アクセス制御をしてみます.
mruby is 何
組み込み向けRuby実装です.
今回は, ngx_mrubyを使って, nginxのハンドラをかきました
実装
実装をする上で, 今回は下記の点を考えます
そんなわけでこんなかんじ. バーン
githook.rb
# For github webhook req = Nginx::Request.new hin = req.headers_in v = Nginx::Var.new # allow POST only if req.method != 'POST' Nginx.log Nginx::LOG_INFO, "not allowed method!" Nginx.return Nginx::HTTP_NOT_FOUND return end # allow GitHub-Hookshot only unless hin['User-Agent'] =~ /GitHub-Hookshot/ Nginx.log Nginx::LOG_INFO, "not allowed UA!" Nginx.return Nginx::HTTP_NOT_FOUND return end unless hin['X-Hub-Signature'] Nginx.log Nginx::LOG_INFO, "Header is missing!" Nginx.return Nginx::HTTP_NOT_FOUND return end #Do not allow nil body unless v.request_body Nginx.log Nginx::LOG_INFO, "nil body!" Nginx.return Nginx::HTTP_NOT_FOUND return end digest = Digest::HMAC.hexdigest(v.request_body, ENV['SECRET_TOKEN'], Digest::SHA1) if "sha1=#{digest}" != hin['X-Hub-Signature'] Nginx.log(Nginx::LOG_NOTICE, "Signatures didn't match!") Nginx.return Nginx::HTTP_NOT_FOUND return end
allow.rb
ALLOW_IP_RANGE = /192.168.[0-9]+.[0-9]+/ h = Nginx::Headers_in.new c = Nginx::Connection.new if h['User-Agent'] == 'bot' unless ENV['TOKEN'] == h['X-Bot-Token'] Nginx.log(Nginx::LOG_INFO, 'Token doesnt match!') Nginx.return Nginx::HTTP_NOT_FOUND end else unless c.remote_ip =~ ALLOW_IP_RANGE Nginx.log(Nginx::LOG_INFO, 'Not allowed IP address!') Nginx.return Nginx::HTTP_NOT_FOUND end end
設定はこんな感じ
worker_processes auto; env GITHUB_SECRET; env TOKEN; events { worker_connections 768; } http { server_tokens off; server { listen 80; client_max_body_size 100k; client_body_buffer_size 100k; client_body_in_single_buffer on; location /hook/ { mruby_enable_read_request_body on; mruby_content_handler /usr/local/nginx/hook/githook.rb; proxy_pass http://192.168.100.2:8080/hook/; } location / { mruby_access_handler /usr/local/nginx/hook/guardian.rb; proxy_pass http://192.168.100.2:8080; } } }
GitHookについては, 検証方法がここにかいているのを愚直に実装しただけ.
bodyがうまく読めず苦労したのですが, client_body
系の設定を書いてあげたらうまく動きました.
原因はなんなのか謎です.
実行自体は, 公式のDockerを使ってあげれば, Nginxと一緒に手軽に試せると思います.
まとめ
ngx_mruby, すごく手軽でいいですね. luaでもできるらしいけど, やっぱり使い慣れたRubyがいい.
個人的には, mrubyとかのbuildが大変なのが難点かなあ. 公式で, Docker imageが公開されているけど…
これからもうちょっと使ってみようかなと思いました.