なおすけの落書き帳

毎日がエブリデイ。

Jenkins向けにngx_mrubyでアクセス制御をかいてみる

https://jenkins.io/images/logos/plumber/plumber.png

関係ない図を貼ってみたけどこいつかわいいですね*1.

背景

先月中頃, Nginxのアクセスログを眺めていると, 不穏なコードがインジェクトされたリクエストが幾つか来ていました. /securityRealm/user/admin/descruoterByName/org.jenkinsci.(以下略) というエンドポイントであったため, おそらくJenkins関連の攻撃だろうと推測し, 調べてみたらこんな記事がありました.

www.alibabacloud.com

記事を見るに, Jenkinsの2つの脆弱性を狙った攻撃コードが観測されていたらしい.

jenkins.io jenkins.io

もちろん, こういったことにすぐに対応できるように, Jenkinsを常に最新にしておくというのが当然の対策ではあります. とはいえ, CI/CD環境なんて, 油断をすればすぐに巨大戦艦になるし, きちんと設計していてもワークアラウンドの手組み修正が発生しがちでしょう.

そういったときに, なるべく手をかけずにさくっとアクセス制御をしたいと思い, ngx_mrubyを思い立ちました.

環境

f:id:naosuke2dx:20190615114230p:plain:w300

まあこんな一般的な構成です. Nginxで接続を受け付けて, リバプロでJenkinsに流してやる構成ですね.

この図のうち, attackerからのアクセスはJenkinsに到達させたくない, でもBotやWebhookは受け付けたいというのが要求です. しかし, 開発者はまあどうにでもなるとはいえ, attackerはwebhookになりすましたりbotになりすましたりもできるので, そのへんを上手く制御したいですね.

そんなわけで, 図中のnginxにmrubyを仕込んであげて, アクセス制御をしてみます.

mruby is 何

mruby.org

組み込み向けRuby実装です.
今回は, ngx_mrubyを使って, nginxのハンドラをかきました

github.com

実装

実装をする上で, 今回は下記の点を考えます

  • github/botのsrc ipは不定
  • webhookが叩くエンドポイントは1つ (/hook あたり)
  • botは, apiを叩きに来る

そんなわけでこんなかんじ. バーン

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については, 検証方法がここにかいているのを愚直に実装しただけ.

developer.github.com

bodyがうまく読めず苦労したのですが, client_body 系の設定を書いてあげたらうまく動きました.
原因はなんなのか謎です.

実行自体は, 公式のDockerを使ってあげれば, Nginxと一緒に手軽に試せると思います.

hub.docker.com

まとめ

ngx_mruby, すごく手軽でいいですね. luaでもできるらしいけど, やっぱり使い慣れたRubyがいい.
個人的には, mrubyとかのbuildが大変なのが難点かなあ. 公式で, Docker imageが公開されているけど…

これからもうちょっと使ってみようかなと思いました.